From f7c336f52744e865dc2a502c6a931e767a23b4b2 Mon Sep 17 00:00:00 2001 From: Adam Wight Date: Mon, 17 Feb 2025 22:50:45 +0100 Subject: [PATCH] WIP: Forward parent death to descendant processes (unix) If the forker's connection to the parent BEAM is broken or closed, react by killing all spawned children. When a spawned port is closed, kill the associated OS process. A concise demonstration of the problem being solved is to run the following command with and without the patch, then kill the BEAM. Without the patch, the "sleep" process will continue: erl -noshell -eval 'os:cmd("sleep 60")' TODO: * Needs a decision made between killing the process or process group. * Separate patch for win32 --- erts/emulator/sys/unix/erl_child_setup.c | 22 ++++++++++++++++++++++ erts/preloaded/src/erlang.erl | 5 +++++ 2 files changed, 27 insertions(+) diff --git a/erts/emulator/sys/unix/erl_child_setup.c b/erts/emulator/sys/unix/erl_child_setup.c index 74c658389fc..ea31021036f 100644 --- a/erts/emulator/sys/unix/erl_child_setup.c +++ b/erts/emulator/sys/unix/erl_child_setup.c @@ -183,6 +183,8 @@ static ssize_t write_all(int fd, const char *buff, size_t size) { return pos; } +static void kill_child(pid_t os_pid); +static void kill_all_children(void); static int forker_hash_init(void); static int max_files = -1; @@ -571,6 +573,7 @@ main(int argc, char *argv[]) tcsetattr(0,TCSANOW,&initial_tty_mode); } DEBUG_PRINT("erl_child_setup failed to read from uds: %d, %d", res, errno); + kill_all_children(); _exit(0); } @@ -579,6 +582,7 @@ main(int argc, char *argv[]) if (isatty(0) && isatty(1)) { tcsetattr(0,TCSANOW,&initial_tty_mode); } + kill_all_children(); _exit(0); } /* Since we use unix domain sockets and send the entire data in @@ -621,6 +625,7 @@ main(int argc, char *argv[]) est.os_pid = proto.u.stop.os_pid; es = hash_remove(forker_hash, &est); if (es) { + kill_child(es->os_pid); free(es); } } else { @@ -660,6 +665,23 @@ main(int argc, char *argv[]) return 1; } +/* Kill child process groups on VM termination so they don't become orphaned. */ + +static void kill_child(pid_t os_pid) { + if (os_pid > 0 && kill(os_pid, SIGTERM) != 0) { + DEBUG_PRINT("error killing process %d: %d", os_pid, errno); + } +} + +static void fun_kill_foreach(ErtsSysExitStatus *es, void *unused) { + kill_child(es->os_pid); +} + +static void kill_all_children(void) { + DEBUG_PRINT("cleaning up by killing all %d child processes", forker_hash->nobjs); + hash_foreach(forker_hash, (HFOREACH_FUN)fun_kill_foreach, NULL); +} + static int fcmp(void *a, void *b) { ErtsSysExitStatus *sa = a; diff --git a/erts/preloaded/src/erlang.erl b/erts/preloaded/src/erlang.erl index 3b8d01d15ef..b076a7268b6 100644 --- a/erts/preloaded/src/erlang.erl +++ b/erts/preloaded/src/erlang.erl @@ -7475,6 +7475,11 @@ reported to the owning process using signals of the form The maximum number of ports that can be open at the same time can be configured by passing command-line flag [`+Q`](erl_cmd.md#max_ports) to [erl](erl_cmd.md). + +When a port is closed or the VM shuts down, spawned executables are sent a +`SIGTERM` on unix. The child may still outlive the VM if it traps the signal. +Note that any processes started under a shell using `spawn` will not terminate +unless they respond to stdin or stdout being closed. """. -doc #{ category => ports }. -spec open_port(PortName, PortSettings) -> port() when