defmodule ClusterConnectSmartcell do
@moduledoc """
#{File.read!("README.md")}
"""
use Kino.JS, assets_path: "lib/assets/cluster_connect_smartcell"
use Kino.JS.Live
use Kino.SmartCell, name: "OTP Connect"
@impl true
def init(attrs, ctx) do
root_fields = %{
"target_node" => attrs["target_node"] || "",
"erlang_cookie" => attrs["erlang_cookie"] || "",
"module" => attrs["module"] || "",
"function" => attrs["function"] || "",
"arguments" => attrs["arguments"] || ""
}
{:ok, assign(ctx, root_fields: root_fields)}
end
@impl true
def handle_connect(ctx) do
# Called first time the cliet connects, gives initial state
payload = %{
root_fields: ctx.assigns.root_fields
}
{:ok, payload, ctx}
end
@impl true
def to_attrs(ctx) do
ctx.assigns.root_fields
end
@impl true
def to_source(attrs) do
%{
"target_node" => target_node,
"erlang_cookie" => erlang_cookie,
"module" => module,
"function" => function,
"arguments" => arguments
} = attrs
target_node = String.to_atom(target_node)
erlang_cookie = String.to_atom(erlang_cookie)
quote do
with {:cookie, true} <-
{:cookie, :erlang.set_cookie(unquote(erlang_cookie))},
{:connect, true} <-
{:connect,
unquote(target_node)
|> :net_kernel.connect_node()},
{:monitor, :ok} <- {:monitor, :net_kernel.monitor_nodes(true)},
nodes <- Node.list(:connected),
{:command, {:ok, result}} <-
{:command,
ClusterConnectSmartcell.do_execute_remote_command(
unquote(target_node),
unquote(module),
unquote(function),
unquote(arguments)
)} do
%{"result" => result, "nodes" => nodes}
else
{step, res} when step in [:cookie, :connect, :command, :monitor] ->
%{"outcome" => false} |> Map.put(Atom.to_string(step), res)
error ->
%{"outcome" => false, "error" => error}
end
end
|> Kino.SmartCell.quoted_to_string()
end
# Event Handlers
@impl true
def handle_event("update_field", %{"field" => field, "value" => value}, ctx) do
parsed_value = parse_value(field, value)
ctx = update(ctx, :root_fields, &Map.put(&1, field, parsed_value))
broadcast_event(ctx, "update_root", %{"fields" => %{field => parsed_value}})
{:noreply, ctx}
end
def do_execute_remote_command(target_node, module, function, arguments)
when is_binary(module) and byte_size(module) > 0 and
is_binary(function) and byte_size(function) > 0 and
is_binary(arguments) and byte_size(arguments) > 0 do
module = module |> normalize_module_name() |> String.to_atom()
function = String.to_atom(function)
# pass a list for now
{arguments, _bindings} = arguments |> Code.eval_string() |> parse_evaluated_arguments!()
:erpc.call(target_node, module, function, arguments)
rescue
error -> error
end
def do_execute_remote_command(_target_node, _module, _function, _arguments),
do: {:ok, :no_command}
# Privates
defp parse_value(_field, value), do: value
defp normalize_module_name(module) do
if String.starts_with?(module, "Elixir.") do
module
else
"Elixir.#{module}"
end
end
defp parse_evaluated_arguments!({arguments, bindings}) when is_list(arguments),
do: {arguments, bindings}
defp parse_evaluated_arguments!({arguments, _bindings}),
do: raise("Arguments should be a list, got #{inspect(arguments)}")
end