Files
openclaw/apps/macos/Tests/OpenClawIPCTests/TestProcessSupport.swift
Peter Steinberger 8e398d9591 fix(macos): prevent app-owned child processes surviving shutdown (#123538)
* fix(macos): reap app-owned child process groups

macOS-owned SSH, Codex, and node-host descendants no longer survive terminal shutdown. Codex retains EOF-first graceful exit before bounded process-group termination and reaping.

* fix(macos): correct managed cleanup wake binding

* fix(macos): preserve Codex shutdown escalation

Keep the app-owned EOF grace window while allowing abortive requests to interrupt it before process-group TERM and KILL.
2026-08-14 01:09:32 -07:00

48 lines
1.6 KiB
Swift

import Darwin
import Foundation
import Testing
enum TestProcessSupport {
static func pollPID(in file: URL) -> pid_t? {
guard let value = try? String(contentsOf: file, encoding: .utf8)
.trimmingCharacters(in: .whitespacesAndNewlines)
else { return nil }
return pid_t(value)
}
static func waitForPID(in file: URL, timeout: Duration = .seconds(2)) async throws -> pid_t {
let clock = ContinuousClock()
let deadline = clock.now.advanced(by: timeout)
while clock.now < deadline {
if let pid = self.pollPID(in: file) { return pid }
try await Task.sleep(for: .milliseconds(10))
}
return try #require(self.pollPID(in: file))
}
static func processIsGone(_ pid: pid_t) -> Bool {
errno = 0
return kill(pid, 0) == -1 && errno == ESRCH
}
static func waitUntilGone(_ pid: pid_t, timeout: Duration = .seconds(2)) async -> Bool {
let deadline = ContinuousClock.now.advanced(by: timeout)
while ContinuousClock.now < deadline {
if self.processIsGone(pid) { return true }
try? await Task.sleep(for: .milliseconds(10))
}
return self.processIsGone(pid)
}
static func killLeakedProcesses(in files: [URL]) {
let pids = files.compactMap { self.pollPID(in: $0) }
for pid in pids where !self.processIsGone(pid) {
_ = kill(pid, SIGKILL)
}
let deadline = Date().addingTimeInterval(2)
while Date() < deadline, pids.contains(where: { !self.processIsGone($0) }) {
usleep(10000)
}
}
}