diff --git a/assets/css/js_interop.css b/assets/css/js_interop.css index 1b06bb680..2f94f3fcb 100644 --- a/assets/css/js_interop.css +++ b/assets/css/js_interop.css @@ -244,6 +244,11 @@ solely client-side operations. @apply hidden; } +[data-el-session][data-js-side-panel-content="runtime-info"] + [data-el-runtime-indicator] { + @apply border-gray-700; +} + [data-el-session]:not([data-js-side-panel-content="app-info"]) [data-el-app-info] { @apply hidden; diff --git a/lib/livebook/runtime.ex b/lib/livebook/runtime.ex index 4103b9aab..e1fb8e2bb 100644 --- a/lib/livebook/runtime.ex +++ b/lib/livebook/runtime.ex @@ -765,6 +765,17 @@ defprotocol Livebook.Runtime do @type proxy_handler_spec :: {module :: module(), function :: atom(), args :: list()} + @typedoc """ + An information about Elixir nodes that the runtime is connected to. + + Whenever the node list change, the runtime should send an updated + list as: + + * `{:runtime_connected_nodes, connected_nodes()}` + + """ + @type connected_nodes :: list(node()) + @doc """ Returns relevant information about the runtime. @@ -1116,4 +1127,13 @@ defprotocol Livebook.Runtime do """ @spec fetch_proxy_handler_spec(t()) :: {:ok, proxy_handler_spec()} | {:error, :not_found} def fetch_proxy_handler_spec(runtime) + + @doc """ + Asks the runtime to disconnect from the given connected node. + + The node should be one of `connected_nodes()` reported by the runtime + earlier. + """ + @spec disconnect_node(t(), node()) :: :ok + def disconnect_node(runtime, node) end diff --git a/lib/livebook/runtime/attached.ex b/lib/livebook/runtime/attached.ex index 8e79a4d6a..eac182b71 100644 --- a/lib/livebook/runtime/attached.ex +++ b/lib/livebook/runtime/attached.ex @@ -208,4 +208,8 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Attached do def fetch_proxy_handler_spec(runtime) do RuntimeServer.fetch_proxy_handler_spec(runtime.server_pid) end + + def disconnect_node(runtime, node) do + RuntimeServer.disconnect_node(runtime.server_pid, node) + end end diff --git a/lib/livebook/runtime/elixir_standalone.ex b/lib/livebook/runtime/elixir_standalone.ex index 57d93ec13..02fbb5934 100644 --- a/lib/livebook/runtime/elixir_standalone.ex +++ b/lib/livebook/runtime/elixir_standalone.ex @@ -327,4 +327,8 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.ElixirStandalone do def fetch_proxy_handler_spec(runtime) do RuntimeServer.fetch_proxy_handler_spec(runtime.server_pid) end + + def disconnect_node(runtime, node) do + RuntimeServer.disconnect_node(runtime.server_pid, node) + end end diff --git a/lib/livebook/runtime/embedded.ex b/lib/livebook/runtime/embedded.ex index c255cb076..3f24940dc 100644 --- a/lib/livebook/runtime/embedded.ex +++ b/lib/livebook/runtime/embedded.ex @@ -175,6 +175,10 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Embedded do RuntimeServer.fetch_proxy_handler_spec(runtime.server_pid) end + def disconnect_node(runtime, node) do + RuntimeServer.disconnect_node(runtime.server_pid, node) + end + defp config() do Application.get_env(:livebook, Livebook.Runtime.Embedded, []) end diff --git a/lib/livebook/runtime/erl_dist/runtime_server.ex b/lib/livebook/runtime/erl_dist/runtime_server.ex index af8de4209..286f02e04 100644 --- a/lib/livebook/runtime/erl_dist/runtime_server.ex +++ b/lib/livebook/runtime/erl_dist/runtime_server.ex @@ -328,6 +328,10 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do end end + def disconnect_node(pid, node) do + GenServer.cast(pid, {:disconnect_node, node}) + end + @doc """ Stops the runtime server. @@ -362,10 +366,11 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do smart_cell_supervisor: nil, smart_cell_gl: nil, smart_cells: %{}, - smart_cell_definitions: nil, + smart_cell_definitions: [], smart_cell_definitions_module: Keyword.get(opts, :smart_cell_definitions_module, Kino.SmartCell), extra_smart_cell_definitions: Keyword.get(opts, :extra_smart_cell_definitions, []), + connected_nodes: [], memory_timer_ref: nil, last_evaluator: nil, base_env_path: @@ -408,7 +413,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do def handle_info({:evaluation_finished, locator}, state) do {:noreply, state - |> report_smart_cell_definitions() + |> report_environment() |> report_transient_state() |> scan_binding_after_evaluation(locator)} end @@ -480,7 +485,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do Process.monitor(owner) state = %{state | owner: owner, runtime_broadcast_to: opts[:runtime_broadcast_to]} - state = report_smart_cell_definitions(state) + state = report_environment(state) report_memory_usage(state) {:ok, smart_cell_supervisor} = DynamicSupervisor.start_link(strategy: :one_for_one) @@ -710,6 +715,11 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do {:noreply, state} end + def handle_cast({:disconnect_node, node}, state) do + Node.disconnect(node) + {:noreply, report_connected_nodes(state)} + end + @impl true def handle_call({:read_file, path}, {from_pid, _}, state) do # Delegate reading to a separate task and let the caller @@ -817,6 +827,12 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do send(state.owner, {:runtime_memory_usage, Evaluator.memory()}) end + defp report_environment(state) do + state + |> report_smart_cell_definitions() + |> report_connected_nodes() + end + defp report_smart_cell_definitions(state) do smart_cell_definitions = get_smart_cell_definitions(state.smart_cell_definitions_module) @@ -837,6 +853,19 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do end end + defp report_connected_nodes(state) do + owner_node = node(state.owner) + nodes = Node.list(:connected) |> List.delete(owner_node) |> Enum.sort() + + if nodes == state.connected_nodes do + state + else + send(state.owner, {:runtime_connected_nodes, nodes}) + + %{state | connected_nodes: nodes} + end + end + defp get_smart_cell_definitions(module) do if Code.ensure_loaded?(module) and function_exported?(module, :definitions, 0) do module.definitions() diff --git a/lib/livebook/session.ex b/lib/livebook/session.ex index 1012294f4..36b3e3644 100644 --- a/lib/livebook/session.ex +++ b/lib/livebook/session.ex @@ -1735,6 +1735,11 @@ defmodule Livebook.Session do {:noreply, state |> put_memory_usage(runtime_memory) |> notify_update()} end + def handle_info({:runtime_connected_nodes, nodes}, state) do + operation = {:set_runtime_connected_nodes, @client_id, nodes} + {:noreply, handle_operation(state, operation)} + end + def handle_info({:runtime_smart_cell_definitions, definitions}, state) do operation = {:set_smart_cell_definitions, @client_id, definitions} {:noreply, handle_operation(state, operation)} diff --git a/lib/livebook/session/data.ex b/lib/livebook/session/data.ex index 12776950d..dd179cbe2 100644 --- a/lib/livebook/session/data.ex +++ b/lib/livebook/session/data.ex @@ -28,6 +28,7 @@ defmodule Livebook.Session.Data do :bin_entries, :runtime, :runtime_transient_state, + :runtime_connected_nodes, :smart_cell_definitions, :clients_map, :users_map, @@ -55,6 +56,7 @@ defmodule Livebook.Session.Data do bin_entries: list(cell_bin_entry()), runtime: Runtime.t(), runtime_transient_state: Runtime.transient_state(), + runtime_connected_nodes: list(node()), smart_cell_definitions: list(Runtime.smart_cell_definition()), clients_map: %{client_id() => User.id()}, users_map: %{User.id() => User.t()}, @@ -214,6 +216,7 @@ defmodule Livebook.Session.Data do | {:set_input_value, client_id(), input_id(), value :: term()} | {:set_runtime, client_id(), Runtime.t()} | {:set_runtime_transient_state, client_id(), Runtime.transient_state()} + | {:set_runtime_connected_nodes, client_id(), list(node())} | {:set_smart_cell_definitions, client_id(), list(Runtime.smart_cell_definition())} | {:set_file, client_id(), FileSystem.File.t() | nil} | {:set_autosave_interval, client_id(), non_neg_integer() | nil} @@ -303,6 +306,7 @@ defmodule Livebook.Session.Data do bin_entries: [], runtime: default_runtime, runtime_transient_state: %{}, + runtime_connected_nodes: [], smart_cell_definitions: [], clients_map: %{}, users_map: %{}, @@ -871,6 +875,13 @@ defmodule Livebook.Session.Data do |> wrap_ok() end + def apply_operation(data, {:set_runtime_connected_nodes, _client_id, nodes}) do + data + |> with_actions() + |> set_runtime_connected_nodes(nodes) + |> wrap_ok() + end + def apply_operation(data, {:set_smart_cell_definitions, _client_id, definitions}) do data |> with_actions() @@ -1955,7 +1966,13 @@ defmodule Livebook.Session.Data do end defp set_runtime(data_actions, prev_data, runtime) do - {data, _} = data_actions = set!(data_actions, runtime: runtime, smart_cell_definitions: []) + {data, _} = + data_actions = + set!(data_actions, + runtime: runtime, + runtime_connected_nodes: [], + smart_cell_definitions: [] + ) if not Runtime.connected?(prev_data.runtime) and Runtime.connected?(data.runtime) do data_actions @@ -2006,6 +2023,12 @@ defmodule Livebook.Session.Data do end end + defp set_runtime_connected_nodes(data_actions, nodes) do + data_actions + |> set!(runtime_connected_nodes: nodes) + |> maybe_start_smart_cells() + end + defp set_smart_cell_definitions(data_actions, smart_cell_definitions) do data_actions |> set!(smart_cell_definitions: smart_cell_definitions) diff --git a/lib/livebook_web/components/core_components.ex b/lib/livebook_web/components/core_components.ex index 99241485a..26fa876d8 100644 --- a/lib/livebook_web/components/core_components.ex +++ b/lib/livebook_web/components/core_components.ex @@ -539,9 +539,11 @@ defmodule LivebookWeb.CoreComponents do <%= @label %> - + <%= render_slot(@inner_block) %> diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index ce284c2f1..0a4455f2b 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -578,6 +578,17 @@ defmodule LivebookWeb.SessionLive do {:noreply, socket} end + def handle_event("runtime_disconnect_node", %{"node" => node}, socket) do + node = Enum.find(socket.private.data.runtime_connected_nodes, &(Atom.to_string(&1) == node)) + runtime = socket.private.data.runtime + + if node && Runtime.connected?(runtime) do + Runtime.disconnect_node(runtime, node) + end + + {:noreply, socket} + end + def handle_event("deploy_app", _, socket) do data = socket.private.data app_settings = data.notebook.app_settings @@ -1778,6 +1789,7 @@ defmodule LivebookWeb.SessionLive do dirty: data.dirty, persistence_warnings: data.persistence_warnings, runtime: data.runtime, + runtime_connected_nodes: Enum.sort(data.runtime_connected_nodes), smart_cell_definitions: Enum.sort_by(data.smart_cell_definitions, & &1.name), example_snippet_definitions: data.runtime diff --git a/lib/livebook_web/live/session_live/render.ex b/lib/livebook_web/live/session_live/render.ex index eddd29da5..d44a88736 100644 --- a/lib/livebook_web/live/session_live/render.ex +++ b/lib/livebook_web/live/session_live/render.ex @@ -18,7 +18,12 @@ defmodule LivebookWeb.SessionLive.Render do data-p-global-status={hook_prop(elem(@data_view.global_status, 0))} data-p-autofocus-cell-id={hook_prop(@autofocus_cell_id)} > - <.sidebar app={@app} session={@session} live_action={@live_action} current_user={@current_user} /> + <.sidebar + session={@session} + live_action={@live_action} + current_user={@current_user} + runtime_connected_nodes={@data_view.runtime_connected_nodes} + /> <.side_panel app={@app} session={@session} data_view={@data_view} client_id={@client_id} />
@@ -333,11 +338,21 @@ defmodule LivebookWeb.SessionLive.Render do button_attrs={["data-el-clients-list-toggle": true]} /> - <.button_item - icon="cpu-line" - label="Runtime settings (sr)" - button_attrs={["data-el-runtime-info-toggle": true]} - /> +
+ <.button_item + icon="cpu-line" + label="Runtime settings (sr)" + button_attrs={["data-el-runtime-info-toggle": true]} + /> +
+
<%!-- Hub functionality --%> @@ -626,7 +641,7 @@ defmodule LivebookWeb.SessionLive.Render do
-
+
<.labeled_text :for={{label, value} <- Runtime.describe(@data_view.runtime)} @@ -636,7 +651,7 @@ defmodule LivebookWeb.SessionLive.Render do <%= value %>
-
+
<%= if Runtime.connected?(@data_view.runtime) do %> <.button phx-click="reconnect_runtime"> <.remix_icon icon="wireless-charging-line" /> @@ -651,21 +666,6 @@ defmodule LivebookWeb.SessionLive.Render do <.button color="gray" outlined patch={~p"/sessions/#{@session.id}/settings/runtime"}> Configure -
- -
- <%= if uses_memory?(@session.memory_usage) do %> - <.memory_info memory_usage={@session.memory_usage} /> - <% else %> -
- Memory -

- <%= format_bytes(@session.memory_usage.system.free) %> available out of <%= format_bytes( - @session.memory_usage.system.total - ) %> -

-
- <% end %> <.button :if={Runtime.connected?(@data_view.runtime)} @@ -673,27 +673,50 @@ defmodule LivebookWeb.SessionLive.Render do outlined type="button" phx-click="disconnect_runtime" + class="col-span-2" > Disconnect
+ + <.memory_usage_info memory_usage={@session.memory_usage} /> + + <.runtime_connected_nodes_info runtime_connected_nodes={@data_view.runtime_connected_nodes} />
""" end - defp memory_info(assigns) do - assigns = assign(assigns, :runtime_memory, runtime_memory(assigns.memory_usage)) - + defp memory_usage_info(assigns) do ~H""" -
-
- Memory - +
+
+ + Memory + + <%= format_bytes(@memory_usage.system.free) %> available
-
+ <%= if uses_memory?(@memory_usage) do %> + <.runtime_memory_info memory_usage={@memory_usage} /> + <% else %> +

+ <%= format_bytes(@memory_usage.system.free) %> available out of <%= format_bytes( + @memory_usage.system.total + ) %> +

+ <% end %> +
+ """ + end + + defp runtime_memory_info(assigns) do + assigns = assign(assigns, :runtime_memory, runtime_memory(assigns.memory_usage)) + + ~H""" +
+
-
+
<%= type %> <%= memory.unit %>
-
- Total: <%= format_bytes(@memory_usage.runtime.total) %> -
+
+
+ Total: <%= format_bytes(@memory_usage.runtime.total) %>
""" @@ -735,6 +758,41 @@ defmodule LivebookWeb.SessionLive.Render do end) end + defp runtime_connected_nodes_info(assigns) do + ~H""" +
+ + Connected nodes + + <%= if @runtime_connected_nodes == [] do %> +
+ No connected nodes +
+ <% else %> +
+
+ <.remix_icon icon="circle-fill" class="mr-2 text-xs text-blue-500" /> +
+ <%= node %> +
+ + <.icon_button phx-click="runtime_disconnect_node" phx-value-node={node} small> + <.remix_icon icon="close-line" /> + + +
+
+ <% end %> +
+ """ + end + defp section_status(%{status: :evaluating} = assigns) do ~H"""