diff --git a/README.md b/README.md index 186849c..c113a48 100644 --- a/README.md +++ b/README.md @@ -135,30 +135,31 @@ The program is distributed under BSD license. Copyright (c) 2003 Serge Aleynikov ## Architecture -``` - ┌───────────────────────────┐ - │ ┌────┐ ┌────┐ ┌────┐ │ - │ │Pid1│ │Pid2│ │PidN│ │ Erlang light-weight Pids associated - │ └────┘ └────┘ └────┘ │ one-to-one with managed OsPids - │ ╲ │ ╱ │ - │ ╲ │ ╱ │ - │ ╲ │ ╱ (links) │ - │ ┌──────┐ │ - │ │ exec │ │ Exec application running in Erlang VM - │ └──────┘ │ - │ Erlang VM │ │ - └─────────────┼─────────────┘ - │ - ┌───────────┐ - │ exec-port │ Port program (separate OS process) - └───────────┘ - ╱ │ ╲ - (optional stdin/stdout/stderr pipes) - ╱ │ ╲ - ┌──────┐ ┌──────┐ ┌──────┐ - │OsPid1│ │OsPid2│ │OsPidN│ Managed Child OS processes - └──────┘ └──────┘ └──────┘ -``` + +
+┌───────────────────────────┐
+│   ┌────┐ ┌────┐ ┌────┐    │
+│   │Pid1│ │Pid2│ │PidN│    │   Erlang light-weight Pids associated
+│   └────┘ └────┘ └────┘    │   one-to-one with managed OsPids
+│         ╲   │   ╱         │
+│          ╲  │  ╱          │
+│           ╲ │ ╱ (links)   │
+│         ┌──────┐          │
+│         │ exec │          │   Exec application running in Erlang VM
+│         └──────┘          │
+│ Erlang VM   │             │
+└─────────────┼─────────────┘
+              │
+        ┌───────────┐
+        │ exec-port │           Port program (separate OS process)
+        └───────────┘
+         ╱    │    ╲
+(optional stdin/stdout/stderr pipes)
+       ╱      │      ╲
+  ┌──────┐ ┌──────┐ ┌──────┐
+  │OsPid1│ │OsPid2│ │OsPidN│    Managed Child OS processes
+  └──────┘ └──────┘ └──────┘
+
## Configuration Options diff --git a/rebar.config b/rebar.config index 513d78c..d142a20 100644 --- a/rebar.config +++ b/rebar.config @@ -13,8 +13,7 @@ {"(linux|darwin|solaris)", clean, "make -C c_src clean"} ]}. -{plugins, [rebar3_hex, rebar3_ex_doc]}. - +{plugins, [rebar3_hex, {rebar3_ex_doc, "0.2.12"}]}. {hex, [{doc, ex_doc}]}. {ex_doc, [ @@ -23,6 +22,5 @@ {"LICENSE", #{title => "License"}} ]}, {main, "README.md"}, - {authors, ["Serge Aleynikov"]}, {source_url, "https://github.com/saleyn/erlexec"} ]}. diff --git a/src/exec.erl b/src/exec.erl index 689f7ef..6a4f6db 100644 --- a/src/exec.erl +++ b/src/exec.erl @@ -1,59 +1,50 @@ %%% vim:ts=4:sw=4:et -%%%------------------------------------------------------------------------ -%%% File: $Id$ -%%%------------------------------------------------------------------------ -%%% @doc OS shell command runner. -%%% It communicates with a separate C++ port process `exec-port' -%%% spawned by this module, which is responsible -%%% for starting, killing, listing, terminating, and notifying of -%%% state changes. -%%% -%%% The port program serves as a middle-man between -%%% the OS and the virtual machine to carry out OS-specific low-level -%%% process control. The Erlang/C++ protocol is described in the -%%% `exec.cpp' file. The `exec' application can execute tasks by -%%% impersonating as a different effective user. This impersonation -%%% can be accomplished in one of the following two ways (assuming -%%% that the emulator is not running as `root': -%%% -%%% In either of these two cases, `exec:start_link/2' must be started -%%% with options `[root, {user, User}, {limit_users, Users}]', -%%% so that `exec-port' process will not actually run as -%%% root but will switch to the effective `User', and set the kernel -%%% capabilities so that it's able to start processes as other -%%% effective users given in the `Users' list and adjust process -%%% priorities. -%%% -%%% Though, in the initial design, `exec' prohibited such use, upon -%%% user requests a feature was added (in order to support `docker' -%%% deployment and CI testing) to be able to execute `exec-port' as -%%% `root' without switching the effective user to anying other than -%%% `root'. To accomplish this use the following options to start -%%% `exec': `[root, {user, "root"}, {limit_users, ["root"]}]'. -%%% -%%% At exit the port program makes its best effort to perform -%%% clean shutdown of all child OS processes. -%%% Every started OS process is linked to a spawned light-weight -%%% Erlang process returned by the run/2, run_link/2 command. -%%% The application ensures that termination of spawned OsPid -%%% leads to termination of the associated Erlang Pid, and vice -%%% versa. -%%% -%%% @author Serge Aleynikov -%%% @version {@vsn} -%%% @end -%%%------------------------------------------------------------------------ -%%% Created: 2003-06-10 by Serge Aleynikov -%%% $Header$ -%%%------------------------------------------------------------------------ -module(exec). +-moduledoc """ +OS shell command runner. +It communicates with a separate C++ port process `exec-port` +spawned by this module, which is responsible +for starting, killing, listing, terminating, and notifying of +state changes. + +The port program serves as a middle-man between +the OS and the virtual machine to carry out OS-specific low-level +process control. The Erlang/C++ protocol is described in the +`exec.cpp` file. The `exec` application can execute tasks by +impersonating as a different effective user. This impersonation +can be accomplished in one of the following two ways (assuming +that the emulator is not running as `root`: + +- Having the user account running the erlang emulator added to + the `/etc/sudoers` file, so that it can execute `exec-port` + task as `root`. (Preferred option) +- Setting `root` ownership on `exec-port`, and setting the + SUID bit: `chown root:root exec-port; chmod 4755 exec-port`. + (This option is discouraged as it's less secure). + +In either of these two cases, `exec:start_link/2` must be started +with options `[root, {user, User}, {limit_users, Users}]`, +so that `exec-port` process will not actually run as +root but will switch to the effective `User`, and set the kernel +capabilities so that it's able to start processes as other +effective users given in the `Users` list and adjust process +priorities. + +Though, in the initial design, `exec` prohibited such use, upon +user requests a feature was added (in order to support `docker` +deployment and CI testing) to be able to execute `exec-port` as +`root` without switching the effective user to anying other than +`root`. To accomplish this use the following options to start +`exec`: `[root, {user, "root"}, {limit_users, ["root"]}]`. + +At exit the port program makes its best effort to perform +clean shutdown of all child OS processes. +Every started OS process is linked to a spawned light-weight +Erlang process returned by the run/2, run_link/2 command. +The application ensures that termination of spawned OsPid +leads to termination of the associated Erlang Pid, and vice +versa. +""". -author('saleyn@gmail.com'). -behaviour(gen_server). @@ -94,6 +85,46 @@ }). -type exec_options() :: [exec_option()]. +-doc """ +Options passed to the exec process at startup. They can be specified in the +`sys.config` file for the `erlexec` application to customize application +startup. +- `debug` + : Same as `{debug, 1}` +- `{debug, Level}` + : Enable port-programs debug trace at `Level`. +- `verbose` + : Enable verbose prints of the Erlang process. +- `root | {root, Boolean}` + : Allow running child processes as root. +- `{args, Args}` + : Append `Args` to the port command. +- `{alarm, Secs}` + : Give `Secs` deadline for the port program to clean up + child pids before exiting +- `{user, User}` + : When the port program was compiled with capability (Linux) + support enabled, and is owned by root with a a suid bit set, + this option must be specified so that upon startup the port + program is running under the effective user different from root. + This is a security measure that will also prevent the port program + to execute root commands. +- `{limit_users, LimitUsers}` + : Limit execution of external commands to these set of users. + This option is only valid when the port program is owned + by root. +- `{portexe, Exe}` + : Provide an alternative location of the port program. + This option is useful when this application is stored + on NFS and the port program needs to be copied locally + so that root suid bit can be set. +- `{env, Env}` + : Extend environment of the port program by using `Env` specification. + `Env` should be a list of tuples `{Name, Val}`, where Name is the + name of an environment variable, and Val is the value it is to have + in the spawned port process. If Val is `false`, then the `Name` + environment variable is unset. +""". -type exec_option() :: debug | {debug, integer()} @@ -105,85 +136,144 @@ | {limit_users, [string()|binary(), ...]} | {portexe, string()|binary()} | {env, [{string()|binary(), string()|binary()|false}, ...]}. -%% Options passed to the exec process at startup. They can be specified in the -%% `sys.config' file for the `erlexec' application to customize application -%% startup. -%%
-%%
debug
Same as {debug, 1}
-%%
{debug, Level}
Enable port-programs debug trace at `Level'.
-%%
verbose
Enable verbose prints of the Erlang process.
-%%
root | {root, Boolean}
Allow running child processes as root.
-%%
{args, Args}
Append `Args' to the port command.
-%%
{alarm, Secs}
-%%
Give `Secs' deadline for the port program to clean up -%% child pids before exiting
-%%
{user, User}
-%%
When the port program was compiled with capability (Linux) -%% support enabled, and is owned by root with a a suid bit set, -%% this option must be specified so that upon startup the port -%% program is running under the effective user different from root. -%% This is a security measure that will also prevent the port program -%% to execute root commands.
-%%
{limit_users, LimitUsers}
-%%
Limit execution of external commands to these set of users. -%% This option is only valid when the port program is owned -%% by root.
-%%
{portexe, Exe}
-%%
Provide an alternative location of the port program. -%% This option is useful when this application is stored -%% on NFS and the port program needs to be copied locally -%% so that root suid bit can be set.
-%%
{env, Env}
-%%
Extend environment of the port program by using `Env' specification. -%% `Env' should be a list of tuples `{Name, Val}', where Name is the -%% name of an environment variable, and Val is the value it is to have -%% in the spawned port process. If Val is `false', then the `Name' -%% environment variable is unset.
-%%
-export_type([exec_option/0, exec_options/0]). +-doc """ +Command to be executed. If specified as a string, the specified command +will be executed through the shell. The current shell is obtained +from environment variable `SHELL`. This can be useful if you +are using Erlang primarily for the enhanced control flow it +offers over most system shells and still want convenient +access to other shell features such as shell pipes, filename +wildcards, environment variable expansion, and expansion of +`~` to a user's home directory. All command arguments must +be properly escaped including whitespace and shell +metacharacters. + +Any part of the command string can contain unicode characters. + +**Warning:** Executing shell commands that +incorporate unsanitized input from an untrusted source makes +a program vulnerable to +[shell injection](http://en.wikipedia.org/wiki/Shell_injection#Shell_injection), +a serious security flaw which can result in arbitrary command +execution. For this reason, the use of `shell` is strongly +discouraged in cases where the command string is constructed +from external input: + +``` + 1> {ok, Filename} = io:read("Enter filename: "). + Enter filename: "non_existent; rm -rf / #". + {ok, "non_existent; rm -rf / #"} + 2> exec(Filename, []) % Argh!!! This is not good! +``` + +When command is given in the form of a list of strings, +it is passed to `execve(3)` library call directly without +involving the shell process, so the list of strings +represents the program to be executed given with a full path, +followed by the list of arguments (e.g. `["/bin/echo", "ok"]`). +In this case all shell-based features are disabled +and there's no shell injection vulnerability. +""". -type cmd() :: binary() | string() | [string()]. -%% Command to be executed. If specified as a string, the specified command -%% will be executed through the shell. The current shell is obtained -%% from environment variable `SHELL'. This can be useful if you -%% are using Erlang primarily for the enhanced control flow it -%% offers over most system shells and still want convenient -%% access to other shell features such as shell pipes, filename -%% wildcards, environment variable expansion, and expansion of -%% `~' to a user's home directory. All command arguments must -%% be properly escaped including whitespace and shell -%% metacharacters. -%% -%% Any part of the command string can contain unicode characters. -%% -%% -%% -%% ``` -%% 1> {ok, Filename} = io:read("Enter filename: "). -%% Enter filename: "non_existent; rm -rf / #". -%% {ok, "non_existent; rm -rf / #"} -%% 2> exec(Filename, []) % Argh!!! This is not good! -%% ''' -%% -%% When command is given in the form of a list of strings, -%% it is passed to `execve(3)' library call directly without -%% involving the shell process, so the list of strings -%% represents the program to be executed given with a full path, -%% followed by the list of arguments (e.g. `["/bin/echo", "ok"]'). -%% In this case all shell-based features are disabled -%% and there's no shell injection vulnerability. -export_type([cmd/0]). -type cmd_options() :: [cmd_option()]. +-doc """ +Command options: +- `monitor` + : Set up a monitor for the spawned process. The monitor is not + a standard `erlang:montior/2` function call, but it's emulated + by ensuring that the monitoring process receives notification + in the form: + `{'DOWN', OsPid::integer(), process, Pid::pid(), Reason}`. + If the `Reason` is `normal`, then process exited with status `0`, + otherwise there was an error. If the Reason is `{status, Status}` + the returned `Status` can be decoded with `status/1` to determine + the exit code of the process and if it was killed by signal. +- `sync` + : Block the caller until the OS command exits +- `{executable, Executable::string()}` + : Specifies a replacement program to execute. It is very seldom + needed. When the port program executes a child process using + `execve(3)` call, the call takes the following arguments: + `(Executable, Args, Env)`. When `Cmd` argument passed to the + `run/2` function is specified as the list of strings, + the executable replaces the first parameter in the call, and + the original args provided in the `Cmd` parameter are passed as + as the second parameter. Most programs treat the program + specified by args as the command name, which can then be different + from the program actually executed. On Unix, the args name becomes + the display name for the executable in utilities such as `ps`. + + If `Cmd` argument passed to the `run/2` function is given as a + string, on Unix the `Executable` specifies a replacement shell + for the default `/bin/sh`. +- `{cd, WorkDir}` + : Working directory +- `{env, Env :: [{Name,Value}|string()|clear]}` + : List of "VAR=VALUE" environment variables or + list of {Name, Value} tuples or strings (like "NAME=VALUE") or `clear`. + `clear` will clear environment of a spawned child OS process + (so that it doesn't inherit parent's environment). + If `Value` is `false` then the `Var` env variable is unset. +- `{kill, KillCmd}` + : This command will be used for killing the process. After + a 5-sec timeout if the process is still alive, it'll be + killed with SIGKILL. The kill command will have a `CHILD_PID` + environment variable set to the pid of the process it is + expected to kill. If the `kill` option is not specified, + by default first the command is sent a `SIGTERM` signal, + followed by `SIGKILL` after a default timeout. +- `{kill_timeout, Sec::integer()}` + : Number of seconds to wait after issuing a SIGTERM or + executing the custom `kill` command (if specified) before + killing the process with the `SIGKILL` signal +- `kill_group` + : At process exit kill the whole process group associated with this pid. + The process group is obtained by the call to getpgid(3). +- `{group, GID}` + : Sets the effective group ID of the spawned process. The value 0 + means to create a new group ID equal to the OS pid of the process. +- `{user, RunAsUser}` + : When exec-port was compiled with capability (Linux) support + enabled and has a suid bit set, it's capable of running + commands with a different RunAsUser effective user. Passing + "root" value of `RunAsUser` is prohibited. +- `{success_exit_code, IntExitCode}` + : On success use `IntExitCode` return value instead of default 0. +- `{nice, Priority}` + : Set process priority between -20 and 20. Note that + negative values can be specified only when `exec-port` + is started with a root suid bit set. +- `stdin | {stdin, null | close | Filename}` + : Enable communication with an OS process via its `stdin`. The + input to the process is sent by `exec:send(OsPid, Data)`. + When specified as a tuple, `null` means redirection from `/dev/null`, + `close` means to close `stdin` stream, and `Filename` means to + take input from file. +- `stdout` + : Same as `{stdout, self()}`. +- `stderr` + : Same as `{stderr, self()}`. +- `{stdout, output_device()}` + : Redirect process's standard output stream +- `{stderr, output_device()}` + : Redirect process's standard error stream +- `{stdout | stderr, Filename::string(), [output_dev_opt()]}` + : Redirect process's stdout/stderr stream to file +- `{winsz, {Rows, Cols}}` + : Set the (psudo) terminal's dimensions of rows and columns +- `pty` + : Use pseudo terminal for the process's stdin, stdout and stderr +- `pty_echo` + : Allow the pty to run in echo mode, disabled by default +- `debug` + : Same as `{debug, 1}` +- `{debug, Level::integer()}` + : Enable debug printing in port program for this command +""". -type cmd_option() :: monitor | sync @@ -207,131 +297,42 @@ | pty | {pty, pty_opts()} | pty_echo | debug | {debug, integer()}. -%% Command options: -%%
-%%
monitor
-%%
Set up a monitor for the spawned process. The monitor is not -%% a standard `erlang:montior/2' function call, but it's emulated -%% by ensuring that the monitoring process receives notification -%% in the form: -%% ``{'DOWN', OsPid::integer(), process, Pid::pid(), Reason}''. -%% If the `Reason' is `normal', then process exited with status `0', -%% otherwise there was an error. If the Reason is `{status, Status}' -%% the returned `Status' can be decoded with `status/1' to determine -%% the exit code of the process and if it was killed by signal. -%%
-%%
sync
Block the caller until the OS command exits
-%%
{executable, Executable::string()}
-%%
Specifies a replacement program to execute. It is very seldom -%% needed. When the port program executes a child process using -%% `execve(3)' call, the call takes the following arguments: -%% `(Executable, Args, Env)'. When `Cmd' argument passed to the -%% `run/2' function is specified as the list of strings, -%% the executable replaces the first parameter in the call, and -%% the original args provided in the `Cmd' parameter are passed as -%% as the second parameter. Most programs treat the program -%% specified by args as the command name, which can then be different -%% from the program actually executed. On Unix, the args name becomes -%% the display name for the executable in utilities such as `ps'. -%% -%% If `Cmd' argument passed to the `run/2' function is given as a -%% string, on Unix the `Executable' specifies a replacement shell -%% for the default `/bin/sh'.
-%%
{cd, WorkDir}
Working directory
-%%
{env, Env :: [{Name,Value}|string()|clear]}
-%%
List of "VAR=VALUE" environment variables or -%% list of {Name, Value} tuples or strings (like "NAME=VALUE") or `clear'. -%% `clear' will clear environment of a spawned child OS process -%% (so that it doesn't inherit parent's environment). -%% If `Value' is `false' then the `Var' env variable is unset. -%%
-%%
{kill, KillCmd}
-%%
This command will be used for killing the process. After -%% a 5-sec timeout if the process is still alive, it'll be -%% killed with SIGKILL. The kill command will have a `CHILD_PID' -%% environment variable set to the pid of the process it is -%% expected to kill. If the `kill' option is not specified, -%% by default first the command is sent a `SIGTERM' signal, -%% followed by `SIGKILL' after a default timeout.
-%%
{kill_timeout, Sec::integer()}
-%%
Number of seconds to wait after issuing a SIGTERM or -%% executing the custom `kill' command (if specified) before -%% killing the process with the `SIGKILL' signal
-%%
kill_group
-%%
At process exit kill the whole process group associated with this pid. -%% The process group is obtained by the call to getpgid(3).
-%%
{group, GID}
-%%
Sets the effective group ID of the spawned process. The value 0 -%% means to create a new group ID equal to the OS pid of the process.
-%%
{user, RunAsUser}
-%%
When exec-port was compiled with capability (Linux) support -%% enabled and has a suid bit set, it's capable of running -%% commands with a different RunAsUser effective user. Passing -%% "root" value of `RunAsUser' is prohibited.
-%%
{success_exit_code, IntExitCode}
-%%
On success use `IntExitCode' return value instead of default 0.
-%%
{nice, Priority}
-%%
Set process priority between -20 and 20. Note that -%% negative values can be specified only when `exec-port' -%% is started with a root suid bit set.
-%%
stdin | {stdin, null | close | Filename}
-%%
Enable communication with an OS process via its `stdin'. The -%% input to the process is sent by `exec:send(OsPid, Data)'. -%% When specified as a tuple, `null' means redirection from `/dev/null', -%% `close' means to close `stdin' stream, and `Filename' means to -%% take input from file.
-%%
stdout
-%%
Same as `{stdout, self()}'.
-%%
stderr
-%%
Same as `{stderr, self()}'.
-%%
{stdout, output_device()}
-%%
Redirect process's standard output stream
-%%
{stderr, output_device()}
-%%
Redirect process's standard error stream
-%%
{stdout | stderr, Filename::string(), [output_dev_opt()]}
-%%
Redirect process's stdout/stderr stream to file
-%%
{winsz, {Rows, Cols}}
-%%
Set the (psudo) terminal's dimensions of rows and columns
-%%
pty
-%%
Use pseudo terminal for the process's stdin, stdout and stderr
-%%
pty_echo
-%%
Allow the pty to run in echo mode, disabled by default
-%%
debug
-%%
Same as `{debug, 1}'
-%%
{debug, Level::integer()}
-%%
Enable debug printing in port program for this command
-%%
-export_type([cmd_option/0, cmd_options/0]). +-doc """ +Output device option: +- `null` + : Suppress output. +- `close` + : Close file descriptor for writing. +- `print` + : A debugging convenience device that prints the output to the + console shell +- `Filename` + : Save output to file by overwriting it. +- `pid()` + : Redirect output to this pid. +- `fun((Stream, OsPid, Data) -> none())` + : Execute this callback on receiving output data +""". -type output_dev_opt() :: null | close | print | string() | binary() | pid() | fun((stdout | stderr, integer(), binary()) -> none()). -%% Output device option: -%%
-%%
null
Suppress output.
-%%
close
Close file descriptor for writing.
-%%
print
-%%
A debugging convenience device that prints the output to the -%% console shell
-%%
Filename
Save output to file by overwriting it.
-%%
pid()
Redirect output to this pid.
-%%
fun((Stream, OsPid, Data) -> none())
-%%
Execute this callback on receiving output data
-%%
-export_type([output_dev_opt/0]). +-doc """ +Defines file opening attributes: +- `append` + : Open the file in `append` mode +- `{mode, Mode}` + : File creation access mode specified in base 8 (e.g. 8#0644) +""". -type output_file_opt() :: append | {mode, Mode::integer()}. -%% Defines file opening attributes: -%%
-%%
append
Open the file in `append' mode
-%%
{mode, Mode}
-%%
File creation access mode specified in base 8 (e.g. 8#0644)
-%%
-export_type([output_file_opt/0]). +-doc "Representation of OS process ID". -type ospid() :: integer(). -%% Representation of OS process ID. +-doc "Representation of OS group ID". -type osgid() :: integer(). -%% Representation of OS group ID. -export_type([ospid/0, osgid/0]). -type tty_char() :: @@ -345,47 +346,48 @@ echoke | pendin | opost | olcuc | onlcr | ocrnl | onocr | onlret | cs7 | cs8 | parenb | parodd. -type tty_speed() :: tty_op_ispeed | tty_op_ospeed. + +-doc """ +Pty options. + +See [termios(3)](https://man7.org/linux/man-pages/man3/termios.3.html). +See [RFC4254](https://datatracker.ietf.org/doc/html/rfc4254#section-8). + +- `{tty_char(), Byte}` + : A special character with value from 0 to 255 +- `{tty_mode(), Enable}` + : Enable/disable a tty mode +- `{tty_speed(), Speed}` + : Specify input or output baud rate. Provided for + completeness. Not useful for pseudo terminals. +""". -type pty_opt() :: {tty_char(), byte()} | {tty_mode(), boolean()|0|1} | {tty_speed(), non_neg_integer()}. -%% Pty options, see: -%% -%%
-%%
{tty_char(), Byte}
-%%
A special character with value from 0 to 255
-%%
{tty_mode(), Enable}
-%%
Enable/disable a tty mode
-%%
{tty_speed(), Speed}
-%%
Specify input or output baud rate. Provided for -%% completeness. Not useful for pseudo terminals.
-%%
+-doc "List of pty options". -type pty_opts() :: list(pty_opt()). -%% List of pty options. -export_type([pty_opt/0, pty_opts/0]). %%------------------------------------------------------------------------- -%% @doc Supervised start an external program manager. -%% Note that the port program requires `SHELL' environment variable to -%% be set. -%% @end -%%------------------------------------------------------------------------- +-doc """ +Supervised start an external program manager. + +Note that the port program requires `SHELL` environment variable to be set. +""". -spec start_link(exec_options()) -> {ok, pid()} | {error, any()}. start_link(Options) when is_list(Options) -> % Debug = {debug, [trace, log, statistics, {log_to_file, "./execserver.log"}]}, gen_server:start_link({local, ?MODULE}, ?MODULE, [Options], []). % , [Debug]). %%------------------------------------------------------------------------- -%% @equiv start_link/1 -%% @doc Start of an external program manager without supervision. -%% Note that the port program requires `SHELL' environment variable to -%% be set. -%% @end -%%------------------------------------------------------------------------- +-doc #{equiv => start_link/1}. +-doc """ +Start of an external program manager without supervision. +Note that the port program requires `SHELL` environment variable to +be set. +""". -spec start() -> {ok, pid()} | {error, any()}. start() -> start([]). @@ -400,13 +402,13 @@ start(Options) when is_list(Options) -> end. %%------------------------------------------------------------------------- -%% @doc Run an external program. `OsPid' is the OS process identifier of -%% the new process. If `sync' is specified in `Options' the return -%% value is `{ok, Status}' where `Status' is OS process exit status. -%% The `Status' can be decoded with `status/1' to determine the -%% process's exit code and if it was killed by signal. -%% @end -%%------------------------------------------------------------------------- +-doc """ +Run an external program. `OsPid` is the OS process identifier of +the new process. If `sync` is specified in `Options` the return +value is `{ok, Status}` where `Status` is OS process exit status. +The `Status` can be decoded with `status/1` to determine the +process's exit code and if it was killed by signal. +""". -spec run(cmd(), cmd_options(), integer()) -> {ok, pid(), ospid()} | {ok, [{stdout | stderr, [binary()]}]} | {error, any()}. run(Exe, Options, Timeout) when (is_binary(Exe) orelse is_list(Exe)) @@ -416,15 +418,15 @@ run(Exe, Options) -> run(Exe, Options, ?TIMEOUT). %%------------------------------------------------------------------------- -%% @equiv run/2 -%% @doc Run an external program and link to the OsPid. If OsPid exits, -%% the calling process will be killed or if it's trapping exits, -%% it'll get {'EXIT', OsPid, Status} message. If the calling process -%% dies the OsPid will be killed. -%% The `Status' can be decoded with `status/1' to determine the -%% process's exit code and if it was killed by signal. -%% @end -%%------------------------------------------------------------------------- +-doc #{equiv => run/2}. +-doc """ +Run an external program and link to the OsPid. If OsPid exits, +the calling process will be killed or if it's trapping exits, +it'll get {'EXIT', OsPid, Status} message. If the calling process +dies the OsPid will be killed. +The `Status` can be decoded with `status/1` to determine the +process's exit code and if it was killed by signal. +""". -spec run_link(cmd(), cmd_options(), integer()) -> {ok, pid(), ospid()} | {ok, [{stdout | stderr, [binary()]}]} | {error, any()}. run_link(Exe, Options, Timeout) when (is_binary(Exe) orelse is_list(Exe)) @@ -434,11 +436,11 @@ run_link(Exe, Options) -> run_link(Exe, Options, ?TIMEOUT). %%------------------------------------------------------------------------- -%% @doc Manage an existing external process. `OsPid' is the OS process -%% identifier of the external OS process or an Erlang `Port' that -%% would be managed by erlexec. -%% @end -%%------------------------------------------------------------------------- +-doc """ +Manage an existing external process. `OsPid` is the OS process +identifier of the external OS process or an Erlang `Port` that +would be managed by erlexec. +""". -spec manage(ospid() | port(), Options::cmd_options(), Timeout::integer()) -> {ok, pid(), ospid()} | {error, any()}. manage(Pid, Options, Timeout) when is_integer(Pid), is_integer(Timeout) -> @@ -450,17 +452,13 @@ manage(Port, Options) -> manage(Port, Options, ?TIMEOUT). %%------------------------------------------------------------------------- -%% @doc Get a list of children managed by port program. -%% @end -%%------------------------------------------------------------------------- +-doc "Get a list of children managed by port program". -spec which_children() -> [ospid(), ...]. which_children() -> gen_server:call(?MODULE, {port, {list}}). %%------------------------------------------------------------------------- -%% @doc Send a `Signal' to a child `Pid', `OsPid' or an Erlang `Port'. -%% @end -%%------------------------------------------------------------------------- +-doc "Send a `Signal` to a child `Pid`, `OsPid` or an Erlang `Port`". -spec kill(pid() | ospid(), atom()|integer()) -> ok | {error, any()}. kill(Pid, Signal) when is_atom(Signal) -> kill(Pid, signal_to_int(Signal)); @@ -472,23 +470,21 @@ kill(Port, Signal) when is_port(Port) -> kill(Pid, Signal). %%------------------------------------------------------------------------- -%% @doc Change group ID of a given `OsPid' to `Gid'. -%% @end -%%------------------------------------------------------------------------- +-doc "Change group ID of a given `OsPid` to `Gid`". -spec setpgid(ospid(), osgid()) -> ok | {error, any()}. setpgid(OsPid, Gid) when is_integer(OsPid), is_integer(Gid) -> gen_server:call(?MODULE, {port, {setpgid, OsPid, Gid}}). %%------------------------------------------------------------------------- -%% @doc Terminate a managed `Pid', `OsPid', or `Port' process. The OS process is -%% terminated gracefully. If it was given a `{kill, Cmd}' option at -%% startup, that command is executed and a timer is started. If -%% the program doesn't exit, then the default termination is -%% performed. Default termination implies sending a `SIGTERM' command -%% followed by `SIGKILL' in 5 seconds, if the program doesn't get -%% killed. -%% @end -%%------------------------------------------------------------------------- +-doc """ +Terminate a managed `Pid`, `OsPid`, or `Port` process. The OS process is +terminated gracefully. If it was given a `{kill, Cmd}` option at +startup, that command is executed and a timer is started. If +the program doesn't exit, then the default termination is +performed. Default termination implies sending a `SIGTERM` command +followed by `SIGKILL` in 5 seconds, if the program doesn't get +killed. +""". -spec stop(pid() | ospid() | port()) -> ok | {error, any()}. stop(Pid) when is_pid(Pid); is_integer(Pid) -> gen_server:call(?MODULE, {port, {stop, Pid}}, 30000); @@ -497,11 +493,10 @@ stop(Port) when is_port(Port) -> stop(Pid). %%------------------------------------------------------------------------- -%% @doc Terminate a managed `Pid', `OsPid', or `Port' process, like -%% `stop/1', and wait for it to exit. -%% @end -%%------------------------------------------------------------------------- - +-doc """ +Terminate a managed `Pid`, `OsPid`, or `Port` process, like +`stop/1`, and wait for it to exit. +""". -spec stop_and_wait(pid() | ospid() | port(), integer()) -> term() | {error, any()}. stop_and_wait(Port, Timeout) when is_port(Port) -> {os_pid, OsPid} = erlang:port_info(Port, os_pid), @@ -527,10 +522,10 @@ stop_and_wait(Port, Timeout) when is_port(Port) -> stop_and_wait(Pid, Timeout). %%------------------------------------------------------------------------- -%% @doc Get `OsPid' of the given Erlang `Pid'. The `Pid' must be created -%% previously by running the run/2 or run_link/2 commands. -%% @end -%%------------------------------------------------------------------------- +-doc """ +Get `OsPid` of the given Erlang `Pid`. The `Pid` must be created +previously by running the run/2 or run_link/2 commands. +""". -spec ospid(pid()) -> ospid() | {error, Reason::any()}. ospid(Pid) when is_pid(Pid) -> Ref = make_ref(), @@ -542,22 +537,21 @@ ospid(Pid) when is_pid(Pid) -> end. %%------------------------------------------------------------------------- -%% @doc Get `Pid' of the given `OsPid'. The `OsPid' must be created -%% previously by running the run/2 or run_link/2 commands. -%% @end -%%------------------------------------------------------------------------- +-doc """ +Get `Pid` of the given `OsPid`. The `OsPid` must be created +previously by running the run/2 or run_link/2 commands. +""". -spec pid(OsPid::ospid()) -> pid() | undefined | {error, timeout}. pid(OsPid) when is_integer(OsPid) -> gen_server:call(?MODULE, {pid, OsPid}). %%------------------------------------------------------------------------- -%% @doc Send `Data' to stdin of the OS process identified by `OsPid'. -%% -%% Sending eof instead of binary Data causes close of stdin of the -%% corresponding process. Data sent to closed stdin is ignored. -%% -%% @end -%%------------------------------------------------------------------------- +-doc """ +Send `Data` to stdin of the OS process identified by `OsPid`. + +Sending eof instead of binary Data causes close of stdin of the +corresponding process. Data sent to closed stdin is ignored. +""". -spec send(OsPid :: ospid() | pid(), binary() | 'eof') -> ok. send(OsPid, Data) when (is_integer(OsPid) orelse is_pid(OsPid)), @@ -565,12 +559,11 @@ send(OsPid, Data) gen_server:call(?MODULE, {port, {send, OsPid, Data}}). %%------------------------------------------------------------------------- -%% @doc Set the pty terminal `Rows' and `Cols' of the OS process identified by `OsPid'. -%% -%% The process must have been created with the `pty' option. -%% -%% @end -%%------------------------------------------------------------------------- +-doc """ +Set the pty terminal `Rows` and `Cols` of the OS process identified by `OsPid`. + +The process must have been created with the `pty` option. +""". -spec winsz(OsPid :: ospid() | pid(), integer(), integer()) -> ok | {error, Reason::any()}. winsz(OsPid, Rows, Cols) when (is_integer(OsPid) orelse is_pid(OsPid)), @@ -579,12 +572,11 @@ winsz(OsPid, Rows, Cols) gen_server:call(?MODULE, {port, {winsz, OsPid, Rows, Cols}}). %%------------------------------------------------------------------------- -%% @doc Set the pty terminal options of the OS process identified by `OsPid'. -%% -%% The process must have been created with the `pty' option. -%% -%% @end -%%------------------------------------------------------------------------- +-doc """ +Set the pty terminal options of the OS process identified by `OsPid`. + +The process must have been created with the `pty` option. +""". -spec pty_opts(OsPid :: ospid() | pid(), pty_opts()) -> ok | {error, Reason::any()}. pty_opts(OsPid, Opts) when (is_integer(OsPid) orelse is_pid(OsPid)), @@ -592,20 +584,18 @@ pty_opts(OsPid, Opts) gen_server:call(?MODULE, {port, {pty_opts, OsPid, Opts}}). %%------------------------------------------------------------------------- -%% @doc Set debug level of the port process. -%% @end -%%------------------------------------------------------------------------- +-doc "Set debug level of the port process". -spec debug(Level::integer()) -> {ok, OldLevel::integer()} | {error, timeout}. debug(Level) when is_integer(Level), Level >= 0, Level =< 10 -> gen_server:call(?MODULE, {port, {debug, Level}}). %%------------------------------------------------------------------------- -%% @doc Decode the program's exit_status. If the program exited by signal -%% the function returns `{signal, Signal, Core}' where the `Signal' -%% is the signal number or atom, and `Core' indicates if the core file -%% was generated. -%% @end -%%------------------------------------------------------------------------- +-doc """ +Decode the program's exit_status. If the program exited by signal +the function returns `{signal, Signal, Core}` where the `Signal` +is the signal number or atom, and `Core` indicates if the core file +was generated. +""". -spec status(integer()) -> {status, ExitStatus :: integer()} | {signal, Signal :: integer() | atom(), Core :: boolean()}. @@ -622,9 +612,7 @@ status(Status) when is_integer(Status) -> end. %%------------------------------------------------------------------------- -%% @doc Convert a signal number to atom -%% @end -%%------------------------------------------------------------------------- +-doc "Convert a signal number to atom". -spec signal(integer()) -> atom() | integer(). signal( 1) -> sighup; signal( 2) -> sigint; @@ -692,9 +680,7 @@ signal_to_int(sigrtmin) -> 34; signal_to_int(sigrtmax) -> 64. %%------------------------------------------------------------------------- -%% @private -%% @doc Provide default value of a given option. -%% @end +%% Provide default value of a given option. %%------------------------------------------------------------------------- -spec default() -> [{atom(), term()}]. default() -> @@ -1009,13 +995,11 @@ maybe_add_monitor(Reply, _Pid, _MonType, _Sync, _PidOpts, _Debug) -> Reply. %%---------------------------------------------------------------------- -%% @doc Every OsPid is associated with an Erlang process started with -%% this function. The `Parent' is the ?MODULE port manager that -%% spawned this process and linked to it. `Pid' is the process -%% that ran an OS command associated with OsPid. If that process -%% requested a link (LinkType = 'link') we'll link to it. -%% @end -%% @private +%% Every OsPid is associated with an Erlang process started with +%% this function. The `Parent` is the ?MODULE port manager that +%% spawned this process and linked to it. `Pid` is the process +%% that ran an OS command associated with OsPid. If that process +%% requested a link (LinkType = 'link') we'll link to it. %%---------------------------------------------------------------------- -spec ospid_init(Pid::pid(), OsPid::integer(), link | monitor | undefined, Sync::boolean(), Parent::pid(), list(), Debug::boolean()) -> @@ -1145,9 +1129,8 @@ check_options(Options) when is_list(Options) -> end. %%---------------------------------------------------------------------- -%% @doc Pid died or requested to unlink - remove linked Pid records and +%% Pid died or requested to unlink - remove linked Pid records and %% optionally kill all OsPids linked to the Pid. -%% @end %%---------------------------------------------------------------------- -spec do_unlink_ospid(Pid::pid(), term(), State::#state{}) -> ok | true.