Current section
Files
Jump to
Current section
Files
lib/plds_cli/server.ex
defmodule PLDSCli.Server do
@moduledoc false
# Starts the PLDS server.
# Most of the code here was copied from Livebook code.
# Ref: https://github.com/livebook-dev/livebook/blob/2a338b6b83671a54c4d3fce5e58b3ccba0666c37/lib/livebook_cli/server.ex
def usage do
"""
Usage: plds server [options]
Available options:
-c, --connect One or more node names to connect.
You can specify more nodes by using this option multiple times.
It accepts both short and long names.
--cookie Sets a cookie for the app distributed node.
--ip The ip address to start the web application on, defaults to 127.0.0.1
Must be a valid IPv4 or IPv6 address.
--name Set a name for the app distributed node.
--open Open browser window pointing to the application.
-p, --port The port to start the web application on. Defaults to 8090.
--sname Set a short name for the app distributed node. Defaults to "plds".
The --help option can be given to print this notice.
"""
end
def call(args) do
opts = args_to_options(args)
config_entries = opts_to_config(opts, [])
put_config_entries(config_entries)
endpoint_config = Application.fetch_env!(:plds, PLDSWeb.Endpoint)
port = endpoint_config[:http][:port]
base_url = "http://localhost:#{port}"
case check_endpoint_availability(base_url) do
:plds_running ->
IO.puts("PLDS already running on #{base_url}")
open_from_options(base_url, opts)
:taken ->
print_error(
"Another application is already running on port #{port}." <>
" Either ensure this port is free or specify a different port using the --port option"
)
:available ->
case start_server() do
:ok ->
open_from_options(PLDSWeb.Endpoint.url(), opts)
Process.sleep(:infinity)
:error ->
print_error("PLDS failed to start")
end
end
end
# Takes a list of {app, key, value} config entries
# and overrides the current applications' configuration accordingly.
# Multiple values for the same key are deeply merged (provided they are keyword lists).
defp put_config_entries(config_entries) do
config_entries
|> Enum.reduce([], fn {app, key, value}, acc ->
acc = Keyword.put_new_lazy(acc, app, fn -> Application.get_all_env(app) end)
Config.Reader.merge(acc, [{app, [{key, value}]}])
end)
|> Application.put_all_env(persistent: true)
end
defp check_endpoint_availability(base_url) do
url = base_url <> "/health"
{:ok, _} = Application.ensure_all_started(:inets)
case :httpc.request(:get, {url, []}, [timeout: 4_000], body_format: :binary) do
{:ok, {{_, status, _}, _headers, body}} ->
with 200 <- status,
{:ok, body} <- Jason.decode(body),
%{"application" => "plds"} <- body do
:plds_running
else
_ -> :taken
end
{:error, _error} ->
:available
end
end
defp start_server() do
# We configure the endpoint in prod with `server: true`,
# so it's gonna start listening
case Application.ensure_all_started(:plds) do
{:ok, _} -> :ok
{:error, _} -> :error
end
end
defp open_from_options(base_url, opts) do
if opts[:open] do
browser_open(base_url)
end
end
@switches [
cookie: :string,
ip: :string,
name: :string,
open: :boolean,
port: :integer,
connect: :keep,
sname: :string
]
@aliases [
p: :port,
c: :connect
]
defp args_to_options(args) do
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
validate_options!(opts)
opts
end
defp validate_options!(opts) do
if Keyword.has_key?(opts, :name) and Keyword.has_key?(opts, :sname) do
raise "the provided --sname and --name options are mutually exclusive, please specify only one of them"
end
end
defp opts_to_config([], config), do: config
defp opts_to_config([{:port, port} | opts], config) do
opts_to_config(opts, [{:plds, PLDSWeb.Endpoint, http: [port: port]} | config])
end
defp opts_to_config([{:ip, ip} | opts], config) do
ip = PLDS.Utils.ip!("--ip", ip)
opts_to_config(opts, [{:plds, PLDSWeb.Endpoint, http: [ip: ip]} | config])
end
defp opts_to_config([{:sname, sname} | opts], config) do
sname = String.to_atom(sname)
opts_to_config(opts, [{:plds, :node, {:shortnames, sname}} | config])
end
defp opts_to_config([{:name, name} | opts], config) do
name = String.to_atom(name)
opts_to_config(opts, [{:plds, :node, {:longnames, name}} | config])
end
defp opts_to_config([{:cookie, cookie} | opts], config) do
cookie = String.to_atom(cookie)
opts_to_config(opts, [{:plds, :cookie, cookie} | config])
end
defp opts_to_config([{:connect, name} | opts], config) do
name = String.to_atom(name)
previous_nodes_to_connect =
Enum.find_index(config, &match?({:plds, :nodes_to_connect, _}, &1))
config =
case previous_nodes_to_connect do
nil ->
[{:plds, :nodes_to_connect, [name]} | config]
idx ->
List.update_at(config, idx, fn {:plds, :nodes_to_connect, existing_names} ->
{:plds, :nodes_to_connect, existing_names ++ [name]}
end)
end
opts_to_config(opts, config)
end
defp opts_to_config([_opt | opts], config), do: opts_to_config(opts, config)
defp browser_open(url) do
{cmd, args} =
case :os.type() do
{:win32, _} -> {"cmd", ["/c", "start", url]}
{:unix, :darwin} -> {"open", [url]}
{:unix, _} -> {"xdg-open", [url]}
end
System.cmd(cmd, args)
end
defp print_error(message) do
IO.ANSI.format([:red, message]) |> IO.puts()
end
end