Erlang: How to make connected external OS process automatically die when controlling Erlang process crashes?

Viewed 143

I am using Erlang port to read output of Linux process. I'd like the Linux process to be automatically killed whenever my connected Erlang process dies. From the docs, it seems to me that this should automatically happen, but it does not.

Minimal example. Put this in the file test.erl:

-module(test).
-export([start/0, spawn/0]).

start() ->
    Pid = spawn_link(?MODULE, spawn, []),
    register(test, Pid).

spawn() ->
    Port = open_port({spawn, "watch date"},[stream, exit_status]),
    loop([{port, Port}]).

loop(State) ->
    receive
        die ->
            error("died");
        Any ->
            io:fwrite("Received: ~p~n", [Any]),
            loop(State)
    end.

Then, in erl shell:

1> c(test).
{ok,test}
2> test:start().
true

The process starts and prints some data received from the Linux "watch" command every 2 seconds.

Then, I make the Erlang process crash:

3> test ! die.
=ERROR REPORT==== 26-May-2021::13:24:01.057065 ===
Error in process <0.95.0> with exit value:
{"died",[{test,loop,1,[{file,"test.erl"},{line,15}]}]}

** exception exit: "died"
     in function  test:loop/1 (test.erl, line 15)

The Erlang process dies as expected, the data from "watch" stops appearing but the watch process still keeps running in the background as can be seen in Linux (not erl) terminal:

fuxoft@frantisek:~$ pidof watch
1880127

In my real-life scenario, I am not using "watch" command but other process that outputs data and accepts no input. How can I make it automaticall die when my connected Erlang process crashes? I can do this using Erlang supervisor and manually issuing the "kill" command when Erlang process crashes but I thought this could be done easier and cleaner.

1 Answers

The open_port function creates a port() and links it to the calling process. If the owning process dies, the port() closes.

In order to communicate with the externally spawned command, Erlang creates several pipes, which are by default tied to the stdin and stdout (file descriptors) of the external process. Anything that the external process writes through the stdout will arrive as a message to the owning process.

When the Port is closed, the pipes attaching it to the external process are broken, and so trying to read or write to them will give you a SIGPIPE/EPIPE.

You can detect that from your external process when writing or reading from the FDs and exiting the process then.

E.g.: With your current code, you can retrieve the external process OS pid with proplists:get_value(os_pid, erlang:port_info(Port)). If you strace it, you will see:

write(1, ..., 38) = -1 EPIPE (Broken pipe)
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=31297, si_uid=1001} ---

SIGPIPE in ports and Erlang

It seems that although the default action for SIGPIPE is to terminate the process, Erlang sets it to ignore the signal (and thus the children processes inherit this configuration).

If you're unable to modify the external process code to detect the EPIPE, you can use this c wrapper to reset the action:

#include <unistd.h>
#include <signal.h>

int main(int argc, char* argv[]) {
    if (signal(SIGPIPE, SIG_DFL) == SIG_ERR)
        return 1;
    if (argc < 2)
        return 2;
    execv(argv[1], &(argv[1]));
}

just compile it and run it as wrapper path-to-executable [arg1 [arg2 [...]]] with open_port

Related