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} />
-
+
<% 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