defmodule RNS.InterfaceManager do @moduledoc """ Start and supervises interfaces. """ # TODO: More interfaces: socket(shared instances!), serial, pipe, autointerface, rnode and all the other radio stuff(I don't have any of that :( ), and maybe some cool stuff like direct ethernet and covert interfaces. use GenServer require RNS.Interface require Logger @spec start_link({[{atom(), map()}], RNS.Config.transport()}) :: {:ok, pid()} def start_link({interfaces_to_start, transport}) do GenServer.start_link(__MODULE__, {interfaces_to_start, transport}, name: __MODULE__) end # Client side @doc "Called by an interface when it is started, this will link the interface and interface manager." @spec interface_started(RNS.Interface.record()) :: :ok def interface_started(interface) do Logger.debug( "Interface #{RNS.Interface.id(interface)} started with pid #{inspect(RNS.Interface.pid(interface))}" ) GenServer.cast(RNS.InterfaceManager, {:interface_started, self(), interface}) end @spec all(virtuals: boolean()) :: [RNS.Interface.record()] def all(opts \\ []) do case Keyword.get(opts, :virtuals, true) do true -> :ets.tab2list(:interfaces) false -> # TLDR: Only returns interfaces where :virtual is set to false match = [ { :"$1", [{:==, {:element, RNS.Interface.interface(:virtual) + 1, :"$1"}, false}], [:"$_"] } ] :ets.select(:interfaces, match) end end @spec find(RNS.Interface.id()) :: {:ok, RNS.Interface.record()} | {:error, :not_found} def find(interface_id) do case :ets.lookup(:interfaces, interface_id) do [interface] -> {:ok, interface} [] -> {:error, :not_found} end end # Callbacks @impl true def init({interfaces_to_start, transport}) do # Keypos expects a 1 based index, but the record macro uses a zero based index. :ets.new(:interfaces, [ :named_table, {:keypos, RNS.Interface.id_position() + 1} ]) # Instead of crashing when a linked process crashes, the interface manager will receive a message. Process.flag(:trap_exit, true) Enum.each(interfaces_to_start, fn {interface, params} -> Logger.debug( "Starting interface #{inspect(interface)} with parameters #{inspect(params)}..." ) Process.spawn(interface, :start, [params, transport], []) end) # Will be able to receive messages when transport is enabled/disabled. RNS.Config.transport_subscribe(transport) {:ok, transport} end @impl true def handle_cast({:interface_started, pid, interface}, state) do Process.link(pid) :ets.insert(:interfaces, interface) {:noreply, state} end @doc "If an interface exits, an :EXIT message will be send to the manager, which will(for now) just remove the interface from it's list" @impl true def handle_info({:EXIT, pid, reason}, state) do # Worse than SQL! # The first :"$1" just gets the entire entry, so we can use it later, then the nex element is the guards. # We just verify if the element at the position where the pid should be(the +1 is because ets expects a 1 based index, not 0 based) is equals to the pid we want. # Then we return the element at the position where the id should be. Easy peasy. match = [ { :"$1", [{:==, {:element, RNS.Interface.pid_position() + 1, :"$1"}, pid}], [{:element, RNS.Interface.id_position() + 1, :"$1"}] } ] case :ets.select(:interfaces, match) do [id] -> Logger.warning( "Interface with pid #{inspect(pid)} and id #{inspect(id)} has exited with reason #{inspect(reason)}!" ) :ets.delete(:interfaces, id) _ -> Logger.error( "Interface with pid #{inspect(pid)} has exited with reason #{inspect(reason)} but it was not present in the interface table, and could not be removed!" ) end {:noreply, state} end @impl true def handle_info({:transport_change, transport}, _state) do for interface <- all() do RNS.Interface.pid(interface) |> send({:transport_change, transport}) end {:noreply, transport} end end