Implement AvahiMdnsClient

Implement the AvahiMdnsClient interface.  This is based on code
from peerd/avahi_client.cc and avahi_service_publisher.cc.

Bug: 23288773
Change-Id: If58df0bf3d71c665c764ca8994b43dcff237549e
diff --git a/buffet/avahi_mdns_client.cc b/buffet/avahi_mdns_client.cc
index fde28d5..3216546 100644
--- a/buffet/avahi_mdns_client.cc
+++ b/buffet/avahi_mdns_client.cc
@@ -14,31 +14,331 @@
  * limitations under the License.
  */
 
-#include "buffet/avahi_mdns_client.h"
+#include <vector>
 
+#include "buffet/avahi_mdns_client.h"
+#include "buffet/dbus_constants.h"
+
+#include <avahi-common/defs.h>
+#include <avahi-common/address.h>
 #include <base/guid.h>
+#include <chromeos/dbus/async_event_sequencer.h>
+#include <chromeos/dbus/dbus_signal_handler.h>
+#include <chromeos/dbus/dbus_method_invoker.h>
+#include <chromeos/errors/error.h>
+#include <dbus/object_path.h>
+#include <dbus/object_proxy.h>
+
+using chromeos::ErrorPtr;
+using chromeos::dbus_utils::AsyncEventSequencer;
+using chromeos::dbus_utils::CallMethodAndBlock;
+using chromeos::dbus_utils::ExtractMethodCallResults;
+using CompletionAction =
+    chromeos::dbus_utils::AsyncEventSequencer::CompletionAction;
 
 namespace buffet {
 
-AvahiMdnsClient::AvahiMdnsClient() {
+AvahiMdnsClient::AvahiMdnsClient(const scoped_refptr<dbus::Bus> &bus):
+    bus_(bus) {
 }
 
 AvahiMdnsClient::~AvahiMdnsClient() {
 }
 
+// NB: This should be the one-and-only definition of this MdnsClient static
+// method.
+std::unique_ptr<MdnsClient> MdnsClient::CreateInstance(
+    const scoped_refptr<dbus::Bus> &bus) {
+  return std::unique_ptr<MdnsClient>{new AvahiMdnsClient(bus)};
+}
+
+// TODO(rginda): Report errors back to the caller.
+// TODO(rginda): Support publishing more than one service.
 void AvahiMdnsClient::PublishService(
-    const std::string& service_name,
+    const std::string& service_type,
     uint16_t port,
     const std::map<std::string, std::string>& txt) {
-  // TODO(avakulenko)
+
+  if (service_state_ == READY) {
+    // TODO(rginda): Instead of stop/start, we should update the existing
+    // service when only the txt record changes.
+    StopPublishing(service_type);
+    if (service_state_ != UNDEF) {
+      LOG(ERROR) << "Failed to disable existing service.";
+      return;
+    }
+  }
+
+  service_name_ = base::GenerateGUID();
+  service_type_ = service_type;
+  port_ = port;
+  txt_ = GetTxtRecord(txt);
+
+  if (avahi_state_ == UNDEF || avahi_state_ == ERROR) {
+    ConnectToAvahi();
+  } else if (avahi_state_ == READY) {
+    CreateService();
+  } else {
+    CHECK(avahi_state_ == PENDING);
+  }
 }
 
-void AvahiMdnsClient::StopPublishing(const std::string& service_name) {
-  // TODO(avakulenko)
+// TODO(rginda): If we support publishing more than one service then we
+// may need a less ambiguous way of unpublishing them.
+void AvahiMdnsClient::StopPublishing(const std::string& service_type) {
+  if (service_type_.compare(service_type) != 0) {
+    LOG(ERROR) << "Unknown service type: " << service_type;
+    return;
+  }
+
+  if (service_state_ != READY) {
+    LOG(ERROR) << "Service is not published.";
+  }
+
+  service_name_.clear();
+  service_type_.clear();
+  port_ = 0;
+
+  FreeEntryGroup();
 }
 
-std::unique_ptr<MdnsClient> MdnsClient::CreateInstance() {
-  return std::unique_ptr<MdnsClient>{new AvahiMdnsClient};
+// Transform a service_info to a mDNS compatible TXT record value.
+// Concretely, a TXT record consists of a list of strings in the format
+// "key=value".  Each string must be less than 256 bytes long, since they are
+// length/value encoded.  Keys may not contain '=' characters, but are
+// otherwise unconstrained.
+//
+// We need a DBus type of "aay", which is a vector<vector<uint8_t>> in our
+// bindings.
+AvahiMdnsClient::TxtRecord AvahiMdnsClient::GetTxtRecord(
+    const std::map<std::string, std::string>& txt) {
+  TxtRecord result;
+  result.reserve(txt.size());
+  for (const auto& kv : txt) {
+    result.emplace_back();
+    std::vector<uint8_t>& record = result.back();
+    record.reserve(kv.first.length() + kv.second.length() + 1);
+    record.insert(record.end(), kv.first.begin(), kv.first.end());
+    record.push_back('=');
+    record.insert(record.end(), kv.second.begin(), kv.second.end());
+  }
+  return result;
+}
+
+void AvahiMdnsClient::ConnectToAvahi() {
+  avahi_state_ = PENDING;
+
+  avahi_ = bus_->GetObjectProxy(
+      dbus_constants::avahi::kServiceName,
+      dbus::ObjectPath(dbus_constants::avahi::kServerPath));
+
+  // This callback lives for the lifetime of the ObjectProxy.
+  avahi_->SetNameOwnerChangedCallback(
+      base::Bind(&AvahiMdnsClient::OnAvahiOwnerChanged,
+                 weak_ptr_factory_.GetWeakPtr()));
+
+  // Reconnect to our signals on a new Avahi instance.
+  scoped_refptr<AsyncEventSequencer> sequencer(new AsyncEventSequencer());
+  chromeos::dbus_utils::ConnectToSignal(
+      avahi_,
+      dbus_constants::avahi::kServerInterface,
+      dbus_constants::avahi::kServerSignalStateChanged,
+      base::Bind(&AvahiMdnsClient::OnAvahiStateChanged,
+                 weak_ptr_factory_.GetWeakPtr()),
+      sequencer->GetExportHandler(
+          dbus_constants::avahi::kServerInterface,
+          dbus_constants::avahi::kServerSignalStateChanged,
+          "Failed to subscribe to Avahi state change.",
+          true));
+  sequencer->OnAllTasksCompletedCall(
+      {// Get a onetime callback with the initial state of Avahi.
+       AsyncEventSequencer::WrapCompletionTask(
+            base::Bind(&dbus::ObjectProxy::WaitForServiceToBeAvailable,
+                       avahi_,
+                       base::Bind(&AvahiMdnsClient::OnAvahiAvailable,
+                                  weak_ptr_factory_.GetWeakPtr()))),
+      });
+}
+
+void AvahiMdnsClient::CreateEntryGroup() {
+  ErrorPtr error;
+
+  service_state_ = PENDING;
+
+  auto resp = CallMethodAndBlock(
+      avahi_, dbus_constants::avahi::kServerInterface,
+      dbus_constants::avahi::kServerMethodEntryGroupNew,
+      &error);
+
+  dbus::ObjectPath group_path;
+  if (!resp || !ExtractMethodCallResults(resp.get(), &error, &group_path)) {
+    service_state_ = ERROR;
+    LOG(ERROR) << "Error creating group.";
+    return;
+  }
+  entry_group_ = bus_->GetObjectProxy(dbus_constants::avahi::kServiceName,
+                                      group_path);
+
+  // If we fail to connect to the the StateChange signal for this group, just
+  // report that the whole thing has failed.
+  auto on_connect_cb = [](const std::string& interface_name,
+                          const std::string& signal_name,
+                          bool success) {
+    if (!success) {
+      LOG(ERROR) << "Failed to connect to StateChange signal "
+        "from EntryGroup.";
+      return;
+    }
+  };
+
+  chromeos::dbus_utils::ConnectToSignal(
+      entry_group_,
+      dbus_constants::avahi::kGroupInterface,
+      dbus_constants::avahi::kGroupSignalStateChanged,
+      base::Bind(&AvahiMdnsClient::HandleGroupStateChanged,
+                 weak_ptr_factory_.GetWeakPtr()),
+      base::Bind(on_connect_cb));
+
+  if (!service_name_.empty())
+    CreateService();
+}
+
+void AvahiMdnsClient::FreeEntryGroup() {
+  if (!entry_group_) {
+    LOG(ERROR) << "No group to free.";
+    return;
+  }
+
+  ErrorPtr error;
+  auto resp = CallMethodAndBlock(entry_group_,
+                                 dbus_constants::avahi::kGroupInterface,
+                                 dbus_constants::avahi::kGroupMethodFree,
+                                 &error);
+  // Extract and log relevant errors.
+  bool success = resp && ExtractMethodCallResults(resp.get(), &error);
+  if (!success) {
+    LOG(ERROR) << "Error freeing service group";
+  }
+
+  // Ignore any signals we may have registered for from this proxy.
+  entry_group_->Detach();
+  entry_group_ = nullptr;
+
+  service_state_ = UNDEF;
+}
+
+void AvahiMdnsClient::CreateService() {
+  ErrorPtr error;
+
+  CHECK_EQ(service_type_, "privet");
+
+  VLOG(1) << "CreateService: name " << service_name_ << ", type: " <<
+      service_type_ << ", port: " << port_;
+
+  auto resp = CallMethodAndBlock(
+      entry_group_,
+      dbus_constants::avahi::kGroupInterface,
+      dbus_constants::avahi::kGroupMethodAddService,
+      &error,
+      int32_t{AVAHI_IF_UNSPEC},
+      int32_t{AVAHI_PROTO_UNSPEC},
+      uint32_t{0},  // No flags.
+      service_name_,
+      // For historical purposes, we convert the string "privet" into a proper
+      // mdns service type.  Everyone else should just pass a proper service
+      // type.
+      service_type_.compare("privet") == 0 ? "_privet._tcp" : service_type_,
+      std::string{},  // domain.
+      std::string{},  // hostname
+      port_,
+      txt_);
+  if (!resp || !ExtractMethodCallResults(resp.get(), &error)) {
+    LOG(ERROR) << "Error creating service";
+    service_state_ = ERROR;
+    return;
+  }
+
+  resp = CallMethodAndBlock(entry_group_,
+                            dbus_constants::avahi::kGroupInterface,
+                            dbus_constants::avahi::kGroupMethodCommit,
+                            &error);
+  if (!resp || !ExtractMethodCallResults(resp.get(), &error)) {
+    LOG(ERROR) << "Error committing service.";
+    service_state_ = ERROR;
+    return;
+  }
+
+  service_state_ = READY;
+}
+
+void AvahiMdnsClient::OnAvahiOwnerChanged(const std::string& old_owner,
+                                          const std::string& new_owner) {
+  if (new_owner.empty()) {
+    OnAvahiAvailable(false);
+    return;
+  }
+  OnAvahiAvailable(true);
+}
+
+void AvahiMdnsClient::OnAvahiStateChanged(int32_t state,
+                                          const std::string& error) {
+  HandleAvahiStateChange(state);
+}
+
+void AvahiMdnsClient::OnAvahiAvailable(bool avahi_is_on_dbus) {
+  VLOG(1) << "Avahi is "  << (avahi_is_on_dbus ? "up." : "down.");
+  int32_t state = AVAHI_SERVER_FAILURE;
+  if (avahi_is_on_dbus) {
+    auto resp = CallMethodAndBlock(
+        avahi_, dbus_constants::avahi::kServerInterface,
+        dbus_constants::avahi::kServerMethodGetState,
+        nullptr);
+    if (!resp || !ExtractMethodCallResults(resp.get(), nullptr, &state)) {
+      LOG(WARNING) << "Failed to get avahi initial state.  Relying on signal.";
+    }
+  }
+  VLOG(1) << "Initial Avahi state=" << state << ".";
+  HandleAvahiStateChange(state);
+}
+
+void AvahiMdnsClient::HandleAvahiStateChange(int32_t state) {
+  switch (state) {
+    case AVAHI_SERVER_RUNNING: {
+      // All host RRs have been established.
+      VLOG(1) << "Avahi ready for action.";
+      if (avahi_state_ == READY) {
+        LOG(INFO) << "Ignoring redundant Avahi up event.";
+        return;
+      }
+      avahi_state_ = READY;
+      if (!service_name_.empty())
+        CreateEntryGroup();
+    } break;
+    case AVAHI_SERVER_INVALID:
+      // Invalid state (initial).
+    case AVAHI_SERVER_REGISTERING:
+      // Host RRs are being registered.
+    case AVAHI_SERVER_COLLISION:
+      // There is a collision with a host RR. All host RRs have been withdrawn,
+      // the user should set a new host name via avahi_server_set_host_name().
+    case AVAHI_SERVER_FAILURE:
+      // Some fatal failure happened, the server is unable to proceed.
+      avahi_state_ = ERROR;
+      LOG(ERROR) << "Avahi changed to error state: " << state;
+      break;
+    default:
+      LOG(ERROR) << "Unknown Avahi server state change to " << state;
+      break;
+  }
+}
+
+void AvahiMdnsClient::HandleGroupStateChanged(
+    int32_t state,
+    const std::string& error_message) {
+  if (state == AVAHI_ENTRY_GROUP_COLLISION ||
+      state == AVAHI_ENTRY_GROUP_FAILURE) {
+    LOG(ERROR) << "Avahi service group error: " << state;
+  }
 }
 
 }  // namespace buffet