mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
1257279de6
Writing to a pipe whose reader has already exited raises SIGPIPE, which kills the whole process instead of throwing. MacNodeHostWorker already guarded its stdin pipe with F_SETNOSIGPIPE; the codex App Server client and the MLX TTS helper transport did not, so a child exiting mid-write could take down the app. Suppressing the signal exposed that an undelivered App Server request write was reported as appServerUnavailable even though the frame was provably never sent, so it now requeues once onto a fresh child instead of failing the caller. Test-side pipe write ends whose readers are spawned children (or a readability handler that can close the pipe mid-test) get the same suppression so a racing reader exit fails the assertion instead of killing swiftpm-testing-helper with signal 13, which is what caused the macos-swift CI lane's intermittent unrelated-test crashes (e.g. PR #126559, run 32341197738 job 96340683947).
58 lines
2.1 KiB
Swift
58 lines
2.1 KiB
Swift
import Darwin
|
|
import Foundation
|
|
import Testing
|
|
@testable import OpenClaw
|
|
|
|
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)
|
|
}
|
|
|
|
/// SIGPIPE from a write whose reader already exited kills the entire test
|
|
/// process (swiftpm-testing-helper dies with signal 13, blaming whatever
|
|
/// test happens to be running). Mirror the production F_SETNOSIGPIPE guard
|
|
/// (MacNodeHostWorker) so a racing reader exit surfaces as a thrown EPIPE
|
|
/// on that one write instead.
|
|
static func suppressSIGPIPE(_ writeEnd: FileHandle) throws {
|
|
try #require(writeEnd.disableSIGPIPE())
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|