Current section
Files
Jump to
Current section
Files
lib/rns/interface_manager.ex
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