lib/vintage_net/interface/ifup_daemon.ex

defmodule VintageNet.Interface.IfupDaemon do
  use GenServer
  require Logger

  @moduledoc """
  Wrap MuonTrap.Daemon to start and stop a program based on whether the network is up

  Unlike MuonTrap.Daemon, the arguments are called out in the child_spec so it looks like
  this:

  ```
  {VintageNet.Interface.IfupDaemon, ifname: ifname, command: program, args: arguments, opts: options]}
  ```
  """

  @typedoc false
  @type init_args :: [
          ifname: VintageNet.ifname(),
          command: binary(),
          args: [binary()],
          opts: keyword()
        ]

  @enforce_keys [:ifname, :command, :args]
  defstruct [:ifname, :command, :args, :opts, :pid]

  @doc """
  Start the IfupDaemon
  """
  @spec start_link(init_args()) :: GenServer.on_start()
  def start_link(init_args) do
    GenServer.start_link(__MODULE__, init_args)
  end

  @doc """
  Return whether the daemon is running
  """
  @spec running?(GenServer.server()) :: boolean()
  def running?(server) do
    GenServer.call(server, :running?)
  end

  @impl GenServer
  def init(init_args) do
    state = struct!(__MODULE__, init_args)
    {:ok, state, {:continue, :continue}}
  end

  @impl GenServer
  def handle_continue(:continue, %{ifname: ifname} = state) do
    VintageNet.subscribe(lower_up_property(ifname))

    new_state =
      case VintageNet.get(lower_up_property(ifname)) do
        true ->
          start_daemon(state)

        _not_true ->
          # If the physical layer isn't up, don't start until
          # we're notified that it is available.
          state
      end

    {:noreply, new_state}
  end

  @impl GenServer
  def handle_call(:running?, _from, state) do
    {:reply, state.pid != nil and Process.alive?(state.pid), state}
  end

  @impl GenServer
  def handle_info(
        {VintageNet, ["interface", ifname, "lower_up"], _old_value, true, _meta},
        %{ifname: ifname} = state
      ) do
    # Physical layer is up. Optimistically assume that the LAN is accessible.
    {:noreply, start_daemon(state)}
  end

  def handle_info(
        {VintageNet, ["interface", ifname, "lower_up"], _old_value, _false_or_nil, _meta},
        %{ifname: ifname} = state
      ) do
    # Physical layer is down or disconnected. We're definitely disconnected.
    {:noreply, stop_daemon(state)}
  end

  defp start_daemon(%{pid: nil} = state) do
    Logger.debug("[vintage_net(#{state.ifname})] starting #{state.command}")

    {:ok, pid} = MuonTrap.Daemon.start_link(state.command, state.args, state.opts)
    %{state | pid: pid}
  end

  defp start_daemon(state), do: state

  defp stop_daemon(%{pid: pid} = state) when is_pid(pid) do
    Logger.debug("[vintage_net(#{state.ifname})] stopping #{state.command}")

    if Process.alive?(pid), do: GenServer.stop(pid)

    %{state | pid: nil}
  end

  defp stop_daemon(state), do: state

  defp lower_up_property(ifname) do
    ["interface", ifname, "lower_up"]
  end
end