fix(macos): use safe FileHandle reads

This commit is contained in:
Peter Steinberger
2025-12-16 10:41:18 +01:00
parent b443c20cef
commit 64d6d25d65
10 changed files with 77 additions and 14 deletions

View File

@@ -0,0 +1,28 @@
import Foundation
extension FileHandle {
/// Reads until EOF using the throwing FileHandle API and returns empty `Data` on failure.
///
/// Important: Avoid legacy, non-throwing FileHandle read APIs (e.g. `readDataToEndOfFile()` and
/// `availableData`). They can raise Objective-C exceptions when the handle is closed/invalid, which
/// will abort the process.
func readToEndSafely() -> Data {
do {
return try self.readToEnd() ?? Data()
} catch {
return Data()
}
}
/// Reads up to `count` bytes using the throwing FileHandle API and returns empty `Data` on failure/EOF.
///
/// Important: Use this instead of `availableData` in callbacks like `readabilityHandler` to avoid
/// Objective-C exceptions terminating the process.
func readSafely(upToCount count: Int) -> Data {
do {
return try self.read(upToCount: count) ?? Data()
} catch {
return Data()
}
}
}

View File

@@ -202,7 +202,7 @@ enum GatewayEnvironment {
do {
try process.run()
process.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let data = pipe.fileHandleForReading.readToEndSafely()
let raw = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
return Semver.parse(raw)
} catch {

View File

@@ -257,7 +257,7 @@ actor PortGuardian {
} catch {
return nil
}
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let data = pipe.fileHandleForReading.readToEndSafely()
guard !data.isEmpty else { return nil }
return String(data: data, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines)

View File

@@ -132,7 +132,7 @@ enum RuntimeLocator {
do {
try process.run()
process.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let data = pipe.fileHandleForReading.readToEndSafely()
return String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
} catch {
return nil

View File

@@ -24,8 +24,8 @@ enum ShellExecutor {
let waitTask = Task { () -> Response in
process.waitUntilExit()
let out = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
let err = stderrPipe.fileHandleForReading.readDataToEndOfFile()
let out = stdoutPipe.fileHandleForReading.readToEndSafely()
let err = stderrPipe.fileHandleForReading.readToEndSafely()
let status = process.terminationStatus
let combined = out.isEmpty ? err : out
return Response(ok: status == 0, message: status == 0 ? nil : "exit \(status)", payload: combined)

View File

@@ -463,7 +463,7 @@ private enum ToolInstaller {
process.standardOutput = pipe
process.standardError = pipe
process.terminationHandler = { proc in
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let data = pipe.fileHandleForReading.readToEndSafely()
let output = String(data: data, encoding: .utf8) ?? ""
continuation.resume(returning: (proc.terminationStatus, output))
}

View File

@@ -191,7 +191,7 @@ enum CLIInstaller {
do {
try proc.run()
proc.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let data = pipe.fileHandleForReading.readToEndSafely()
let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if proc.terminationStatus == 0 {
return output.isEmpty ? "CLI helper linked into \(targetList)" : output

View File

@@ -74,9 +74,9 @@ final class WebChatTunnel {
// Consume stderr so ssh cannot block if it logs.
stderrHandle.readabilityHandler = { handle in
let data = handle.availableData
let data = handle.readSafely(upToCount: 64 * 1024)
guard !data.isEmpty else {
// EOF: stop monitoring to avoid spinning on a closed pipe.
// EOF (or read failure): stop monitoring to avoid spinning on a closed pipe.
Self.cleanupStderr(handle)
return
}
@@ -223,15 +223,25 @@ final class WebChatTunnel {
private static func drainStderr(_ handle: FileHandle) -> String {
handle.readabilityHandler = nil
let data = handle.readDataToEndOfFile()
try? handle.close()
return String(data: data, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
defer { try? handle.close() }
do {
let data = try handle.readToEnd() ?? Data()
return String(data: data, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
} catch {
self.logger.debug("Failed to drain ssh stderr: \(error, privacy: .public)")
return ""
}
}
#if SWIFT_PACKAGE
static func _testPortIsFree(_ port: UInt16) -> Bool {
self.portIsFree(port)
}
static func _testDrainStderr(_ handle: FileHandle) -> String {
self.drainStderr(handle)
}
#endif
}

View File

@@ -263,7 +263,7 @@ enum BrowserCLI {
return try String(contentsOfFile: jsFile, encoding: .utf8)
}
if options.jsStdin {
let data = FileHandle.standardInput.readDataToEndOfFile()
let data = FileHandle.standardInput.readToEndSafely()
return String(data: data, encoding: .utf8) ?? ""
}
if let js = options.js, !js.isEmpty {

View File

@@ -0,0 +1,25 @@
import Foundation
extension FileHandle {
/// Reads until EOF using the throwing FileHandle API and returns empty `Data` on failure.
///
/// Important: Avoid legacy, non-throwing FileHandle read APIs (e.g. `readDataToEndOfFile()` and
/// `availableData`). They can raise Objective-C exceptions when the handle is closed/invalid, which
/// will abort the process.
func readToEndSafely() -> Data {
do {
return try self.readToEnd() ?? Data()
} catch {
return Data()
}
}
/// Reads up to `count` bytes using the throwing FileHandle API and returns empty `Data` on failure/EOF.
func readSafely(upToCount count: Int) -> Data {
do {
return try self.read(upToCount: count) ?? Data()
} catch {
return Data()
}
}
}