Skip to content

Commit 435a270

Browse files
authored
Merge pull request #208 from artemcm/ProcessInputSergio
Bring back Sergio's "Add support for Process stdin stream"
2 parents 8506725 + b7097b0 commit 435a270

File tree

3 files changed

+82
-24
lines changed

3 files changed

+82
-24
lines changed

Sources/TSCBasic/Process.swift

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -361,8 +361,11 @@ public final class Process: ObjectIdentifierProtocol {
361361
}
362362
}
363363

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

@@ -387,6 +390,9 @@ public final class Process: ObjectIdentifierProtocol {
387390
_process?.executableURL = executablePath.asURL
388391
_process?.environment = environment
389392

393+
let stdinPipe = Pipe()
394+
_process?.standardInput = stdinPipe
395+
390396
if outputRedirection.redirectsOutput {
391397
let stdoutPipe = Pipe()
392398
let stderrPipe = Pipe()
@@ -409,6 +415,7 @@ public final class Process: ObjectIdentifierProtocol {
409415
}
410416

411417
try _process?.run()
418+
return stdinPipe.fileHandleForWriting
412419
#else
413420
// Initialize the spawn attributes.
414421
#if canImport(Darwin) || os(Android)
@@ -483,14 +490,17 @@ public final class Process: ObjectIdentifierProtocol {
483490
#endif
484491
}
485492

486-
// Workaround for https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=89e435f3559c53084498e9baad22172b64429362
487-
// Change allowing for newer version of glibc
488-
guard let devNull = strdup("/dev/null") else {
489-
throw SystemError.posix_spawn(0, arguments)
490-
}
491-
defer { free(devNull) }
492-
// Open /dev/null as stdin.
493-
posix_spawn_file_actions_addopen(&fileActions, 0, devNull, O_RDONLY, 0)
493+
var stdinPipe: [Int32] = [-1, -1]
494+
try open(pipe: &stdinPipe)
495+
496+
let stdinStream = try LocalFileOutputByteStream(filePointer: fdopen(stdinPipe[1], "wb"), closeOnDeinit: true)
497+
498+
// Dupe the read portion of the remote to 0.
499+
posix_spawn_file_actions_adddup2(&fileActions, stdinPipe[0], 0)
500+
501+
// Close the other side's pipe since it was dupped to 0.
502+
posix_spawn_file_actions_addclose(&fileActions, stdinPipe[0])
503+
posix_spawn_file_actions_addclose(&fileActions, stdinPipe[1])
494504

495505
var outputPipe: [Int32] = [-1, -1]
496506
var stderrPipe: [Int32] = [-1, -1]
@@ -501,7 +511,7 @@ public final class Process: ObjectIdentifierProtocol {
501511
// Open the write end of the pipe.
502512
posix_spawn_file_actions_adddup2(&fileActions, outputPipe[1], 1)
503513

504-
// Close the other ends of the pipe.
514+
// Close the other ends of the pipe since they were dupped to 1.
505515
posix_spawn_file_actions_addclose(&fileActions, outputPipe[0])
506516
posix_spawn_file_actions_addclose(&fileActions, outputPipe[1])
507517

@@ -513,7 +523,7 @@ public final class Process: ObjectIdentifierProtocol {
513523
try open(pipe: &stderrPipe)
514524
posix_spawn_file_actions_adddup2(&fileActions, stderrPipe[1], 2)
515525

516-
// Close the other ends of the pipe.
526+
// Close the other ends of the pipe since they were dupped to 2.
517527
posix_spawn_file_actions_addclose(&fileActions, stderrPipe[0])
518528
posix_spawn_file_actions_addclose(&fileActions, stderrPipe[1])
519529
}
@@ -534,11 +544,14 @@ public final class Process: ObjectIdentifierProtocol {
534544
throw SystemError.posix_spawn(rv, arguments)
535545
}
536546

547+
// Close the local read end of the input pipe.
548+
try close(fd: stdinPipe[0])
549+
537550
if outputRedirection.redirectsOutput {
538551
let outputClosures = outputRedirection.outputClosures
539552

540-
// Close the write end of the output pipe.
541-
try close(fd: &outputPipe[1])
553+
// Close the local write end of the output pipe.
554+
try close(fd: outputPipe[1])
542555

543556
// Create a thread and start reading the output on it.
544557
var thread = Thread { [weak self] in
@@ -551,8 +564,8 @@ public final class Process: ObjectIdentifierProtocol {
551564

552565
// Only schedule a thread for stderr if no redirect was requested.
553566
if !outputRedirection.redirectStderr {
554-
// Close the write end of the stderr pipe.
555-
try close(fd: &stderrPipe[1])
567+
// Close the local write end of the stderr pipe.
568+
try close(fd: stderrPipe[1])
556569

557570
// Create a thread and start reading the stderr output on it.
558571
thread = Thread { [weak self] in
@@ -564,7 +577,8 @@ public final class Process: ObjectIdentifierProtocol {
564577
self.stderr.thread = thread
565578
}
566579
}
567-
#endif // POSIX implementation
580+
return stdinStream
581+
#endif // POSIX implementation
568582
}
569583

570584
/// Blocks the calling process until the subprocess finishes execution.
@@ -771,11 +785,15 @@ private func open(pipe: inout [Int32]) throws {
771785
}
772786

773787
/// Close the given fd.
774-
private func close(fd: inout Int32) throws {
775-
let rv = TSCLibc.close(fd)
776-
guard rv == 0 else {
777-
throw SystemError.close(rv)
788+
private func close(fd: Int32) throws {
789+
func innerClose(_ fd: inout Int32) throws {
790+
let rv = TSCLibc.close(fd)
791+
guard rv == 0 else {
792+
throw SystemError.close(rv)
793+
}
778794
}
795+
var innerFd = fd
796+
try innerClose(&innerFd)
779797
}
780798

781799
extension Process.Error: CustomStringConvertible {
@@ -836,8 +854,26 @@ extension ProcessResult.Error: CustomStringConvertible {
836854
}
837855
}
838856

839-
extension ProcessResult.Error: CustomNSError {
840-
public var errorUserInfo: [String : Any] {
841-
return [NSLocalizedDescriptionKey: self.description]
857+
#if os(Windows)
858+
extension FileHandle: WritableByteStream {
859+
public var position: Int {
860+
return Int(offsetInFile)
861+
}
862+
863+
public func write(_ byte: UInt8) {
864+
write(Data([byte]))
865+
}
866+
867+
public func write<C: Collection>(_ bytes: C) where C.Element == UInt8 {
868+
write(Data(bytes))
869+
}
870+
871+
public func flush() {
872+
synchronizeFile()
873+
}
874+
875+
public func close() throws {
876+
closeFile()
842877
}
843878
}
879+
#endif

Tests/TSCBasicTests/ProcessTests.swift

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

219+
func testStdin() throws {
220+
var stdout = [UInt8]()
221+
let process = Process(args: script("in-to-out"), outputRedirection: .stream(stdout: { stdoutBytes in
222+
stdout += stdoutBytes
223+
}, stderr: { _ in }))
224+
let stdinStream = try process.launch()
225+
226+
stdinStream.write("hello\n")
227+
stdinStream.flush()
228+
229+
try stdinStream.close()
230+
231+
try process.waitUntilExit()
232+
233+
XCTAssertEqual(String(decoding: stdout, as: UTF8.self), "hello\n")
234+
}
235+
219236
func testStdoutStdErr() throws {
220237
// A simple script to check that stdout and stderr are captured separatly.
221238
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)