IO.inspect(System.cmd("echo", ["hello"]), label: "echo")
echo: {"hello\n", 0}
Sooner or later Elixir code needs something outside the BEAM: a command-line tool, a C library, a Python script. There are two ways in, with opposite risks:
System.cmd/3 is the simplest port: run a program,
wait for it to exit, and get its output and exit code.
IO.inspect(System.cmd("echo", ["hello"]), label: "echo")
echo: {"hello\n", 0}
A failing program is just a non-zero exit code. Nothing crashes on the Elixir side.
IO.inspect(
System.cmd("sh", ["-c", "echo oops >&2; exit 3"], stderr_to_stdout: true),
label: "failing"
)
failing: {"oops\n", 3}
Port.open/2 keeps the program running and lets you
talk to it. The port behaves like a process: the
process that opened it (its owner) sends it data and
receives {port, {:data, ...}} messages back.
cat = System.find_executable("cat")
port = Port.open({:spawn_executable, cat}, [:binary, :exit_status])
Port.command(port, "ping\n")
receive do: ({^port, {:data, data}} -> IO.inspect(data, label: "cat echoed"))
Port.close(port)
cat echoed: "ping\n"
If the program dies, even from kill -9, the owner
gets its exit status as a message (137 is 128 + signal
9), and the node carries on.
sh = System.find_executable("sh")
port = Port.open({:spawn_executable, sh}, [:exit_status, args: ["-c", "kill -9 $$"]])
receive do: ({^port, {:exit_status, status}} -> IO.inspect(status, label: "killed program"))
killed program: 137
The other direction is the surprise. When the owner
exits, the port closes, but that only closes the
program's stdin. A program that never reads stdin
keeps running. Here the owner starts sleep 30 and
exits at once.
me = self()
{_owner, ref} =
spawn_monitor(fn ->
sleep = System.find_executable("sleep")
port = Port.open({:spawn_executable, sleep}, args: ["30"])
{:os_pid, os_pid} = Port.info(port, :os_pid)
send(me, {:os_pid, Integer.to_string(os_pid)})
end)
os_pid = receive do: ({:os_pid, os_pid} -> os_pid)
receive do: ({:DOWN, ^ref, :process, _, _} -> :ok)
kill -0 sends no signal, it only checks that the
process exists. Exit code 0: sleep is still there.
running? = fn -> match?({_, 0}, System.cmd("kill", ["-0", os_pid], stderr_to_stdout: true)) end
IO.inspect(running?.(), label: "sleep still running after its owner died")
sleep still running after its owner died: true
Clean up the orphan ourselves, and wait until it's gone.
System.cmd("kill", [os_pid])
Stream.repeatedly(running?) |> Enum.find(&(&1 == false))
IO.inspect(running?.(), label: "after kill")
after kill: false
Pitfall: programs started from a port can outlive the process (and even the node) that started them. Use programs that exit when stdin closes, or a wrapper that does it for them: the
muontrapanderlexeclibraries kill the OS process when its Elixir owner dies.
Pitfall: a NIF that crashes (a segfault, a
panicthat isn't caught) takes the whole node down with every process on it. No supervisor can help. Prefer a port unless you need the speed. If you do write a NIF, Rustler (Rust) is safer than raw C: it catches Rust panics and raises them as Elixir errors.
Pitfall: a NIF that runs for more than about a millisecond blocks its scheduler, and every process waiting on that scheduler stalls. Long NIFs must run on a dirty scheduler (see Schedulers and Reductions).
Pitfall:
:os.cmd/1andSystem.shell/2run a string through the shell. With user input in that string, that's shell injection.System.cmd/3passes arguments as a list, with no shell between.
Go deeper: The BEAM Book by Erik Stenman explains ports (chapter 13) and NIFs, BIFs and linked-in drivers (chapter 16).