defmodule XMAVLink.Util.FocusManager do @moduledoc """ Manage a protected ETS table representing the nominated MAV focus of zero or more local PIDs. The API uses this to streamline iex sessions by letting the user select a MAV to work with and transparently adding {system_id, component_id} tuples to call arguments. Pass `context: context` to read or write focus in a scoped utility table namespace. """ use GenServer require Logger alias XMAVLink.Util.{Context, Tables} defstruct monitors: %{}, sessions: %{}, context: nil, table_prefix: nil, tables: Tables.names() # API def start_link(state, opts \\ []) do GenServer.start_link(__MODULE__, state, Keyword.put_new(opts, :name, __MODULE__)) end def focus() do self() |> focus() end def focus(opts) when is_list(opts), do: focus(self(), opts) def focus(pid) when is_pid(pid), do: focus(pid, []) def focus(system_id) when is_integer(system_id), do: focus(system_id, 1) def focus(pid, opts) when is_pid(pid) do sessions = Context.new(opts).tables.sessions with :ok <- require_table(sessions), [{^pid, scid}] <- :ets.lookup(sessions, pid) do if pid == self() do Logger.info("Vehicle #{format(scid)}") else Logger.info("Vehicle #{format(scid)} for #{inspect(pid)}") end {:ok, scid} else {:error, reason} -> {:error, reason} _ -> Logger.warning("#{inspect(pid)} has no vehicle focus") {:error, :not_focussed} end end def focus(system_id, component_id) when is_integer(system_id) do focus(system_id, component_id, []) end def focus(system_id, component_id, opts) when is_integer(system_id) and is_integer(component_id) and is_list(opts) do with pid when is_pid(pid) <- GenServer.whereis(__MODULE__), {:ok, {system_id, component_id, _}} <- GenServer.call(pid, {:focus, {system_id, component_id}, Context.new(opts)}) do configure_iex_prompt(system_id, component_id) else nil -> {:error, :not_started} {:error, reason} -> {:error, reason} end end @impl true def init(opts) do opts = Map.new(opts) context = Context.new(opts) :ets.new(context.tables.sessions, [:named_table, :protected, {:read_concurrency, true}, :set]) {:ok, struct( __MODULE__, opts |> Map.put(:context, context) |> Map.put(:table_prefix, context.table_prefix) |> Map.put(:tables, context.tables) )} end @impl true def handle_call( {:focus, scid = {system_id, component_id}, context}, {caller_pid, _}, state ) do tables = context.tables with :ok <- require_table(tables.systems), :ok <- require_writable_table(tables.sessions), mavlink_major_version when is_number(mavlink_major_version) <- :ets.foldl( fn {next_scid, %{mavlink_major_version: mmv}}, acc -> if next_scid == scid do mmv else acc end end, 0, tables.systems ) do if mavlink_major_version > 0 do state = put_focus_session(state, tables, caller_pid, { system_id, component_id, mavlink_major_version }) Logger.info("Set focus to #{format(scid)}") {:reply, {:ok, {system_id, component_id, mavlink_major_version}}, state} else Logger.warning("No such vehicle #{format(scid)}") {:reply, {:error, :no_such_mav}, state} end else {:error, reason} -> {:reply, {:error, reason}, state} _ -> {:reply, {:error, :no_mav_data}, state} end end @impl true def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do case Map.pop(state.monitors, ref) do {nil, _monitors} -> {:noreply, state} {session_key, monitors} -> {session, sessions} = Map.pop(state.sessions, session_key) delete_session(session, session_pid(session_key)) {:noreply, %{ state | monitors: monitors, sessions: sessions }} end end defp configure_iex_prompt(system_id, component_id) do if Code.ensure_loaded?(IEx) and function_exported?(IEx, :configure, 1) and Process.whereis(IEx.Config) do apply(IEx, :configure, [ [default_prompt: "iex(%counter) vehicle #{system_id}.#{component_id}>"] ]) end :ok end defp put_focus_session(state, tables, pid, scid) do session_key = {pid, tables.sessions} state = drop_focus_monitor(state, session_key) ref = Process.monitor(pid) :ets.insert(tables.sessions, {pid, scid}) %{ state | monitors: Map.put(state.monitors, ref, session_key), sessions: Map.put(state.sessions, session_key, {ref, tables.sessions}) } end defp drop_focus_monitor(state, session_key = {pid, _sessions_table}) do case Map.pop(state.sessions, session_key) do {nil, _sessions} -> state {{ref, sessions_table}, sessions} -> Process.demonitor(ref, [:flush]) delete_session_table_entry(sessions_table, pid) %{ state | monitors: Map.delete(state.monitors, ref), sessions: sessions } end end defp delete_session(nil, _pid), do: :ok defp delete_session({_ref, sessions_table}, pid), do: delete_session_table_entry(sessions_table, pid) defp delete_session_table_entry(sessions_table, pid) do if :ets.info(sessions_table) != :undefined do :ets.delete(sessions_table, pid) end end defp session_pid({pid, _sessions_table}), do: pid defp format({s, c, _}), do: format({s, c}) defp format({s, c}), do: "#{s}.#{c}" defp require_table(table) do case :ets.info(table) do :undefined -> {:error, :not_started} _ -> :ok end end defp require_writable_table(table) do case :ets.info(table) do :undefined -> {:error, :not_started} _ -> if :ets.info(table, :owner) == self() or :ets.info(table, :protection) == :public do :ok else {:error, :not_started} end end end end