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