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"] || "", "target_node_bound" => attrs["target_node_bound"] || "", "erlang_cookie" => attrs["erlang_cookie"] || "", "erlang_cookie_bound" => attrs["erlang_cookie_bound"] || "", "module" => attrs["module"] || "", "function" => attrs["function"] || "", "arguments" => attrs["arguments"] || "", "bind_result" => Kino.SmartCell.prefixed_var_name("rpc_result", attrs["bind_result"]) } ctx = ctx |> assign(root_fields: root_fields) |> assign(node_bounds: %{}) |> assign(cookie_bounds: %{}) {:ok, ctx} end @impl true def handle_connect(ctx) do # Called first time the client connects, gives initial state payload = %{ root_fields: ctx.assigns.root_fields, node_bounds: ctx.assigns.node_bounds, cookie_bounds: ctx.assigns.cookie_bounds } {: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, "target_node_bound" => target_node_bound, "erlang_cookie" => erlang_cookie, "erlang_cookie_bound" => erlang_cookie_bound, "module" => module, "function" => function, "arguments" => arguments, "bind_result" => bind_result } = attrs target_node = target_node |> get_target_node(target_node_bound) |> String.to_atom() erlang_cookie = erlang_cookie |> get_erlang_cookie(erlang_cookie_bound) |> String.to_atom() quote do unquote(quoted_var(bind_result)) = 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 # implementations to find out possible bindings @impl true def handle_info({:scan_binding_result, data}, ctx) do ctx = assign(ctx, data_options: data) broadcast_event(ctx, "update_binding", %{"bindings" => data}) {:noreply, ctx} end @doc """ Try to identify possible autocompletion targets possible nodes: atoms contianing "@" character possible secrets: atoms """ @impl true def scan_binding(pid, binding, _env) do possible_nodes = binding |> Enum.filter(fn {_key, value} -> is_atom(value) end) |> Enum.filter(fn {_key, value} -> value |> Atom.to_string() |> String.contains?("@") end) |> Enum.into(%{}) possible_secrets = binding |> Enum.filter(fn {_key, value} -> is_atom(value) end) |> Enum.into(%{}) data = %{ possible_nodes: possible_nodes, possible_secrets: possible_secrets } send(pid, {:scan_binding_result, data}) end # defp to_select_pair({key, value}) when is_atom(key), do: {Atom.to_string(key), value} # defp to_select_pair({key, value}), do: {key, value} # NOTE: this function needs to remain public, so it can be called by generated code # Candidate for refactor :) 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 # NOTE: this function needs to remain public, so it can be called by generated code # Candidate for refactor :) def do_execute_remote_command(_target_node, _module, _function, _arguments), do: {:ok, :no_command} # Privates defp quoted_var(value) when is_binary(value), do: {String.to_atom(value), [], nil} defp quoted_var(value) when is_atom(value), do: {value, [], nil} defp parse_value(_field, value), do: value defp normalize_module_name(<<":", rest::binary>> = _module), do: rest defp normalize_module_name(<<"Elixir.", _rest::binary>> = module), do: module defp normalize_module_name(module), do: "Elixir.#{module}" 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)}") defp get_target_node("", target_node_bound), do: target_node_bound defp get_target_node(target_node, ""), do: target_node defp get_target_node(_target_node, target_node_bound), do: target_node_bound defp get_erlang_cookie("", erlang_cookie_bound), do: erlang_cookie_bound defp get_erlang_cookie(erlang_cookie, ""), do: erlang_cookie defp get_erlang_cookie(_target_node, erlang_cookie_bound), do: erlang_cookie_bound end