defmodule RNS.Config do @moduledoc """ Config parsing and genserver. """ @type t() :: tuple() @type transport() :: boolean() @type interfaces() :: [map()] require Record Record.defrecord(:config, transport: false, interfaces: [], data_directory: "~/.local/share/reticulum_ex/" ) require Logger @doc "Get the transport value of the config record." @spec transport(t()) :: transport() def transport(config), do: config(config, :transport) @doc "Get the interfaces(to start) of the config record." @spec interfaces(t()) :: interfaces() def interfaces(config), do: config(config, :interfaces) @doc "Get the data directory of the config record." @spec data_directory(t()) :: Path.t() def data_directory(config), do: config(config, :data_directory) @spec new( transport: boolean(), interfaces: [map()], data_directory: Path.t() ) :: t() def new(opts \\ []) do config( transport: Keyword.get(opts, :transport, false), interfaces: Keyword.get(opts, :interfaces, []), data_directory: Keyword.get(opts, :data_directory, "~/.local/share/reticulum_ex/") ) end @doc """ Read the config file and return a config record. Will also create the config file and directories if they don't exist. """ @spec parse() :: {:ok, tuple()} | {:error, atom()} def parse() do config_path = if File.exists?("reticulum_ex.toml") do "reticulum_ex.toml" else "~/.config/reticulum_ex.toml" end |> Path.expand() if not File.exists?(config_path) do config_directory = config_path |> Path.dirname() case File.mkdir_p(config_directory) do :ok -> File.write(config_path, example_config()) {:error, :eaccess} -> Logger.emergency( "Failed to write configuration: Read/write access needed for '#{config_directory}'!" ) exit(:config_rw_required) {:error, :enospc} -> Logger.emergency("Failed to create configuration file: No space left on drive!") exit(:config_no_space) {:error, :enotdir} -> Logger.emergency( "Failed to create configuration file: One of the directories in the configuration path is not a directory!" ) exit(:config_not_dir) end end case Toml.decode_file(config_path) do {:ok, config} -> parse(config) {:error, {:invalid_toml, _}} -> Logger.emergency("Invalid configuration file TOML!") {:error, :invalid_toml} end end @spec parse(map()) :: tuple() def parse(config_toml) when is_map(config_toml) do interfaces = Enum.reduce(config_toml["interfaces"], [], fn {name, interface_toml}, interfaces -> case interface_toml["enabled"] do true -> case map_to_interface(interface_toml, name) do {:ok, interface} -> [interface | interfaces] {:error, error} -> Logger.warning( "Failed to parse TOML in config on interface #{name}: #{error}. The interface won't be added." ) interfaces end false -> interfaces nil -> Logger.warning( "Interface '#{name}' has not been enabled as the enabled field is not present in the config." ) interfaces end end) transport = case config_toml["transport"] do transport when is_boolean(transport) -> transport nil -> Logger.debug( "The config file does not contain 'transport'. We will instead use the default value, which is false(disabled)." ) false _ -> Logger.warning( "An invalid value has been given to 'transport' in the config, it can only be true(enabled) or false(disabled). We will instead use the default value, which is false(disabled)." ) false end data_directory = case config_toml["data_directory"] do data_directory when is_binary(data_directory) -> data_directory nil -> Logger.debug( "The config file does not contain 'data_directory'. We will instead use the default value, which is '~/.local/share/reticulum_ex/'." ) "~/.local/share/reticulum_ex/" _ -> Logger.warning( "An invalid value has been given to 'data_directory' in the config, it should be a valid path. We will instead use the default value, which is '~/.local/share/reticulum_ex/'." ) "~/.local/share/reticulum_ex/" end |> Path.expand() case File.mkdir_p(data_directory) do :ok -> :ok {:error, :eaccess} -> Logger.emergency( "Failed to create data directory: Read/write access needed for '#{data_directory}'!" ) exit(:data_dir_rw_required) {:error, :enospc} -> Logger.emergency("Failed to create configuration file: No space left on drive!") exit(:data_dir_no_space) {:error, :enotdir} -> Logger.emergency( "Failed to create configuration file: One of the directories in the configuration path is not a directory!" ) exit(:data_dir_not_dir) end config = config( interfaces: interfaces, transport: transport, data_directory: data_directory ) {:ok, config} end # Converts a map of an interface object from parse/1 to the tuple used by the interface manager. If all goes well, of course! @spec map_to_interface(map(), String.t()) :: {:ok, {atom(), map()}} | {:error, :unknown_interface} defp map_to_interface(interface_toml, name) do type = interface_toml["type"] |> parse_interface_type() case type do :unknown -> Logger.emergency("Unknown interface type on interface #{name}!") {:error, :unknown_interface} type -> params = interface_params(interface_toml) {:ok, {type, params}} end end @spec interface_params(map()) :: map() defp interface_params(interface_toml) do Enum.reduce(interface_toml, %{}, fn {key, value}, params -> case key do "enabled" -> params "type" -> params _ -> Map.put(params, key, value) end end) end @spec parse_interface_type(String.t()) :: atom() defp parse_interface_type(type) when is_binary(type) do case type do "TCPClient" -> RNS.Interface.TCPClient "TCPServer" -> RNS.Interface.TCPServer _ -> :unknown end end @spec example_config() :: String.t() def example_config() do """ [interfaces] [interfaces.nodns1] type = "TCPClient" host = "202.61.243.41" port = 4965 enabled = false [interfaces.server] type = "TCPServer" port = 4343 enabled = true mode = "access_point" """ end use GenServer # GenServer client. @spec start_link(t()) :: {:ok, pid()} def start_link(state) do GenServer.start_link(__MODULE__, state, name: __MODULE__) end @doc """ Subscribe to changes of transport's value. The value is sent by the subscriber so that it can be compared with the actual one. """ @spec transport_subscribe(transport()) :: :ok def transport_subscribe(my_transport) do GenServer.cast(__MODULE__, {:transport_subscribe, self(), my_transport}) end @spec fetch_config() :: t() def fetch_config() do GenServer.call(__MODULE__, :fetch_config) end # GenServer callbacks. @impl true def init(config) do {:ok, {config, []}} end @impl true def handle_cast({:transport_subscribe, from, its_transport}, {config, transport_subscribers}) do transport = config(config, :transport) # If the subscriber's transport value is different at the time it sent the subscribe message, send the correct value to it. if its_transport != transport do send(from, {:transport_change, transport}) end new_state = {config, [from | transport_subscribers]} {:noreply, new_state} end @impl true def handle_call(:fetch_config, _from, {config, _} = state) do {:reply, config, state} end end