




















@@ -16,6 +16,32 @@ final class RemotePortTunnel: @unchecked Sendable {
1616let localPort: UInt16?
1717private let stderrHandle: FileHandle?
181819+private final class StderrCapture: @unchecked Sendable {
20+private let lock = NSLock()
21+private var text = ""
22+private let limit = 4096
23+24+func append(_ chunk: String) {
25+let trimmed = chunk.trimmingCharacters(in: .whitespacesAndNewlines)
26+guard !trimmed.isEmpty else { return }
27+self.lock.lock()
28+defer { self.lock.unlock() }
29+if !self.text.isEmpty {
30+self.text += "\n"
31+}
32+self.text += trimmed
33+if self.text.count > self.limit {
34+self.text = String(self.text.suffix(self.limit))
35+}
36+}
37+38+func snapshot() -> String {
39+self.lock.lock()
40+defer { self.lock.unlock() }
41+return self.text.trimmingCharacters(in: .whitespacesAndNewlines)
42+}
43+}
44+1945private init(process: Process, localPort: UInt16?, stderrHandle: FileHandle?) {
2046self.process = process
2147self.localPort = localPort
@@ -93,6 +119,7 @@ final class RemotePortTunnel: @unchecked Sendable {
93119let pipe = Pipe()
94120 process.standardError = pipe
95121let stderrHandle = pipe.fileHandleForReading
122+let stderrCapture = StderrCapture()
9612397124 // Consume stderr so ssh cannot block if it logs.
98125 stderrHandle.readabilityHandler = { handle in
@@ -106,6 +133,7 @@ final class RemotePortTunnel: @unchecked Sendable {
106133.trimmingCharacters(in: .whitespacesAndNewlines),
107134 !line.isEmpty
108135else { return }
136+ stderrCapture.append(line)
109137Self.logger.error("ssh tunnel stderr: \(line, privacy: .public)")
110138}
111139 process.terminationHandler = { _ in
@@ -114,7 +142,11 @@ final class RemotePortTunnel: @unchecked Sendable {
114142115143try process.run()
116144117-try await Self.waitForListener(process: process, localPort: localPort, stderrHandle: stderrHandle)
145+try await Self.waitForListener(
146+ process: process,
147+ localPort: localPort,
148+ stderrHandle: stderrHandle,
149+ stderrCapture: stderrCapture)
118150119151 // Track tunnel so we can clean up stale listeners on restart.
120152Task {
@@ -131,12 +163,13 @@ final class RemotePortTunnel: @unchecked Sendable {
131163private static func waitForListener(
132164 process: Process,
133165 localPort: UInt16,
134- stderrHandle: FileHandle) async throws
166+ stderrHandle: FileHandle,
167+ stderrCapture: StderrCapture) async throws
135168{
136169let deadline = Date().addingTimeInterval(6)
137170repeat {
138171if !process.isRunning {
139-let stderr = Self.drainStderr(stderrHandle)
172+let stderr = Self.drainStderr(stderrHandle, captured: stderrCapture.snapshot())
140173let msg = stderr.isEmpty ? "ssh tunnel exited before listening" : "ssh tunnel failed: \(stderr)"
141174throw NSError(domain: "RemotePortTunnel", code: 4, userInfo: [NSLocalizedDescriptionKey: msg])
142175}
@@ -152,7 +185,7 @@ final class RemotePortTunnel: @unchecked Sendable {
152185} while Date() < deadline
153186154187 process.terminate()
155-let stderr = Self.drainStderr(stderrHandle)
188+let stderr = Self.drainStderr(stderrHandle, captured: stderrCapture.snapshot())
156189let msg = stderr.isEmpty ? "ssh tunnel did not open local port \(localPort)" : "ssh tunnel failed: \(stderr)"
157190throw NSError(domain: "RemotePortTunnel", code: 4, userInfo: [NSLocalizedDescriptionKey: msg])
158191}
@@ -311,16 +344,27 @@ final class RemotePortTunnel: @unchecked Sendable {
311344}
312345313346private static func drainStderr(_ handle: FileHandle) -> String {
347+self.drainStderr(handle, captured: "")
348+}
349+350+private static func drainStderr(_ handle: FileHandle, captured: String) -> String {
314351 handle.readabilityHandler = nil
315352defer { try? handle.close() }
316353317354do {
318355let data = try handle.readToEnd() ?? Data()
319-return String(data: data, encoding: .utf8)?
356+let remaining = String(data: data, encoding: .utf8)?
320357.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
358+if captured.isEmpty {
359+return remaining
360+}
361+if remaining.isEmpty {
362+return captured
363+}
364+return captured + "\n" + remaining
321365} catch {
322366self.logger.debug("Failed to drain ssh stderr: \(error, privacy: .public)")
323-return ""
367+return captured
324368}
325369}
326370此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。