diff --git a/docs/source/building/macos.rst b/docs/source/building/macos.rst index c2a0adbbd2e..6d90a651b25 100644 --- a/docs/source/building/macos.rst +++ b/docs/source/building/macos.rst @@ -12,7 +12,7 @@ MacPorts Install Requirements .. code-block:: bash - sudo port install avahi cmake curl doxygen graphviz libopus miniupnpc npm9 pkgconfig python311 py311-pip + sudo port install cmake curl doxygen graphviz libopus miniupnpc npm9 pkgconfig python311 py311-pip Homebrew """""""" diff --git a/packaging/macos/Portfile b/packaging/macos/Portfile index 4a89b1d8848..3fdb7b93718 100644 --- a/packaging/macos/Portfile +++ b/packaging/macos/Portfile @@ -38,8 +38,7 @@ depends_build-append port:doxygen \ port:python311 \ port:py311-pip -depends_lib port:avahi \ - port:curl \ +depends_lib port:curl \ port:libopus \ port:miniupnpc diff --git a/packaging/sunshine.rb b/packaging/sunshine.rb index 78d8f3e75bb..cb97f7f097a 100644 --- a/packaging/sunshine.rb +++ b/packaging/sunshine.rb @@ -37,6 +37,7 @@ class @PROJECT_NAME@ < Formula depends_on "icu4c" => :recommended on_linux do + depends_on "avahi" depends_on "libcap" depends_on "libdrm" depends_on "libnotify" diff --git a/src/platform/linux/publish.cpp b/src/platform/linux/publish.cpp index 2115ec8a798..29641411e50 100644 --- a/src/platform/linux/publish.cpp +++ b/src/platform/linux/publish.cpp @@ -2,7 +2,6 @@ * @file src/platform/linux/publish.cpp * @brief Definitions for publishing services on Linux. * @note Adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html - * @todo Use a common file for this and src/platform/macos/publish.cpp */ #include diff --git a/src/platform/macos/publish.cpp b/src/platform/macos/publish.cpp index 3eda292bf73..1b517ba2e12 100644 --- a/src/platform/macos/publish.cpp +++ b/src/platform/macos/publish.cpp @@ -1,442 +1,122 @@ /** * @file src/platform/macos/publish.cpp * @brief Definitions for publishing services on macOS. - * @note Adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html - * @todo Use a common file for this and src/platform/linux/publish.cpp */ +#include #include -#include "misc.h" #include "src/logging.h" #include "src/network.h" #include "src/nvhttp.h" #include "src/platform/common.h" -#include "src/utility.h" using namespace std::literals; -namespace avahi { - - /** - * @brief Error codes used by avahi. - */ - enum err_e { - OK = 0, ///< OK - ERR_FAILURE = -1, ///< Generic error code - ERR_BAD_STATE = -2, ///< Object was in a bad state - ERR_INVALID_HOST_NAME = -3, ///< Invalid host name - ERR_INVALID_DOMAIN_NAME = -4, ///< Invalid domain name - ERR_NO_NETWORK = -5, ///< No suitable network protocol available - ERR_INVALID_TTL = -6, ///< Invalid DNS TTL - ERR_IS_PATTERN = -7, ///< RR key is pattern - ERR_COLLISION = -8, ///< Name collision - ERR_INVALID_RECORD = -9, ///< Invalid RR - - ERR_INVALID_SERVICE_NAME = -10, ///< Invalid service name - ERR_INVALID_SERVICE_TYPE = -11, ///< Invalid service type - ERR_INVALID_PORT = -12, ///< Invalid port number - ERR_INVALID_KEY = -13, ///< Invalid key - ERR_INVALID_ADDRESS = -14, ///< Invalid address - ERR_TIMEOUT = -15, ///< Timeout reached - ERR_TOO_MANY_CLIENTS = -16, ///< Too many clients - ERR_TOO_MANY_OBJECTS = -17, ///< Too many objects - ERR_TOO_MANY_ENTRIES = -18, ///< Too many entries - ERR_OS = -19, ///< OS error - - ERR_ACCESS_DENIED = -20, ///< Access denied - ERR_INVALID_OPERATION = -21, ///< Invalid operation - ERR_DBUS_ERROR = -22, ///< An unexpected D-Bus error occurred - ERR_DISCONNECTED = -23, ///< Daemon connection failed - ERR_NO_MEMORY = -24, ///< Memory exhausted - ERR_INVALID_OBJECT = -25, ///< The object passed to this function was invalid - ERR_NO_DAEMON = -26, ///< Daemon not running - ERR_INVALID_INTERFACE = -27, ///< Invalid interface - ERR_INVALID_PROTOCOL = -28, ///< Invalid protocol - ERR_INVALID_FLAGS = -29, ///< Invalid flags - - ERR_NOT_FOUND = -30, ///< Not found - ERR_INVALID_CONFIG = -31, ///< Configuration error - ERR_VERSION_MISMATCH = -32, ///< Version mismatch - ERR_INVALID_SERVICE_SUBTYPE = -33, ///< Invalid service subtype - ERR_INVALID_PACKET = -34, ///< Invalid packet - ERR_INVALID_DNS_ERROR = -35, ///< Invalid DNS return code - ERR_DNS_FORMERR = -36, ///< DNS Error: Form error - ERR_DNS_SERVFAIL = -37, ///< DNS Error: Server Failure - ERR_DNS_NXDOMAIN = -38, ///< DNS Error: No such domain - ERR_DNS_NOTIMP = -39, ///< DNS Error: Not implemented - - ERR_DNS_REFUSED = -40, ///< DNS Error: Operation refused - ERR_DNS_YXDOMAIN = -41, ///< TODO - ERR_DNS_YXRRSET = -42, ///< TODO - ERR_DNS_NXRRSET = -43, ///< TODO - ERR_DNS_NOTAUTH = -44, ///< DNS Error: Not authorized - ERR_DNS_NOTZONE = -45, ///< TODO - ERR_INVALID_RDATA = -46, ///< Invalid RDATA - ERR_INVALID_DNS_CLASS = -47, ///< Invalid DNS class - ERR_INVALID_DNS_TYPE = -48, ///< Invalid DNS type - ERR_NOT_SUPPORTED = -49, ///< Not supported - - ERR_NOT_PERMITTED = -50, ///< Operation not permitted - ERR_INVALID_ARGUMENT = -51, ///< Invalid argument - ERR_IS_EMPTY = -52, ///< Is empty - ERR_NO_CHANGE = -53, ///< The requested operation is invalid because it is redundant - - ERR_MAX = -54 ///< TODO - }; - - constexpr auto IF_UNSPEC = -1; - enum proto { - PROTO_INET = 0, ///< IPv4 - PROTO_INET6 = 1, ///< IPv6 - PROTO_UNSPEC = -1 ///< Unspecified/all protocol(s) - }; - - enum ServerState { - SERVER_INVALID, ///< Invalid state (initial) - SERVER_REGISTERING, ///< Host RRs are being registered - SERVER_RUNNING, ///< All host RRs have been established - 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() - SERVER_FAILURE ///< Some fatal failure happened, the server is unable to proceed - }; - - enum ClientState { - CLIENT_S_REGISTERING = SERVER_REGISTERING, ///< Server state: REGISTERING - CLIENT_S_RUNNING = SERVER_RUNNING, ///< Server state: RUNNING - CLIENT_S_COLLISION = SERVER_COLLISION, ///< Server state: COLLISION - CLIENT_FAILURE = 100, ///< Some kind of error happened on the client side - CLIENT_CONNECTING = 101 ///< We're still connecting. This state is only entered when AVAHI_CLIENT_NO_FAIL has been passed to avahi_client_new() and the daemon is not yet available. - }; - - enum EntryGroupState { - ENTRY_GROUP_UNCOMMITED, ///< The group has not yet been committed, the user must still call avahi_entry_group_commit() - ENTRY_GROUP_REGISTERING, ///< The entries of the group are currently being registered - ENTRY_GROUP_ESTABLISHED, ///< The entries have successfully been established - ENTRY_GROUP_COLLISION, ///< A name collision for one of the entries in the group has been detected, the entries have been withdrawn - ENTRY_GROUP_FAILURE ///< Some kind of failure happened, the entries have been withdrawn - }; - - enum ClientFlags { - CLIENT_IGNORE_USER_CONFIG = 1, ///< Don't read user configuration - CLIENT_NO_FAIL = 2 ///< Don't fail if the daemon is not available when avahi_client_new() is called, instead enter CLIENT_CONNECTING state and wait for the daemon to appear - }; - - /** - * @brief Flags for publishing functions. - */ - enum PublishFlags { - PUBLISH_UNIQUE = 1, ///< For raw records: The RRset is intended to be unique - PUBLISH_NO_PROBE = 2, ///< For raw records: Though the RRset is intended to be unique no probes shall be sent - PUBLISH_NO_ANNOUNCE = 4, ///< For raw records: Do not announce this RR to other hosts - PUBLISH_ALLOW_MULTIPLE = 8, ///< For raw records: Allow multiple local records of this type, even if they are intended to be unique - PUBLISH_NO_REVERSE = 16, ///< For address records: don't create a reverse (PTR) entry - PUBLISH_NO_COOKIE = 32, ///< For service records: do not implicitly add the local service cookie to TXT data - PUBLISH_UPDATE = 64, ///< Update existing records instead of adding new ones - PUBLISH_USE_WIDE_AREA = 128, ///< Register the record using wide area DNS (i.e. unicast DNS update) - PUBLISH_USE_MULTICAST = 256 ///< Register the record using multicast DNS - }; - - using IfIndex = int; - using Protocol = int; - - struct EntryGroup; - struct Poll; - struct SimplePoll; - struct Client; - - typedef void (*ClientCallback)(Client *, ClientState, void *userdata); - typedef void (*EntryGroupCallback)(EntryGroup *g, EntryGroupState state, void *userdata); - - typedef void (*free_fn)(void *); - - typedef Client *(*client_new_fn)(const Poll *poll_api, ClientFlags flags, ClientCallback callback, void *userdata, int *error); - typedef void (*client_free_fn)(Client *); - typedef char *(*alternative_service_name_fn)(char *); - - typedef Client *(*entry_group_get_client_fn)(EntryGroup *); - - typedef EntryGroup *(*entry_group_new_fn)(Client *, EntryGroupCallback, void *userdata); - typedef int (*entry_group_add_service_fn)( - EntryGroup *group, - IfIndex interface, - Protocol protocol, - PublishFlags flags, - const char *name, - const char *type, - const char *domain, - const char *host, - uint16_t port, - ...); - - typedef int (*entry_group_is_empty_fn)(EntryGroup *); - typedef int (*entry_group_reset_fn)(EntryGroup *); - typedef int (*entry_group_commit_fn)(EntryGroup *); - - typedef char *(*strdup_fn)(const char *); - typedef char *(*strerror_fn)(int); - typedef int (*client_errno_fn)(Client *); - - typedef Poll *(*simple_poll_get_fn)(SimplePoll *); - typedef int (*simple_poll_loop_fn)(SimplePoll *); - typedef void (*simple_poll_quit_fn)(SimplePoll *); - typedef SimplePoll *(*simple_poll_new_fn)(); - typedef void (*simple_poll_free_fn)(SimplePoll *); - - free_fn free; - client_new_fn client_new; - client_free_fn client_free; - alternative_service_name_fn alternative_service_name; - entry_group_get_client_fn entry_group_get_client; - entry_group_new_fn entry_group_new; - entry_group_add_service_fn entry_group_add_service; - entry_group_is_empty_fn entry_group_is_empty; - entry_group_reset_fn entry_group_reset; - entry_group_commit_fn entry_group_commit; - strdup_fn strdup; - strerror_fn strerror; - client_errno_fn client_errno; - simple_poll_get_fn simple_poll_get; - simple_poll_loop_fn simple_poll_loop; - simple_poll_quit_fn simple_poll_quit; - simple_poll_new_fn simple_poll_new; - simple_poll_free_fn simple_poll_free; - - int - init_common() { - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if (funcs_loaded) return 0; - - if (!handle) { - handle = dyn::handle({ "libavahi-common.3.dylib", "libavahi-common.dylib" }); - if (!handle) { - return -1; - } - } - - std::vector> funcs { - { (dyn::apiproc *) &alternative_service_name, "avahi_alternative_service_name" }, - { (dyn::apiproc *) &free, "avahi_free" }, - { (dyn::apiproc *) &strdup, "avahi_strdup" }, - { (dyn::apiproc *) &strerror, "avahi_strerror" }, - { (dyn::apiproc *) &simple_poll_get, "avahi_simple_poll_get" }, - { (dyn::apiproc *) &simple_poll_loop, "avahi_simple_poll_loop" }, - { (dyn::apiproc *) &simple_poll_quit, "avahi_simple_poll_quit" }, - { (dyn::apiproc *) &simple_poll_new, "avahi_simple_poll_new" }, - { (dyn::apiproc *) &simple_poll_free, "avahi_simple_poll_free" }, - }; - - if (dyn::load(handle, funcs)) { - return -1; - } - - funcs_loaded = true; - return 0; - } - - int - init_client() { - if (init_common()) { - return -1; - } - - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if (funcs_loaded) return 0; - - if (!handle) { - handle = dyn::handle({ "libavahi-client.3.dylib", "libavahi-client.dylib" }); - if (!handle) { - return -1; +namespace platf::publish { + namespace { + /** @brief Custom deleter intended to be used for `std::unique_ptr`. */ + struct ServiceRefDeleter { + typedef DNSServiceRef pointer; ///< Type of object to be deleted. + void + operator()(pointer serviceRef) { + DNSServiceRefDeallocate(serviceRef); + BOOST_LOG(info) << "Deregistered DNS service."sv; } - } - - std::vector> funcs { - { (dyn::apiproc *) &client_new, "avahi_client_new" }, - { (dyn::apiproc *) &client_free, "avahi_client_free" }, - { (dyn::apiproc *) &entry_group_get_client, "avahi_entry_group_get_client" }, - { (dyn::apiproc *) &entry_group_new, "avahi_entry_group_new" }, - { (dyn::apiproc *) &entry_group_add_service, "avahi_entry_group_add_service" }, - { (dyn::apiproc *) &entry_group_is_empty, "avahi_entry_group_is_empty" }, - { (dyn::apiproc *) &entry_group_reset, "avahi_entry_group_reset" }, - { (dyn::apiproc *) &entry_group_commit, "avahi_entry_group_commit" }, - { (dyn::apiproc *) &client_errno, "avahi_client_errno" }, }; - - if (dyn::load(handle, funcs)) { - return -1; - } - - funcs_loaded = true; - return 0; - } -} // namespace avahi - -namespace platf::publish { - - template - void - free(T *p) { - avahi::free(p); - } - - template - using ptr_t = util::safe_ptr>; - using client_t = util::dyn_safe_ptr; - using poll_t = util::dyn_safe_ptr; - - avahi::EntryGroup *group = nullptr; - - poll_t poll; - client_t client; - - ptr_t name; - - void - create_services(avahi::Client *c); - - void - entry_group_callback(avahi::EntryGroup *g, avahi::EntryGroupState state, void *) { - group = g; - - switch (state) { - case avahi::ENTRY_GROUP_ESTABLISHED: - BOOST_LOG(info) << "Avahi service " << name.get() << " successfully established."; - break; - case avahi::ENTRY_GROUP_COLLISION: - name.reset(avahi::alternative_service_name(name.get())); - - BOOST_LOG(info) << "Avahi service name collision, renaming service to " << name.get(); - - create_services(avahi::entry_group_get_client(g)); - break; - case avahi::ENTRY_GROUP_FAILURE: - BOOST_LOG(error) << "Avahi entry group failure: " << avahi::strerror(avahi::client_errno(avahi::entry_group_get_client(g))); - avahi::simple_poll_quit(poll.get()); - break; - case avahi::ENTRY_GROUP_UNCOMMITED: - case avahi::ENTRY_GROUP_REGISTERING:; - } - } - - void - create_services(avahi::Client *c) { - int ret; - - auto fg = util::fail_guard([]() { - avahi::simple_poll_quit(poll.get()); - }); - - if (!group) { - if (!(group = avahi::entry_group_new(c, entry_group_callback, nullptr))) { - BOOST_LOG(error) << "avahi::entry_group_new() failed: "sv << avahi::strerror(avahi::client_errno(c)); - return; + /** @brief This class encapsulates the polling and deinitialization of our connection with + * the mDNS service. Implements the `::platf::deinit_t` interface. + */ + class deinit_t: public ::platf::deinit_t, std::unique_ptr { + public: + /** @brief Construct deinit_t object. + * + * Create a thread that will use `select(2)` to wait for a response from the mDNS service. + * The thread will give up if an error is received or if `_stopRequested` becomes true. + * + * @param serviceRef An initialized reference to the mDNS service. + */ + deinit_t(DNSServiceRef serviceRef): + unique_ptr(serviceRef) { + _thread = std::thread { [serviceRef, &_stopRequested = std::as_const(_stopRequested)]() { + const auto socket = DNSServiceRefSockFD(serviceRef); + while (!_stopRequested) { + auto fdset = fd_set {}; + FD_ZERO(&fdset); + FD_SET(socket, &fdset); + auto timeout = timeval { .tv_sec = 3, .tv_usec = 0 }; // 3 second timeout + const auto ready = select(socket + 1, &fdset, nullptr, nullptr, &timeout); + if (ready == -1) { + BOOST_LOG(error) << "Failed to obtain response from DNS service."sv; + break; + } + else if (ready != 0) { + DNSServiceProcessResult(serviceRef); + break; + } + } + } }; } - } - - if (avahi::entry_group_is_empty(group)) { - BOOST_LOG(info) << "Adding avahi service "sv << name.get(); - - ret = avahi::entry_group_add_service( - group, - avahi::IF_UNSPEC, avahi::PROTO_UNSPEC, - avahi::PublishFlags(0), - name.get(), - SERVICE_TYPE, - nullptr, nullptr, - net::map_port(nvhttp::PORT_HTTP), - nullptr); - - if (ret < 0) { - if (ret == avahi::ERR_COLLISION) { - // A service name collision with a local service happened. Let's pick a new name - name.reset(avahi::alternative_service_name(name.get())); - BOOST_LOG(info) << "Service name collision, renaming service to "sv << name.get(); - - avahi::entry_group_reset(group); - - create_services(c); - - fg.disable(); - return; - } - - BOOST_LOG(error) << "Failed to add "sv << SERVICE_TYPE << " service: "sv << avahi::strerror(ret); - return; + /** @brief Ensure that we gracefully finish polling the mDNS service before freeing our + * connection to it. + */ + ~deinit_t() override { + _stopRequested = true; + _thread.join(); } + deinit_t(const deinit_t &) = delete; + deinit_t & + operator=(const deinit_t &) = delete; + private: + std::thread _thread; ///< Thread for polling the mDNS service for a response. + std::atomic _stopRequested = false; //< Whether to stop polling the mDNS service. + }; - ret = avahi::entry_group_commit(group); - if (ret < 0) { - BOOST_LOG(error) << "Failed to commit entry group: "sv << avahi::strerror(ret); + /** @brief Callback that will be invoked when the mDNS service finishes registering our service. + * @param errorCode Describes whether the registration was successful. + */ + void + registrationCallback(DNSServiceRef /*serviceRef*/, DNSServiceFlags /*flags*/, + DNSServiceErrorType errorCode, const char * /*name*/, + const char * /*regtype*/, const char * /*domain*/, void * /*context*/) { + if (errorCode != kDNSServiceErr_NoError) { + BOOST_LOG(error) << "Failed to register DNS service: Error "sv << errorCode; return; } + BOOST_LOG(info) << "Successfully registered DNS service."sv; } + } // anonymous namespace - fg.disable(); - } - - void - client_callback(avahi::Client *c, avahi::ClientState state, void *) { - switch (state) { - case avahi::CLIENT_S_RUNNING: - create_services(c); - break; - case avahi::CLIENT_FAILURE: - BOOST_LOG(error) << "Client failure: "sv << avahi::strerror(avahi::client_errno(c)); - avahi::simple_poll_quit(poll.get()); - break; - case avahi::CLIENT_S_COLLISION: - case avahi::CLIENT_S_REGISTERING: - if (group) - avahi::entry_group_reset(group); - break; - case avahi::CLIENT_CONNECTING:; - } - } - - class deinit_t: public ::platf::deinit_t { - public: - std::thread poll_thread; - - explicit deinit_t(std::thread poll_thread): - poll_thread { std::move(poll_thread) } {} - - ~deinit_t() override { - if (avahi::simple_poll_quit && poll) { - avahi::simple_poll_quit(poll.get()); - } - - if (poll_thread.joinable()) { - poll_thread.join(); - } - } - }; - + /** + * @brief Main entry point for publication of our service on macOS. + * + * This function initiates a connection to the macOS mDNS service and requests to register + * our Sunshine service. Registration will occur asynchronously (unless it fails immediately, + * which is probably only possible if the host machine is misconfigured). + * + * @return Either `nullptr` (if the registration fails immediately) or a `uniqur_ptr`, + * which will manage polling for a response from the mDNS service, and then, when + * deconstructed, will deregister the service. + */ [[nodiscard]] std::unique_ptr<::platf::deinit_t> start() { - if (avahi::init_client()) { - return nullptr; - } - - int avhi_error; - - poll.reset(avahi::simple_poll_new()); - if (!poll) { - BOOST_LOG(error) << "Failed to create simple poll object."sv; + auto serviceRef = DNSServiceRef {}; + const auto status = DNSServiceRegister( + &serviceRef, + 0, // flags + 0, // interfaceIndex + SERVICE_NAME, SERVICE_TYPE, + nullptr, // domain + nullptr, // host + htons(net::map_port(nvhttp::PORT_HTTP)), + 0, // txtLen + nullptr, // txtRecord + registrationCallback, + nullptr // context + ); + if (status != kDNSServiceErr_NoError) { + BOOST_LOG(error) << "Failed immediately to register DNS service: Error "sv << status; return nullptr; } - - name.reset(avahi::strdup(SERVICE_NAME)); - - client.reset( - avahi::client_new(avahi::simple_poll_get(poll.get()), avahi::ClientFlags(0), client_callback, nullptr, &avhi_error)); - - if (!client) { - BOOST_LOG(error) << "Failed to create client: "sv << avahi::strerror(avhi_error); - return nullptr; - } - - return std::make_unique(std::thread { avahi::simple_poll_loop, poll.get() }); + return std::make_unique(serviceRef); } } // namespace platf::publish