Elixir/Ports and external process wiring: Difference between revisions

Adamw (talk | contribs)
light edits
Adamw (talk | contribs)
 
(4 intermediate revisions by the same user not shown)
Line 59: Line 59:
; <code>--itemize-changes</code> : list the operations taken on each file
; <code>--itemize-changes</code> : list the operations taken on each file


; <code>--out-format=FORMAT</code> : any custom format string following rsyncd.conf's <code>log format</code><ref>https://man.freebsd.org/cgi/man.cgi?query=rsyncd.conf</ref>
; <code>--out-format=FORMAT</code> : any custom format string following rsyncd.conf's <code>log format</code><ref>[https://man.archlinux.org/man/rsyncd.conf.5#log~2 rsyncd.conf log format] docs</ref>
}}
}}


Line 110: Line 110:
The unpleasant real-world consequence is that rsync transfers will continue to run in the background even after Elixir kills our gen_server or shuts down, because the BEAM has no way of stopping the external process.
The unpleasant real-world consequence is that rsync transfers will continue to run in the background even after Elixir kills our gen_server or shuts down, because the BEAM has no way of stopping the external process.


It's possible to send a signal by shelling out to unix <code>kill PID</code>, but BEAM doesn't expose the child process ID and doesn't include any built-in functions to send a signal to an OS process.  Clearly we're expected to do this another way.  Another problem with "kill" is that we want the external process to stop no matter how badly the BEAM is damaged, so we shouldn't rely on stored data or on running final clean-up logic before exiting.
It's possible to find the operating system PID of the child process with <code>Port.info(port, :os_pid)</code> and send it a signal by shelling out to unix <code>kill PID</code>, but BEAM doesn't include built-in functions to send a signal to an OS process, and there is an ugly race condition between closing the port and sending this signalWe'll keep looking for another way to "link" the processes.


To debug what happens during <code>port_close</code> and to eliminate variables, I tried spawning  <code>sleep 60</code> instead of rsync and I found that it behaves in exactly the same way: hanging until <code>sleep</code> ends naturally regardless of what happened in Elixir or whether its pipes are still open.  This happens to have been a lucky choice as I learned later: "sleep" is daemon-like so similar to rsync, but its behavior is much simpler to reason about.
To debug what happens during <code>port_close</code> and to eliminate variables, I tried spawning  <code>sleep 60</code> instead of rsync and I found that it behaves in exactly the same way: hanging until <code>sleep</code> ends naturally regardless of what happened in Elixir or whether its pipes are still open.  This happens to have been a lucky choice as I learned later: "sleep" is daemon-like so similar to rsync, but its behavior is much simpler to reason about.
Line 123: Line 123:
</syntaxhighlight>
</syntaxhighlight>


If the program does blocking I/O, then a zero-byte <code>read</code> indicates the end of file condition.  A program which does asynchronous I/O with <code>O_NONBLOCK</code><ref>[https://man.archlinux.org/man/open.2.en#O_NONBLOCK O_NONBLOCK docs]</ref> might instead detect EOF by listening for the <code>HUP</code> hang-up signal which is normally sent when input is closed.
If the program does blocking I/O, then a zero-byte <code>read</code> indicates the end of file condition.  A program which does asynchronous I/O with <code>O_NONBLOCK</code><ref>[https://man.archlinux.org/man/open.2.en#O_NONBLOCK O_NONBLOCK docs]</ref> might instead detect EOF by listening for the <code>HUP</code> hang-up signal which is can be arranged (TODO: document how this can be done with <code>prctl</code>, and on which platforms).


But here we'll focus on how processes can more generally affect each other through pipes.  Surprising answer: without much effect!  You can experiment with the <code>/dev/null</code> device which behaves like a closed pipe, for example compare these two commands:
But here we'll focus on how processes can more generally affect each other through pipes.  Surprising answer: without much effect!  You can experiment with the <code>/dev/null</code> device which behaves like a closed pipe, for example compare these two commands:
Line 168: Line 168:
Which signal to use is still an open question:
Which signal to use is still an open question:


; <code>HUP</code> : sent to a process when its standard input stream is closed<ref>[https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap11.html#tag_11_01_10 POSIX standard "General Terminal Interface: Modem Disconnect"</ref>
; <code>HUP</code> : sent to a process when its standard input stream is closed<ref>[https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap11.html#tag_11_01_10 POSIX standard "General Terminal Interface: Modem Disconnect"]</ref>


; <code>TERM</code> : has a clear intention of "kill this thing" but still possible to trap at the target and handle in a customized way
; <code>TERM</code> : has a clear intention of "kill this thing" but still possible to trap at the target and handle in a customized way
Line 176: Line 176:
There is a refreshing diversity of opinion, so it could be worthwhile to make the signal configurable for each port.
There is a refreshing diversity of opinion, so it could be worthwhile to make the signal configurable for each port.
}}
}}
== TODO: consistency with unix process groups ==
... there is something fun here about how unix already has process tree behaviors which are close analogues to a BEAM supervisor tree.


== Future directions ==
== Future directions ==