Skip to content

Commit 39c51cf

Browse files
committed
Update AsyncProcess output streaming on Windows
1 parent ba4bc64 commit 39c51cf

File tree

1 file changed

+65
-13
lines changed

1 file changed

+65
-13
lines changed

Sources/Basics/Concurrency/AsyncProcess.swift

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import _Concurrency
1212
import Dispatch
1313
import Foundation
14+
import System
1415

1516
#if os(Windows)
1617
import TSCLibc
@@ -514,14 +515,15 @@ package final class AsyncProcess {
514515
if self.outputRedirection.redirectsOutput {
515516
let stdoutPipe = Pipe()
516517
let stderrPipe = Pipe()
518+
let stdoutStream = DispatchFD(fileHandle: stdoutPipe.fileHandleForReading).dataStream()
519+
let stderrStream = DispatchFD(fileHandle: stderrPipe.fileHandleForReading).dataStream()
517520

518521
group.enter()
519-
stdoutPipe.fileHandleForReading.readabilityHandler = { (fh: FileHandle) in
520-
let data = (try? fh.read(upToCount: Int.max)) ?? Data()
521-
if data.count == 0 {
522-
stdoutPipe.fileHandleForReading.readabilityHandler = nil
522+
Task {
523+
defer {
523524
group.leave()
524-
} else {
525+
}
526+
for try await data in stdoutStream {
525527
let contents = data.withUnsafeBytes { [UInt8]($0) }
526528
self.outputRedirection.outputClosures?.stdoutClosure(contents)
527529
stdoutLock.withLock {
@@ -531,16 +533,15 @@ package final class AsyncProcess {
531533
}
532534

533535
group.enter()
534-
stderrPipe.fileHandleForReading.readabilityHandler = { (fh: FileHandle) in
535-
let data = (try? fh.read(upToCount: Int.max)) ?? Data()
536-
if data.count == 0 {
537-
stderrPipe.fileHandleForReading.readabilityHandler = nil
536+
Task {
537+
defer {
538538
group.leave()
539-
} else {
539+
}
540+
for try await data in stderrStream {
540541
let contents = data.withUnsafeBytes { [UInt8]($0) }
541-
self.outputRedirection.outputClosures?.stderrClosure(contents)
542-
stderrLock.withLock {
543-
stderr += contents
542+
self.outputRedirection.outputClosures?.stdoutClosure(contents)
543+
stdoutLock.withLock {
544+
stdout += contents
544545
}
545546
}
546547
}
@@ -1354,3 +1355,54 @@ extension FileHandle: WritableByteStream {
13541355
}
13551356
}
13561357
#endif
1358+
1359+
extension DispatchFD {
1360+
public func readChunk(upToLength maxLength: Int) async throws -> DispatchData {
1361+
return try await withCheckedThrowingContinuation { continuation in
1362+
DispatchIO.read(fromFileDescriptor: numericCast(self.rawValue), maxLength: maxLength, runningHandlerOn: DispatchQueue.global())
1363+
{ data, error in
1364+
if error != 0 {
1365+
continuation.resume(throwing: StringError("POSIX error: \(error)"))
1366+
return
1367+
}
1368+
if data.count == 0 {
1369+
continuation.resume(throwing: StringError("No more data"))
1370+
}
1371+
continuation.resume(returning: data)
1372+
}
1373+
}
1374+
1375+
}
1376+
1377+
/// Returns an async stream which reads bytes from the specified file descriptor. Unlike `FileHandle.bytes`, it does not block the caller.
1378+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
1379+
public func dataStream() -> some AsyncSequence<DispatchData, any Error> {
1380+
AsyncThrowingStream<DispatchData, any Error> {
1381+
while !Task.isCancelled {
1382+
let chunk = try await readChunk(upToLength: 4096)
1383+
if chunk.isEmpty {
1384+
return nil
1385+
}
1386+
return chunk
1387+
}
1388+
throw CancellationError()
1389+
}
1390+
}
1391+
}
1392+
1393+
public struct DispatchFD {
1394+
#if os(Windows)
1395+
fileprivate let rawValue: Int
1396+
#else
1397+
fileprivate let rawValue: Int32
1398+
#endif
1399+
1400+
init(fileHandle: FileHandle) {
1401+
#if os(Windows)
1402+
// This may look unsafe, but is how swift-corelibs-dispatch works. Basically, dispatch_fd_t directly represents either a POSIX file descriptor OR a Windows HANDLE pointer address, meaning that the fileDescriptor parameter of various Dispatch APIs is actually NOT a file descriptor on Windows but rather a HANDLE. This means that the handle should NOT be converted using _open_osfhandle, and the return value of this function should ONLY be passed to Dispatch functions where the fileDescriptor parameter is masquerading as a HANDLE in this manner. Use with extreme caution.
1403+
rawValue = .init(bitPattern: fileHandle._handle)
1404+
#else
1405+
rawValue = fileHandle.fileDescriptor
1406+
#endif
1407+
}
1408+
}

0 commit comments

Comments
 (0)