Skip to content

Commit 7e8381f

Browse files
committed
Add support for Process stdin stream
1 parent 58a9c90 commit 7e8381f

File tree

3 files changed

+87
-21
lines changed

3 files changed

+87
-21
lines changed

Sources/TSCBasic/Process.swift

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -331,8 +331,11 @@ public final class Process: ObjectIdentifierProtocol {
331331
}
332332
}
333333

334-
/// Launch the subprocess.
335-
public func launch() throws {
334+
/// Launch the subprocess. Returns a WritableByteStream object that can be used to communicate to the process's
335+
/// stdin. If needed, the stream can be closed using the close() API. Otherwise, the stream will be closed
336+
/// automatically.
337+
@discardableResult
338+
public func launch() throws -> WritableByteStream {
336339
precondition(arguments.count > 0 && !arguments[0].isEmpty, "Need at least one argument to launch the process.")
337340
precondition(!launched, "It is not allowed to launch the same process object again.")
338341

@@ -351,12 +354,15 @@ public final class Process: ObjectIdentifierProtocol {
351354
throw Process.Error.missingExecutableProgram(program: executable)
352355
}
353356

354-
#if os(Windows)
357+
#if os(Windows)
355358
_process = Foundation.Process()
356359
_process?.arguments = Array(arguments.dropFirst()) // Avoid including the executable URL twice.
357360
_process?.executableURL = executablePath.asURL
358361
_process?.environment = environment
359362

363+
let stdinPipe = Pipe()
364+
_process?.standardInput = stdinPipe
365+
360366
if outputRedirection.redirectsOutput {
361367
let stdoutPipe = Pipe()
362368
let stderrPipe = Pipe()
@@ -379,6 +385,8 @@ public final class Process: ObjectIdentifierProtocol {
379385
}
380386

381387
try _process?.run()
388+
389+
return stdinPipe.fileHandleForWriting
382390
#else
383391
// Initialize the spawn attributes.
384392
#if canImport(Darwin) || os(Android)
@@ -453,14 +461,17 @@ public final class Process: ObjectIdentifierProtocol {
453461
#endif
454462
}
455463

456-
// Workaround for https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=89e435f3559c53084498e9baad22172b64429362
457-
// Change allowing for newer version of glibc
458-
guard let devNull = strdup("/dev/null") else {
459-
throw SystemError.posix_spawn(0, arguments)
460-
}
461-
defer { free(devNull) }
462-
// Open /dev/null as stdin.
463-
posix_spawn_file_actions_addopen(&fileActions, 0, devNull, O_RDONLY, 0)
464+
var stdinPipe: [Int32] = [-1, -1]
465+
try open(pipe: &stdinPipe)
466+
467+
let stdinStream = try LocalFileOutputByteStream(filePointer: fdopen(stdinPipe[1], "wb"), closeOnDeinit: true)
468+
469+
// Dupe the read portion of the remote to 0.
470+
posix_spawn_file_actions_adddup2(&fileActions, stdinPipe[0], 0)
471+
472+
// Close the other side's pipe since it was dupped to 0.
473+
posix_spawn_file_actions_addclose(&fileActions, stdinPipe[0])
474+
posix_spawn_file_actions_addclose(&fileActions, stdinPipe[1])
464475

465476
var outputPipe: [Int32] = [-1, -1]
466477
var stderrPipe: [Int32] = [-1, -1]
@@ -471,7 +482,7 @@ public final class Process: ObjectIdentifierProtocol {
471482
// Open the write end of the pipe.
472483
posix_spawn_file_actions_adddup2(&fileActions, outputPipe[1], 1)
473484

474-
// Close the other ends of the pipe.
485+
// Close the other ends of the pipe since they were dupped to 1.
475486
posix_spawn_file_actions_addclose(&fileActions, outputPipe[0])
476487
posix_spawn_file_actions_addclose(&fileActions, outputPipe[1])
477488

@@ -483,7 +494,7 @@ public final class Process: ObjectIdentifierProtocol {
483494
try open(pipe: &stderrPipe)
484495
posix_spawn_file_actions_adddup2(&fileActions, stderrPipe[1], 2)
485496

486-
// Close the other ends of the pipe.
497+
// Close the other ends of the pipe since they were dupped to 2.
487498
posix_spawn_file_actions_addclose(&fileActions, stderrPipe[0])
488499
posix_spawn_file_actions_addclose(&fileActions, stderrPipe[1])
489500
}
@@ -500,11 +511,14 @@ public final class Process: ObjectIdentifierProtocol {
500511
throw SystemError.posix_spawn(rv, arguments)
501512
}
502513

514+
// Close the local read end of the input pipe.
515+
try close(fd: stdinPipe[0])
516+
503517
if outputRedirection.redirectsOutput {
504518
let outputClosures = outputRedirection.outputClosures
505519

506-
// Close the write end of the output pipe.
507-
try close(fd: &outputPipe[1])
520+
// Close the local write end of the output pipe.
521+
try close(fd: outputPipe[1])
508522

509523
// Create a thread and start reading the output on it.
510524
var thread = Thread { [weak self] in
@@ -517,8 +531,8 @@ public final class Process: ObjectIdentifierProtocol {
517531

518532
// Only schedule a thread for stderr if no redirect was requested.
519533
if !outputRedirection.redirectStderr {
520-
// Close the write end of the stderr pipe.
521-
try close(fd: &stderrPipe[1])
534+
// Close the local write end of the stderr pipe.
535+
try close(fd: stderrPipe[1])
522536

523537
// Create a thread and start reading the stderr output on it.
524538
thread = Thread { [weak self] in
@@ -530,6 +544,8 @@ public final class Process: ObjectIdentifierProtocol {
530544
self.stderr.thread = thread
531545
}
532546
}
547+
548+
return stdinStream
533549
#endif // POSIX implementation
534550
}
535551

@@ -731,11 +747,15 @@ private func open(pipe: inout [Int32]) throws {
731747
}
732748

733749
/// Close the given fd.
734-
private func close(fd: inout Int32) throws {
735-
let rv = TSCLibc.close(fd)
736-
guard rv == 0 else {
737-
throw SystemError.close(rv)
750+
private func close(fd: Int32) throws {
751+
func innerClose(_ fd: inout Int32) throws {
752+
let rv = TSCLibc.close(fd)
753+
guard rv == 0 else {
754+
throw SystemError.close(rv)
755+
}
738756
}
757+
var innerFd = fd
758+
try innerClose(&innerFd)
739759
}
740760

741761
extension Process.Error: CustomStringConvertible {
@@ -788,3 +808,27 @@ extension ProcessResult.Error: CustomStringConvertible {
788808
}
789809
}
790810
}
811+
812+
#if os(Windows)
813+
extension FileHandle: WritableByteStream {
814+
public var position: Int {
815+
return Int(offsetInFile)
816+
}
817+
818+
public func write(_ byte: UInt8) {
819+
write(Data([byte]))
820+
}
821+
822+
public func write<C: Collection>(_ bytes: C) where C.Element == UInt8 {
823+
write(Data(bytes))
824+
}
825+
826+
public func flush() {
827+
synchronizeFile()
828+
}
829+
830+
public func close() throws {
831+
closeFile()
832+
}
833+
}
834+
#endif

Tests/TSCBasicTests/ProcessTests.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,23 @@ class ProcessTests: XCTestCase {
188188
XCTAssertEqual(result2, "hello\n")
189189
}
190190

191+
func testStdin() throws {
192+
var stdout = [UInt8]()
193+
let process = Process(args: script("in-to-out"), outputRedirection: .stream(stdout: { stdoutBytes in
194+
stdout += stdoutBytes
195+
}, stderr: { _ in }))
196+
let stdinStream = try process.launch()
197+
198+
stdinStream.write("hello\n")
199+
stdinStream.flush()
200+
201+
try stdinStream.close()
202+
203+
try process.waitUntilExit()
204+
205+
XCTAssertEqual(String(decoding: stdout, as: UTF8.self), "hello\n")
206+
}
207+
191208
func testStdoutStdErr() throws {
192209
// A simple script to check that stdout and stderr are captured separatly.
193210
do {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env python
2+
3+
import sys
4+
5+
sys.stdout.write(sys.stdin.readline())

0 commit comments

Comments
 (0)