diff --git a/assets/js/session/index.js b/assets/js/session/index.js index ad9cd8c3e..e554835f9 100644 --- a/assets/js/session/index.js +++ b/assets/js/session/index.js @@ -301,6 +301,8 @@ function handleDocumentKeyDown(hook, event) { showBin(hook); } else if (keyBuffer.tryMatch(["e", "x"])) { cancelFocusedCellEvaluation(hook); + } else if (keyBuffer.tryMatch(["0", "0"])) { + restartRuntime(hook); } else if (keyBuffer.tryMatch(["?"])) { showShortcuts(hook); } else if (keyBuffer.tryMatch(["i"])) { @@ -594,6 +596,10 @@ function cancelFocusedCellEvaluation(hook) { } } +function restartRuntime(hook) { + hook.pushEvent("restart_runtime", {}); +} + function showShortcuts(hook) { hook.pushEvent("show_shortcuts", {}); } diff --git a/lib/livebook/evaluator/default_formatter.ex b/lib/livebook/evaluator/default_formatter.ex index 774e37aa2..a95d6bca6 100644 --- a/lib/livebook/evaluator/default_formatter.ex +++ b/lib/livebook/evaluator/default_formatter.ex @@ -27,7 +27,7 @@ defmodule Livebook.Evaluator.DefaultFormatter do def format_response({:error, kind, error, stacktrace}) do formatted = Exception.format(kind, error, stacktrace) - {:error, formatted} + {:error, formatted, error_type(error)} end @compile {:no_warn_undefined, {Kino.Render, :to_livebook, 1}} @@ -80,4 +80,17 @@ defmodule Livebook.Evaluator.DefaultFormatter do reset: :reset ] end + + defp error_type(error) do + cond do + mix_install_vm_error?(error) -> :runtime_restart_required + true -> :other + end + end + + defp mix_install_vm_error?(exception) do + is_struct(exception, Mix.Error) and + Exception.message(exception) =~ + "Mix.install/2 can only be called with the same dependencies" + end end diff --git a/lib/livebook/notebook/cell/elixir.ex b/lib/livebook/notebook/cell/elixir.ex index 09ba29749..f9a7aef73 100644 --- a/lib/livebook/notebook/cell/elixir.ex +++ b/lib/livebook/notebook/cell/elixir.ex @@ -38,7 +38,7 @@ defmodule Livebook.Notebook.Cell.Elixir do # Interactive data table | {:table_dynamic, widget_process :: pid()} # Internal output format for errors - | {:error, message :: binary()} + | {:error, message :: binary(), type :: :other | :runtime_restart_required} @doc """ Returns an empty cell. diff --git a/lib/livebook/runtime.ex b/lib/livebook/runtime.ex index 95061142e..370240104 100644 --- a/lib/livebook/runtime.ex +++ b/lib/livebook/runtime.ex @@ -106,4 +106,10 @@ defprotocol Livebook.Runtime do container_ref, evaluation_ref ) + + @doc """ + Synchronously starts a runtime of the same type with the same parameters. + """ + @spec duplicate(Runtime.t()) :: {:ok, Runtime.t()} | {:error, String.t()} + def duplicate(runtime) end diff --git a/lib/livebook/runtime/attached.ex b/lib/livebook/runtime/attached.ex index 2c6dff0fe..4eb1a099d 100644 --- a/lib/livebook/runtime/attached.ex +++ b/lib/livebook/runtime/attached.ex @@ -89,4 +89,8 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Attached do evaluation_ref ) end + + def duplicate(_runtime) do + {:error, "attached runtime is connected to a specific VM and cannot be duplicated"} + end end diff --git a/lib/livebook/runtime/elixir_standalone.ex b/lib/livebook/runtime/elixir_standalone.ex index 84932032e..fe28dc482 100644 --- a/lib/livebook/runtime/elixir_standalone.ex +++ b/lib/livebook/runtime/elixir_standalone.ex @@ -113,4 +113,8 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.ElixirStandalone do evaluation_ref ) end + + def duplicate(_runtime) do + Livebook.Runtime.ElixirStandalone.init() + end end diff --git a/lib/livebook/runtime/embedded.ex b/lib/livebook/runtime/embedded.ex index d4716ac2a..eef66e0e2 100644 --- a/lib/livebook/runtime/embedded.ex +++ b/lib/livebook/runtime/embedded.ex @@ -95,4 +95,9 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Embedded do evaluation_ref ) end + + def duplicate(_runtime) do + {:error, + "embedded runtime is connected to the Livebook application VM and cannot be duplicated"} + end end diff --git a/lib/livebook/runtime/mix_standalone.ex b/lib/livebook/runtime/mix_standalone.ex index 72d2a3e78..f9dce1f45 100644 --- a/lib/livebook/runtime/mix_standalone.ex +++ b/lib/livebook/runtime/mix_standalone.ex @@ -166,4 +166,8 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.MixStandalone do evaluation_ref ) end + + def duplicate(runtime) do + Livebook.Runtime.MixStandalone.init(runtime.project_path) + end end diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index 7009955a9..8e401cc17 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -566,6 +566,24 @@ defmodule LivebookWeb.SessionLive do push_patch(socket, to: Routes.session_path(socket, :bin, socket.assigns.session_id))} end + def handle_event("restart_runtime", %{}, socket) do + socket = + if runtime = socket.private.data.runtime do + case Runtime.duplicate(runtime) do + {:ok, new_runtime} -> + Session.connect_runtime(socket.assigns.session_id, new_runtime) + socket + + {:error, message} -> + put_flash(socket, :error, "Failed to setup runtime - #{message}") + end + else + socket + end + + {:noreply, socket} + end + def handle_event("completion_request", %{"hint" => hint, "cell_id" => cell_id}, socket) do data = socket.private.data diff --git a/lib/livebook_web/live/session_live/cell_component.ex b/lib/livebook_web/live/session_live/cell_component.ex index cba276bdc..a8214a0c6 100644 --- a/lib/livebook_web/live/session_live/cell_component.ex +++ b/lib/livebook_web/live/session_live/cell_component.ex @@ -337,7 +337,22 @@ defmodule LivebookWeb.SessionLive.CellComponent do ) end - defp render_output(_socket, {:error, formatted}, _id) do + defp render_output(_socket, {:error, formatted, :runtime_restart_required}, _id) do + assigns = %{formatted: formatted} + + ~L""" +