Skip to content

Commit 2b4b5f7

Browse files
committed
Add support for Process stdin stream
1 parent 8b60325 commit 2b4b5f7

File tree

3 files changed

+80
-21
lines changed

3 files changed

+80
-21
lines changed

Sources/TSCBasic/Process.swift

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

334-
/// Launch the subprocess.
335-
public func launch() throws {
334+
/// Launch the subprocess. Returns an OutputByteStream object that can be used to communicate to the process's
335+
/// stdin.
336+
@discardableResult
337+
public func launch() throws -> OutputByteStream {
336338
precondition(arguments.count > 0 && !arguments[0].isEmpty, "Need at least one argument to launch the process.")
337339
precondition(!launched, "It is not allowed to launch the same process object again.")
338340

@@ -351,12 +353,15 @@ public final class Process: ObjectIdentifierProtocol {
351353
throw Process.Error.missingExecutableProgram(program: executable)
352354
}
353355

354-
#if os(Windows)
356+
#if os(Windows)
355357
_process = Foundation.Process()
356358
_process?.arguments = Array(arguments.dropFirst()) // Avoid including the executable URL twice.
357359
_process?.executableURL = executablePath.asURL
358360
_process?.environment = environment
359361

362+
let stdinPipe = Pipe()
363+
_process?.standardInput = stdinPipe
364+
360365
if outputRedirection.redirectsOutput {
361366
let stdoutPipe = Pipe()
362367
let stderrPipe = Pipe()
@@ -379,6 +384,8 @@ public final class Process: ObjectIdentifierProtocol {
379384
}
380385

381386
try _process?.run()
387+
388+
return stdinPipe.fileHandleForWriting
382389
#else
383390
// Initialize the spawn attributes.
384391
#if canImport(Darwin) || os(Android)
@@ -453,14 +460,17 @@ public final class Process: ObjectIdentifierProtocol {
453460
#endif
454461
}
455462

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)
463+
var stdinPipe: [Int32] = [-1, -1]
464+
try open(pipe: &stdinPipe)
465+
466+
let stdinStream = try LocalFileOutputByteStream(filePointer: fdopen(stdinPipe[1], "wb"), closeOnDeinit: true)
467+
468+
// Dupe the read portion of the remote to 0.
469+
posix_spawn_file_actions_adddup2(&fileActions, stdinPipe[0], 0)
470+
471+
// Close the other side's pipe since it was dupped to 0.
472+
posix_spawn_file_actions_addclose(&fileActions, stdinPipe[0])
473+
posix_spawn_file_actions_addclose(&fileActions, stdinPipe[1])
464474

465475
var outputPipe: [Int32] = [-1, -1]
466476
var stderrPipe: [Int32] = [-1, -1]
@@ -471,7 +481,7 @@ public final class Process: ObjectIdentifierProtocol {
471481
// Open the write end of the pipe.
472482
posix_spawn_file_actions_adddup2(&fileActions, outputPipe[1], 1)
473483

474-
// Close the other ends of the pipe.
484+
// Close the other ends of the pipe since they were dupped to 1.
475485
posix_spawn_file_actions_addclose(&fileActions, outputPipe[0])
476486
posix_spawn_file_actions_addclose(&fileActions, outputPipe[1])
477487

@@ -483,7 +493,7 @@ public final class Process: ObjectIdentifierProtocol {
483493
try open(pipe: &stderrPipe)
484494
posix_spawn_file_actions_adddup2(&fileActions, stderrPipe[1], 2)
485495

486-
// Close the other ends of the pipe.
496+
// Close the other ends of the pipe since they were dupped to 2.
487497
posix_spawn_file_actions_addclose(&fileActions, stderrPipe[0])
488498
posix_spawn_file_actions_addclose(&fileActions, stderrPipe[1])
489499
}
@@ -500,11 +510,14 @@ public final class Process: ObjectIdentifierProtocol {
500510
throw SystemError.posix_spawn(rv, arguments)
501511
}
502512

513+
// Close the local read end of the input pipe.
514+
try close(fd: stdinPipe[0])
515+
503516
if outputRedirection.redirectsOutput {
504517
let outputClosures = outputRedirection.outputClosures
505518

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

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

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

523536
// Create a thread and start reading the stderr output on it.
524537
thread = Thread { [weak self] in
@@ -531,6 +544,8 @@ public final class Process: ObjectIdentifierProtocol {
531544
}
532545
}
533546
#endif // POSIX implementation
547+
548+
return stdinStream
534549
}
535550

536551
/// Blocks the calling process until the subprocess finishes execution.
@@ -731,11 +746,15 @@ private func open(pipe: inout [Int32]) throws {
731746
}
732747

733748
/// 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)
749+
private func close(fd: Int32) throws {
750+
func innerClose(_ fd: inout Int32) throws {
751+
let rv = TSCLibc.close(fd)
752+
guard rv == 0 else {
753+
throw SystemError.close(rv)
754+
}
738755
}
756+
var innerFd = fd
757+
try innerClose(&innerFd)
739758
}
740759

741760
extension Process.Error: CustomStringConvertible {
@@ -788,3 +807,23 @@ extension ProcessResult.Error: CustomStringConvertible {
788807
}
789808
}
790809
#endif
810+
811+
#if os(Windows)
812+
extension FileHandle: OutputByteStream {
813+
public var position: Int {
814+
return Int(offsetInFile)
815+
}
816+
817+
public func write(_ byte: UInt8) {
818+
write(Data([byte]))
819+
}
820+
821+
public func write<C: Collection>(_ bytes: C) where C.Element == UInt8 {
822+
write(Data(bytes))
823+
}
824+
825+
public func flush() {
826+
synchronizeFile()
827+
}
828+
}
829+
#endif

Tests/TSCBasicTests/ProcessTests.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,21 @@ class ProcessTests: XCTestCase {
186186
XCTAssertEqual(result2, "hello\n")
187187
}
188188

189+
func testStdin() throws {
190+
var stdout = [UInt8]()
191+
let process = Process(args: script("in-to-out"), outputRedirection: .stream(stdout: { stdoutBytes in
192+
stdout += stdoutBytes
193+
}, stderr: { _ in }))
194+
let stdinStream = try process.launch()
195+
196+
stdinStream.write("hello\n")
197+
stdinStream.flush()
198+
199+
try process.waitUntilExit()
200+
201+
XCTAssertEqual(String(decoding: stdout, as: UTF8.self), "hello\n")
202+
}
203+
189204
func testStdoutStdErr() throws {
190205
// A simple script to check that stdout and stderr are captured separatly.
191206
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)