[erlang-questions] Sending signals to non-erlang processes
Michael Santos
michael.santos@REDACTED
Sat Oct 17 02:17:42 CEST 2015
On Fri, Oct 16, 2015 at 01:18:40PM +0200, Jesper Louis Andersen wrote:
> On Fri, Oct 16, 2015 at 11:30 AM, Nicolas Martyanoff <khaelin@REDACTED>
> wrote:
>
> > I also cannot find a way to actually stop the spawned application,
> > port_close() does do it. The "UNIX way" is to send SIGTERM, wait for a bit,
> > then send SIGKILL if the application did not stop. But I cannot find an
> > erlang
> > function to send a signal to an external process. I made a temporary fix
> > using
> > os:cmd("kill ..."), but it feels like a hack.
> >
>
> Two options: misbehaving programs can be handled through Aleynikov's
> https://github.com/saleyn/erlexec which wraps[0] them in a C++ helper
> process which understands how to gracefully communicate to the Erlang world.
>
> Port programs normally communicate through a set of file descriptors, so
> the program you spawn should detect and terminate if there are errors when
> reading on the fd. I've been down this rabbit hole before, but I'm afraid I
> forgot, again, how it all works. Perhaps this is good to document in a "How
> to write behaving port programs" document and make it part of the OTP
> documentation.
A well behaved port program is like a process started from inetd.
When the port is closed:
* the read from stdin will return 0 bytes (EOF) or an error
* writing to stdout will result in a SIGPIPE being sent to the port
process or return an error (EPIPE)
In both cases, it is up to the port process to exit if there is an error
condition.
A simple way of testing the behaviour of closing stdin:
% Start a port running the cat command
1> Port = open_port({spawn, "/bin/cat"}, []).
#Port<0.21242>
% Send some data and get a response
2> port_command(Port, "test\n", []).
true
3> flush().
Shell got {#Port<0.21242>,{data,"test\n"}}
ok
% Get the PID of the command
4> erlang:port_info(Port).
[{name,"/bin/cat"},
{links,[<0.60.0>]},
{id,12197},
{connected,<0.60.0>},
{input,5},
{output,5},
{os_pid,7584}]
% Close the port
5> port_close(Port).
true
% Confirm the port has exited
6> os:cmd("kill -0 7584").
"/bin/sh: 1: kill: No such process\n\n"
Similarly, we can test the behaviour when writing to stdout:
% Write to stdout
1> Port = open_port({spawn, "/usr/bin/yes"}, []), Info = erlang:port_info(Port), port_close(Port), Info.
[{name,"/usr/bin/yes"},
{links,[<0.60.0>]},
{id,12197},
{connected,<0.60.0>},
{input,0},
{output,0},
{os_pid,7845}]
2> os:cmd("kill -0 7845").
"/bin/sh: 1: kill: No such process\n\n"
Here is an example of a badly behaving port:
#!/bin/bash
while :; do
echo test
sleep 1
done
Observing the behaviour under strace:
% erlang shell
1> Port = open_port({spawn, "bad.sh"}, []).
#Port<0.21242>
2> flush().
Shell got {#Port<0.21242>,{data,"test\n"}}
Shell got {#Port<0.21242>,{data,"test\n"}}
Shell got {#Port<0.21242>,{data,"test\n"}}
Shell got {#Port<0.21242>,{data,"test\n"}}
ok
3> port_close(Port).
true
# strace
$ strace -p 8323
write(1, "test\n", 5) = 5
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0xb6f133c8) = 8420
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 8420
--- SIGCHLD (Child exited) @ 0 (0) ---
sigreturn() = ? (mask now [QUIT ILL TRAP BUS SEGV USR2 CHLD STOP TSTP])
write(1, "test\n", 5) = -1 EPIPE (Broken pipe)
--- SIGPIPE (Broken pipe) @ 0 (0) ---
clone(child_stack=0,
flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0xb6f133c8) = 8421
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 8421
--- SIGCHLD (Child exited) @ 0 (0) ---
sigreturn() = ? (mask now [QUIT ILL TRAP BUS SEGV USR2 CHLD STOP TSTP])
write(1, "test\n", 5) = -1 EPIPE (Broken pipe)
--- SIGPIPE (Broken pipe) @ 0 (0) ---
So the shell script is running the echo in a forked process. When the
child writes to stdout, it is being killed by SIGPIPE but the shell is
ignoring the child's non-zero exit status.
And the same shell script modified to exit when stdin has been closed:
#!/bin/bash
while read l; do
echo "test"
sleep 1
done
So:
* a port process must be written to exit if stdin or stdout are closed
* badly behaving port processes may not exit. They may be unkillable
for many reasons: masking signals, running commands in a subprocess or
setuid and running as different user.
If we're running port processes, why not run another port process to
clean up? For example:
#!/bin/sh
# Port = open_port({spawn, "kill.sh"}, []), port_command(Port, "1234 9", []).
while read l; do
set -- $l
kill -s $2 $1
done
Which is equivalent to running:
os:cmd("kill -9 1234").
More information about the erlang-questions
mailing list