diff --git a/README.md b/README.md index 3f8a9f299..ed9a7eb25 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 e81b154ed..2d8b34cad 100644 --- a/lib/livebook/config.ex +++ b/lib/livebook/config.ex @@ -244,10 +244,16 @@ 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) + 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}) @@ -264,6 +270,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/runtime/mix_standalone.ex b/lib/livebook/runtime/mix_standalone.ex index 83a5b77cb..e43b6539f 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 86e6dafd9..db37e59f6 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/utils.ex b/lib/livebook/utils.ex index e9dea591c..85d3ebe02 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_cli/server.ex b/lib/livebook_cli/server.ex index 90637afa6..7617f3a23 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 08f1d64df..f326901a3 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 %><%= for {output, i} <- @outputs do %><%= ansi_string_to_html(output) %><% end %>
+ ><%= for {output, i} <- @outputs do %><%= ansi_string_to_html(output) %><% end %>