Erlexec
Execute and control OS processes from Erlang/OTP
Install / Use
/learn @saleyn/ErlexecREADME
Erlexec - OS Process Manager for the Erlang VM
Author Serge Aleynikov <saleyn(at)gmail.com>
Summary
Execute and control OS processes from Erlang/OTP.
This project implements an Erlang application with a C++ port program that gives light-weight Erlang processes fine-grain control over execution of OS processes.
The following features are supported:
- Start/stop OS commands and get their OS process IDs, and termination reason (exit code, signal number, core dump status).
- Support OS commands with unicode characters.
- Manage/monitor externally started OS processes.
- Execute OS processes synchronously and asynchronously.
- Set OS command's working directory, environment, process group, effective user, process priority.
- Provide custom termination command for killing a process or relying on default SIGTERM/SIGKILL behavior.
- Specify custom timeout for SIGKILL after the termination command or SIGTERM was executed and the running OS child process is still alive.
- Link Erlang processes to OS processes (via intermediate Erlang Pids that are linked to an associated OS process), so that the termination of an OS process results in termination of an Erlang Pid, and vice versa.
- Monitor termination of OS processes using
erlang:monitor/2. - Terminate all processes belonging to an OS process group.
- Kill processes belonging to an OS process group at process exit.
- Communicate with an OS process via its STDIN.
- Redirect STDOUT and STDERR of an OS process to a file, erlang process, or a custom function. When redirected to a file, the file can be open in append/truncate mode, and given at creation an access mask.
- Run interactive processes with psudo-terminal pty support.
- Support all RFC4254 pty psudo-terminal options defined in section-8 of the spec.
- Execute OS processes under different user credentials (using Linux capabilities).
- Perform proper cleanup of OS child processes at port program termination time.
This application provides significantly better control
over OS processes than built-in erlang:open_port/2 command with a
{spawn, Command} option, and performs proper OS child process cleanup
when the emulator exits.
The erlexec application has been in production use by Erlang and Elixir systems,
and is considered stable.
Donations
If you find this project useful, please donate to:
- Bitcoin:
12pt8TcoMWMkF6iY66VJQk95ntdN4pFihg - Ethereum:
0x268295486F258037CF53E504fcC1E67eba014218
Supported Platforms
Linux, Solaris, FreeBSD, DragonFly, OpenBSD, MacOS X
DOCUMENTATION
See https://hexdocs.pm/erlexec/readme.html
USAGE
Erlang: import as a dependency
- Add dependency in
rebar.config:
{deps,
[% ...
{erlexec, "~> 2.0"}
]}.
- Include in your
*.app.src:
{applications,
[kernel,
stdlib,
% ...
erlexec
]}
Elixir: import as a dependency
defp deps do
[
# ...
{:erlexec, "~> 2.0"}
]
end
BUILDING FROM SOURCE
Make sure you have rebar or rebar3 installed locally and the rebar script is in the path.
If you are deploying the application on Linux and would like to
take advantage of exec-port running tasks using effective user IDs
different from the real user ID that started exec-port, then
either make sure that libcap-dev[el] library is installed or make
sure that the user running the port program has sudo rights.
OS-specific libcap-dev installation instructions:
- Fedora, CentOS: "yum install libcap-devel"
- Ubuntu: "apt-get install libcap-dev"
$ git clone git@github.com:saleyn/erlexec.git
$ make
# NOTE: for disabling optimized build of exec-port, do the following instead:
$ OPTIMIZE=0 make
By default port program's implementation uses poll(2) call for event
demultiplexing. If you prefer to use select(2), set the following environment
variable:
$ USE_POLL=0 make
LICENSE
The program is distributed under BSD license.
Copyright (c) 2003 Serge Aleynikov
Architecture
<pre> ┌───────────────────────────┐ │ ┌────┐ ┌────┐ ┌────┐ │ │ │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 └──────┘ └──────┘ └──────┘ </pre>Configuration Options
See description of types in {@link exec:exec_options()}.
The exec-port program requires the SHELL variable to be set. If you are
running Erlang inside a docker container, you might need to ensure that SHELL
is properly set prior to starting the emulator.
Debugging
exec supports several debugging options passed to exec:start/1:
{debug, Level}- turns on debug verbosity in the port program.verbose- turns on verbosity in the Erlang code.valgrind- runs exec under the Valgrind tool, which needs to be installed in the OS. This generates a localvalgrind.YYYYMMDDhhmmss.logfile containing Valgrind's output. If you need to customize the Valgrind command options, use{valgrind, "/path/to/valgrind Args ..."}option.
Examples
Starting/stopping an OS process
1> exec:start(). % Start the port program.
{ok,<0.32.0>}
2> {ok, _, I} = exec:run_link("sleep 1000", []). % Run a shell command to sleep for 1000s.
{ok,<0.34.0>,23584}
3> exec:stop(I). % Kill the shell command.
ok % Note that this could also be accomplished
% by doing exec:stop(pid(0,34,0)).
In Elixir:
iex(1)> :exec.start
{:ok, #PID<0.112.0>}
iex(2)> :exec.run("echo ok", [:sync, :stdout])
{:ok, [stdout: ["ok\n"]]}
iex(3)> :exec.run(["/bin/echo", "ok"], [:sync, :stdout])
{:ok, [stdout: ["ok\n"]]}
Clearing environment or unsetting an env variable of the child process
%% Clear environment with {env, [clear]} option:
10> f(Bin), {ok, [{stdout, [Bin]}]} = exec:run("env", [sync, stdout, {env, [clear]}]), p(re:split(Bin, <<"\n">>)).
[<<"PWD=/home/...">>,<<"SHLVL=0">>, <<"_=/usr/bin/env">>,<<>>]
ok
%% Clear env and add a "TEST" env variable:
11> f(Bin), {ok, [{stdout, [Bin]}]} = exec:run("env", [sync, stdout, {env, [clear, {"TEST", "xxx"}]}]), p(re:split(Bin, <<"\n">>)).
[<<"PWD=/home/...">>,<<"SHLVL=0">>, <<"_=/usr/bin/env">>,<<"TEST=xxx">>,<<>>]
%% Unset an "EMU" env variable:
11> f(Bin), {ok, [{stdout, [Bin]}]} = exec:run("env", [sync, stdout, {env, [{"EMU", false}]}]), p(re:split(Bin, <<"\n">>)).
[...]
ok
Running exec-port as another effective user
In order to be able to use this feature the current user must either have sudo
rights or the exec-port file must be owned by root and have the SUID bit set
(use: chown root:root exec-port; chmod 4555 exec-port):
$ ll priv/x86_64-unknown-linux-gnu/exec-port
-rwsr-xr-x 1 root root 777336 Dec 8 10:02 ./priv/x86_64-unknown-linux-gnu/exec-port
If the effective user doesn't have rights to access the exec-port
program in the real user's directory, then the exec-port can be copied to some
shared location, which will be specified at startup using
{portexe, "/path/to/exec-port"}.
$ cp $(find . -name exec-port) /tmp
$ chmod 755 /tmp/exec-port
$ whoami
serge
$ erl
1> exec:start([{user, "wheel"}, {portexe, "/tmp/exec-port"}]). % Start the port program as effective user "wheel".
{ok,<0.32.0>}
$ ps haxo user,comm | grep exec-port
wheel exec-port
Allowing exec-port to run commands as other effective users
In order to be able to use this feature the current user must either have sudo
rights or the exec-port file must have the SUID bit set, and the exec-port file
must have the capabilities set as described in the "Build" section above.
The port program will initially be started as root, and then it will
switch the effective user to {user, User} and set process capabilities to
cap_setuid,cap_kill,cap_sys_nice. After that it'll allow to run child programs
under effective users listed in the {limit_users, Users} option.
$ whoami
serge
$ erl
1> Opts = [root, {user, "wheel"}, {limit_users, ["alex","guest"]}],
2> exec:start(Opts). % Start the port program as effective user "wheel"
% and allow it to execute commands as "alex" or "guest".
{ok,<0.32.0>}
3> exec:run("whoami", [sync, stdout, {user, "alex"}]). % Command is executed under effective user "alex"
{ok,[{stdout,[<<"alex\n">>]}]}
$ ps haxo user,comm | grep exec-port
wheel exec-port
Running the port program as root
While running the port program as root is highly discouraged, since it opens a security hole that gives users an ability to damage the system, for those who might need such an option, here is how to get it done (PROCEED AT YOUR OWN RISK!!!).
Related Skills
node-connect
339.3kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
83.9kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
339.3kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
83.9kCommit, push, and open a PR
