From 17f1236560237bf90363c42405904746cd682b6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Thu, 14 Apr 2022 01:29:35 +0200 Subject: [PATCH 1/4] Add support for mix run flags to the Mix runtime --- lib/livebook/runtime/mix_standalone.ex | 32 +++++----- lib/livebook/runtime/standalone_init.ex | 2 +- .../live/session_live/mix_standalone_live.ex | 62 +++++++++++++------ 3 files changed, 60 insertions(+), 36 deletions(-) diff --git a/lib/livebook/runtime/mix_standalone.ex b/lib/livebook/runtime/mix_standalone.ex index 83a5b77cb5c..e43b6539fef 100644 --- a/lib/livebook/runtime/mix_standalone.ex +++ b/lib/livebook/runtime/mix_standalone.ex @@ -1,5 +1,5 @@ defmodule Livebook.Runtime.MixStandalone do - defstruct [:node, :server_pid, :project_path] + defstruct [:node, :server_pid, :project_path, :flags] # A runtime backed by a standalone Elixir node managed by Livebook. # @@ -13,6 +13,7 @@ defmodule Livebook.Runtime.MixStandalone do @type t :: %__MODULE__{ project_path: String.t(), + flags: String.t(), node: node() | nil, server_pid: pid() | nil } @@ -20,9 +21,9 @@ defmodule Livebook.Runtime.MixStandalone do @doc """ Returns a new runtime instance. """ - @spec new(String.t()) :: t() - def new(project_path) do - %__MODULE__{project_path: project_path} + @spec new(String.t(), String.t()) :: t() + def new(project_path, flags \\ "") do + %__MODULE__{project_path: project_path, flags: flags} end @doc """ @@ -46,6 +47,7 @@ defmodule Livebook.Runtime.MixStandalone do @spec connect_async(t(), Emitter.t()) :: :ok def connect_async(runtime, emitter) do %{project_path: project_path} = runtime + flags = OptionParser.split(runtime.flags) output_emitter = Emitter.mapper(emitter, fn output -> {:output, output} end) spawn_link(fn -> @@ -59,7 +61,8 @@ defmodule Livebook.Runtime.MixStandalone do :ok <- run_mix_task("deps.get", project_path, output_emitter), :ok <- run_mix_task("compile", project_path, output_emitter), eval = child_node_eval_string(), - port = start_elixir_mix_node(elixir_path, child_node, eval, argv, project_path), + port = + start_elixir_mix_node(elixir_path, child_node, flags, eval, argv, project_path), {:ok, server_pid} <- parent_init_sequence(child_node, port, emitter: output_emitter) do runtime = %{runtime | node: child_node, server_pid: server_pid} Emitter.emit(emitter, {:ok, runtime}) @@ -115,7 +118,7 @@ defmodule Livebook.Runtime.MixStandalone do end end - defp start_elixir_mix_node(elixir_path, node_name, eval, argv, project_path) do + defp start_elixir_mix_node(elixir_path, node_name, flags, eval, argv, project_path) do # Here we create a port to start the system process in a non-blocking way. Port.open({:spawn_executable, elixir_path}, [ :binary, @@ -127,7 +130,8 @@ defmodule Livebook.Runtime.MixStandalone do cd: project_path, args: elixir_flags(node_name) ++ - ["-S", "mix", "run", "--eval", eval, "--" | Enum.map(argv, &to_string/1)] + ["-S", "mix", "run", "--eval", eval | flags] ++ + ["--" | Enum.map(argv, &to_string/1)] ]) end end @@ -138,13 +142,11 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.MixStandalone do def describe(runtime) do [ {"Type", "Mix standalone"}, - {"Project", runtime.project_path} - ] ++ - if connected?(runtime) do - [{"Node name", Atom.to_string(runtime.node)}] - else - [] - end + {"Project", runtime.project_path}, + runtime.flags != "" && {"Flags", runtime.flags}, + connected?(runtime) && {"Node name", Atom.to_string(runtime.node)} + ] + |> Enum.filter(&is_tuple/1) end def connect(runtime) do @@ -166,7 +168,7 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.MixStandalone do end def duplicate(runtime) do - Livebook.Runtime.MixStandalone.new(runtime.project_path) + Livebook.Runtime.MixStandalone.new(runtime.project_path, runtime.flags) end def evaluate_code(runtime, code, locator, base_locator, opts \\ []) do diff --git a/lib/livebook/runtime/standalone_init.ex b/lib/livebook/runtime/standalone_init.ex index 86e6dafd95f..db37e59f652 100644 --- a/lib/livebook/runtime/standalone_init.ex +++ b/lib/livebook/runtime/standalone_init.ex @@ -110,7 +110,7 @@ defmodule Livebook.Runtime.StandaloneInit do loop.(loop) {:DOWN, ^port_ref, :port, _object, _reason} -> - {:error, "Elixir process terminated unexpectedly"} + {:error, "Elixir terminated unexpectedly, please check the terminal for errors"} after # Use a longer timeout to account for longer child node startup, # as may happen when starting with Mix. 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 08f1d64df9a..c07e0d876f4 100644 --- a/lib/livebook_web/live/session_live/mix_standalone_live.ex +++ b/lib/livebook_web/live/session_live/mix_standalone_live.ex @@ -20,8 +20,9 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do session: session, status: :initial, current_runtime: current_runtime, - file: initial_file(current_runtime), + data: initial_data(current_runtime), outputs: [], + outputs_version: 0, emitter: nil ), temporary_assigns: [outputs: []]} end @@ -41,27 +42,40 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do inside a Mix project because the dependencies of the project itself have been installed instead.

- <%= if @status == :initial do %> + <%= if @status != :initializing do %>
<.live_component module={LivebookWeb.FileSelectComponent} id="mix-project-dir" - file={@file} + file={@data.file} extnames={[]} running_files={[]} - submit_event={if(disabled?(@file.path), do: nil, else: :init)} + submit_event={if(data_valid?(@data), do: :init, else: nil)} file_system_select_disabled={true} />
- +
+
+
Mix run flags
+ +
+ +
<% end %> <%= if @status != :initial do %>
<%= for {output, i} <- @outputs do %><%= ansi_string_to_html(output) %><% end %>
+ ><%= for {output, i} <- @outputs do %><%= ansi_string_to_html(output) %><% end %>
<% end %> @@ -73,9 +87,13 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do handle_init(socket) end + def handle_event("validate", %{"flags" => flags}, socket) do + {:noreply, update(socket, :data, &%{&1 | flags: flags})} + end + @impl true def handle_info({:set_file, file, _info}, socket) do - {:noreply, assign(socket, :file, file)} + {:noreply, update(socket, :data, &%{&1 | file: file})} end def handle_info(:init, socket) do @@ -104,31 +122,35 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do defp handle_init(socket) do emitter = Utils.Emitter.new(self()) - runtime = Runtime.MixStandalone.new(socket.assigns.file.path) + runtime = Runtime.MixStandalone.new(socket.assigns.data.file.path, socket.assigns.data.flags) Runtime.MixStandalone.connect_async(runtime, emitter) - {:noreply, assign(socket, status: :initializing, emitter: emitter)} + + {:noreply, + socket + |> assign(status: :initializing, emitter: emitter, outputs: []) + |> update(:outputs_version, &(&1 + 1))} end defp add_output(socket, output) do assign(socket, outputs: socket.assigns.outputs ++ [{output, Utils.random_id()}]) end - defp initial_file(%Runtime.MixStandalone{} = current_runtime) do - FileSystem.File.local(current_runtime.project_path) + defp initial_data(%Runtime.MixStandalone{project_path: project_path, flags: flags}) do + %{file: FileSystem.File.local(project_path), flags: flags} end - defp initial_file(_runtime) do - Livebook.Config.local_filesystem_home() + defp initial_data(_runtime) do + %{file: Livebook.Config.local_filesystem_home(), flags: ""} end - defp matching_runtime?(%Runtime.MixStandalone{} = runtime, path) do - Path.expand(runtime.project_path) == Path.expand(path) + defp matching_runtime?(%Runtime.MixStandalone{} = runtime, data) do + Path.expand(runtime.project_path) == Path.expand(data.file.path) end defp matching_runtime?(_runtime, _path), do: false - defp disabled?(path) do - not mix_project_root?(path) + defp data_valid?(data) do + mix_project_root?(data.file.path) end defp mix_project_root?(path) do From 990a5afd75109ca237fcc7ceb0162d009f4a1a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Thu, 14 Apr 2022 01:42:48 +0200 Subject: [PATCH 2/4] Support mix run flags in runtime config string --- README.md | 2 +- lib/livebook/config.ex | 13 +++++++++++-- lib/livebook_cli/server.ex | 2 +- .../live/session_live/mix_standalone_live.ex | 7 ++++++- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3f8a9f29925..ed9a7eb2506 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ The following environment variables configure Livebook: * LIVEBOOK_DEFAULT_RUNTIME - sets the runtime type that is used by default when none is started explicitly for the given notebook. Must be either - "standalone" (Elixir standalone), "mix[:PATH]" (Mix standalone), + "standalone" (Elixir standalone), "mix[:PATH][:FLAGS]" (Mix standalone), "attached:NODE:COOKIE" (Attached node) or "embedded" (Embedded). Defaults to "standalone". diff --git a/lib/livebook/config.ex b/lib/livebook/config.ex index e81b154ed55..a538e966a08 100644 --- a/lib/livebook/config.ex +++ b/lib/livebook/config.ex @@ -244,10 +244,12 @@ defmodule Livebook.Config do ) end - "mix:" <> path -> + "mix:" <> config -> + {path, flags} = parse_mix_config!(config) + case mix_path(path) do {:ok, path} -> - Livebook.Runtime.MixStandalone.new(path) + Livebook.Runtime.MixStandalone.new(path, flags) :error -> abort!(~s{"#{path}" does not point to a Mix project}) @@ -264,6 +266,13 @@ defmodule Livebook.Config do end end + defp parse_mix_config!(config) do + case String.split(config, ":", parts: 2) do + [path] -> {path, ""} + [path, flags] -> {path, flags} + end + end + defp mix_path(path) do path = Path.expand(path) mixfile = Path.join(path, "mix.exs") diff --git a/lib/livebook_cli/server.ex b/lib/livebook_cli/server.ex index 90637afa646..7617f3a2347 100644 --- a/lib/livebook_cli/server.ex +++ b/lib/livebook_cli/server.ex @@ -45,7 +45,7 @@ defmodule LivebookCLI.Server do explicitly for the given notebook, defaults to standalone Supported options: * standalone - Elixir standalone - * mix[:PATH] - Mix standalone + * mix[:PATH][:FLAGS] - Mix standalone * attached:NODE:COOKIE - Attached * embedded - Embedded --home The home path for the Livebook instance 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 c07e0d876f4..e22e4c00c14 100644 --- a/lib/livebook_web/live/session_live/mix_standalone_live.ex +++ b/lib/livebook_web/live/session_live/mix_standalone_live.ex @@ -136,7 +136,12 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do end defp initial_data(%Runtime.MixStandalone{project_path: project_path, flags: flags}) do - %{file: FileSystem.File.local(project_path), flags: flags} + file = + project_path + |> FileSystem.Utils.ensure_dir_path() + |> FileSystem.File.local() + + %{file: file, flags: flags} end defp initial_data(_runtime) do From 98a349dc464f08ef0e03e3e00c678276e5f13d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Thu, 14 Apr 2022 01:52:53 +0200 Subject: [PATCH 3/4] Add flag validation --- lib/livebook/config.ex | 6 ++++- lib/livebook/utils.ex | 24 +++++++++++++++++++ .../live/session_live/mix_standalone_live.ex | 2 +- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/livebook/config.ex b/lib/livebook/config.ex index a538e966a08..2d8b34cad6f 100644 --- a/lib/livebook/config.ex +++ b/lib/livebook/config.ex @@ -249,7 +249,11 @@ defmodule Livebook.Config do case mix_path(path) do {:ok, path} -> - Livebook.Runtime.MixStandalone.new(path, flags) + if Livebook.Utils.valid_cli_flags?(flags) do + Livebook.Runtime.MixStandalone.new(path, flags) + else + abort!(~s{"#{flags}" is not a valid flag sequence}) + end :error -> abort!(~s{"#{path}" does not point to a Mix project}) diff --git a/lib/livebook/utils.ex b/lib/livebook/utils.ex index e9dea591cd0..85d3ebe02eb 100644 --- a/lib/livebook/utils.ex +++ b/lib/livebook/utils.ex @@ -193,6 +193,30 @@ defmodule Livebook.Utils do @spec valid_hex_color?(String.t()) :: boolean() def valid_hex_color?(hex_color), do: hex_color =~ ~r/^#[0-9a-fA-F]{6}$/ + @doc ~S""" + Validates if the given string forms valid CLI flags. + + ## Examples + + iex> Livebook.Utils.valid_cli_flags?("") + true + + iex> Livebook.Utils.valid_cli_flags?("--arg1 value --arg2 'value'") + true + + iex> Livebook.Utils.valid_cli_flags?("--arg1 \"") + false + """ + @spec valid_cli_flags?(String.t()) :: boolean() + def valid_cli_flags?(flags) do + try do + OptionParser.split(flags) + true + rescue + _ -> false + end + end + @doc """ Changes the first letter in the given string to upper case. 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 e22e4c00c14..ed79fa88592 100644 --- a/lib/livebook_web/live/session_live/mix_standalone_live.ex +++ b/lib/livebook_web/live/session_live/mix_standalone_live.ex @@ -155,7 +155,7 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do defp matching_runtime?(_runtime, _path), do: false defp data_valid?(data) do - mix_project_root?(data.file.path) + mix_project_root?(data.file.path) and Livebook.Utils.valid_cli_flags?(data.flags) end defp mix_project_root?(path) do From c3b0a9a6445b1acfec536ceef4689c173ed9c0db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Thu, 14 Apr 2022 11:43:16 +0200 Subject: [PATCH 4/4] Update lib/livebook_web/live/session_live/mix_standalone_live.ex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Valim --- lib/livebook_web/live/session_live/mix_standalone_live.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ed79fa88592..f326901a3cf 100644 --- a/lib/livebook_web/live/session_live/mix_standalone_live.ex +++ b/lib/livebook_web/live/session_live/mix_standalone_live.ex @@ -54,7 +54,7 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do
-
Mix run flags
+
mix run command-line flags