Madness.Responder.MDNSResponder (Madness v0.5.0)
View SourcePublishes through Apple's mDNSResponder, by way of its Unix-socket IPC.
This is the backend for macOS, where the daemon is always running and cannot
practically be turned off, and for anything else running Apple's daemon —
notably a Nerves target that bundles the POSIX mdnsd. Automatic selection
probes it after Avahi; name it explicitly to use only this backend:
config :madness, backend: :mdns_responderOne socket per registration
Each publication opens its own connection to the daemon and keeps it for exactly as long as the registration should live. Nothing is pooled and nothing is shared, including with the presence connection that Madness's connection process holds.
The daemon withdraws a client's records when its socket closes, so a service is revoked even if its publication process is killed without cleanup.
Interface scoping, and the absence of family scoping
Madness.Responder.capabilities/0 declares :interface_scope and nothing
else. The interface_index field of a registration restricts probing,
announcing and answering to one interface, and madness fills it with the OS
index resolved immediately before the call.
Apple's IPC has no per-address-family control, so Madness.publish/2 refuses
a family scope on this backend. family: :any is the portable choice; Avahi
can narrow it.
Service subtypes
Madness.Service represents a subtype by its DNS name, such as
"_printer._sub._http._tcp". Apple's registration API spells the same
request as "_http._tcp,_printer"; this backend performs that conversion
when it builds the IPC request. Plain service types are sent unchanged.
Conflicts and the accepted name
Registrations are made with auto-rename left on. When the requested name is
already taken, the daemon appends a discriminator — "My Printer (2)" — and
reports the name it settled on in an asynchronous reply, which is what
Madness.Responder.publish/3 returns as the accepted name.
Publishing waits a bounded time for the reply and otherwise reports the requested name; the daemon may merge an identical registration without a callback.
A rename can also happen long after publishing, when a conflicting host
appears on the network. Each registration socket is drained for the life of
the publication, so a later rename is delivered to the publication as a
{:renamed, name} event and reaches the caller of Madness.publish/2 when
it asked for notify: true.
When the daemon restarts
A registration lives in the daemon, so a daemon that restarts loses it. That
arrives here as the registration socket closing, and is reported to the
publication as {:error, {:connection_lost, reason}} — again reaching the
caller when it asked for notify: true. Madness then terminates the
publication with reason {:shutdown, {:connection_lost, reason}}; it does
not guess whether that publication is still wanted or should move to a
replacement network. Monitor the pid in the returned Madness.Publication
and publish explicitly again if the service should return. The node's
separate presence connection reconnects on its own and makes publishing
available again when the daemon is reachable.
Configuration
DNSSD_UDS_PATH- an environment variable naming the daemon socket, honoured by Apple's own client library and useful for pointing madness at a daemon somewhere other than the standard location. When it is set it replaces the default socket search.Otherwise
/var/run/mDNSResponderis tried first, then/var/run/mdnsd, which is where the stock POSIX build listens.config :madness, mdnsresponder_reply_timeout: 2_500- how longMadness.Responder.publish/3waits for the reply carrying the accepted name before falling back to the requested one.config :madness, mdnsresponder_publish_budget: 4_000- the maximum time for the complete backend registration. The caller'stimeout:may shorten this budget but never extends it.
Summary
Types
The presence connection held by Madness's connection process
One registration, and the socket whose closure revokes it
Functions
The scopes this backend can enforce: :interface_scope only.
Opens the presence connection.
Closes the presence connection.
Interprets a message delivered for the presence connection.
Registers service, on its own connection, owned by the calling process.
Withdraws a registration by closing its connection.
Types
Functions
@spec capabilities() :: [Madness.Responder.capability()]
The scopes this backend can enforce: :interface_scope only.
See "Interface scoping, and the absence of family scoping" above for why
:family_scope is not among them.
@spec connect() :: {:ok, connection()} | {:error, term()}
Opens the presence connection.
Connecting alone would only prove that something is bound to the socket path,
so this also asks the daemon for its DaemonVersion and requires an answer.
The value itself is not interpreted — it is a build identifier whose encoding
is Apple's business, and madness has no version policy to apply to it. What
matters is that a request went in and a well-formed reply came back, which a
stale socket file or a wedged daemon cannot produce.
The connection is then left open and switched to active mode, so that the daemon going away arrives as a message rather than having to be polled for.
@spec disconnect(connection()) :: :ok
Closes the presence connection.
@spec handle_info(term(), connection()) :: {:ok, connection()} | :disconnected
Interprets a message delivered for the presence connection.
The connection carries no traffic once it is established — nothing is subscribed to it — so what this is really watching for is its closure, which is how the daemon exiting becomes visible. Anything else is ignored rather than treated as a fault: a stray message is not evidence the daemon has gone.
@spec publish(Madness.Service.t(), Madness.Responder.scope(), [ Madness.Responder.publish_opt() ]) :: {:ok, handle(), String.t()} | {:error, term()}
Registers service, on its own connection, owned by the calling process.
Every wait inside the call is bounded by the earlier of two moments: the end
of this backend's own publish budget, and the :deadline in opts if one is
given. So a caller who will stop waiting sooner is answered sooner, and one
who would wait forever is still bounded by the budget. See "Configuration".
@spec unpublish(handle()) :: :ok
Withdraws a registration by closing its connection.
Closing is the whole of it: the daemon drops the records and multicasts the goodbyes. That makes this both prompt — a close does not wait on the daemon — and idempotent, since closing an already-closed socket is a no-op.