Conversation
Note: Types.h change already present in upstream poco. Only TestResult.cpp needed porting.
…ect #5201 On Windows, TerminateProcess only kills the target process — grandchildren survive as orphans. PROCESS_KILL_TREE creates a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the entire process tree is torn down when ProcessRunner is stopped or destroyed.
…s groups #5201 On fork, the child calls setpgid(0, 0) to create a new process group. On stop, kill(-pid, sig) signals the entire group instead of just the leader. Gated on PROCESS_KILL_TREE to preserve default behavior.
There was a problem hiding this comment.
Pull request overview
This PR improves process termination behavior across platforms, primarily by adding an option to terminate an entire spawned process tree and by addressing a Windows termination-event lifetime race.
Changes:
- Add
PROCESS_KILL_TREEprocess option and implement tree-termination support (Windows Job Objects, Unix process groups). - Adjust Windows
ServerApplication::hasConsole()detection to avoid misclassifying interactive launches when stdout/stderr are closed. - Refine CppUnit
cxxabi.havailability detection to avoid_MSC_VERtoolchains (clang-clin particular).
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| Util/src/ServerApplication.cpp | Expands console detection (console window + stdin/out/err handles) to avoid false “no console” when std handles are closed. |
| Foundation/src/Process_UNIX.cpp | Adds setpgid(0, 0) for PROCESS_KILL_TREE to enable process-group based signaling. |
| Foundation/src/ProcessRunner.cpp | Implements Windows Job Object assignment for kill-tree, and Unix group-based polling/kill logic; adjusts Windows termination signaling to keep the event alive. |
| Foundation/include/Poco/ProcessRunner.h | Stores a Windows Job Object handle (_hJob) when PROCESS_KILL_TREE is enabled. |
| Foundation/include/Poco/ProcessOptions.h | Introduces PROCESS_KILL_TREE and documents intended platform behavior. |
| CppUnit/src/TestResult.cpp | Updates HAVE_CXXABI_H detection to exclude MSVC toolchains. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
doc update Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ject/poco into 5199-process-termination
|
Related ticket #5201 is actually a new feature that adds functionality and extends the API. Shall we add new feature to a bugfix release? |
It doesn't break anything, it is just one more bit in options, off by default. strictly speaking, we shouldn't, but we need it and I don't want to wait for next release |
|
The name of the PR does not reflect the actual changes: it fixes multiple issies. |
matejk
left a comment
There was a problem hiding this comment.
PR Review: Process Termination Fixes
CI is green (all 88 checks pass, including ASAN/TSAN/UBSAN). The clang-cl fix in TestResult.cpp and the QNX SPAWN_SETGROUP/SPAWN_NEWPGROUP path look correct and clean.
Critical
-
_hJobhandle leaked on normal process exit — The Job Object is created inrun()but only cleaned up instop()inside the_pPHguard. If the child exits on its own,run()completes and nulls_pPH, sostop()skips the cleanup block. Handle leaks on every natural-exit cycle. -
Stale
errnoinkill(-pid, 0)polling loop —Thread::sleep(10)between iterations can modifyerrno. Ifkill()returns -1/ESRCH (group gone) buterrnowas clobbered to EPERM by sleep internals, the|| errno == EPERMcondition passes and the loop spins until timeout. -
Unchecked
setpgid(0, 0)return — If it fails, the child continues in the parent's process group. Laterkill(-pid, SIGINT)targets a nonexistent group (ESRCH), the polling loop exits immediately, andstop()thinks the tree is dead.
Important
-
start()catch block doesn't close_hJob— Handle leak on startup failure. -
Windows
stop()doesn't wait for child tree — Only waits for the main PID; closes the Job Object but returns immediately. Asymmetric with Unix behavior. -
kill(-pid, SIGINT)return unchecked — EPERM on the initial signal gets masked as a timeout. -
fork/setpgid race — Parent could call
kill(-pid, ...)before child reachessetpgid(0, 0). Standard fix: also callsetpgid(pid, pid)in parent after fork.
Minor
-
No tests for
PROCESS_KILL_TREE— The existing ProcessRunnerTest doesn't exercise the new option. -
Silent degradation on Windows — If Job Object creation fails, PROCESS_KILL_TREE is silently ignored with no log/warning.
-
hasConsole()may over-detect — Piped handles (e.g. from a service manager) would cause a false positive. ConsiderGetFileType(h) == FILE_TYPE_CHARto confirm it's a console device.
See inline comments for details.
matejk
left a comment
There was a problem hiding this comment.
PR Review — Round 2 (post 3dc180d2)
All 5 issues from the first review have been addressed ✅ — HandleGuard RAII, stale errno, kill return check, setpgid failure handling, and hasConsole GetFileType. Good work.
Remaining / new issues below. See inline comments for details.
Critical
- Job Object setup fails silently — KILL_TREE degrades to no-op — The entire
CreateJobObjectW→SetInformationJobObject→OpenProcess→AssignProcessToJobObjectchain can fail at any stage (most commonlyAssignProcessToJobObjectreturnsERROR_ACCESS_DENIEDwhen the child is already in a non-breakaway job — common in CI, containers, and service managers). Every failure path just calls_hJob.reset()and continues. No log, no warning, no exception. The user explicitly opts in to tree-kill but the feature silently becomes a no-op.
Important
-
start()catch block:Process::kill()can throw, corrupting state — In the catch-all at lines 421-433,Process::kill(p)is called unguarded. It can throwNotFoundException/SystemException(seeProcess_WIN32U.cpp/Process_UNIX.cpp). If it does,_t.join()and_started.store(false)are never reached. The ProcessRunner is stuck in a state where_started==truebut the thread is orphaned — subsequentstart()always throwsInvalidAccessException. -
Windows
stop()sends NamedEvent unconditionally when KILL_TREE is active — asymmetric with Unix — On Unix,stop()branches onPROCESS_KILL_TREEto signal the group vs individual process. On Windows, the NamedEvent graceful signal is always sent (regardless of KILL_TREE), and tree termination relies on closing_hJoblater. This means: (a) the NamedEvent triggers graceful shutdown in the leader, which may start cleanup — then_hJobclose forcefully terminates the tree mid-cleanup; (b) the NamedEvent only works for PocoServerApplication-based processes — non-Poco children get no graceful signal at all. -
_exit(73)magic number is opaque to the parent — Ifsetpgid(0,0)fails in the child, exit code 73 is returned. The parent seesreturn=73in the error string with no indication this was an infrastructure failure vs the application itself returning 73. The actualerrnofromsetpgidis lost. -
Documentation/code mismatch in
ProcessOptions.h— The doc says the termination request is sent to the main process, but the code sendskill(-pid, SIGINT)to the entire process group. The doc should match the actual group-wide SIGINT behavior.
Other
-
No tests for
PROCESS_KILL_TREE— ProcessRunnerTest has zero test cases exercising the new option. No coverage for Job Object creation, process group setup, or tree termination on stop. -
HandleGuard design concerns — Public
handlemember bypasses the RAII invariant (code already accesses_hJob.handledirectly).reset()has no self-assignment guard (reset(handle)would close then lose the resource). Inconsistent withProcessHandleImplin the same codebase which uses a private_hProcess+process()accessor.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
| else | ||
| { | ||
| Poco::format(jobErr, "CreateJobObjectW: %s", Error::getMessage(Error::last())); |
There was a problem hiding this comment.
Same as the previous comment - verify the correct signature for Poco::format. The typical pattern is either format(output, formatString, args...) or output = format(formatString, args...).
| { | ||
| if (!AssignProcessToJobObject(_hJob.handle(), hProc)) | ||
| { | ||
| Poco::format(jobErr, "AssignProcessToJobObject: %s", Error::getMessage(Error::last())); |
There was a problem hiding this comment.
Same Poco::format signature issue as above. Ensure consistent usage throughout this code block.
No description provided.