Working with Operating System Processes
Mix.install([
{:kino, "~> 0.12.3"},
{:rambo, "~> 0.3.4"},
{:muontrap, "~> 1.4"}
])
Introduction
Lets explore some options for the classical system
function:
- :os.cmd Access to running a command in an OS shell.
- Port Fundamental mechanism for starting operating system processes.
-
System.cmd/3 Handy abstraction on top of
Port
. - Rambo Appears fairly mature and easy to use.
- MuonTrap Supports supervision and cgroups control of external processes, and proper management of long-running processes.
Below, you will find examples for a subset of these. The set of examples is in no way complete.
Porcelain appears to have issues and has thus been left out.
Note: These examples are going to assume that you are on a UNIX system.
Using :os.cmd/1
Execute command in a shell:
:os.cmd(~c"echo hello")
Both input and output are charlists. To convert to a string→string interface, you could do this:
"echo hello"
|> to_charlist()
|> :os.cmd()
|> to_string()
Using System.cmd/3
Execute a command with a parameter and get printouts and return code:
{output, return_code} = System.cmd("echo", ["hello"])
Execute command within a custom environment (derived from the current one):
{output, return_code} = System.cmd("env", [], env: [{"CUTEST_ANIMAL", "alpaca"}])
output
|> String.split("\n")
|> Enum.sort()
|> Enum.map(fn line ->
case String.split(line, "=") do
[key, value] -> " - **#{key}:** #{value}\n"
_ -> ""
end
end)
|> Enum.join()
|> Kino.Markdown.new()
Note: A few parameters of this function have been left out of this coverage:
-
:cd
for setting the current directory for the OS process. -
:stderr_to_stdout
for redirectingSTDERR
intoSTDOUT
.
Using Port
Data can be sent to an OS process (via STDIN) and received from it (via STDOUT):
port = Port.open({:spawn, "cat"}, [:binary])
send(port, {self(), {:command, "hello"}})
receive do
{_port, {:data, msg}} -> IO.puts(msg)
end
Note: The port
keys the messages should you wish to communicate with two of these.
An OS process that has been started in this way can be killed:
send(port, {self(), :close})
Note: Port
can do a number of other things. See documentation for details.
Using Rambo
Execute a command with a parameter and get printouts and return code:
{:ok, %Rambo{status: status, out: out, err: err}} = Rambo.run("echo", ["hello"])
Execute command within a custom environment (derived from current one):
{:ok, %Rambo{status: status, out: out, err: err}} =
Rambo.run("env", [], env: [{"CUTEST_ANIMAL", "alpaca"}])
out
|> String.split("\n")
|> Enum.sort()
|> Enum.map(fn line ->
case String.split(line, "=") do
[key, value] -> " - **#{key}:** #{value}\n"
_ -> ""
end
end)
|> Enum.join()
|> Kino.Markdown.new()
Providing one-shot input (via STDIN):
{:ok, %Rambo{status: status, out: out, err: err}} =
Rambo.run("cat", in: "hello")
Pipeline of external programs to list the (alphabetically) first 10 files in the current directory:
{:ok, %Rambo{status: status, out: out, err: err}} =
Rambo.run("ls")
|> Rambo.run("sort")
|> Rambo.run("head", ["-10"])
out
|> String.split("\n")
|> Enum.filter(fn line -> not (line == "") end)
|> List.foldl({1, ""}, fn line, {i, acc} -> {i + 1, acc <> "#{i}. #{line}\n"} end)
|> elem(1)
|> Kino.Markdown.new()
Note: To run Rambo inside a GenServer, use this trick.
Using MuonTrap
Execute a command with a parameter and get printouts and return code:
{output, return_code} = MuonTrap.cmd("echo", ["hello"])
MuonTrap.cmd/3
supports all the System.cmd/3
options that have been covered in this document.
In addition to this, it (among others) supports:
-
:timeout
and:delay_to_sigkill
to define when and how to automatically make the command exit. -
:uid
andgid
to define the UID and GID values for the command. - A number of cgroup related options.
Conclusions
There are lots of options with some nice and easily accessible features, especially at the simple end.
So far, however, I haven’t found exactly what I was looking for. That is:
- Supervision of OS process like MuonTrap.
- Wrapping in a GenServer for supervision of itself.
- An interface function for sending to STDIN.
- An interface function for sending aritrary signals.
- Something pubsuby that allows STDOUT and STDERR to be treated either separably or the same.
- Implementation of he GenStage behaviour. Not sure about the tradeoffs here though.