Current section
Files
Jump to
Current section
Files
lib/snapcast/advertiser.ex
defmodule Snapcast.Advertiser do
@moduledoc """
Advertises the embedded Snap server through mDNS/DNS-SD.
Snap clients can discover Snapcast servers by browsing `_snapcast._tcp`. On
macOS this uses the system `dns-sd` command. If the publisher exits, it is
started again after a short delay.
"""
use GenServer
require Logger
@restart_ms 2_000
@service_type "_snapcast._tcp"
@domain "local"
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(_opts) do
Process.flag(:trap_exit, true)
{:ok, %{port: nil}, {:continue, :advertise}}
end
@impl true
def handle_continue(:advertise, state), do: {:noreply, advertise(state)}
@impl true
def handle_info(:restart_advertise, state), do: {:noreply, advertise(state)}
def handle_info({port, {:exit_status, status}}, %{port: port} = state) do
Logger.warning("Snap mDNS advertiser exited with status #{inspect(status)}; restarting")
schedule_restart()
{:noreply, %{state | port: nil}}
end
def handle_info({port, {:data, data}}, %{port: port} = state) do
log_advertiser_output(data)
{:noreply, state}
end
def handle_info(_msg, state), do: {:noreply, state}
@impl true
def terminate(_reason, %{port: port}) when is_port(port) do
Port.close(port)
:ok
rescue
_error -> :ok
end
def terminate(_reason, _state), do: :ok
defp advertise(%{port: port} = state) when is_port(port), do: state
defp advertise(state) do
case Snapcast.dns_sd_path() do
path when is_binary(path) ->
args = [
"-R",
Snapcast.advertise_name(),
@service_type,
@domain,
Integer.to_string(Snapcast.port())
]
Logger.info("advertising Snap server over mDNS: #{path} #{Enum.join(args, " ")}")
port =
Port.open({:spawn_executable, path}, [
:binary,
:exit_status,
:stderr_to_stdout,
args: args
])
%{state | port: port}
_missing ->
Logger.warning("Snap mDNS advertising is enabled but dns-sd was not found")
schedule_restart()
state
end
end
defp schedule_restart, do: Process.send_after(self(), :restart_advertise, @restart_ms)
defp log_advertiser_output(data) do
data
|> to_string()
|> String.split(["\r", "\n"], trim: true)
|> Enum.each(&Logger.debug("snap advertiser: #{&1}"))
end
end