mirror of
https://github.com/Cacti/spine.git
synced 2026-10-23 03:27:10 +09:00
fix(process): unify subprocess ownership and child lifecycle safety (#614)
## Summary - make subprocess-registry detach atomic so exactly one closer owns each entry - make cancellation cleanup release only the detached node it owns - replace `vfork`/`execve` child setup with `posix_spawn` and explicit file actions - mark every pipe and stdio-collision duplicate close-on-exec - restore the child's default SIGPIPE disposition while preserving the parent's handling - bound TERM/KILL reaping and park unreaped children in a fixed-capacity sweep list - harden PHP script-server slot ownership, startup, restart, signal, descriptor, and cleanup paths - retire and restart failed or oversized reads so stale response tails cannot poison later data sources - revalidate every command retry and cap recovery handshakes at two seconds This folds the child-process lifecycle portion of #597 into the narrower ownership PR. It does not absorb unrelated poll-host, SQL, SNMP, ICMP, or build-infrastructure work. ## Review-driven corrections - close-on-exec is explicitly restored after `dup()` in both read and write stdio-collision branches - the source invariant check now uses portable POSIX character classes - direct tests cover duplicate flags, both collision branches, failure after pipe creation, abandoned-child sweeping, and capacity overflow - collision cleanup preserves current-child redirects even when an older registry entry occupies fd 0 or 1 - script children receive SIGTERM before SIGKILL and unreaped PHP children transfer to the bounded sweep registry - the Docker integration suite now starts a real PHP script server, verifies its startup protocol and database result, and checks that shutdown leaves no zombie PHP process ## Validation - Ubuntu 24.04 container with GCC and GNU ld: `make check`, 6/6 targets passed with no skips, including `test_php_runtime` - real PHP script-server Docker integration: 6/6 checks passed; exact value `42` reached `poller_output` and no zombie remained - macOS Homebrew dependency build passed - macOS `make check`: 5/5 available binaries passed; GNU `--wrap` runtime suite is Linux-only - PHP fixture syntax checked through mise PHP 8.3 - actionlint, shellcheck, and `git diff --check` passed Closes #610. --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: TheWitness <thewitness@cacti.net>
This commit is contained in:
1 parent
f989722458
commit
90da94b94f
19 files changed
+2749
-270
No files matched your search
+66
@@ -49,6 +49,8 @@
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include <spawn.h>
|
||||
|
||||
/*!
|
||||
* The nft_popen() function forks a command in a child process, and returns
|
||||
* a pipe that is connected to the child's standard input and output. It is
|
||||
@@ -94,4 +96,68 @@ extern int nft_pchild(int fd);
|
||||
*/
|
||||
extern int nft_pclose(int fd);
|
||||
|
||||
/*!
|
||||
* spine_set_cloexec
|
||||
*
|
||||
* Mark a descriptor close-on-exec.
|
||||
*
|
||||
* Returns 0 on success, -1 on failure with errno set by fcntl().
|
||||
*/
|
||||
extern int spine_set_cloexec(int fd);
|
||||
|
||||
/* Duplicate a descriptor and mark the duplicate close-on-exec. On failure,
|
||||
* no descriptor is returned and errno describes dup() or fcntl(). */
|
||||
extern int spine_dup_cloexec(int fd);
|
||||
|
||||
/*!
|
||||
* spine_open_pipe_cloexec
|
||||
*
|
||||
* Open a pipe whose descriptors are not inherited across exec. Spine spawns
|
||||
* children from several threads, so a descriptor left inheritable is held by
|
||||
* an unrelated child and the reader never sees EOF.
|
||||
*
|
||||
* Returns TRUE on success. On failure the descriptors are closed and FALSE is
|
||||
* returned, so the caller owns nothing.
|
||||
*/
|
||||
extern int spine_open_pipe_cloexec(int pdes[2]);
|
||||
|
||||
/* Restore SIGPIPE's default disposition in posix_spawned children while the
|
||||
* Spine parent handles broken pipes itself. */
|
||||
extern int spine_spawnattr_sigpipe_default(posix_spawnattr_t *attr);
|
||||
|
||||
/*!
|
||||
* spine_reap_child_bounded
|
||||
*
|
||||
* Reap a child with WNOHANG, sleeping between attempts, so a wedged script
|
||||
* cannot pin a poller thread indefinitely.
|
||||
*
|
||||
* Returns 0 when the child was reaped, 1 when it is still running after
|
||||
* attempts, and -1 on a waitpid() error other than EINTR or ECHILD.
|
||||
*/
|
||||
extern int spine_reap_child_bounded(pid_t pid, int *pstat, int attempts);
|
||||
|
||||
/*!
|
||||
* The cap on parked pids. Past it a child is logged and dropped, because an
|
||||
* unbounded list would trade a pid leak for a memory leak.
|
||||
*/
|
||||
#define NFT_ABANDONED_MAX 64
|
||||
|
||||
/*!
|
||||
* nft_abandon_child
|
||||
*
|
||||
* Record a child that outlived nft_pclose()'s kill budget. Nothing else in
|
||||
* spine reaps, so a dropped child would stay a zombie for the daemon's
|
||||
* lifetime; parked pids are swept on the next script poll. The pid and the
|
||||
* reason are logged either way.
|
||||
*/
|
||||
extern void nft_abandon_child(pid_t pid, const char *reason);
|
||||
|
||||
/*!
|
||||
* nft_abandoned_pending
|
||||
*
|
||||
* Sweep the parked pids with WNOHANG and return how many are still running.
|
||||
* Zero means nothing is leaking.
|
||||
*/
|
||||
extern int nft_abandoned_pending(void);
|
||||
|
||||
#endif /* SPINE_NFT_POPEN_H */
|
||||
Reference in new issue
Block a user