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 {
|
do {
|
||||||
try process.run()
|
try process.run()
|
||||||
process.waitUntilExit()
|
process.waitUntilExit()
|
||||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
let data = pipe.fileHandleForReading.readToEndSafely()
|
||||||
let raw = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
let raw = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
return Semver.parse(raw)
|
return Semver.parse(raw)
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ actor PortGuardian {
|
|||||||
} catch {
|
} catch {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
let data = pipe.fileHandleForReading.readToEndSafely()
|
||||||
guard !data.isEmpty else { return nil }
|
guard !data.isEmpty else { return nil }
|
||||||
return String(data: data, encoding: .utf8)?
|
return String(data: data, encoding: .utf8)?
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ enum RuntimeLocator {
|
|||||||
do {
|
do {
|
||||||
try process.run()
|
try process.run()
|
||||||
process.waitUntilExit()
|
process.waitUntilExit()
|
||||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
let data = pipe.fileHandleForReading.readToEndSafely()
|
||||||
return String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
return String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
} catch {
|
} catch {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ enum ShellExecutor {
|
|||||||
|
|
||||||
let waitTask = Task { () -> Response in
|
let waitTask = Task { () -> Response in
|
||||||
process.waitUntilExit()
|
process.waitUntilExit()
|
||||||
let out = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
|
let out = stdoutPipe.fileHandleForReading.readToEndSafely()
|
||||||
let err = stderrPipe.fileHandleForReading.readDataToEndOfFile()
|
let err = stderrPipe.fileHandleForReading.readToEndSafely()
|
||||||
let status = process.terminationStatus
|
let status = process.terminationStatus
|
||||||
let combined = out.isEmpty ? err : out
|
let combined = out.isEmpty ? err : out
|
||||||
return Response(ok: status == 0, message: status == 0 ? nil : "exit \(status)", payload: combined)
|
return Response(ok: status == 0, message: status == 0 ? nil : "exit \(status)", payload: combined)
|
||||||
|
|||||||
@@ -463,7 +463,7 @@ private enum ToolInstaller {
|
|||||||
process.standardOutput = pipe
|
process.standardOutput = pipe
|
||||||
process.standardError = pipe
|
process.standardError = pipe
|
||||||
process.terminationHandler = { proc in
|
process.terminationHandler = { proc in
|
||||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
let data = pipe.fileHandleForReading.readToEndSafely()
|
||||||
let output = String(data: data, encoding: .utf8) ?? ""
|
let output = String(data: data, encoding: .utf8) ?? ""
|
||||||
continuation.resume(returning: (proc.terminationStatus, output))
|
continuation.resume(returning: (proc.terminationStatus, output))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ enum CLIInstaller {
|
|||||||
do {
|
do {
|
||||||
try proc.run()
|
try proc.run()
|
||||||
proc.waitUntilExit()
|
proc.waitUntilExit()
|
||||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
let data = pipe.fileHandleForReading.readToEndSafely()
|
||||||
let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
if proc.terminationStatus == 0 {
|
if proc.terminationStatus == 0 {
|
||||||
return output.isEmpty ? "CLI helper linked into \(targetList)" : output
|
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.
|
// Consume stderr so ssh cannot block if it logs.
|
||||||
stderrHandle.readabilityHandler = { handle in
|
stderrHandle.readabilityHandler = { handle in
|
||||||
let data = handle.availableData
|
let data = handle.readSafely(upToCount: 64 * 1024)
|
||||||
guard !data.isEmpty else {
|
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)
|
Self.cleanupStderr(handle)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -223,15 +223,25 @@ final class WebChatTunnel {
|
|||||||
|
|
||||||
private static func drainStderr(_ handle: FileHandle) -> String {
|
private static func drainStderr(_ handle: FileHandle) -> String {
|
||||||
handle.readabilityHandler = nil
|
handle.readabilityHandler = nil
|
||||||
let data = handle.readDataToEndOfFile()
|
defer { try? handle.close() }
|
||||||
try? handle.close()
|
|
||||||
return String(data: data, encoding: .utf8)?
|
do {
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
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
|
#if SWIFT_PACKAGE
|
||||||
static func _testPortIsFree(_ port: UInt16) -> Bool {
|
static func _testPortIsFree(_ port: UInt16) -> Bool {
|
||||||
self.portIsFree(port)
|
self.portIsFree(port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func _testDrainStderr(_ handle: FileHandle) -> String {
|
||||||
|
self.drainStderr(handle)
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ enum BrowserCLI {
|
|||||||
return try String(contentsOfFile: jsFile, encoding: .utf8)
|
return try String(contentsOfFile: jsFile, encoding: .utf8)
|
||||||
}
|
}
|
||||||
if options.jsStdin {
|
if options.jsStdin {
|
||||||
let data = FileHandle.standardInput.readDataToEndOfFile()
|
let data = FileHandle.standardInput.readToEndSafely()
|
||||||
return String(data: data, encoding: .utf8) ?? ""
|
return String(data: data, encoding: .utf8) ?? ""
|
||||||
}
|
}
|
||||||
if let js = options.js, !js.isEmpty {
|
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