Current section

Files

Jump to
livebook lib livebook runtime epmd.ex
Raw

lib/livebook/runtime/epmd.ex

defmodule Livebook.Runtime.EPMD do
# A custom EPMD module used to bypass the epmd OS daemon in the
# standalone runtime.
#
# We used to start epmd on application boot, however sometimes it
# would fail. In particular, on Windows starting epmd may require
# accepting a firewall pop up, and the first boot could still fail.
# To avoid this, we use a custom port resolution that does not rely
# on the epmd OS daemon running.
# From Erlang/OTP 23+
@epmd_dist_version 6
@doc """
Persists parent information, used when connecting to the parent.
"""
def register_parent(parent_node, parent_port) do
[name, host] = parent_node |> Atom.to_charlist() |> :string.split(~c"@")
:persistent_term.put(:livebook_parent, {name, host, parent_node, parent_port})
end
@doc """
Returns the current distribution port.
"""
def dist_port do
:persistent_term.get(:livebook_dist_port)
end
# Custom EPMD callbacks
# 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)
:persistent_term.put(:livebook_dist_family, family)
case :erl_epmd.register_node(name, port, family) do
{:ok, creation} -> {:ok, creation}
{:error, :already_registered} -> {:error, :already_registered}
# If registration fails because EPMD is not running, we ignore
# that, because we do not rely on EPMD
_ -> {:ok, -1}
end
end
# Custom callback for resolving parent and sibling node ports.
def port_please(name, host), do: port_please(name, host, :infinity)
def port_please(name, host, timeout) do
# If the target node is on the same host, check if it's a Livebook
# server or runtime to bypass EPMD. If it is a different node, we
# always fall back to :erl_epmd, even if it's a Livebook node.
if host_to_ip!(host()) == host_to_ip!(host) do
case livebook_port(name) do
0 -> :erl_epmd.port_please(name, host, timeout)
port -> {:port, port, @epmd_dist_version}
end
else
:erl_epmd.port_please(name, host, timeout)
end
end
defp host() do
[_, host] = node() |> Atom.to_charlist() |> :string.split(~c"@")
host
end
import Record
defrecordp :hostent, Record.extract(:hostent, from_lib: "kernel/include/inet.hrl")
defp host_to_ip!(host) when is_tuple(host), do: host
defp host_to_ip!(host) when is_atom(host), do: host_to_ip!(Atom.to_charlist(host))
defp host_to_ip!(host) when is_list(host) do
family = :persistent_term.get(:livebook_dist_family)
case :inet.gethostbyname(host, family) do
{:ok, hostent(h_addrtype: ^family, h_addr_list: [ip | _])} ->
ip
other ->
raise "failed to resolve hostname #{inspect(host)}, reason: #{inspect(other)}"
end
end
defp livebook_port(name) do
{parent_name, parent_host, parent_node, parent_port} = :persistent_term.get(:livebook_parent)
case match_name(name, parent_name) do
:parent -> parent_port
:sibling -> sibling_port(parent_node, name, parent_host)
:none -> 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 start_link(), to: :erl_epmd
defdelegate address_please(name, host, address_family), to: :erl_epmd
defdelegate listen_port_please(name, host), to: :erl_epmd
defdelegate names(host_name), to: :erl_epmd
end