defmodule Runbox.Slave do @moduledoc """ Utilities for starting a slave node with a scenario release and communicating with the node. """ @doc """ Starts a slave node. The `release_dir` is a directory with scenario release. If `app_env` is set, given app environment variables are set on slave. """ @spec start(String.t(), [{Application.app(), [{Application.key(), Application.value()}]}]) :: {:ok, node()} | {:error, term()} def start(release_dir, app_env \\ []) do host = slave_host() |> to_charlist() slave_name = UUID.uuid4() |> to_charlist() args = [ "-setcookie #{Node.get_cookie()}", ~s(-boot "#{boot_file(release_dir)}"), ~s(-boot_var RELEASE_LIB "#{release_lib(release_dir)}"), ~s(-config "#{config_file(release_dir)}"), "-mode interactive", "-runbox mode slave", "-hidden", erl_opt_toolbox_scenario_config_dir(), erl_opt_logger_console() ] |> Enum.join(" ") |> to_charlist() link_to = self() # The progname has to be stated explicitly, otherwise it can be wrongly set # to the name of the release script. progname = 'erl' case :slave.start(host, slave_name, args, link_to, progname) do {:ok, slave} -> with :ok <- check_runbox_started(slave), :ok <- check_runbox_same_version(slave), :ok <- check_runbox_scenario_app_configured(slave), :ok <- put_app_env(slave, app_env) do {:ok, slave} else {:error, _} = error -> :ok = :slave.stop(slave) error end {:error, _} = error -> error end end @doc """ Spawns a slave and executes the `fun`. `fun` recives the slave node as a parameter. The slave is terminated after the `fun` finishes. """ @spec with_slave(String.t(), (node() -> result)) :: {:ok, result} | {:error, term} when result: any() def with_slave(release_dir, fun) do with {:ok, slave} <- start(release_dir) do try do {:ok, fun.(slave)} after :ok = :slave.stop(slave) end end end @doc """ Calls a function on the slave. Calls MFA on the `slave`. See `:erpc.call/4` for details. """ @spec call(node(), module(), atom(), list()) :: term() def call(slave, m, f, a) do :erpc.call(slave, m, f, a) end defp check_runbox_started(slave) do apps = call(slave, Application, :started_applications, []) if Enum.any?(apps, &match?({:runbox, _, _}, &1)) do :ok else {:error, :runbox_not_running_on_slave} end end defp check_runbox_same_version(slave) do {:ok, runbox_master} = :application.get_key(:runbox, :vsn) {:ok, runbox_slave} = call(slave, :application, :get_key, [:runbox, :vsn]) runbox_master_vsn = Version.parse!(to_string(runbox_master)) runbox_slave_vsn = Version.parse!(to_string(runbox_slave)) if runbox_master_vsn.major == runbox_slave_vsn.major do :ok else {:error, {:runbox_version_mismatch_on_slave, %{master: runbox_master, slave: runbox_slave}}} end end defp check_runbox_scenario_app_configured(slave) do case call(slave, Application, :fetch_env, [:runbox, :scenario_app]) do {:ok, _} -> :ok :error -> {:error, {:runbox_misconfigured, "Missing app config key :scenario_app"}} end end defp put_app_env(slave, app_env) do if app_env == [], do: :ok, else: call(slave, Application, :put_all_env, [app_env]) end defp release_lib(release_dir) do Path.join([release_dir, "lib"]) end defp boot_file(release_dir) do Path.join([release_dir, "releases", release_version(release_dir), "start"]) end defp config_file(release_dir) do Path.join([release_dir, "releases", release_version(release_dir), "sys"]) end defp release_version(release_dir) do data_file = Path.join([release_dir, "releases", "start_erl.data"]) [_, release_version] = data_file |> File.read!() |> String.split(" ") release_version end defp slave_host, do: this_host() # The name of the host has to be derived from the node name. If taken from # `:inet.gethostname`, it does not work inside docker. defp this_host do [_name, host] = node() |> Atom.to_string() |> String.split("@") host end defp erl_opt_toolbox_scenario_config_dir do scenario_config_dir = Application.fetch_env!(:toolbox, :scenario_config_dir) ~s(-toolbox scenario_config_dir "#{erl_format_term(scenario_config_dir)}") end defp erl_opt_logger_console do case Application.fetch_env(:logger, :console) do {:ok, conf} -> ~s(-logger console "#{erl_format_term(conf)}") :error -> "" end end defp erl_format_term(term) do IO.chardata_to_string(:io_lib.format("~tw", [term])) end end