lib/mdns_lite/core_monitor.ex

defmodule MdnsLite.CoreMonitor do
  @moduledoc """
  Core logic for network monitors

  This module contains most of the logic needed for writing a network monitor.
  It's only intended to be called from `MdnsLite.InetMonitor` and
  `MdnsLite.VintageNetMonitor`.
  """

  @typedoc """
  Monitor options

  * `:excluded_ifnames` - a list of network interface names to ignore
  * `:ipv4_only` - set to `true` to ignore all IPv6 addresses
  """
  @type option() ::
          {:excluded_ifnames, [String.t()]}
          | {:ipv4_only, boolean()}

  @typedoc false
  @type state() :: %{
          excluded_ifnames: [String.t()],
          ip_list: %{String.t() => [:inet.ip_address()]},
          filter: ([:inet.ip_address()] -> [:inet.ip_address()]),
          todo: [mfa()]
        }

  @spec init([option()]) :: state()
  def init(opts) do
    excluded_ifnames = Keyword.get(opts, :excluded_ifnames, [])
    filter = if Keyword.get(opts, :ipv4_only, true), do: &filter_by_ipv4/1, else: &no_filter/1

    %{
      excluded_ifnames: excluded_ifnames,
      ip_list: %{},
      filter: filter,
      todo: []
    }
  end

  @spec set_ip_list(state(), String.t(), [:inet.ip_address()]) :: state()
  def set_ip_list(%{} = state, ifname, ip_list) do
    if ifname in state.excluded_ifnames do
      # Ignore excluded interface
      state
    else
      current_list = Map.get(state.ip_list, ifname, [])
      new_list = state.filter.(ip_list)

      {to_remove, to_add} = compute_delta(current_list, new_list)

      new_todo =
        state.todo ++
          Enum.map(to_remove, &{MdnsLite.ResponderSupervisor, :stop_child, [ifname, &1]}) ++
          Enum.map(to_add, &{MdnsLite.ResponderSupervisor, :start_child, [ifname, &1]})

      %{state | todo: new_todo, ip_list: Map.put(state.ip_list, ifname, new_list)}
    end
  end

  @spec flush_todo_list(state()) :: state()
  def flush_todo_list(state) do
    Enum.each(state.todo, fn {m, f, a} -> apply(m, f, a) end)

    %{state | todo: []}
  end

  defp compute_delta(old_list, new_list) do
    {old_list -- new_list, new_list -- old_list}
  end

  defp no_filter(ip_list) do
    ip_list
  end

  defp filter_by_ipv4(ip_list) do
    Enum.filter(ip_list, &(MdnsLite.Utilities.ip_family(&1) == :inet))
  end
end