diff --git a/lib/livebook/runtime.ex b/lib/livebook/runtime.ex index 9f90f2462..34162116e 100644 --- a/lib/livebook/runtime.ex +++ b/lib/livebook/runtime.ex @@ -199,4 +199,15 @@ defprotocol Livebook.Runtime do """ @spec duplicate(Runtime.t()) :: {:ok, Runtime.t()} | {:error, String.t()} def duplicate(runtime) + + @doc """ + Returns true if the given runtime is self-contained. + + A standalone runtime always starts fresh and frees all + resources when terminated. This may not be the case for + for runtimes that connect to an external running system + and use it for code evaluation. + """ + @spec standalone?(Runtime.t()) :: boolean() + def standalone?(runtime) end diff --git a/lib/livebook/runtime/attached.ex b/lib/livebook/runtime/attached.ex index 2b65d280b..4d7ebb320 100644 --- a/lib/livebook/runtime/attached.ex +++ b/lib/livebook/runtime/attached.ex @@ -71,4 +71,6 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Attached do {:error, :unreachable} -> {:error, "node #{inspect(runtime.node)} is unreachable"} end end + + def standalone?(_runtime), do: false end diff --git a/lib/livebook/runtime/elixir_standalone.ex b/lib/livebook/runtime/elixir_standalone.ex index 4958cb196..ec95ee133 100644 --- a/lib/livebook/runtime/elixir_standalone.ex +++ b/lib/livebook/runtime/elixir_standalone.ex @@ -97,4 +97,6 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.ElixirStandalone do def duplicate(_runtime) do Livebook.Runtime.ElixirStandalone.init() end + + def standalone?(_runtime), do: true end diff --git a/lib/livebook/runtime/embedded.ex b/lib/livebook/runtime/embedded.ex index 1ce38edbc..06fed0fa9 100644 --- a/lib/livebook/runtime/embedded.ex +++ b/lib/livebook/runtime/embedded.ex @@ -70,4 +70,6 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Embedded do def duplicate(_runtime) do Livebook.Runtime.Embedded.init() end + + def standalone?(_runtime), do: false end diff --git a/lib/livebook/runtime/mix_standalone.ex b/lib/livebook/runtime/mix_standalone.ex index 506fdbf2b..d54ebb49f 100644 --- a/lib/livebook/runtime/mix_standalone.ex +++ b/lib/livebook/runtime/mix_standalone.ex @@ -150,4 +150,6 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.MixStandalone do def duplicate(runtime) do Livebook.Runtime.MixStandalone.init(runtime.project_path) end + + def standalone?(_runtime), do: true end diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index b803cdec5..42f2f59ee 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -240,6 +240,7 @@ defmodule LivebookWeb.SessionLive do id: section_view.id, index: index, session_id: @session.id, + runtime: @data_view.runtime, section_view: section_view %> <% end %>
diff --git a/lib/livebook_web/live/session_live/attached_live.ex b/lib/livebook_web/live/session_live/attached_live.ex index c77cbe852..41675fe68 100644 --- a/lib/livebook_web/live/session_live/attached_live.ex +++ b/lib/livebook_web/live/session_live/attached_live.ex @@ -2,12 +2,18 @@ defmodule LivebookWeb.SessionLive.AttachedLive do use LivebookWeb, :live_view alias Livebook.{Session, Runtime, Utils} + alias LivebookWeb.SessionLive.RuntimeHelpers @impl true def mount(_params, %{"session" => session, "current_runtime" => current_runtime}, socket) do + if connected?(socket) do + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") + end + {:ok, assign(socket, session: session, + current_runtime: current_runtime, error_message: nil, data: initial_data(current_runtime) )} @@ -22,6 +28,7 @@ defmodule LivebookWeb.SessionLive.AttachedLive do <%= @error_message %> <% end %> +

Connect the session to an already running node and evaluate code in the context of that node. @@ -57,13 +64,19 @@ defmodule LivebookWeb.SessionLive.AttachedLive do """ end + defp matching_runtime?(%Runtime.Attached{} = runtime, data) do + initial_data(runtime) == data + end + + defp matching_runtime?(_runtime, _data), do: false + @impl true def handle_event("validate", %{"data" => data}, socket) do {:noreply, assign(socket, data: data)} @@ -76,7 +89,7 @@ defmodule LivebookWeb.SessionLive.AttachedLive do case Runtime.Attached.init(node, cookie) do {:ok, runtime} -> Session.connect_runtime(socket.assigns.session.pid, runtime) - {:noreply, assign(socket, data: data, error_message: nil)} + {:noreply, assign(socket, data: initial_data(runtime), error_message: nil)} {:error, error} -> message = runtime_error_to_message(error) @@ -84,6 +97,13 @@ defmodule LivebookWeb.SessionLive.AttachedLive do end end + @impl true + def handle_info({:operation, {:set_runtime, _pid, runtime}}, socket) do + {:noreply, assign(socket, current_runtime: runtime)} + end + + def handle_info(_message, socket), do: {:noreply, socket} + defp initial_data(%Runtime.Attached{node: node, cookie: cookie}) do %{ "name" => Atom.to_string(node), diff --git a/lib/livebook_web/live/session_live/cell_component.ex b/lib/livebook_web/live/session_live/cell_component.ex index d37c3a6d0..c8b239de2 100644 --- a/lib/livebook_web/live/session_live/cell_component.ex +++ b/lib/livebook_web/live/session_live/cell_component.ex @@ -99,7 +99,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do <%= if @cell_view.outputs != [] do %>

- <.outputs cell_view={@cell_view} socket={@socket} /> + <.outputs cell_view={@cell_view} runtime={@runtime} socket={@socket} />
<% end %> @@ -431,7 +431,8 @@ defmodule LivebookWeb.SessionLive.CellComponent do
<%= render_output(output, %{ id: "cell-#{@cell_view.id}-evaluation#{evaluation_number(@cell_view.evaluation_status, @cell_view.number_of_evaluations)}-output#{index}", - socket: @socket + socket: @socket, + runtime: @runtime }) %>
<% end %> @@ -483,17 +484,26 @@ defmodule LivebookWeb.SessionLive.CellComponent do ) end - defp render_output({:error, formatted, :runtime_restart_required}, %{}) do - assigns = %{formatted: formatted} + defp render_output({:error, formatted, :runtime_restart_required}, %{runtime: runtime}) + when runtime != nil do + assigns = %{formatted: formatted, is_standalone: Livebook.Runtime.standalone?(runtime)} ~H"""
<%= render_error_message_output(@formatted) %> -
- -
+ <%= if @is_standalone do %> +
+ +
+ <% else %> +
+ Note: + This operation requires restarting the runtime, but we cannot + do it automatically for the current runtime +
+ <% end %>
""" end diff --git a/lib/livebook_web/live/session_live/elixir_standalone_live.ex b/lib/livebook_web/live/session_live/elixir_standalone_live.ex index d6853cd14..ea307f066 100644 --- a/lib/livebook_web/live/session_live/elixir_standalone_live.ex +++ b/lib/livebook_web/live/session_live/elixir_standalone_live.ex @@ -2,6 +2,7 @@ defmodule LivebookWeb.SessionLive.ElixirStandaloneLive do use LivebookWeb, :live_view alias Livebook.{Session, Runtime} + alias LivebookWeb.SessionLive.RuntimeHelpers @impl true def mount(_params, %{"session" => session, "current_runtime" => current_runtime}, socket) do @@ -21,10 +22,9 @@ defmodule LivebookWeb.SessionLive.ElixirStandaloneLive do <%= @error_message %> <% end %> +

Start a new local node to handle code evaluation. - This is the default runtime and is started automatically - as soon as you evaluate the first cell.

""" end + defp matching_runtime?(%Runtime.Embedded{}), do: true + defp matching_runtime?(_runtime), do: false + @impl true def handle_event("init", _params, socket) do {:ok, runtime} = Runtime.Embedded.init() Session.connect_runtime(socket.assigns.session.pid, runtime) {:noreply, socket} end + + @impl true + def handle_info({:operation, {:set_runtime, _pid, runtime}}, socket) do + {:noreply, assign(socket, current_runtime: runtime)} + end + + def handle_info(_message, socket), do: {:noreply, socket} end diff --git a/lib/livebook_web/live/session_live/mix_standalone_live.ex b/lib/livebook_web/live/session_live/mix_standalone_live.ex index b700f5732..7aecc2ccf 100644 --- a/lib/livebook_web/live/session_live/mix_standalone_live.ex +++ b/lib/livebook_web/live/session_live/mix_standalone_live.ex @@ -2,6 +2,7 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do use LivebookWeb, :live_view alias Livebook.{Session, Runtime, Utils, FileSystem} + alias LivebookWeb.SessionLive.RuntimeHelpers @type status :: :initial | :initializing | :finished @@ -26,6 +27,7 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do def render(assigns) do ~H"""
+

Start a new local node in the context of a Mix project. This way all your code and dependencies will be available diff --git a/lib/livebook_web/live/session_live/runtime_component.ex b/lib/livebook_web/live/session_live/runtime_component.ex index 9bce49ad7..175e6f009 100644 --- a/lib/livebook_web/live/session_live/runtime_component.ex +++ b/lib/livebook_web/live/session_live/runtime_component.ex @@ -16,7 +16,8 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do if assigns.runtime do runtime_type(assigns.runtime) else - "elixir_standalone" + {runtime_module, _args} = Livebook.Config.default_runtime() + runtime_module |> struct() |> runtime_type() end Map.put(assigns, :type, type) diff --git a/lib/livebook_web/live/session_live/runtime_helpers.ex b/lib/livebook_web/live/session_live/runtime_helpers.ex new file mode 100644 index 000000000..0f242f9cd --- /dev/null +++ b/lib/livebook_web/live/session_live/runtime_helpers.ex @@ -0,0 +1,22 @@ +defmodule LivebookWeb.SessionLive.RuntimeHelpers do + use Phoenix.Component + + @doc """ + Displays an info text if `@module` is the default runtime. + """ + def default_runtime_note(assigns) do + ~H""" + <%= if default_runtime_module?(@module) do %> +

+ Note: This is the default runtime and starts + automatically as soon as you evaluate the first cell. +

+ <% end %> + """ + end + + defp default_runtime_module?(module) do + {default_module, _args} = Livebook.Config.default_runtime() + default_module == module + end +end diff --git a/lib/livebook_web/live/session_live/section_component.ex b/lib/livebook_web/live/session_live/section_component.ex index a3f44dcc8..13536858e 100644 --- a/lib/livebook_web/live/session_live/section_component.ex +++ b/lib/livebook_web/live/session_live/section_component.ex @@ -96,6 +96,7 @@ defmodule LivebookWeb.SessionLive.SectionComponent do <%= live_component LivebookWeb.SessionLive.CellComponent, id: cell_view.id, session_id: @session_id, + runtime: @runtime, cell_view: cell_view %> <% end %> diff --git a/test/livebook_web/live/session_live_test.exs b/test/livebook_web/live/session_live_test.exs index 0ff0015d7..1195c3008 100644 --- a/test/livebook_web/live/session_live_test.exs +++ b/test/livebook_web/live/session_live_test.exs @@ -189,6 +189,10 @@ defmodule LivebookWeb.SessionLiveTest do Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") + view + |> element("button", "Elixir standalone") + |> render_click() + [elixir_standalone_view] = live_children(view) elixir_standalone_view diff --git a/test/support/noop_runtime.ex b/test/support/noop_runtime.ex index 64abb0375..645f71c7f 100644 --- a/test/support/noop_runtime.ex +++ b/test/support/noop_runtime.ex @@ -16,5 +16,6 @@ defmodule Livebook.Runtime.NoopRuntime do def drop_container(_, _), do: :ok def handle_intellisense(_, _, _, _, _), do: :ok def duplicate(_), do: {:ok, Livebook.Runtime.NoopRuntime.new()} + def standalone?(_runtime), do: false end end