From eef3df9fa562711ec54011117e563690baa541f5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 17 Jan 2026 08:24:34 +0000 Subject: [PATCH] fix(macos): drain subprocess pipes before wait (#1081) Thanks @thesash. Co-authored-by: Sash Catanzarite --- CHANGELOG.md | 1 + apps/macos/Sources/Clawdbot/GatewayEnvironment.swift | 3 ++- apps/macos/Sources/Clawdbot/Launchctl.swift | 5 ++++- apps/macos/Sources/Clawdbot/PortGuardian.swift | 9 +++++---- apps/macos/Sources/Clawdbot/RuntimeLocator.swift | 3 ++- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55eee5bd3..6ec44cd9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ - Plugins: add zip installs and `--link` to avoid copying local paths. ### Fixes +- macOS: drain subprocess pipes before waiting to avoid deadlocks. (#1081) — thanks @thesash. - Telegram: accept tg/group/telegram prefixes + topic targets for inline button validation. (#1072) — thanks @danielz1z. - Sub-agents: normalize announce delivery origin + queue bucketing by accountId to keep multi-account routing stable. (#1061, #1058) — thanks @adam91holt. - Sessions: include deliveryContext in sessions.list and reuse normalized delivery routing for announce/restart fallbacks. (#1058) diff --git a/apps/macos/Sources/Clawdbot/GatewayEnvironment.swift b/apps/macos/Sources/Clawdbot/GatewayEnvironment.swift index d1371cdf1..e2ee0e4ba 100644 --- a/apps/macos/Sources/Clawdbot/GatewayEnvironment.swift +++ b/apps/macos/Sources/Clawdbot/GatewayEnvironment.swift @@ -279,6 +279,8 @@ enum GatewayEnvironment { process.standardError = pipe do { try process.run() + // Read pipe before waitUntilExit to avoid potential deadlock + let data = pipe.fileHandleForReading.readToEndSafely() process.waitUntilExit() let elapsedMs = Int(Date().timeIntervalSince(start) * 1000) if elapsedMs > 500 { @@ -294,7 +296,6 @@ enum GatewayEnvironment { bin=\(binary, privacy: .public) """) } - let data = pipe.fileHandleForReading.readToEndSafely() let raw = String(data: data, encoding: .utf8)? .trimmingCharacters(in: .whitespacesAndNewlines) return Semver.parse(raw) diff --git a/apps/macos/Sources/Clawdbot/Launchctl.swift b/apps/macos/Sources/Clawdbot/Launchctl.swift index 74690f7b9..50d9052b2 100644 --- a/apps/macos/Sources/Clawdbot/Launchctl.swift +++ b/apps/macos/Sources/Clawdbot/Launchctl.swift @@ -17,8 +17,11 @@ enum Launchctl { process.standardError = pipe do { try process.run() - process.waitUntilExit() + // Read pipe output BEFORE waitUntilExit to avoid deadlock. + // If the process writes enough to fill the pipe buffer (~64KB), + // it will block until someone reads. Reading first prevents this. let data = pipe.fileHandleForReading.readToEndSafely() + process.waitUntilExit() let output = String(data: data, encoding: .utf8) ?? "" return Result(status: process.terminationStatus, output: output) } catch { diff --git a/apps/macos/Sources/Clawdbot/PortGuardian.swift b/apps/macos/Sources/Clawdbot/PortGuardian.swift index fb9adf52b..da7e523c1 100644 --- a/apps/macos/Sources/Clawdbot/PortGuardian.swift +++ b/apps/macos/Sources/Clawdbot/PortGuardian.swift @@ -204,14 +204,15 @@ actor PortGuardian { proc.standardError = Pipe() do { try proc.run() + // Read pipe before waitUntilExit to avoid potential deadlock + let data = pipe.fileHandleForReading.readToEndSafely() proc.waitUntilExit() + guard !data.isEmpty else { return nil } + return String(data: data, encoding: .utf8)? + .trimmingCharacters(in: .whitespacesAndNewlines) } catch { return nil } - let data = pipe.fileHandleForReading.readToEndSafely() - guard !data.isEmpty else { return nil } - return String(data: data, encoding: .utf8)? - .trimmingCharacters(in: .whitespacesAndNewlines) } private static func parseListeners(from text: String) -> [Listener] { diff --git a/apps/macos/Sources/Clawdbot/RuntimeLocator.swift b/apps/macos/Sources/Clawdbot/RuntimeLocator.swift index 8fd47073f..2cdead17e 100644 --- a/apps/macos/Sources/Clawdbot/RuntimeLocator.swift +++ b/apps/macos/Sources/Clawdbot/RuntimeLocator.swift @@ -134,6 +134,8 @@ enum RuntimeLocator { do { try process.run() + // Read pipe before waitUntilExit to avoid potential deadlock + let data = pipe.fileHandleForReading.readToEndSafely() process.waitUntilExit() let elapsedMs = Int(Date().timeIntervalSince(start) * 1000) if elapsedMs > 500 { @@ -149,7 +151,6 @@ enum RuntimeLocator { bin=\(binary, privacy: .public) """) } - let data = pipe.fileHandleForReading.readToEndSafely() return String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) } catch { let elapsedMs = Int(Date().timeIntervalSince(start) * 1000)