From 869ef0c5ba8f7bdcf8b6843d57c64be5e7dabeda Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 17 Jan 2026 08:53:03 +0000 Subject: [PATCH] refactor(macos): centralize process pipe draining --- apps/macos/Sources/Clawdbot/GatewayEnvironment.swift | 5 +---- apps/macos/Sources/Clawdbot/LaunchAgentManager.swift | 8 ++++---- apps/macos/Sources/Clawdbot/Launchctl.swift | 7 +------ .../Clawdbot/NodePairingApprovalPrompter.swift | 3 +-- apps/macos/Sources/Clawdbot/PortGuardian.swift | 5 +---- apps/macos/Sources/Clawdbot/Process+PipeRead.swift | 11 +++++++++++ apps/macos/Sources/Clawdbot/RuntimeLocator.swift | 5 +---- 7 files changed, 20 insertions(+), 24 deletions(-) create mode 100644 apps/macos/Sources/Clawdbot/Process+PipeRead.swift diff --git a/apps/macos/Sources/Clawdbot/GatewayEnvironment.swift b/apps/macos/Sources/Clawdbot/GatewayEnvironment.swift index e2ee0e4ba..1e7bfd98e 100644 --- a/apps/macos/Sources/Clawdbot/GatewayEnvironment.swift +++ b/apps/macos/Sources/Clawdbot/GatewayEnvironment.swift @@ -278,10 +278,7 @@ enum GatewayEnvironment { process.standardOutput = pipe process.standardError = pipe do { - try process.run() - // Read pipe before waitUntilExit to avoid potential deadlock - let data = pipe.fileHandleForReading.readToEndSafely() - process.waitUntilExit() + let data = try process.runAndReadToEnd(from: pipe) let elapsedMs = Int(Date().timeIntervalSince(start) * 1000) if elapsedMs > 500 { self.logger.warning( diff --git a/apps/macos/Sources/Clawdbot/LaunchAgentManager.swift b/apps/macos/Sources/Clawdbot/LaunchAgentManager.swift index 8c525129a..2d2c3342a 100644 --- a/apps/macos/Sources/Clawdbot/LaunchAgentManager.swift +++ b/apps/macos/Sources/Clawdbot/LaunchAgentManager.swift @@ -72,11 +72,11 @@ enum LaunchAgentManager { let process = Process() process.launchPath = "/bin/launchctl" process.arguments = args - process.standardOutput = Pipe() - process.standardError = Pipe() + let pipe = Pipe() + process.standardOutput = pipe + process.standardError = pipe do { - try process.run() - process.waitUntilExit() + _ = try process.runAndReadToEnd(from: pipe) return process.terminationStatus } catch { return -1 diff --git a/apps/macos/Sources/Clawdbot/Launchctl.swift b/apps/macos/Sources/Clawdbot/Launchctl.swift index 50d9052b2..4059a1e20 100644 --- a/apps/macos/Sources/Clawdbot/Launchctl.swift +++ b/apps/macos/Sources/Clawdbot/Launchctl.swift @@ -16,12 +16,7 @@ enum Launchctl { process.standardOutput = pipe process.standardError = pipe do { - try process.run() - // 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 data = try process.runAndReadToEnd(from: pipe) let output = String(data: data, encoding: .utf8) ?? "" return Result(status: process.terminationStatus, output: output) } catch { diff --git a/apps/macos/Sources/Clawdbot/NodePairingApprovalPrompter.swift b/apps/macos/Sources/Clawdbot/NodePairingApprovalPrompter.swift index 01f0879a5..2f6c3d0f5 100644 --- a/apps/macos/Sources/Clawdbot/NodePairingApprovalPrompter.swift +++ b/apps/macos/Sources/Clawdbot/NodePairingApprovalPrompter.swift @@ -580,11 +580,10 @@ final class NodePairingApprovalPrompter { process.standardError = pipe do { - try process.run() + _ = try process.runAndReadToEnd(from: pipe) } catch { return false } - process.waitUntilExit() return process.terminationStatus == 0 }.value } diff --git a/apps/macos/Sources/Clawdbot/PortGuardian.swift b/apps/macos/Sources/Clawdbot/PortGuardian.swift index da7e523c1..071de5b2a 100644 --- a/apps/macos/Sources/Clawdbot/PortGuardian.swift +++ b/apps/macos/Sources/Clawdbot/PortGuardian.swift @@ -203,10 +203,7 @@ actor PortGuardian { proc.standardOutput = pipe proc.standardError = Pipe() do { - try proc.run() - // Read pipe before waitUntilExit to avoid potential deadlock - let data = pipe.fileHandleForReading.readToEndSafely() - proc.waitUntilExit() + let data = try proc.runAndReadToEnd(from: pipe) guard !data.isEmpty else { return nil } return String(data: data, encoding: .utf8)? .trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/apps/macos/Sources/Clawdbot/Process+PipeRead.swift b/apps/macos/Sources/Clawdbot/Process+PipeRead.swift new file mode 100644 index 000000000..7c0f7fe0c --- /dev/null +++ b/apps/macos/Sources/Clawdbot/Process+PipeRead.swift @@ -0,0 +1,11 @@ +import Foundation + +extension Process { + /// Runs the process and drains the given pipe before waiting to avoid blocking on full buffers. + func runAndReadToEnd(from pipe: Pipe) throws -> Data { + try self.run() + let data = pipe.fileHandleForReading.readToEndSafely() + self.waitUntilExit() + return data + } +} diff --git a/apps/macos/Sources/Clawdbot/RuntimeLocator.swift b/apps/macos/Sources/Clawdbot/RuntimeLocator.swift index 2cdead17e..761c63b17 100644 --- a/apps/macos/Sources/Clawdbot/RuntimeLocator.swift +++ b/apps/macos/Sources/Clawdbot/RuntimeLocator.swift @@ -133,10 +133,7 @@ enum RuntimeLocator { process.standardError = pipe do { - try process.run() - // Read pipe before waitUntilExit to avoid potential deadlock - let data = pipe.fileHandleForReading.readToEndSafely() - process.waitUntilExit() + let data = try process.runAndReadToEnd(from: pipe) let elapsedMs = Int(Date().timeIntervalSince(start) * 1000) if elapsedMs > 500 { self.logger.warning(