defmodule Exexec do @moduledoc """ Execute and control OS processes from Elixir. An idiomatic Elixir wrapper for Serge Aleynikov's excellent [erlexec](https://github.com/saleyn/erlexec), Exexec provides an Elixir interface as well as some nice Elixir-y goodies on top. """ import Exexec.ToErl @type command :: String.t() | [Path.t() | [String.t()]] @type os_pid :: non_neg_integer @type gid :: non_neg_integer @type output_file_option :: {:append, boolean} | {:mode, non_neg_integer} @type output_device :: :stdout | :stderr @type output_file_options :: [output_file_option] @type output_device_option :: boolean | :null | :close | :print | Path.t() | {Path.t(), output_file_options} | pid | (output_device, os_pid, binary -> any) @type command_option :: {:monitor, boolean} | {:sync, boolean} | {:executable, Path.t()} | {:cd, Path.t()} | {:env, %{String.t() => String.t()}} | {:kill_command, String.t()} | {:kill_timeout, non_neg_integer} | {:kill_group, boolean} | {:group, String.t()} | {:user, String.t()} | {:success_exit_code, exit_code} | {:nice, -20..20} | {:stdin, boolean | :null | :close | Path.t()} | {:stdout, :stderr | output_device_option} | {:stderr, :stdout | output_device_option} | {:pty, boolean} @type command_options :: [command_option] @type exec_option :: {:debug, boolean | non_neg_integer} | {:root, boolean} | {:verbose, boolean} | {:args, [String.t()]} | {:alarm, non_neg_integer} | {:user, String.t()} | {:limit_users, [String.t()]} | {:port_path, Path.t()} | {:env, %{String.t() => String.t()}} @type exec_options :: [exec_option] @type signal :: pos_integer @type on_run :: {:ok, pid, os_pid} | {:ok, [{output_device, [binary]}]} | {:error, any} @type exit_code :: non_neg_integer @doc """ Send `signal` to `pid`. `pid` can be an `Exexec` pid, OS pid, or port. """ @spec kill(pid | os_pid | port, signal) :: :ok | {:error, any} defdelegate kill(pid, signal), to: :exec @doc """ Start an `Exexec` process to manage existing `os_pid` with options `options`. `os_pid` can also be a port. """ @spec manage(os_pid | port) :: {:ok, pid, os_pid} | {:error, any} @spec manage(os_pid | port, command_options) :: {:ok, pid, os_pid} | {:error, any} def manage(os_pid, options \\ []), do: :exec.manage(os_pid, command_options_to_erl(options)) @doc """ Returns the OS pid for `Exexec` process `pid`. """ @spec os_pid(pid) :: {:ok, os_pid} | {:error, any} def os_pid(pid) do case :exec.ospid(pid) do {:error, reason} -> {:error, reason} os_pid -> {:ok, os_pid} end end @doc """ Returns the `Exexec` pid for `os_pid`. """ @spec pid(os_pid) :: {:ok, pid} | {:error, any} def pid(os_pid) do case :exec.pid(os_pid) do {:error, reason} -> {:error, reason} :undefined -> {:error, :undefined} pid -> {:ok, pid} end end @doc """ Run an external `command` with `options`. """ @spec run(command) :: on_run @spec run(command, command_options) :: on_run def run(command, options \\ []) do command = command_to_erl(command) options = command_options_to_erl(options) :exec.run(command, options) end @doc """ Run an external `command` with `options`, linking to the current process. If the external process exits with code 0, the linked process will not exit. """ @spec run_link(command) :: on_run @spec run_link(command, command_options) :: on_run def run_link(command, options \\ []) do command = command_to_erl(command) options = command_options_to_erl(options) :exec.run_link(command, options) end @doc """ Send `data` to the stdin of `pid`. `pid` can be an `Exexec` pid or an OS pid. """ @spec send(pid | os_pid, binary | :eof) :: :ok defdelegate send(pid, data), to: :exec @doc """ Change group ID of `os_pid` to `gid`. """ @spec set_gid(os_pid, gid) :: :ok | {:error, any} defdelegate set_gid(os_pid, gid), to: :exec, as: :setpgid @doc """ Convert integer `signal` to atom, or return `signal`. """ @spec signal(signal) :: atom | integer defdelegate signal(signal), to: :exec @doc """ Start `Exexec`. """ @spec start() :: {:ok, pid} | {:error, any} defdelegate start(), to: :exec @doc """ Start `Exexec` with `options`. """ @spec start(exec_options) :: {:ok, pid} | {:error, any} def start(options) do :exec.start(exec_options_to_erl(options)) end @doc """ Start `Exexec` and link to calling process. """ @spec start_link :: {:ok, pid} | {:error, any} def start_link(), do: start_link([]) @doc """ Start `Exexec` with `options` and link to calling process. """ @spec start_link(exec_options) :: {:ok, pid} | {:error, any} def start_link(options) do :exec.start_link(exec_options_to_erl(options)) end @doc """ Interpret `exit_code`. If the program exited by signal, returns `{:signal, signal, core}` where `signal` is the atom or integer signal and `core` is whether a core file was generated. """ @spec status(exit_code) :: {:status, signal} | {:signal, signal | :atom, boolean} defdelegate status(exit_code), to: :exec @doc """ Stop `pid`. `pid` can be an `Exexec` pid, OS pid, or port. The OS process is terminated gracefully. If `:kill_command` was specified, that command is executed and a timer is started. If the process doesn't exit immediately, then by default after 5 seconds SIGKILL will be sent to the process. """ @spec stop(pid | os_pid | port) :: :ok | {:error, any} defdelegate stop(pid), to: :exec @doc """ Stop `pid` and wait for it to exit for `timeout` milliseconds. See `Exexec.stop/1`. """ @spec stop_and_wait(pid | os_pid | port) :: :ok | {:error, any} @spec stop_and_wait(pid | os_pid | port, integer) :: :ok | {:error, any} def stop_and_wait(pid, timeout \\ 5_000), do: :exec.stop_and_wait(pid, timeout) @doc """ Return a list of OS pids managed by `Exexec`. """ @spec which_children() :: [os_pid] defdelegate which_children(), to: :exec end