diff --git a/PATCHES.md b/PATCHES.md index c93f2b2..d57e79f 100644 --- a/PATCHES.md +++ b/PATCHES.md @@ -21,7 +21,15 @@ in-tree means the patch can't be lost to a dependency re-resolve. target at runtime for automatic VM memory reclamation — see `MemoryBalloon.swift` / `ContainerEngine` in NucleicCore. -2. **Trimmed for footprint (no behavior change).** `Tests/`, `docs/`, `examples/`, and `images/` +2. **`Sources/Containerization/LinuxProcess.swift` — process-group kill.** + `LinuxProcess` gains `killProcessGroup(_:)`, which signals the negative pid (`-pid`) so the + guest's `kill(2)` targets the exec'd process's whole **process group**, not just the leader. + Every exec is `setsid()`'d by `vmexec`, so the process is its own group leader (pgid == pid) and + a group signal reaches the children it forked. Upstream only exposes the leader-only `kill(_:)`, + which let a forked child survive a Stop in a long-lived shared container. Marked with + `[Nucleic vendored patch]`; used by `ContainerizedProcessHandle.sendSignal` in NucleicCore. + +3. **Trimmed for footprint (no behavior change).** `Tests/`, `docs/`, `examples/`, and `images/` were dropped, and the corresponding `.testTarget(...)` entries removed from `Package.swift`. The library/executable targets we build are untouched. @@ -32,6 +40,7 @@ in-tree means the patch can't be lost to a dependency re-resolve. 2. `rsync -a --exclude=.git --exclude=.build --exclude=.swiftpm --exclude=Tests/ --exclude=docs/ \ --exclude=examples/ --exclude=images/ / third_party/containerization/` 3. Remove the `.testTarget(...)` blocks from `third_party/containerization/Package.swift`. -4. Re-apply patch #1 (the `vmExtensions` field + the `vmConfig.extensions = …` forward). +4. Re-apply patch #1 (the `vmExtensions` field + the `vmConfig.extensions = …` forward) and patch + #2 (`LinuxProcess.killProcessGroup(_:)`). Grep for `[Nucleic vendored patch]` to find every site. 5. Update the commit hash above and in the root `Package.swift` comment. 6. `swift build` and run the balloon tests. diff --git a/Sources/Containerization/LinuxProcess.swift b/Sources/Containerization/LinuxProcess.swift index 0d8300b..9409a5d 100644 --- a/Sources/Containerization/LinuxProcess.swift +++ b/Sources/Containerization/LinuxProcess.swift @@ -320,6 +320,27 @@ extension LinuxProcess { } } + /// [Nucleic vendored patch] Deliver a signal to the whole process GROUP led by this exec'd + /// process, not just the leader. `vmexec` `setsid()`s every exec, so the process is its own + /// session/group leader and its pgid equals its pid; a negative pid makes the guest's `kill(2)` + /// target the entire group, reaching any children the agent forked (model/turn subprocesses, + /// tool shells). `kill(_:)` above signals only the leader, so a wedged child can survive a Stop + /// in a long-lived shared container — this is the group-wide counterpart. Best-effort and + /// guarded against pid ≤ 1 (a non-positive pid would target the caller's group / every process). + public func killProcessGroup(_ signal: Signal) async throws { + let leader = self.pid + guard leader > 1 else { return } + do { + _ = try await agent.kill(pid: -leader, signal: signal.rawValue) + } catch { + throw ContainerizationError( + .internalError, + message: "failed to kill process group", + cause: error + ) + } + } + /// Resize the processes pty (if requested). public func resize(to: Terminal.Size) async throws { do {