fix(macos): use safe FileHandle reads
This commit is contained in:
28
apps/macos/Sources/Clawdis/FileHandle+SafeRead.swift
Normal file
28
apps/macos/Sources/Clawdis/FileHandle+SafeRead.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
25
apps/macos/Sources/ClawdisCLI/FileHandle+SafeRead.swift
Normal file
25
apps/macos/Sources/ClawdisCLI/FileHandle+SafeRead.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user