mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-05 20:44:30 +08:00
Run Livebook Desktop without EPMD (#2591)
This commit is contained in:
parent
eb4887657a
commit
6d7f416f18
21 changed files with 550 additions and 275 deletions
50
.github/workflows/test.yml
vendored
50
.github/workflows/test.yml
vendored
|
@ -6,10 +6,17 @@ on:
|
||||||
- main
|
- main
|
||||||
- "v*.*"
|
- "v*.*"
|
||||||
jobs:
|
jobs:
|
||||||
main:
|
linux:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
MIX_ENV: test
|
MIX_ENV: test
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
build:
|
||||||
|
- name: default
|
||||||
|
- name: epmdless
|
||||||
|
env: LIVEBOOK_EPMDLESS=true ELIXIR_ERL_OPTIONS="-epmd_module Elixir.Livebook.EPMD -start_epmd false -erl_epmd_port 0"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout git repo
|
- name: Checkout git repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
@ -41,23 +48,7 @@ jobs:
|
||||||
run: mix compile --warnings-as-errors
|
run: mix compile --warnings-as-errors
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: mix test
|
run: mix test
|
||||||
- name: Install Node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: "18.x"
|
|
||||||
- name: Cache npm dependencies
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-node-
|
|
||||||
- name: Install npm dependencies
|
|
||||||
run: npm ci --prefix assets
|
|
||||||
- name: Check assets formatting
|
|
||||||
run: npm run format-check --prefix assets
|
|
||||||
- name: Run assets tests
|
|
||||||
run: npm test --prefix assets
|
|
||||||
windows:
|
windows:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
|
@ -134,3 +125,26 @@ jobs:
|
||||||
key: ${{ runner.os }}-elixir-${{ env.elixir }}
|
key: ${{ runner.os }}-elixir-${{ env.elixir }}
|
||||||
- name: Build the app
|
- name: Build the app
|
||||||
run: .github/scripts/app/build_macos.sh
|
run: .github/scripts/app/build_macos.sh
|
||||||
|
|
||||||
|
assets:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout git repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "18.x"
|
||||||
|
- name: Cache npm dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-node-
|
||||||
|
- name: Install npm dependencies
|
||||||
|
run: npm ci --prefix assets
|
||||||
|
- name: Check assets formatting
|
||||||
|
run: npm run format-check --prefix assets
|
||||||
|
- name: Run assets tests
|
||||||
|
run: npm test --prefix assets
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -33,3 +33,6 @@ npm-debug.log
|
||||||
|
|
||||||
# The built Escript
|
# The built Escript
|
||||||
/livebook
|
/livebook
|
||||||
|
|
||||||
|
# The priv directory with the EPMD file
|
||||||
|
/priv/epmd
|
||||||
|
|
|
@ -224,7 +224,10 @@ The following environment variables can be used to configure Livebook on boot:
|
||||||
cluster. Must be "name" (long names) or "sname" (short names). Note that this
|
cluster. Must be "name" (long names) or "sname" (short names). Note that this
|
||||||
sets RELEASE_DISTRIBUTION if present when creating a release. Defaults to "sname".
|
sets RELEASE_DISTRIBUTION if present when creating a release. Defaults to "sname".
|
||||||
|
|
||||||
* `LIVEBOOK_FIPS` - if set to "true" will try to enable the FIPS mode on startup.
|
* `LIVEBOOK_EPMDLESS` - if set to "true", it disables the usage of EPMD. This is
|
||||||
|
only supported within releases and defaults to true for the Desktop app.
|
||||||
|
|
||||||
|
* `LIVEBOOK_FIPS` - if set to "true", it enables the FIPS mode on startup.
|
||||||
See more details in [the documentation](https://hexdocs.pm/livebook/fips.html).
|
See more details in [the documentation](https://hexdocs.pm/livebook/fips.html).
|
||||||
|
|
||||||
* `LIVEBOOK_FORCE_SSL_HOST` - sets a host to redirect to if the request is not over HTTPS.
|
* `LIVEBOOK_FORCE_SSL_HOST` - sets a host to redirect to if the request is not over HTTPS.
|
||||||
|
|
|
@ -24,20 +24,21 @@ config :mime, :types, %{
|
||||||
}
|
}
|
||||||
|
|
||||||
config :livebook,
|
config :livebook,
|
||||||
teams_url: "https://teams.livebook.dev",
|
|
||||||
agent_name: "livebook-agent",
|
agent_name: "livebook-agent",
|
||||||
|
allowed_uri_schemes: [],
|
||||||
app_service_name: nil,
|
app_service_name: nil,
|
||||||
app_service_url: nil,
|
app_service_url: nil,
|
||||||
authentication_mode: :token,
|
authentication_mode: :token,
|
||||||
|
aws_credentials: false,
|
||||||
|
epmdless: false,
|
||||||
feature_flags: [],
|
feature_flags: [],
|
||||||
force_ssl_host: nil,
|
force_ssl_host: nil,
|
||||||
learn_notebooks: [],
|
learn_notebooks: [],
|
||||||
plugs: [],
|
plugs: [],
|
||||||
shutdown_callback: nil,
|
shutdown_callback: nil,
|
||||||
|
teams_url: "https://teams.livebook.dev",
|
||||||
update_instructions_url: nil,
|
update_instructions_url: nil,
|
||||||
within_iframe: false,
|
within_iframe: false
|
||||||
allowed_uri_schemes: [],
|
|
||||||
aws_credentials: false
|
|
||||||
|
|
||||||
config :livebook, Livebook.Apps.Manager, retry_backoff_base_ms: 5_000
|
config :livebook, Livebook.Apps.Manager, retry_backoff_base_ms: 5_000
|
||||||
|
|
||||||
|
|
|
@ -149,6 +149,10 @@ defmodule Livebook do
|
||||||
config :livebook, :aws_credentials, true
|
config :livebook, :aws_credentials, true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if Livebook.Config.boolean!("LIVEBOOK_EPMDLESS", false) do
|
||||||
|
config :livebook, :epmdless, true
|
||||||
|
end
|
||||||
|
|
||||||
config :livebook,
|
config :livebook,
|
||||||
:default_runtime,
|
:default_runtime,
|
||||||
Livebook.Config.default_runtime!("LIVEBOOK_DEFAULT_RUNTIME") ||
|
Livebook.Config.default_runtime!("LIVEBOOK_DEFAULT_RUNTIME") ||
|
||||||
|
|
|
@ -6,8 +6,16 @@ defmodule Livebook.Application do
|
||||||
setup_optional_dependencies()
|
setup_optional_dependencies()
|
||||||
ensure_directories!()
|
ensure_directories!()
|
||||||
set_local_file_system!()
|
set_local_file_system!()
|
||||||
ensure_distribution!()
|
|
||||||
validate_hostname_resolution!()
|
if Application.fetch_env!(:livebook, :epmdless) do
|
||||||
|
validate_epmdless!()
|
||||||
|
ensure_distribution!()
|
||||||
|
else
|
||||||
|
ensure_epmd!()
|
||||||
|
ensure_distribution!()
|
||||||
|
validate_hostname_resolution!()
|
||||||
|
end
|
||||||
|
|
||||||
set_cookie()
|
set_cookie()
|
||||||
|
|
||||||
children =
|
children =
|
||||||
|
@ -38,7 +46,7 @@ defmodule Livebook.Application do
|
||||||
# Start the tracker server for sessions and apps on this node
|
# Start the tracker server for sessions and apps on this node
|
||||||
{Livebook.Tracker, pubsub_server: Livebook.PubSub},
|
{Livebook.Tracker, pubsub_server: Livebook.PubSub},
|
||||||
# Start the node pool for managing node names
|
# Start the node pool for managing node names
|
||||||
Livebook.Runtime.NodePool,
|
Livebook.EPMD.NodePool,
|
||||||
# Start the server responsible for associating files with sessions
|
# Start the server responsible for associating files with sessions
|
||||||
Livebook.Session.FileGuard,
|
Livebook.Session.FileGuard,
|
||||||
# Start the supervisor dynamically managing sessions
|
# Start the supervisor dynamically managing sessions
|
||||||
|
@ -117,7 +125,21 @@ defmodule Livebook.Application do
|
||||||
:persistent_term.put(:livebook_local_file_system, local_file_system)
|
:persistent_term.put(:livebook_local_file_system, local_file_system)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp ensure_distribution!() do
|
defp validate_epmdless!() do
|
||||||
|
with {:ok, [[~c"Elixir.Livebook.EPMD"]]} <- :init.get_argument(:epmd_module),
|
||||||
|
{:ok, [[~c"false"]]} <- :init.get_argument(:start_epmd),
|
||||||
|
{:ok, [[~c"0"]]} <- :init.get_argument(:erl_epmd_port) do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
Livebook.Config.abort!("""
|
||||||
|
You must specify ELIXIR_ERL_OPTIONS=\"-epmd_module Elixir.Livebook.EPMD -start_epmd false -erl_epmd_port 0\" with LIVEBOOK_EPMDLESS. \
|
||||||
|
The epmd module can be found inside #{Application.app_dir(:livebook, "priv/ebin")}.
|
||||||
|
""")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp ensure_epmd!() do
|
||||||
unless Node.alive?() do
|
unless Node.alive?() do
|
||||||
case System.cmd("epmd", ["-daemon"]) do
|
case System.cmd("epmd", ["-daemon"]) do
|
||||||
{_, 0} ->
|
{_, 0} ->
|
||||||
|
@ -125,7 +147,7 @@ defmodule Livebook.Application do
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
Livebook.Config.abort!("""
|
Livebook.Config.abort!("""
|
||||||
Could not start epmd (Erlang Port Mapper Driver). Livebook uses epmd to \
|
Could not start epmd (Erlang Port Mapper Daemon). Livebook uses epmd to \
|
||||||
talk to different runtimes. You may have to start epmd explicitly by calling:
|
talk to different runtimes. You may have to start epmd explicitly by calling:
|
||||||
|
|
||||||
epmd -daemon
|
epmd -daemon
|
||||||
|
@ -137,7 +159,11 @@ defmodule Livebook.Application do
|
||||||
Then you can try booting Livebook again
|
Then you can try booting Livebook again
|
||||||
""")
|
""")
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp ensure_distribution!() do
|
||||||
|
unless Node.alive?() do
|
||||||
{type, name} = get_node_type_and_name()
|
{type, name} = get_node_type_and_name()
|
||||||
|
|
||||||
case Node.start(name, type) do
|
case Node.start(name, type) do
|
||||||
|
@ -393,6 +419,11 @@ defmodule Livebook.Application do
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# We set ELIXIR_ERL_OPTIONS when LIVEBOOK_EPMDLESS is set to true.
|
||||||
|
# By design, we don't allow ELIXIR_ERL_OPTIONS to pass through.
|
||||||
|
# Use ERL_AFLAGS and ERL_ZFLAGS if you want to configure both
|
||||||
|
# Livebook and spawned runtimes.
|
||||||
|
defp config_env_var?("ELIXIR_ERL_OPTIONS"), do: true
|
||||||
defp config_env_var?("LIVEBOOK_" <> _), do: true
|
defp config_env_var?("LIVEBOOK_" <> _), do: true
|
||||||
defp config_env_var?("RELEASE_" <> _), do: true
|
defp config_env_var?("RELEASE_" <> _), do: true
|
||||||
defp config_env_var?("MIX_ENV"), do: true
|
defp config_env_var?("MIX_ENV"), do: true
|
||||||
|
|
|
@ -471,8 +471,8 @@ defmodule Livebook.Config do
|
||||||
def port!(env) do
|
def port!(env) do
|
||||||
if port = System.get_env(env) do
|
if port = System.get_env(env) do
|
||||||
case Integer.parse(port) do
|
case Integer.parse(port) do
|
||||||
{port, ""} -> port
|
{port, ""} when port >= 0 -> port
|
||||||
:error -> abort!("expected #{env} to be an integer, got: #{inspect(port)}")
|
:error -> abort!("expected #{env} to be a non-negative integer, got: #{inspect(port)}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
111
lib/livebook/epmd.ex
Normal file
111
lib/livebook/epmd.ex
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
defmodule Livebook.EPMD do
|
||||||
|
# A custom EPMD module used to bypass the epmd OS daemon
|
||||||
|
# on both Livebook and the runtimes.
|
||||||
|
@after_compile __MODULE__
|
||||||
|
|
||||||
|
# From Erlang/OTP 23+
|
||||||
|
@epmd_dist_version 6
|
||||||
|
@external_resource "priv/epmd/Elixir.Livebook.EPMD.beam"
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets a random child node name.
|
||||||
|
"""
|
||||||
|
def random_child_node do
|
||||||
|
String.to_atom(Livebook.EPMD.NodePool.get_name())
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Updates the port information for the given node.
|
||||||
|
"""
|
||||||
|
def update_child_node(node, port) do
|
||||||
|
Livebook.EPMD.NodePool.update_name(Atom.to_string(node), port)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the Livebook distribution port, if Livebook.EPMD is running, otherwise 0.
|
||||||
|
"""
|
||||||
|
def dist_port do
|
||||||
|
:persistent_term.get(:livebook_dist_port, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Custom EPMD callbacks
|
||||||
|
|
||||||
|
# Custom callback that registers the parent information.
|
||||||
|
# We read this information when trying to connect to the parent.
|
||||||
|
def start_link() do
|
||||||
|
with {:ok, [[node, port]]} <- :init.get_argument(:livebook_parent) do
|
||||||
|
[name, host] = :string.split(node, ~c"@")
|
||||||
|
|
||||||
|
:persistent_term.put(
|
||||||
|
:livebook_parent,
|
||||||
|
{name, host, List.to_atom(node), List.to_integer(port)}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
:erl_epmd.start_link()
|
||||||
|
end
|
||||||
|
|
||||||
|
# Custom callback to register our current node port.
|
||||||
|
def register_node(name, port), do: register_node(name, port, :inet)
|
||||||
|
|
||||||
|
def register_node(name, port, family) do
|
||||||
|
:persistent_term.put(:livebook_dist_port, port)
|
||||||
|
:erl_epmd.register_node(name, port, family)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Custom callback that accesses the parent information.
|
||||||
|
def port_please(name, host), do: port_please(name, host, :infinity)
|
||||||
|
|
||||||
|
def port_please(name, host, timeout) do
|
||||||
|
case livebook_port(name) do
|
||||||
|
0 -> :erl_epmd.port_please(name, host, timeout)
|
||||||
|
port -> {:port, port, @epmd_dist_version}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# If we are running inside a Livebook Runtime,
|
||||||
|
# we should be able to reach the parent directly
|
||||||
|
# or reach siblings through the parent.
|
||||||
|
defp livebook_port(name) do
|
||||||
|
case :persistent_term.get(:livebook_parent, nil) do
|
||||||
|
{parent_name, parent_host, parent_node, parent_port} ->
|
||||||
|
case match_name(name, parent_name) do
|
||||||
|
:parent -> parent_port
|
||||||
|
:sibling -> sibling_port(parent_node, name, parent_host)
|
||||||
|
:none -> 0
|
||||||
|
end
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp match_name([x | name], [x | parent_name]), do: match_name(name, parent_name)
|
||||||
|
defp match_name([?-, ?- | _name], _parent), do: :sibling
|
||||||
|
defp match_name([], []), do: :parent
|
||||||
|
defp match_name(_name, _parent), do: :none
|
||||||
|
|
||||||
|
defp sibling_port(parent_node, name, host) do
|
||||||
|
:gen_server.call(
|
||||||
|
{Livebook.EPMD.NodePool, parent_node},
|
||||||
|
{:get_port, :erlang.list_to_binary(name ++ [?@] ++ host)},
|
||||||
|
5000
|
||||||
|
)
|
||||||
|
catch
|
||||||
|
_, _ -> 0
|
||||||
|
end
|
||||||
|
|
||||||
|
# Default EPMD callbacks
|
||||||
|
|
||||||
|
defdelegate listen_port_please(name, host), to: :erl_epmd
|
||||||
|
defdelegate names(host_name), to: :erl_epmd
|
||||||
|
defdelegate address_please(name, host, address_family), to: :erl_epmd
|
||||||
|
|
||||||
|
# Store .beam file in priv as well
|
||||||
|
|
||||||
|
def __after_compile__(_env, binary) do
|
||||||
|
File.mkdir_p!("priv/epmd")
|
||||||
|
File.write!("priv/epmd/Elixir.Livebook.EPMD.beam", binary)
|
||||||
|
Mix.Project.build_structure()
|
||||||
|
end
|
||||||
|
end
|
142
lib/livebook/epmd/node_pool.ex
Normal file
142
lib/livebook/epmd/node_pool.ex
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
defmodule Livebook.EPMD.NodePool do
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
# A pool with generated node names.
|
||||||
|
#
|
||||||
|
# The names are randomly generated, however to avoid atom exhaustion
|
||||||
|
# unused names return back to the pool and can be reused later.
|
||||||
|
|
||||||
|
@default_time 60_000
|
||||||
|
|
||||||
|
# Client interface
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Starts the GenServer from a Supervision tree
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
* `:name` - the name to register the pool process under. Defaults
|
||||||
|
to `Livebook.Runtime.NodePool`
|
||||||
|
|
||||||
|
* `:buffer_time` - the time that is awaited before a disconnected
|
||||||
|
node's name is added to pool. Defaults to 1 minute
|
||||||
|
|
||||||
|
"""
|
||||||
|
def start_link(opts) do
|
||||||
|
name = opts[:name] || __MODULE__
|
||||||
|
buffer_time = opts[:buffer_time] || @default_time
|
||||||
|
|
||||||
|
GenServer.start_link(
|
||||||
|
__MODULE__,
|
||||||
|
%{buffer_time: buffer_time},
|
||||||
|
name: name
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns a node name.
|
||||||
|
|
||||||
|
Generates a new name if pool is empty, or takes one from pool.
|
||||||
|
"""
|
||||||
|
def get_name(server \\ __MODULE__) do
|
||||||
|
GenServer.call(server, :get_name, :infinity)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns port for the given name.
|
||||||
|
"""
|
||||||
|
def get_port(server \\ __MODULE__, name) do
|
||||||
|
GenServer.call(server, {:get_port, name}, :infinity)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Updates a port for a name.
|
||||||
|
"""
|
||||||
|
def update_name(server \\ __MODULE__, name, port) do
|
||||||
|
GenServer.call(server, {:update_name, name, port}, :infinity)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Server side code
|
||||||
|
|
||||||
|
@impl GenServer
|
||||||
|
def init(opts) do
|
||||||
|
:net_kernel.monitor_nodes(true, node_type: :all)
|
||||||
|
[name, host] = node() |> Atom.to_string() |> :binary.split("@")
|
||||||
|
|
||||||
|
state = %{
|
||||||
|
buffer_time: opts.buffer_time,
|
||||||
|
active_names: %{},
|
||||||
|
free_names: [],
|
||||||
|
prefix: name,
|
||||||
|
host: host
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl GenServer
|
||||||
|
def handle_call(:get_name, _, state) do
|
||||||
|
{name, state} = server_get_name(state)
|
||||||
|
{:reply, name, put_in(state.active_names[name], 0)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl GenServer
|
||||||
|
def handle_call({:get_port, name}, _, state) do
|
||||||
|
{:reply, Map.get(state.active_names, name, 0), state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl GenServer
|
||||||
|
def handle_call({:update_name, name, port}, _, state) do
|
||||||
|
{:reply, :ok, server_update_name(name, port, state)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl GenServer
|
||||||
|
def handle_info({:nodedown, node, _info}, state) do
|
||||||
|
case state.buffer_time do
|
||||||
|
0 -> send(self(), {:release_node, node})
|
||||||
|
t -> Process.send_after(self(), {:release_node, node}, t)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl GenServer
|
||||||
|
def handle_info({:nodeup, _node, _info}, state) do
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl GenServer
|
||||||
|
def handle_info({:release_node, node}, state) do
|
||||||
|
{:noreply, server_release_name(Atom.to_string(node), state)}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper functions
|
||||||
|
|
||||||
|
defp server_get_name(state) do
|
||||||
|
case state.free_names do
|
||||||
|
[] -> {server_generate_name(state), state}
|
||||||
|
[name | free_names] -> {name, %{state | free_names: free_names}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp server_update_name(name, port, state) do
|
||||||
|
case state.active_names do
|
||||||
|
%{^name => _} -> put_in(state.active_names[name], port)
|
||||||
|
%{} -> state
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp server_generate_name(%{prefix: prefix, host: host}) do
|
||||||
|
"#{prefix}--#{Livebook.Utils.random_short_id()}@#{host}"
|
||||||
|
end
|
||||||
|
|
||||||
|
defp server_release_name(name, state) do
|
||||||
|
{port, state} = pop_in(state.active_names[name])
|
||||||
|
|
||||||
|
if port do
|
||||||
|
%{state | free_names: [name | state.free_names]}
|
||||||
|
else
|
||||||
|
state
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -39,12 +39,9 @@ defmodule Livebook.Runtime.ElixirStandalone do
|
||||||
"""
|
"""
|
||||||
@spec connect(t()) :: {:ok, t()} | {:error, String.t()}
|
@spec connect(t()) :: {:ok, t()} | {:error, String.t()}
|
||||||
def connect(runtime) do
|
def connect(runtime) do
|
||||||
parent_node = node()
|
child_node = Livebook.EPMD.random_child_node()
|
||||||
child_node = child_node_name(parent_node)
|
|
||||||
|
|
||||||
Utils.temporarily_register(self(), child_node, fn ->
|
Utils.temporarily_register(self(), child_node, fn ->
|
||||||
argv = [parent_node]
|
|
||||||
|
|
||||||
init_opts = [
|
init_opts = [
|
||||||
runtime_server_opts: [
|
runtime_server_opts: [
|
||||||
extra_smart_cell_definitions: Livebook.Runtime.Definitions.smart_cell_definitions()
|
extra_smart_cell_definitions: Livebook.Runtime.Definitions.smart_cell_definitions()
|
||||||
|
@ -52,7 +49,7 @@ defmodule Livebook.Runtime.ElixirStandalone do
|
||||||
]
|
]
|
||||||
|
|
||||||
with {:ok, elixir_path} <- find_elixir_executable(),
|
with {:ok, elixir_path} <- find_elixir_executable(),
|
||||||
port = start_elixir_node(elixir_path, child_node, child_node_eval_string(), argv),
|
port = start_elixir_node(elixir_path, child_node),
|
||||||
{:ok, server_pid} <- parent_init_sequence(child_node, port, init_opts: init_opts) do
|
{:ok, server_pid} <- parent_init_sequence(child_node, port, init_opts: init_opts) do
|
||||||
runtime = %{runtime | node: child_node, server_pid: server_pid}
|
runtime = %{runtime | node: child_node, server_pid: server_pid}
|
||||||
{:ok, runtime}
|
{:ok, runtime}
|
||||||
|
@ -62,7 +59,7 @@ defmodule Livebook.Runtime.ElixirStandalone do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp start_elixir_node(elixir_path, node_name, eval, argv) do
|
defp start_elixir_node(elixir_path, node_name) 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,
|
||||||
|
@ -71,7 +68,7 @@ defmodule Livebook.Runtime.ElixirStandalone do
|
||||||
# to the terminal
|
# to the terminal
|
||||||
:nouse_stdio,
|
:nouse_stdio,
|
||||||
:hide,
|
:hide,
|
||||||
args: elixir_flags(node_name) ++ ["--eval", eval, "--" | Enum.map(argv, &to_string/1)]
|
args: elixir_flags(node_name)
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,108 +0,0 @@
|
||||||
defmodule Livebook.Runtime.NodePool do
|
|
||||||
use GenServer
|
|
||||||
|
|
||||||
# A pool with generated node names.
|
|
||||||
#
|
|
||||||
# The names are randomly generated, however to avoid atom exhaustion
|
|
||||||
# unused names return back to the pool and can be reused later.
|
|
||||||
|
|
||||||
@default_time 60_000
|
|
||||||
|
|
||||||
# Client interface
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Starts the GenServer from a Supervision tree
|
|
||||||
|
|
||||||
## Options
|
|
||||||
|
|
||||||
* `:name` - the name to register the pool process under. Defaults
|
|
||||||
to `Livebook.Runtime.NodePool`
|
|
||||||
|
|
||||||
* `:buffer_time` - the time that is awaited before a disconnected
|
|
||||||
node's name is added to pool. Defaults to 1 minute
|
|
||||||
|
|
||||||
"""
|
|
||||||
def start_link(opts) do
|
|
||||||
name = opts[:name] || __MODULE__
|
|
||||||
buffer_time = opts[:buffer_time] || @default_time
|
|
||||||
|
|
||||||
GenServer.start_link(
|
|
||||||
__MODULE__,
|
|
||||||
%{buffer_time: buffer_time},
|
|
||||||
name: name
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Returns a node name.
|
|
||||||
|
|
||||||
Generates a new name if pool is empty, or takes one from pool.
|
|
||||||
"""
|
|
||||||
def get_name(server \\ __MODULE__, basename) do
|
|
||||||
GenServer.call(server, {:get_name, basename})
|
|
||||||
end
|
|
||||||
|
|
||||||
# Server side code
|
|
||||||
|
|
||||||
@impl GenServer
|
|
||||||
def init(opts) do
|
|
||||||
:net_kernel.monitor_nodes(true, node_type: :all)
|
|
||||||
{:ok, %{buffer_time: opts.buffer_time, generated_names: MapSet.new(), free_names: []}}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl GenServer
|
|
||||||
def handle_call({:get_name, basename}, _, state) do
|
|
||||||
{name, new_state} = name(state, basename)
|
|
||||||
{:reply, name, new_state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl GenServer
|
|
||||||
def handle_info({:nodedown, node, _info}, state) do
|
|
||||||
case state.buffer_time do
|
|
||||||
0 -> send(self(), {:add_node, node})
|
|
||||||
t -> Process.send_after(self(), {:add_node, node}, t)
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl GenServer
|
|
||||||
def handle_info({:nodeup, _node, _info}, state) do
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl GenServer
|
|
||||||
def handle_info({:add_node, node}, state) do
|
|
||||||
{:noreply, add_node(state, node)}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper functions
|
|
||||||
|
|
||||||
defp name(state, basename) do
|
|
||||||
if Enum.empty?(state.free_names) do
|
|
||||||
generate_name(state, basename)
|
|
||||||
else
|
|
||||||
get_existing_name(state)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp generate_name(state, basename) do
|
|
||||||
new_name = :"#{Livebook.Utils.random_short_id()}-#{basename}"
|
|
||||||
generated_names = MapSet.put(state.generated_names, new_name)
|
|
||||||
{new_name, %{state | generated_names: generated_names}}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp get_existing_name(state) do
|
|
||||||
[name | free_names] = state.free_names
|
|
||||||
{name, %{state | free_names: free_names}}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp add_node(state, node) do
|
|
||||||
if MapSet.member?(state.generated_names, node) do
|
|
||||||
free_names = [node | state.free_names]
|
|
||||||
%{state | free_names: free_names}
|
|
||||||
else
|
|
||||||
state
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,17 +1,8 @@
|
||||||
defmodule Livebook.Runtime.StandaloneInit do
|
defmodule Livebook.Runtime.StandaloneInit do
|
||||||
|
# TODO: Move logic inside ElixirStandalone module.
|
||||||
# Generic functionality related to starting and setting up
|
# Generic functionality related to starting and setting up
|
||||||
# a new Elixir system process. It's used by ElixirStandalone.
|
# a new Elixir system process. It's used by ElixirStandalone.
|
||||||
|
|
||||||
alias Livebook.Runtime.NodePool
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Returns a random name for a dynamically spawned node.
|
|
||||||
"""
|
|
||||||
@spec child_node_name(atom()) :: atom()
|
|
||||||
def child_node_name(parent) do
|
|
||||||
NodePool.get_name(parent)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Tries locating Elixir executable in PATH.
|
Tries locating Elixir executable in PATH.
|
||||||
"""
|
"""
|
||||||
|
@ -23,30 +14,6 @@ defmodule Livebook.Runtime.StandaloneInit do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
|
||||||
A list of common flags used for spawned Elixir runtimes.
|
|
||||||
"""
|
|
||||||
@spec elixir_flags(node()) :: list()
|
|
||||||
def elixir_flags(node_name) do
|
|
||||||
[
|
|
||||||
if(Livebook.Config.longname(), do: "--name", else: "--sname"),
|
|
||||||
to_string(node_name),
|
|
||||||
"--erl",
|
|
||||||
# Minimize schedulers busy wait threshold,
|
|
||||||
# so that they go to sleep immediately after evaluation.
|
|
||||||
# Increase the default stack for dirty io threads (cuda requires it).
|
|
||||||
# Enable ANSI escape codes as we handle them with HTML.
|
|
||||||
# Disable stdin, so that the system process never tries to read
|
|
||||||
# any input from the terminal.
|
|
||||||
"+sbwt none +sbwtdcpu none +sbwtdio none +sssdio 128 -elixir ansi_enabled true -noinput",
|
|
||||||
# Make the node hidden, so it doesn't automatically join the cluster
|
|
||||||
"--hidden",
|
|
||||||
# Use the cookie in Livebook
|
|
||||||
"--cookie",
|
|
||||||
Atom.to_string(Node.get_cookie())
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
# ---
|
# ---
|
||||||
#
|
#
|
||||||
# Once the new node is spawned we need to establish a connection,
|
# Once the new node is spawned we need to establish a connection,
|
||||||
|
@ -88,9 +55,9 @@ defmodule Livebook.Runtime.StandaloneInit do
|
||||||
|
|
||||||
loop = fn loop ->
|
loop = fn loop ->
|
||||||
receive do
|
receive do
|
||||||
{:node_started, init_ref, ^child_node, primary_pid} ->
|
{:node_started, init_ref, ^child_node, child_port, primary_pid} ->
|
||||||
Port.demonitor(port_ref)
|
Port.demonitor(port_ref)
|
||||||
|
Livebook.EPMD.update_child_node(child_node, child_port)
|
||||||
server_pid = Livebook.Runtime.ErlDist.initialize(child_node, opts[:init_opts] || [])
|
server_pid = Livebook.Runtime.ErlDist.initialize(child_node, opts[:init_opts] || [])
|
||||||
|
|
||||||
send(primary_pid, {:node_initialized, init_ref})
|
send(primary_pid, {:node_initialized, init_ref})
|
||||||
|
@ -117,13 +84,18 @@ defmodule Livebook.Runtime.StandaloneInit do
|
||||||
# so the string cannot have constructs newlines nor strings. That's why we pass
|
# so the string cannot have constructs newlines nor strings. That's why we pass
|
||||||
# the parent node name as ARGV and write the code avoiding newlines.
|
# the parent node name as ARGV and write the code avoiding newlines.
|
||||||
#
|
#
|
||||||
|
# This boot script must be kept in sync with Livebook.EPMD.
|
||||||
|
#
|
||||||
# Also note that we explicitly halt, just in case `System.no_halt(true)` is
|
# Also note that we explicitly halt, just in case `System.no_halt(true)` is
|
||||||
# called within the runtime.
|
# called within the runtime.
|
||||||
@child_node_eval_string """
|
@child_node_eval_string """
|
||||||
[parent_node] = System.argv();\
|
{:ok, [[mode, node]]} = :init.get_argument(:livebook_current);\
|
||||||
|
{:ok, _} = :net_kernel.start(List.to_atom(node), %{name_domain: List.to_atom(mode)});\
|
||||||
|
{:ok, [[parent_node, _port]]} = :init.get_argument(:livebook_parent);\
|
||||||
|
dist_port = :persistent_term.get(:livebook_dist_port, 0);\
|
||||||
init_ref = make_ref();\
|
init_ref = make_ref();\
|
||||||
parent_process = {node(), String.to_atom(parent_node)};\
|
parent_process = {node(), List.to_atom(parent_node)};\
|
||||||
send(parent_process, {:node_started, init_ref, node(), self()});\
|
send(parent_process, {:node_started, init_ref, node(), dist_port, self()});\
|
||||||
receive do {:node_initialized, ^init_ref} ->\
|
receive do {:node_initialized, ^init_ref} ->\
|
||||||
manager_ref = Process.monitor(Livebook.Runtime.ErlDist.NodeManager);\
|
manager_ref = Process.monitor(Livebook.Runtime.ErlDist.NodeManager);\
|
||||||
receive do {:DOWN, ^manager_ref, :process, _object, _reason} -> :ok end;\
|
receive do {:DOWN, ^manager_ref, :process, _object, _reason} -> :ok end;\
|
||||||
|
@ -138,12 +110,42 @@ defmodule Livebook.Runtime.StandaloneInit do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Performs the child side of the initialization contract.
|
A list of common flags used for spawned Elixir runtimes.
|
||||||
|
|
||||||
This function returns AST that should be evaluated in primary
|
|
||||||
process on the newly spawned child node. The executed code expects
|
|
||||||
the parent_node on ARGV. The process on the parent node is assumed
|
|
||||||
to have the same name as the child node.
|
|
||||||
"""
|
"""
|
||||||
def child_node_eval_string(), do: @child_node_eval_string
|
@spec elixir_flags(node()) :: list()
|
||||||
|
def elixir_flags(node_name) do
|
||||||
|
parent_name = node()
|
||||||
|
parent_port = Livebook.EPMD.dist_port()
|
||||||
|
|
||||||
|
mode = if Livebook.Config.longname(), do: :longnames, else: :shortnames
|
||||||
|
|
||||||
|
epmdless_flags =
|
||||||
|
if parent_port != 0 do
|
||||||
|
"-epmd_module Elixir.Livebook.EPMD -start_epmd false -erl_epmd_port 0 "
|
||||||
|
else
|
||||||
|
""
|
||||||
|
end
|
||||||
|
|
||||||
|
[
|
||||||
|
"--erl",
|
||||||
|
# Minimize schedulers busy wait threshold,
|
||||||
|
# so that they go to sleep immediately after evaluation.
|
||||||
|
# Increase the default stack for dirty io threads (cuda requires it).
|
||||||
|
# Enable ANSI escape codes as we handle them with HTML.
|
||||||
|
# Disable stdin, so that the system process never tries to read terminal input.
|
||||||
|
"+sbwt none +sbwtdcpu none +sbwtdio none +sssdio 128 -elixir ansi_enabled true -noinput " <>
|
||||||
|
epmdless_flags <>
|
||||||
|
"-livebook_parent #{parent_name} #{parent_port} -livebook_current #{mode} #{node_name}",
|
||||||
|
# Add the location of Livebook.EPMD
|
||||||
|
"-pa",
|
||||||
|
Application.app_dir(:livebook, "priv/epmd"),
|
||||||
|
# Make the node hidden, so it doesn't automatically join the cluster
|
||||||
|
"--hidden",
|
||||||
|
# Use the cookie in Livebook
|
||||||
|
"--cookie",
|
||||||
|
Atom.to_string(Node.get_cookie()),
|
||||||
|
"--eval",
|
||||||
|
@child_node_eval_string
|
||||||
|
]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,13 +2,22 @@ if exist "!USERPROFILE!\.livebookdesktop.bat" (
|
||||||
call "!USERPROFILE!\.livebookdesktop.bat"
|
call "!USERPROFILE!\.livebookdesktop.bat"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not defined LIVEBOOK_EPMDLESS set LIVEBOOK_EPMDLESS=true
|
||||||
|
if "!LIVEBOOK_EPMDLESS!"=="1" goto epmdless
|
||||||
|
if "!LIVEBOOK_EPMDLESS!"=="true" goto epmdless
|
||||||
|
goto continue
|
||||||
|
|
||||||
|
:epmdless
|
||||||
|
set ELIXIR_ERL_OPTIONS=!ELIXIR_ERL_OPTIONS! -epmd_module Elixir.Livebook.EPMD -start_epmd false -erl_epmd_port 0
|
||||||
|
|
||||||
|
:continue
|
||||||
for /f "skip=1" %%X in ('wmic os get localdatetime') do if not defined TIMESTAMP set TIMESTAMP=%%X
|
for /f "skip=1" %%X in ('wmic os get localdatetime') do if not defined TIMESTAMP set TIMESTAMP=%%X
|
||||||
|
|
||||||
if defined LIVEBOOK_DISTRIBUTION set RELEASE_DISTRIBUTION="!LIVEBOOK_DISTRIBUTION!"
|
if defined LIVEBOOK_DISTRIBUTION set RELEASE_DISTRIBUTION=!LIVEBOOK_DISTRIBUTION!
|
||||||
if not defined RELEASE_DISTRIBUTION set RELEASE_DISTRIBUTION="sname"
|
if not defined RELEASE_DISTRIBUTION set RELEASE_DISTRIBUTION=sname
|
||||||
|
|
||||||
if defined LIVEBOOK_NODE set RELEASE_NODE="!LIVEBOOK_NODE!"
|
if defined LIVEBOOK_NODE set RELEASE_NODE=!LIVEBOOK_NODE!
|
||||||
if not defined RELEASE_NODE set RELEASE_NODE="livebook-app-!TIMESTAMP:~8,6!-!RANDOM!"
|
if not defined RELEASE_NODE set RELEASE_NODE=livebook-app-!TIMESTAMP:~8,6!-!RANDOM!
|
||||||
|
|
||||||
set RELEASE_MODE=interactive
|
set RELEASE_MODE=interactive
|
||||||
set MIX_ARCHIVES=!RELEASE_ROOT!\vendor\archives
|
set MIX_ARCHIVES=!RELEASE_ROOT!\vendor\archives
|
||||||
|
|
|
@ -2,22 +2,21 @@ if [ -f "$HOME/.livebookdesktop.sh" ]; then
|
||||||
. "$HOME/.livebookdesktop.sh"
|
. "$HOME/.livebookdesktop.sh"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
hostname=`hostname`
|
|
||||||
if [[ "$hostname" =~ " " ]]; then
|
|
||||||
echo "[error] system hostname ($hostname) cannot contain whitespaces"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
export RELEASE_DISTRIBUTION=${LIVEBOOK_DISTRIBUTION:-${RELEASE_DISTRIBUTION:-"sname"}}
|
export RELEASE_DISTRIBUTION=${LIVEBOOK_DISTRIBUTION:-${RELEASE_DISTRIBUTION:-"sname"}}
|
||||||
export RELEASE_NODE=${LIVEBOOK_NODE:-${RELEASE_NODE:-"livebook-app-$(cat /dev/urandom | env LC_ALL=C tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1)"}}
|
export RELEASE_NODE=${LIVEBOOK_NODE:-${RELEASE_NODE:-"livebook-app-$(cat /dev/urandom | env LC_ALL=C tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1)"}}
|
||||||
export RELEASE_MODE=interactive
|
export RELEASE_MODE=interactive
|
||||||
export MIX_ARCHIVES="${RELEASE_ROOT}/vendor/archives"
|
export MIX_ARCHIVES="${RELEASE_ROOT}/vendor/archives"
|
||||||
export MIX_REBAR3="${RELEASE_ROOT}/vendor/rebar3"
|
export MIX_REBAR3="${RELEASE_ROOT}/vendor/rebar3"
|
||||||
|
export LIVEBOOK_EPMDLESS=${LIVEBOOK_EPMDLESS:-true}
|
||||||
export LIVEBOOK_SHUTDOWN_ENABLED=${LIVEBOOK_SHUTDOWN_ENABLED:-true}
|
export LIVEBOOK_SHUTDOWN_ENABLED=${LIVEBOOK_SHUTDOWN_ENABLED:-true}
|
||||||
export LIVEBOOK_DESKTOP=true
|
export LIVEBOOK_DESKTOP=true
|
||||||
[ -z "$LIVEBOOK_PORT" ] && export LIVEBOOK_PORT=0
|
[ -z "$LIVEBOOK_PORT" ] && export LIVEBOOK_PORT=0
|
||||||
export PATH="$RELEASE_ROOT/vendor/otp/erts-<%= @release.erts_version%>/bin:$RELEASE_ROOT/vendor/otp/bin:$RELEASE_ROOT/vendor/elixir/bin:$PATH"
|
export PATH="$RELEASE_ROOT/vendor/otp/erts-<%= @release.erts_version%>/bin:$RELEASE_ROOT/vendor/otp/bin:$RELEASE_ROOT/vendor/elixir/bin:$PATH"
|
||||||
|
|
||||||
|
if [[ "$LIVEBOOK_EPMDLESS" == "true" || "$LIVEBOOK_EPMDLESS" == "1" ]]; then
|
||||||
|
export ELIXIR_ERL_OPTIONS="${ELIXIR_ERL_OPTIONS} -epmd_module Elixir.Livebook.EPMD -start_epmd false -erl_epmd_port 0"
|
||||||
|
fi
|
||||||
|
|
||||||
cookie_path="${RELEASE_ROOT}/releases/COOKIE"
|
cookie_path="${RELEASE_ROOT}/releases/COOKIE"
|
||||||
if [ ! -f $cookie_path ]; then
|
if [ ! -f $cookie_path ]; then
|
||||||
RELEASE_COOKIE=$(cat /dev/urandom | env LC_ALL=C tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
|
RELEASE_COOKIE=$(cat /dev/urandom | env LC_ALL=C tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
|
||||||
|
|
|
@ -2,6 +2,14 @@ if exist "!RELEASE_ROOT!\user\env.bat" (
|
||||||
call "!RELEASE_ROOT!\user\env.bat"
|
call "!RELEASE_ROOT!\user\env.bat"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if "!LIVEBOOK_EPMDLESS!"=="1" goto epmdless
|
||||||
|
if "!LIVEBOOK_EPMDLESS!"=="true" goto epmdless
|
||||||
|
goto continue
|
||||||
|
|
||||||
|
:epmdless
|
||||||
|
set ELIXIR_ERL_OPTIONS=!ELIXIR_ERL_OPTIONS! -epmd_module Elixir.Livebook.EPMD -start_epmd false -erl_epmd_port 0
|
||||||
|
|
||||||
|
:continue
|
||||||
set RELEASE_MODE=interactive
|
set RELEASE_MODE=interactive
|
||||||
if defined LIVEBOOK_NODE set RELEASE_NODE="!LIVEBOOK_NODE!"
|
if defined LIVEBOOK_NODE set RELEASE_NODE="!LIVEBOOK_NODE!"
|
||||||
if not defined RELEASE_NODE set RELEASE_NODE=livebook_server
|
if not defined RELEASE_NODE set RELEASE_NODE=livebook_server
|
||||||
|
|
|
@ -19,6 +19,10 @@ export RELEASE_MODE=interactive
|
||||||
export RELEASE_NODE=${LIVEBOOK_NODE:-${RELEASE_NODE:-${NODE_DEFAULT}}}
|
export RELEASE_NODE=${LIVEBOOK_NODE:-${RELEASE_NODE:-${NODE_DEFAULT}}}
|
||||||
export RELEASE_DISTRIBUTION=${LIVEBOOK_DISTRIBUTION:-${RELEASE_DISTRIBUTION:-${DISTRIBUTION_DEFAULT}}}
|
export RELEASE_DISTRIBUTION=${LIVEBOOK_DISTRIBUTION:-${RELEASE_DISTRIBUTION:-${DISTRIBUTION_DEFAULT}}}
|
||||||
|
|
||||||
|
if [[ "$LIVEBOOK_EPMDLESS" == "true" || "$LIVEBOOK_EPMDLESS" == "1" ]]; then
|
||||||
|
export ELIXIR_ERL_OPTIONS="${ELIXIR_ERL_OPTIONS} -epmd_module Elixir.Livebook.EPMD -start_epmd false -erl_epmd_port 0"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ ! -z "${LIVEBOOK_COOKIE}" ]; then export RELEASE_COOKIE=${LIVEBOOK_COOKIE}; fi
|
if [ ! -z "${LIVEBOOK_COOKIE}" ]; then export RELEASE_COOKIE=${LIVEBOOK_COOKIE}; fi
|
||||||
cookie_path="${RELEASE_ROOT}/releases/COOKIE"
|
cookie_path="${RELEASE_ROOT}/releases/COOKIE"
|
||||||
if [ ! -f $cookie_path ] && [ -z "$RELEASE_COOKIE" ]; then
|
if [ ! -f $cookie_path ] && [ -z "$RELEASE_COOKIE" ]; then
|
||||||
|
|
96
test/livebook/epmd/node_pool_test.exs
Normal file
96
test/livebook/epmd/node_pool_test.exs
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
defmodule Livebook.EPMD.NodePoolTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Livebook.EPMD.NodePool
|
||||||
|
|
||||||
|
# Note we do not spawn actual nodes as it can be time
|
||||||
|
# intensive (on low spec machines) and is generally
|
||||||
|
# complicated.
|
||||||
|
|
||||||
|
describe "start_link" do
|
||||||
|
test "correctly starts a registered GenServer", config do
|
||||||
|
start_supervised!({NodePool, name: config.test})
|
||||||
|
|
||||||
|
# Verify Process is running
|
||||||
|
assert Process.whereis(config.test)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "get_name/2" do
|
||||||
|
test "creates a new node name if pool is empty", config do
|
||||||
|
start_supervised!({NodePool, name: config.test})
|
||||||
|
|
||||||
|
result = NodePool.get_name(config.test)
|
||||||
|
assert is_binary(result)
|
||||||
|
[name, host] = node() |> Atom.to_string() |> :binary.split("@")
|
||||||
|
assert String.starts_with?(result, name)
|
||||||
|
assert String.ends_with?(result, "@" <> host)
|
||||||
|
assert result != Atom.to_string(node())
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns an existing name if pool is not empty", config do
|
||||||
|
start_supervised!({NodePool, name: config.test, buffer_time: 0})
|
||||||
|
|
||||||
|
name = NodePool.get_name(config.test)
|
||||||
|
nodedown(config.test, String.to_atom(name))
|
||||||
|
|
||||||
|
assert NodePool.get_name(config.test) == name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "removes an existing name when used", config do
|
||||||
|
start_supervised!({NodePool, name: config.test, buffer_time: 0})
|
||||||
|
|
||||||
|
name = NodePool.get_name(config.test)
|
||||||
|
nodedown(config.test, String.to_atom(name))
|
||||||
|
|
||||||
|
name = NodePool.get_name(config.test)
|
||||||
|
assert NodePool.get_name(config.test) != name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update_name/get_port" do
|
||||||
|
test "updates name info and gets port for name", config do
|
||||||
|
start_supervised!({NodePool, name: config.test})
|
||||||
|
|
||||||
|
result = NodePool.get_name(config.test)
|
||||||
|
assert NodePool.get_port(config.test, result) == 0
|
||||||
|
:ok = NodePool.update_name(config.test, result, 12345)
|
||||||
|
assert NodePool.get_port(config.test, result) == 12345
|
||||||
|
end
|
||||||
|
|
||||||
|
test "erases port info on node down", config do
|
||||||
|
start_supervised!({NodePool, name: config.test, buffer_time: 0})
|
||||||
|
|
||||||
|
result = NodePool.get_name(config.test)
|
||||||
|
:ok = NodePool.update_name(config.test, result, 12345)
|
||||||
|
assert NodePool.get_port(config.test, result) == 12345
|
||||||
|
nodedown(config.test, String.to_atom(result))
|
||||||
|
assert NodePool.get_port(config.test, result) == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns no port for unknown names", config do
|
||||||
|
start_supervised!({NodePool, name: config.test})
|
||||||
|
assert NodePool.get_port(config.test, "never-a-name") == 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "on nodedown" do
|
||||||
|
test "does not add node name to pool if not in generated_names", config do
|
||||||
|
start_supervised!({NodePool, name: config.test, buffer_time: 0})
|
||||||
|
nodedown(config.test, :some_foo)
|
||||||
|
assert NodePool.get_name(config.test) != :some_foo
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Emulate node down and make sure it is processed
|
||||||
|
defp nodedown(process, node) when is_atom(node) do
|
||||||
|
send(process, {:nodedown, node, {}})
|
||||||
|
|
||||||
|
# Make sure the send was processed
|
||||||
|
_ = :sys.get_status(process)
|
||||||
|
# Make sure the send after message processed
|
||||||
|
_ = :sys.get_status(process)
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
18
test/livebook/epmd_test.exs
Normal file
18
test/livebook/epmd_test.exs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
defmodule Livebook.EPMDTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
describe "with epmd" do
|
||||||
|
@describetag :with_epmd
|
||||||
|
test "has a random dist port" do
|
||||||
|
assert Livebook.EPMD.dist_port() == 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "without epmd" do
|
||||||
|
@describetag :without_epmd
|
||||||
|
|
||||||
|
test "has a custom dist port" do
|
||||||
|
assert Livebook.EPMD.dist_port() != 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,8 +1,8 @@
|
||||||
defmodule Livebook.RemoteIntellisenseTest do
|
defmodule Livebook.RemoteIntellisenseTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
alias Livebook.Intellisense
|
alias Livebook.Intellisense
|
||||||
|
|
||||||
|
@moduletag :with_epmd
|
||||||
@tmp_dir "tmp/test/remote_intellisense"
|
@tmp_dir "tmp/test/remote_intellisense"
|
||||||
|
|
||||||
# Returns intellisense context resulting from evaluating
|
# Returns intellisense context resulting from evaluating
|
||||||
|
|
|
@ -1,67 +0,0 @@
|
||||||
defmodule Livebook.Runtime.NodePoolTest do
|
|
||||||
use ExUnit.Case, async: true
|
|
||||||
|
|
||||||
alias Livebook.Runtime.NodePool
|
|
||||||
|
|
||||||
# Note we do not spawn actual nodes as it can be time
|
|
||||||
# intensive (on low spec machines) and is generally
|
|
||||||
# complicated.
|
|
||||||
|
|
||||||
describe "start_link" do
|
|
||||||
test "correctly starts a registered GenServer", config do
|
|
||||||
start_supervised!({NodePool, name: config.test})
|
|
||||||
|
|
||||||
# Verify Process is running
|
|
||||||
assert Process.whereis(config.test)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "get_name/2" do
|
|
||||||
test "creates a new node name if pool is empty", config do
|
|
||||||
start_supervised!({NodePool, name: config.test})
|
|
||||||
|
|
||||||
result = NodePool.get_name(config.test, node())
|
|
||||||
assert is_atom(result)
|
|
||||||
assert result |> Atom.to_string() |> String.ends_with?(Atom.to_string(node()))
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns an existing name if pool is not empty", config do
|
|
||||||
start_supervised!({NodePool, name: config.test, buffer_time: 0})
|
|
||||||
|
|
||||||
name = NodePool.get_name(config.test, node())
|
|
||||||
nodedown(config.test, name)
|
|
||||||
|
|
||||||
assert NodePool.get_name(config.test, node()) == name
|
|
||||||
end
|
|
||||||
|
|
||||||
test "removes an existing name when used", config do
|
|
||||||
start_supervised!({NodePool, name: config.test, buffer_time: 0})
|
|
||||||
|
|
||||||
name = NodePool.get_name(config.test, node())
|
|
||||||
nodedown(config.test, name)
|
|
||||||
|
|
||||||
name = NodePool.get_name(config.test, node())
|
|
||||||
assert NodePool.get_name(config.test, node()) != name
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "on nodedown" do
|
|
||||||
test "does not add node name to pool if not in generated_names", config do
|
|
||||||
start_supervised!({NodePool, name: config.test, buffer_time: 0})
|
|
||||||
nodedown(config.test, :some_foo)
|
|
||||||
assert NodePool.get_name(config.test, node()) != :some_foo
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Emulate node down and make sure it is processed
|
|
||||||
defp nodedown(process, node) do
|
|
||||||
send(process, {:nodedown, node, {}})
|
|
||||||
|
|
||||||
# Make sure the send was processed
|
|
||||||
_ = :sys.get_status(process)
|
|
||||||
# Make sure the send after message processed
|
|
||||||
_ = :sys.get_status(process)
|
|
||||||
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -57,21 +57,29 @@ windows? = match?({:win32, _}, :os.type())
|
||||||
|
|
||||||
erl_docs_exclude =
|
erl_docs_exclude =
|
||||||
if match?({:error, _}, Code.fetch_docs(:gen_server)) do
|
if match?({:error, _}, Code.fetch_docs(:gen_server)) do
|
||||||
[erl_docs: true]
|
[:erl_docs]
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
windows_exclude = if windows?, do: [unix: true], else: []
|
windows_exclude = if windows?, do: [:unix], else: []
|
||||||
|
|
||||||
teams_exclude =
|
teams_exclude =
|
||||||
if not Livebook.TeamsServer.available?() do
|
if Livebook.TeamsServer.available?() do
|
||||||
[teams_integration: true]
|
|
||||||
else
|
|
||||||
[]
|
[]
|
||||||
|
else
|
||||||
|
[:teams_integration]
|
||||||
|
end
|
||||||
|
|
||||||
|
# ELIXIR_ERL_OPTIONS="-epmd_module Elixir.Livebook.EPMD -start_epmd false -erl_epmd_port 0" LIVEBOOK_EPMDLESS=true mix test
|
||||||
|
epmd_exclude =
|
||||||
|
if Application.fetch_env!(:livebook, :epmdless) do
|
||||||
|
[:with_epmd, :teams_integration]
|
||||||
|
else
|
||||||
|
[:without_epmd]
|
||||||
end
|
end
|
||||||
|
|
||||||
ExUnit.start(
|
ExUnit.start(
|
||||||
assert_receive_timeout: if(windows?, do: 2_500, else: 1_500),
|
assert_receive_timeout: if(windows?, do: 2_500, else: 1_500),
|
||||||
exclude: erl_docs_exclude ++ windows_exclude ++ teams_exclude
|
exclude: erl_docs_exclude ++ windows_exclude ++ teams_exclude ++ epmd_exclude
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Reference in a new issue