mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-10 13:38:09 +08:00
Add support for mix run flags to the Mix runtime (#1108)
* Add support for mix run flags to the Mix runtime * Support mix run flags in runtime config string * Add flag validation * Update lib/livebook_web/live/session_live/mix_standalone_live.ex Co-authored-by: José Valim <jose.valim@dashbit.co> Co-authored-by: José Valim <jose.valim@dashbit.co>
This commit is contained in:
parent
e7f62e4ec8
commit
aa3b58d708
7 changed files with 106 additions and 40 deletions
|
@ -153,7 +153,7 @@ The following environment variables configure Livebook:
|
||||||
|
|
||||||
* LIVEBOOK_DEFAULT_RUNTIME - sets the runtime type that is used by default
|
* LIVEBOOK_DEFAULT_RUNTIME - sets the runtime type that is used by default
|
||||||
when none is started explicitly for the given notebook. Must be either
|
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).
|
"attached:NODE:COOKIE" (Attached node) or "embedded" (Embedded).
|
||||||
Defaults to "standalone".
|
Defaults to "standalone".
|
||||||
|
|
||||||
|
|
|
@ -244,10 +244,16 @@ defmodule Livebook.Config do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
"mix:" <> path ->
|
"mix:" <> config ->
|
||||||
|
{path, flags} = parse_mix_config!(config)
|
||||||
|
|
||||||
case mix_path(path) do
|
case mix_path(path) do
|
||||||
{:ok, path} ->
|
{: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 ->
|
:error ->
|
||||||
abort!(~s{"#{path}" does not point to a Mix project})
|
abort!(~s{"#{path}" does not point to a Mix project})
|
||||||
|
@ -264,6 +270,13 @@ defmodule Livebook.Config do
|
||||||
end
|
end
|
||||||
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
|
defp mix_path(path) do
|
||||||
path = Path.expand(path)
|
path = Path.expand(path)
|
||||||
mixfile = Path.join(path, "mix.exs")
|
mixfile = Path.join(path, "mix.exs")
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule Livebook.Runtime.MixStandalone do
|
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.
|
# A runtime backed by a standalone Elixir node managed by Livebook.
|
||||||
#
|
#
|
||||||
|
@ -13,6 +13,7 @@ defmodule Livebook.Runtime.MixStandalone do
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
project_path: String.t(),
|
project_path: String.t(),
|
||||||
|
flags: String.t(),
|
||||||
node: node() | nil,
|
node: node() | nil,
|
||||||
server_pid: pid() | nil
|
server_pid: pid() | nil
|
||||||
}
|
}
|
||||||
|
@ -20,9 +21,9 @@ defmodule Livebook.Runtime.MixStandalone do
|
||||||
@doc """
|
@doc """
|
||||||
Returns a new runtime instance.
|
Returns a new runtime instance.
|
||||||
"""
|
"""
|
||||||
@spec new(String.t()) :: t()
|
@spec new(String.t(), String.t()) :: t()
|
||||||
def new(project_path) do
|
def new(project_path, flags \\ "") do
|
||||||
%__MODULE__{project_path: project_path}
|
%__MODULE__{project_path: project_path, flags: flags}
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -46,6 +47,7 @@ defmodule Livebook.Runtime.MixStandalone do
|
||||||
@spec connect_async(t(), Emitter.t()) :: :ok
|
@spec connect_async(t(), Emitter.t()) :: :ok
|
||||||
def connect_async(runtime, emitter) do
|
def connect_async(runtime, emitter) do
|
||||||
%{project_path: project_path} = runtime
|
%{project_path: project_path} = runtime
|
||||||
|
flags = OptionParser.split(runtime.flags)
|
||||||
output_emitter = Emitter.mapper(emitter, fn output -> {:output, output} end)
|
output_emitter = Emitter.mapper(emitter, fn output -> {:output, output} end)
|
||||||
|
|
||||||
spawn_link(fn ->
|
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("deps.get", project_path, output_emitter),
|
||||||
:ok <- run_mix_task("compile", project_path, output_emitter),
|
:ok <- run_mix_task("compile", project_path, output_emitter),
|
||||||
eval = child_node_eval_string(),
|
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
|
{:ok, server_pid} <- parent_init_sequence(child_node, port, emitter: output_emitter) do
|
||||||
runtime = %{runtime | node: child_node, server_pid: server_pid}
|
runtime = %{runtime | node: child_node, server_pid: server_pid}
|
||||||
Emitter.emit(emitter, {:ok, runtime})
|
Emitter.emit(emitter, {:ok, runtime})
|
||||||
|
@ -115,7 +118,7 @@ defmodule Livebook.Runtime.MixStandalone do
|
||||||
end
|
end
|
||||||
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.
|
# Here we create a port to start the system process in a non-blocking way.
|
||||||
Port.open({:spawn_executable, elixir_path}, [
|
Port.open({:spawn_executable, elixir_path}, [
|
||||||
:binary,
|
:binary,
|
||||||
|
@ -127,7 +130,8 @@ defmodule Livebook.Runtime.MixStandalone do
|
||||||
cd: project_path,
|
cd: project_path,
|
||||||
args:
|
args:
|
||||||
elixir_flags(node_name) ++
|
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
|
||||||
end
|
end
|
||||||
|
@ -138,13 +142,11 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.MixStandalone do
|
||||||
def describe(runtime) do
|
def describe(runtime) do
|
||||||
[
|
[
|
||||||
{"Type", "Mix standalone"},
|
{"Type", "Mix standalone"},
|
||||||
{"Project", runtime.project_path}
|
{"Project", runtime.project_path},
|
||||||
] ++
|
runtime.flags != "" && {"Flags", runtime.flags},
|
||||||
if connected?(runtime) do
|
connected?(runtime) && {"Node name", Atom.to_string(runtime.node)}
|
||||||
[{"Node name", Atom.to_string(runtime.node)}]
|
]
|
||||||
else
|
|> Enum.filter(&is_tuple/1)
|
||||||
[]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def connect(runtime) do
|
def connect(runtime) do
|
||||||
|
@ -166,7 +168,7 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.MixStandalone do
|
||||||
end
|
end
|
||||||
|
|
||||||
def duplicate(runtime) do
|
def duplicate(runtime) do
|
||||||
Livebook.Runtime.MixStandalone.new(runtime.project_path)
|
Livebook.Runtime.MixStandalone.new(runtime.project_path, runtime.flags)
|
||||||
end
|
end
|
||||||
|
|
||||||
def evaluate_code(runtime, code, locator, base_locator, opts \\ []) do
|
def evaluate_code(runtime, code, locator, base_locator, opts \\ []) do
|
||||||
|
|
|
@ -110,7 +110,7 @@ defmodule Livebook.Runtime.StandaloneInit do
|
||||||
loop.(loop)
|
loop.(loop)
|
||||||
|
|
||||||
{:DOWN, ^port_ref, :port, _object, _reason} ->
|
{:DOWN, ^port_ref, :port, _object, _reason} ->
|
||||||
{:error, "Elixir process terminated unexpectedly"}
|
{:error, "Elixir terminated unexpectedly, please check the terminal for errors"}
|
||||||
after
|
after
|
||||||
# Use a longer timeout to account for longer child node startup,
|
# Use a longer timeout to account for longer child node startup,
|
||||||
# as may happen when starting with Mix.
|
# as may happen when starting with Mix.
|
||||||
|
|
|
@ -193,6 +193,30 @@ defmodule Livebook.Utils do
|
||||||
@spec valid_hex_color?(String.t()) :: boolean()
|
@spec valid_hex_color?(String.t()) :: boolean()
|
||||||
def valid_hex_color?(hex_color), do: hex_color =~ ~r/^#[0-9a-fA-F]{6}$/
|
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 """
|
@doc """
|
||||||
Changes the first letter in the given string to upper case.
|
Changes the first letter in the given string to upper case.
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ defmodule LivebookCLI.Server do
|
||||||
explicitly for the given notebook, defaults to standalone
|
explicitly for the given notebook, defaults to standalone
|
||||||
Supported options:
|
Supported options:
|
||||||
* standalone - Elixir standalone
|
* standalone - Elixir standalone
|
||||||
* mix[:PATH] - Mix standalone
|
* mix[:PATH][:FLAGS] - Mix standalone
|
||||||
* attached:NODE:COOKIE - Attached
|
* attached:NODE:COOKIE - Attached
|
||||||
* embedded - Embedded
|
* embedded - Embedded
|
||||||
--home The home path for the Livebook instance
|
--home The home path for the Livebook instance
|
||||||
|
|
|
@ -20,8 +20,9 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do
|
||||||
session: session,
|
session: session,
|
||||||
status: :initial,
|
status: :initial,
|
||||||
current_runtime: current_runtime,
|
current_runtime: current_runtime,
|
||||||
file: initial_file(current_runtime),
|
data: initial_data(current_runtime),
|
||||||
outputs: [],
|
outputs: [],
|
||||||
|
outputs_version: 0,
|
||||||
emitter: nil
|
emitter: nil
|
||||||
), temporary_assigns: [outputs: []]}
|
), temporary_assigns: [outputs: []]}
|
||||||
end
|
end
|
||||||
|
@ -41,27 +42,40 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do
|
||||||
inside a Mix project because the dependencies of the project
|
inside a Mix project because the dependencies of the project
|
||||||
itself have been installed instead.
|
itself have been installed instead.
|
||||||
</p>
|
</p>
|
||||||
<%= if @status == :initial do %>
|
<%= if @status != :initializing do %>
|
||||||
<div class="h-full h-52">
|
<div class="h-full h-52">
|
||||||
<.live_component module={LivebookWeb.FileSelectComponent}
|
<.live_component module={LivebookWeb.FileSelectComponent}
|
||||||
id="mix-project-dir"
|
id="mix-project-dir"
|
||||||
file={@file}
|
file={@data.file}
|
||||||
extnames={[]}
|
extnames={[]}
|
||||||
running_files={[]}
|
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} />
|
file_system_select_disabled={true} />
|
||||||
</div>
|
</div>
|
||||||
<button class="button-base button-blue" phx-click="init" disabled={disabled?(@file.path)}>
|
<form phx-change="validate" phx-submit="init">
|
||||||
<%= if(matching_runtime?(@current_runtime, @file.path), do: "Reconnect", else: "Connect") %>
|
<div>
|
||||||
</button>
|
<div class="input-label"><code>mix run</code> command-line flags</div>
|
||||||
|
<input class="input"
|
||||||
|
type="text"
|
||||||
|
name="flags"
|
||||||
|
value={@data.flags}
|
||||||
|
spellcheck="false"
|
||||||
|
autocomplete="off" />
|
||||||
|
</div>
|
||||||
|
<button class="mt-5 button-base button-blue"
|
||||||
|
type="submit"
|
||||||
|
disabled={not data_valid?(@data)}>
|
||||||
|
<%= if(matching_runtime?(@current_runtime, @data), do: "Reconnect", else: "Connect") %>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if @status != :initial do %>
|
<%= if @status != :initial do %>
|
||||||
<div class="markdown">
|
<div class="markdown">
|
||||||
<pre><code class="max-h-40 overflow-y-auto tiny-scrollbar"
|
<pre><code class="max-h-40 overflow-y-auto tiny-scrollbar"
|
||||||
id="mix-standalone-init-output"
|
id={"mix-standalone-init-output-#{@outputs_version}"}
|
||||||
phx-update="append"
|
phx-update="append"
|
||||||
phx-hook="ScrollOnUpdate"
|
phx-hook="ScrollOnUpdate"
|
||||||
><%= for {output, i} <- @outputs do %><span id={"output-#{i}"}><%= ansi_string_to_html(output) %></span><% end %></code></pre>
|
><%= for {output, i} <- @outputs do %><span id={"mix-standalone-init-output-#{@outputs_version}-#{i}"}><%= ansi_string_to_html(output) %></span><% end %></code></pre>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
@ -73,9 +87,13 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do
|
||||||
handle_init(socket)
|
handle_init(socket)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("validate", %{"flags" => flags}, socket) do
|
||||||
|
{:noreply, update(socket, :data, &%{&1 | flags: flags})}
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:set_file, file, _info}, socket) do
|
def handle_info({:set_file, file, _info}, socket) do
|
||||||
{:noreply, assign(socket, :file, file)}
|
{:noreply, update(socket, :data, &%{&1 | file: file})}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info(:init, socket) do
|
def handle_info(:init, socket) do
|
||||||
|
@ -104,31 +122,40 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do
|
||||||
|
|
||||||
defp handle_init(socket) do
|
defp handle_init(socket) do
|
||||||
emitter = Utils.Emitter.new(self())
|
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)
|
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
|
end
|
||||||
|
|
||||||
defp add_output(socket, output) do
|
defp add_output(socket, output) do
|
||||||
assign(socket, outputs: socket.assigns.outputs ++ [{output, Utils.random_id()}])
|
assign(socket, outputs: socket.assigns.outputs ++ [{output, Utils.random_id()}])
|
||||||
end
|
end
|
||||||
|
|
||||||
defp initial_file(%Runtime.MixStandalone{} = current_runtime) do
|
defp initial_data(%Runtime.MixStandalone{project_path: project_path, flags: flags}) do
|
||||||
FileSystem.File.local(current_runtime.project_path)
|
file =
|
||||||
|
project_path
|
||||||
|
|> FileSystem.Utils.ensure_dir_path()
|
||||||
|
|> FileSystem.File.local()
|
||||||
|
|
||||||
|
%{file: file, flags: flags}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp initial_file(_runtime) do
|
defp initial_data(_runtime) do
|
||||||
Livebook.Config.local_filesystem_home()
|
%{file: Livebook.Config.local_filesystem_home(), flags: ""}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp matching_runtime?(%Runtime.MixStandalone{} = runtime, path) do
|
defp matching_runtime?(%Runtime.MixStandalone{} = runtime, data) do
|
||||||
Path.expand(runtime.project_path) == Path.expand(path)
|
Path.expand(runtime.project_path) == Path.expand(data.file.path)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp matching_runtime?(_runtime, _path), do: false
|
defp matching_runtime?(_runtime, _path), do: false
|
||||||
|
|
||||||
defp disabled?(path) do
|
defp data_valid?(data) do
|
||||||
not mix_project_root?(path)
|
mix_project_root?(data.file.path) and Livebook.Utils.valid_cli_flags?(data.flags)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp mix_project_root?(path) do
|
defp mix_project_root?(path) do
|
||||||
|
|
Loading…
Add table
Reference in a new issue