Current section
Files
Jump to
Current section
Files
lib/mob_dev/remote.ex
defmodule DalaDev.Remote do
@moduledoc """
Easy remote debugging and tracing interface for dala Elixir cluster nodes.
Provides a simple interface to debug and trace remote nodes without needing
to manually handle RPC calls or node selection.
## Usage
### Select a node
iex> DalaDev.Remote.select_node(:"dala_demo@127.0.0.1")
:ok
### List available nodes
iex> DalaDev.Remote.nodes()
[node1@host, node2@host]
### Call remote functions
iex> DalaDev.Remote.Observer.observe()
{:ok, %{system: %{...}, processes: [...], ...}}
iex> DalaDev.Remote.Debugger.memory_report()
{:ok, %{total: "1.2 GB", processes: "45 MB", ...}}
iex> DalaDev.Remote.Debugger.inspect_process(MyApp.Worker)
{:ok, %{pid: "#PID<0.123.0>", state: "...", ...}}
iex> DalaDev.Remote.Debugger.eval("1 + 1")
{:ok, 2}
iex> DalaDev.Remote.Tracer.trace_messages(MyApp.Worker, duration: 5000)
{:ok, [%{type: :send, message: "..."}, ...]}
### Set custom timeout
iex> DalaDev.Remote.set_timeout(10_000)
:ok
### Get current selection
iex> DalaDev.Remote.selected_node()
{:ok, node()}
## Automatic Node Selection
If only one remote node is available, it will be automatically selected.
If multiple nodes are available, you must explicitly select one.
## Default Timeout
The default timeout for all remote operations is 5000ms (5 seconds).
This can be changed using `set_timeout/1`.
"""
use GenServer
@default_timeout 5000
# Client API
@doc """
Starts the Remote helper.
This is automatically started by the application supervisor.
"""
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc """
Lists all connected remote nodes (excluding the current node).
"""
@spec nodes() :: [node()]
def nodes do
GenServer.call(__MODULE__, :nodes)
end
@doc """
Selects a node for subsequent remote operations.
Returns `:ok` on success, `{:error, reason}` on failure.
"""
@spec select_node(node() | String.t()) :: :ok | {:error, term()}
def select_node(node) when is_binary(node) do
select_node(String.to_atom(node))
end
def select_node(node) when is_atom(node) do
GenServer.call(__MODULE__, {:select_node, node})
end
@doc """
Gets the currently selected node.
Returns `{:ok, node}` if a node is selected, `{:error, :no_node_selected}` otherwise.
"""
@spec selected_node() :: {:ok, node()} | {:error, :no_node_selected}
def selected_node do
GenServer.call(__MODULE__, :selected_node)
end
@doc """
Sets the default timeout for remote operations.
Returns `:ok`.
"""
@spec set_timeout(non_neg_integer()) :: :ok
def set_timeout(timeout) when is_integer(timeout) and timeout > 0 do
GenServer.call(__MODULE__, {:set_timeout, timeout})
end
@doc """
Gets the current timeout setting.
Returns the timeout in milliseconds.
"""
@spec get_timeout() :: non_neg_integer()
def get_timeout do
GenServer.call(__MODULE__, :get_timeout)
end
@doc """
Clears the currently selected node.
Returns `:ok`.
"""
@spec clear_selection() :: :ok
def clear_selection do
GenServer.call(__MODULE__, :clear_selection)
end
@doc """
Automatically selects a node if only one is available.
Returns `{:ok, node}` if auto-selected, `{:error, reason}` otherwise.
"""
@spec auto_select() :: {:ok, node()} | {:error, term()}
def auto_select do
GenServer.call(__MODULE__, :auto_select)
end
# Observer submodule
defmodule Observer do
@moduledoc """
Remote observer functions for inspecting node state.
These functions automatically use the node selected via
`DalaDev.Remote.select_node/1`.
"""
@doc """
Observes a remote node and returns comprehensive system information.
Returns `{:ok, data}` on success, `{:error, reason}` on failure.
"""
@spec observe(keyword()) :: {:ok, map()} | {:error, term()}
def observe(opts \\ []) do
with {:ok, node} <- get_target_node(),
timeout <- Keyword.get(opts, :timeout, DalaDev.Remote.get_timeout()) do
DalaDev.Observer.observe(node, timeout: timeout)
end
end
@doc """
Gets system information from the selected node.
"""
@spec system_info(keyword()) :: map()
def system_info(opts \\ []) do
with {:ok, node} <- get_target_node(),
timeout <- Keyword.get(opts, :timeout, DalaDev.Remote.get_timeout()) do
DalaDev.Observer.system_info(node, timeout)
end
end
@doc """
Gets the process list from the selected node.
"""
@spec process_list(keyword()) :: [map()]
def process_list(opts \\ []) do
with {:ok, node} <- get_target_node(),
timeout <- Keyword.get(opts, :timeout, DalaDev.Remote.get_timeout()) do
DalaDev.Observer.process_list(node, timeout)
end
end
@doc """
Gets ETS tables from the selected node.
"""
@spec ets_tables(keyword()) :: [map()]
def ets_tables(opts \\ []) do
with {:ok, node} <- get_target_node(),
timeout <- Keyword.get(opts, :timeout, DalaDev.Remote.get_timeout()) do
DalaDev.Observer.ets_tables(node, timeout)
end
end
defp get_target_node do
case DalaDev.Remote.selected_node() do
{:ok, node} -> {:ok, node}
{:error, _} = err -> err
end
end
end
# Debugger submodule
defmodule Debugger do
@moduledoc """
Remote debugging functions for inspecting and controlling remote nodes.
These functions automatically use the node selected via
`DalaDev.Remote.select_node/1`.
"""
@doc """
Inspects a process on the selected node.
The process can be specified as:
- A PID (e.g., `#PID<0.123.0>`)
- A registered name (atom)
- A module name (atom)
- A `{mod, fun}` tuple
Returns `{:ok, info}` on success, `{:error, reason}` on failure.
"""
@spec inspect_process(term(), keyword()) :: {:ok, map()} | {:error, term()}
def inspect_process(process_ref, opts \\ []) do
with {:ok, node} <- get_target_node(),
timeout <- Keyword.get(opts, :timeout, DalaDev.Remote.get_timeout()) do
if node == Node.self() do
# Local execution - no RPC needed
# Note: Can't inspect current process state due to :sys.get_state limitation
pid = resolve_pid_local(process_ref)
if pid == self() do
{:error, :cannot_inspect_current_process}
else
case DalaDev.Debugger.inspect_process_local(process_ref) do
{:ok, info} -> {:ok, info}
{:error, _} = err -> err
end
end
else
DalaDev.Debugger.inspect_process(node, process_ref, timeout: timeout)
end
end
end
defp resolve_pid_local(process_ref) do
case process_ref do
pid when is_pid(pid) ->
pid
name when is_atom(name) ->
case Process.whereis(name) do
nil -> raise "Process not found: #{inspect(name)}"
pid -> pid
end
{mod, fun} when is_atom(mod) and is_atom(fun) ->
case Process.whereis(mod) do
nil -> raise "Module not found: #{inspect(mod)}"
pid -> pid
end
mod when is_atom(mod) ->
case Process.whereis(mod) do
nil -> raise "Module not found: #{inspect(mod)}"
pid -> pid
end
end
end
@doc """
Gets a memory report from the selected node.
Returns `{:ok, report}` on success, `{:error, reason}` on failure.
"""
@spec memory_report(keyword()) :: {:ok, map()} | {:error, term()}
def memory_report(opts \\ []) do
with {:ok, node} <- get_target_node(),
timeout <- Keyword.get(opts, :timeout, DalaDev.Remote.get_timeout()) do
if node == Node.self() do
# Local execution - no RPC needed
DalaDev.Debugger.memory_report_local()
else
DalaDev.Debugger.memory_report(node, timeout: timeout)
end
end
end
@doc """
Gets the state of a process on the selected node.
Similar to `:sys.get_state/1` from Erlang/OTP, this function retrieves
the internal state of a process. The process must be a system process
(e.g., a GenServer, GenStateMachine, or other process that implements
the sys protocol).
## Parameters
- `pid_or_name` - A PID or registered name of the process
## Returns
- `{:ok, state}` on success, where `state` is the process state
- `{:error, reason}` on failure
## Examples
# Get state of a process by PID
iex> DalaDev.Remote.Debugger.get_state(#PID<0.123.0>)
{:ok, %{data: "...", status: :idle}}
# Get state of a process by registered name
iex> DalaDev.Remote.Debugger.get_state(:my_worker)
{:ok, %{count: 42}}
## See Also
- [Erlang sys:get_state/1](https://www.erlang.org/doc/apps/stdlib/sys.html#get_state/1)
"""
@spec get_state(pid() | atom() | {atom(), atom()}) :: {:ok, term()} | {:error, term()}
def get_state(pid_or_name, opts \\ []) do
with {:ok, node} <- get_target_node(),
timeout <- Keyword.get(opts, :timeout, DalaDev.Remote.get_timeout()) do
if node == Node.self() do
# Local execution - no RPC needed
case DalaDev.Debugger.get_process_state_local(pid_or_name) do
nil -> {:error, :process_not_found}
state -> {:ok, state}
end
else
case :rpc.call(node, DalaDev.Debugger, :get_process_state, [pid_or_name], timeout) do
nil -> {:error, :process_not_found}
{:badrpc, reason} -> {:error, reason}
state -> {:ok, state}
end
end
end
end
@doc """
Evaluates Elixir code on the selected node.
Returns `{:ok, result}` on success, `{:error, reason}` on failure.
## Examples
iex> DalaDev.Remote.Debugger.eval("1 + 1")
{:ok, 2}
iex> DalaDev.Remote.Debugger.eval("Enum.map(1..3, &(&1 * 2))")
{:ok, [2, 4, 6]}
iex> DalaDev.Remote.Debugger.eval("MyApp.Config.get(:api_key)")
{:ok, "secret_key"}
"""
@spec eval(String.t(), keyword()) :: {:ok, term()} | {:error, term()}
def eval(code, opts \\ []) do
with {:ok, node} <- get_target_node(),
timeout <- Keyword.get(opts, :timeout, DalaDev.Remote.get_timeout()),
bindings <- Keyword.get(opts, :bindings, []) do
if node == Node.self() do
# Local execution - no RPC needed
DalaDev.Debugger.eval_remote_local(code, bindings)
else
DalaDev.Debugger.eval_remote(node, code, timeout: timeout, bindings: bindings)
end
end
end
@doc """
Traces messages sent to/from a process on the selected node.
Returns `{:ok, messages}` on success, `{:error, reason}` on failure.
## Options
- `:duration` - Tracing duration in ms (default: 5000)
- `:timeout` - RPC timeout in ms (defaults to remote timeout)
"""
@spec trace_messages(term(), keyword()) :: {:ok, [map()]} | {:error, term()}
def trace_messages(process_ref, opts \\ []) do
with {:ok, node} <- get_target_node(),
duration <- Keyword.get(opts, :duration, 5000),
timeout <- Keyword.get(opts, :timeout, DalaDev.Remote.get_timeout()) do
if node == Node.self() do
# Local execution - no RPC needed
{:ok, DalaDev.Debugger.trace_messages_local(process_ref, duration)}
else
DalaDev.Debugger.trace_messages(node, process_ref, duration: duration, timeout: timeout)
end
end
end
@doc """
Gets the supervision tree from the selected node.
Returns `{:ok, tree}` on success, `{:error, reason}` on failure.
"""
@spec supervision_tree(keyword()) :: {:ok, map()} | {:error, term()}
def supervision_tree(opts \\ []) do
with {:ok, node} <- get_target_node(),
timeout <- Keyword.get(opts, :timeout, DalaDev.Remote.get_timeout()) do
if node == Node.self() do
# Local execution - no RPC needed
DalaDev.Debugger.get_supervision_tree_local()
else
DalaDev.Debugger.get_supervision_tree(node, timeout: timeout)
end
end
end
defp get_target_node do
case DalaDev.Remote.selected_node() do
{:ok, node} -> {:ok, node}
{:error, _} = err -> err
end
end
end
# LogCollector submodule
defmodule LogCollector do
@moduledoc """
Remote log collection functions.
These functions automatically use the node selected via
`DalaDev.Remote.select_node/1`.
"""
@doc """
Collects logs from the selected node.
Returns `{:ok, logs}` on success, `{:error, reason}` on failure.
"""
@spec collect_logs(keyword()) :: {:ok, [map()]} | {:error, term()}
def collect_logs(opts \\ []) do
with {:ok, node} <- get_target_node() do
DalaDev.LogCollector.collect_logs(node, opts)
end
end
@doc """
Collects Android logs from a device.
Returns `{:ok, logs}` on success, `{:error, reason}` on failure.
"""
@spec collect_android_logs(String.t(), keyword()) :: {:ok, String.t()} | {:error, term()}
def collect_android_logs(serial, opts \\ []) do
DalaDev.LogCollector.collect_android_logs(serial, opts)
end
defp get_target_node do
case DalaDev.Remote.selected_node() do
{:ok, node} -> {:ok, node}
{:error, _} = err -> err
end
end
end
# Rpc submodule
defmodule Rpc do
@moduledoc """
Generic RPC call functions for executing arbitrary functions on remote nodes.
These functions automatically use the node selected via
`DalaDev.Remote.select_node/1`.
## Usage
iex> DalaDev.Remote.Rpc.call(MyModule, :my_function, [arg1, arg2])
{:ok, result}
iex> DalaDev.Remote.Rpc.call(MyModule, :my_function, [arg1, arg2], timeout: 10_000)
{:ok, result}
"""
@doc """
Calls a function on the selected remote node.
## Parameters
- `module` - The module containing the function
- `function` - The function name (atom)
- `args` - List of arguments to pass to the function
- `opts` - Options:
- `:timeout` - RPC timeout in ms (defaults to remote timeout)
## Returns
- `{:ok, result}` on success
- `{:error, reason}` on failure
## Examples
# Call a function with no arguments
iex> DalaDev.Remote.Rpc.call(MyModule, :get_status, [])
{:ok, :online}
# Call a function with arguments
iex> DalaDev.Remote.Rpc.call(MyModule, :add, [1, 2])
{:ok, 3}
# Call a function with custom timeout
iex> DalaDev.Remote.Rpc.call(MyModule, :slow_function, [], timeout: 30_000)
{:ok, result}
"""
@spec call(module(), atom(), [term()], keyword()) :: {:ok, term()} | {:error, term()}
def call(module, function, args, opts \\ []) do
with {:ok, node} <- get_target_node(),
timeout <- Keyword.get(opts, :timeout, DalaDev.Remote.get_timeout()) do
if node == Node.self() do
# Local execution - no RPC needed
try do
result = apply(module, function, args)
{:ok, result}
rescue
e -> {:error, Exception.message(e)}
end
else
:rpc.call(node, module, function, args, timeout)
end
end
end
defp get_target_node do
case DalaDev.Remote.selected_node() do
{:ok, node} -> {:ok, node}
{:error, _} = err -> err
end
end
end
# Server callbacks
@impl true
def init(_opts) do
state = %{
selected_node: nil,
timeout: @default_timeout
}
{:ok, state}
end
@impl true
def handle_call(:nodes, _from, state) do
remote_nodes = Node.list()
{:reply, remote_nodes, state}
end
@impl true
def handle_call({:select_node, node}, _from, state) do
if node in Node.list() or node == Node.self() do
{:reply, :ok, %{state | selected_node: node}}
else
{:reply, {:error, :node_not_found}, state}
end
end
@impl true
def handle_call(:selected_node, _from, state) do
case state.selected_node do
nil -> {:reply, {:error, :no_node_selected}, state}
node -> {:reply, {:ok, node}, state}
end
end
@impl true
def handle_call({:set_timeout, timeout}, _from, state) do
{:reply, :ok, %{state | timeout: timeout}}
end
@impl true
def handle_call(:get_timeout, _from, state) do
{:reply, state.timeout, state}
end
@impl true
def handle_call(:clear_selection, _from, state) do
{:reply, :ok, %{state | selected_node: nil}}
end
@impl true
def handle_call(:auto_select, _from, state) do
case Node.list() do
[] ->
{:reply, {:error, :no_remote_nodes}, state}
[single_node] ->
{:reply, :ok, %{state | selected_node: single_node}}
multiple_nodes ->
{:reply,
{:error,
{:multiple_nodes, multiple_nodes,
"Please select a node using DalaDev.Remote.select_node/1"}}, state}
end
end
end