Run Livebook Desktop without EPMD (#2591)

This commit is contained in:
José Valim 2024-05-08 10:05:01 +02:00 committed by GitHub
parent eb4887657a
commit 6d7f416f18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 550 additions and 275 deletions

View file

@ -6,10 +6,17 @@ on:
- main
- "v*.*"
jobs:
main:
linux:
runs-on: ubuntu-latest
env:
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:
- name: Checkout git repo
uses: actions/checkout@v3
@ -41,23 +48,7 @@ jobs:
run: mix compile --warnings-as-errors
- name: Run tests
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:
runs-on: windows-latest
if: github.event_name == 'push'
@ -134,3 +125,26 @@ jobs:
key: ${{ runner.os }}-elixir-${{ env.elixir }}
- name: Build the app
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
View file

@ -33,3 +33,6 @@ npm-debug.log
# The built Escript
/livebook
# The priv directory with the EPMD file
/priv/epmd

View file

@ -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
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).
* `LIVEBOOK_FORCE_SSL_HOST` - sets a host to redirect to if the request is not over HTTPS.

View file

@ -24,20 +24,21 @@ config :mime, :types, %{
}
config :livebook,
teams_url: "https://teams.livebook.dev",
agent_name: "livebook-agent",
allowed_uri_schemes: [],
app_service_name: nil,
app_service_url: nil,
authentication_mode: :token,
aws_credentials: false,
epmdless: false,
feature_flags: [],
force_ssl_host: nil,
learn_notebooks: [],
plugs: [],
shutdown_callback: nil,
teams_url: "https://teams.livebook.dev",
update_instructions_url: nil,
within_iframe: false,
allowed_uri_schemes: [],
aws_credentials: false
within_iframe: false
config :livebook, Livebook.Apps.Manager, retry_backoff_base_ms: 5_000

View file

@ -149,6 +149,10 @@ defmodule Livebook do
config :livebook, :aws_credentials, true
end
if Livebook.Config.boolean!("LIVEBOOK_EPMDLESS", false) do
config :livebook, :epmdless, true
end
config :livebook,
:default_runtime,
Livebook.Config.default_runtime!("LIVEBOOK_DEFAULT_RUNTIME") ||

View file

@ -6,8 +6,16 @@ defmodule Livebook.Application do
setup_optional_dependencies()
ensure_directories!()
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()
children =
@ -38,7 +46,7 @@ defmodule Livebook.Application do
# Start the tracker server for sessions and apps on this node
{Livebook.Tracker, pubsub_server: Livebook.PubSub},
# Start the node pool for managing node names
Livebook.Runtime.NodePool,
Livebook.EPMD.NodePool,
# Start the server responsible for associating files with sessions
Livebook.Session.FileGuard,
# Start the supervisor dynamically managing sessions
@ -117,7 +125,21 @@ defmodule Livebook.Application do
:persistent_term.put(:livebook_local_file_system, local_file_system)
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
case System.cmd("epmd", ["-daemon"]) do
{_, 0} ->
@ -125,7 +147,7 @@ defmodule Livebook.Application do
_ ->
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:
epmd -daemon
@ -137,7 +159,11 @@ defmodule Livebook.Application do
Then you can try booting Livebook again
""")
end
end
end
defp ensure_distribution!() do
unless Node.alive?() do
{type, name} = get_node_type_and_name()
case Node.start(name, type) do
@ -393,6 +419,11 @@ defmodule Livebook.Application do
})
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?("RELEASE_" <> _), do: true
defp config_env_var?("MIX_ENV"), do: true

View file

@ -471,8 +471,8 @@ defmodule Livebook.Config do
def port!(env) do
if port = System.get_env(env) do
case Integer.parse(port) do
{port, ""} -> port
:error -> abort!("expected #{env} to be an integer, got: #{inspect(port)}")
{port, ""} when port >= 0 -> port
:error -> abort!("expected #{env} to be a non-negative integer, got: #{inspect(port)}")
end
end
end

111
lib/livebook/epmd.ex Normal file
View 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

View 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

View file

@ -39,12 +39,9 @@ defmodule Livebook.Runtime.ElixirStandalone do
"""
@spec connect(t()) :: {:ok, t()} | {:error, String.t()}
def connect(runtime) do
parent_node = node()
child_node = child_node_name(parent_node)
child_node = Livebook.EPMD.random_child_node()
Utils.temporarily_register(self(), child_node, fn ->
argv = [parent_node]
init_opts = [
runtime_server_opts: [
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(),
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
runtime = %{runtime | node: child_node, server_pid: server_pid}
{:ok, runtime}
@ -62,7 +59,7 @@ defmodule Livebook.Runtime.ElixirStandalone do
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.
Port.open({:spawn_executable, elixir_path}, [
:binary,
@ -71,7 +68,7 @@ defmodule Livebook.Runtime.ElixirStandalone do
# to the terminal
:nouse_stdio,
:hide,
args: elixir_flags(node_name) ++ ["--eval", eval, "--" | Enum.map(argv, &to_string/1)]
args: elixir_flags(node_name)
])
end
end

View file

@ -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

View file

@ -1,17 +1,8 @@
defmodule Livebook.Runtime.StandaloneInit do
# TODO: Move logic inside ElixirStandalone module.
# Generic functionality related to starting and setting up
# 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 """
Tries locating Elixir executable in PATH.
"""
@ -23,30 +14,6 @@ defmodule Livebook.Runtime.StandaloneInit do
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,
@ -88,9 +55,9 @@ defmodule Livebook.Runtime.StandaloneInit do
loop = fn loop ->
receive do
{:node_started, init_ref, ^child_node, primary_pid} ->
{:node_started, init_ref, ^child_node, child_port, primary_pid} ->
Port.demonitor(port_ref)
Livebook.EPMD.update_child_node(child_node, child_port)
server_pid = Livebook.Runtime.ErlDist.initialize(child_node, opts[:init_opts] || [])
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
# 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
# called within the runtime.
@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();\
parent_process = {node(), String.to_atom(parent_node)};\
send(parent_process, {:node_started, init_ref, node(), self()});\
parent_process = {node(), List.to_atom(parent_node)};\
send(parent_process, {:node_started, init_ref, node(), dist_port, self()});\
receive do {:node_initialized, ^init_ref} ->\
manager_ref = Process.monitor(Livebook.Runtime.ErlDist.NodeManager);\
receive do {:DOWN, ^manager_ref, :process, _object, _reason} -> :ok end;\
@ -138,12 +110,42 @@ defmodule Livebook.Runtime.StandaloneInit do
end
@doc """
Performs the child side of the initialization contract.
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.
A list of common flags used for spawned Elixir runtimes.
"""
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

View file

@ -2,13 +2,22 @@ if exist "!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
if defined LIVEBOOK_DISTRIBUTION set RELEASE_DISTRIBUTION="!LIVEBOOK_DISTRIBUTION!"
if not defined RELEASE_DISTRIBUTION set RELEASE_DISTRIBUTION="sname"
if defined LIVEBOOK_DISTRIBUTION set RELEASE_DISTRIBUTION=!LIVEBOOK_DISTRIBUTION!
if not defined RELEASE_DISTRIBUTION set RELEASE_DISTRIBUTION=sname
if defined LIVEBOOK_NODE set RELEASE_NODE="!LIVEBOOK_NODE!"
if not defined RELEASE_NODE set RELEASE_NODE="livebook-app-!TIMESTAMP:~8,6!-!RANDOM!"
if defined LIVEBOOK_NODE set RELEASE_NODE=!LIVEBOOK_NODE!
if not defined RELEASE_NODE set RELEASE_NODE=livebook-app-!TIMESTAMP:~8,6!-!RANDOM!
set RELEASE_MODE=interactive
set MIX_ARCHIVES=!RELEASE_ROOT!\vendor\archives

View file

@ -2,22 +2,21 @@ if [ -f "$HOME/.livebookdesktop.sh" ]; then
. "$HOME/.livebookdesktop.sh"
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_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 MIX_ARCHIVES="${RELEASE_ROOT}/vendor/archives"
export MIX_REBAR3="${RELEASE_ROOT}/vendor/rebar3"
export LIVEBOOK_EPMDLESS=${LIVEBOOK_EPMDLESS:-true}
export LIVEBOOK_SHUTDOWN_ENABLED=${LIVEBOOK_SHUTDOWN_ENABLED:-true}
export LIVEBOOK_DESKTOP=true
[ -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"
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"
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)

View file

@ -2,6 +2,14 @@ if exist "!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
if defined LIVEBOOK_NODE set RELEASE_NODE="!LIVEBOOK_NODE!"
if not defined RELEASE_NODE set RELEASE_NODE=livebook_server

View file

@ -19,6 +19,10 @@ export RELEASE_MODE=interactive
export RELEASE_NODE=${LIVEBOOK_NODE:-${RELEASE_NODE:-${NODE_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
cookie_path="${RELEASE_ROOT}/releases/COOKIE"
if [ ! -f $cookie_path ] && [ -z "$RELEASE_COOKIE" ]; then

View 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

View 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

View file

@ -1,8 +1,8 @@
defmodule Livebook.RemoteIntellisenseTest do
use ExUnit.Case, async: true
alias Livebook.Intellisense
@moduletag :with_epmd
@tmp_dir "tmp/test/remote_intellisense"
# Returns intellisense context resulting from evaluating

View file

@ -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

View file

@ -57,21 +57,29 @@ windows? = match?({:win32, _}, :os.type())
erl_docs_exclude =
if match?({:error, _}, Code.fetch_docs(:gen_server)) do
[erl_docs: true]
[:erl_docs]
else
[]
end
windows_exclude = if windows?, do: [unix: true], else: []
windows_exclude = if windows?, do: [:unix], else: []
teams_exclude =
if not Livebook.TeamsServer.available?() do
[teams_integration: true]
else
if Livebook.TeamsServer.available?() do
[]
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
ExUnit.start(
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
)