Skip to content

Commit 65904e4

Browse files
committed
Add support for Process stdin stream
1 parent 8b60325 commit 65904e4

File tree

3 files changed

+83
-21
lines changed

3 files changed

+83
-21
lines changed

Sources/TSCBasic/Process.swift

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ public struct ProcessResult: CustomStringConvertible {
113113
}
114114
}
115115

116+
/// Alias to make the type's name closer to its purpose.
117+
public typealias ProcessStdinStream = OutputByteStream
118+
116119
/// Process allows spawning new subprocesses and working with them.
117120
///
118121
/// Note: This class is thread safe.
@@ -331,8 +334,10 @@ public final class Process: ObjectIdentifierProtocol {
331334
}
332335
}
333336

334-
/// Launch the subprocess.
335-
public func launch() throws {
337+
/// Launch the subprocess. Returns a ProcessStdinStream object that can be used to communicate to the process's
338+
/// stdin.
339+
@discardableResult
340+
public func launch() throws -> ProcessStdinStream {
336341
precondition(arguments.count > 0 && !arguments[0].isEmpty, "Need at least one argument to launch the process.")
337342
precondition(!launched, "It is not allowed to launch the same process object again.")
338343

@@ -351,12 +356,15 @@ public final class Process: ObjectIdentifierProtocol {
351356
throw Process.Error.missingExecutableProgram(program: executable)
352357
}
353358

354-
#if os(Windows)
359+
#if os(Windows)
355360
_process = Foundation.Process()
356361
_process?.arguments = Array(arguments.dropFirst()) // Avoid including the executable URL twice.
357362
_process?.executableURL = executablePath.asURL
358363
_process?.environment = environment
359364

365+
let stdinPipe = Pipe()
366+
_process?.standardInput = stdinPipe
367+
360368
if outputRedirection.redirectsOutput {
361369
let stdoutPipe = Pipe()
362370
let stderrPipe = Pipe()
@@ -379,6 +387,8 @@ public final class Process: ObjectIdentifierProtocol {
379387
}
380388

381389
try _process?.run()
390+
391+
return stdinPipe.fileHandleForWriting
382392
#else
383393
// Initialize the spawn attributes.
384394
#if canImport(Darwin) || os(Android)
@@ -453,14 +463,17 @@ public final class Process: ObjectIdentifierProtocol {
453463
#endif
454464
}
455465

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

465478
var outputPipe: [Int32] = [-1, -1]
466479
var stderrPipe: [Int32] = [-1, -1]
@@ -471,7 +484,7 @@ public final class Process: ObjectIdentifierProtocol {
471484
// Open the write end of the pipe.
472485
posix_spawn_file_actions_adddup2(&fileActions, outputPipe[1], 1)
473486

474-
// Close the other ends of the pipe.
487+
// Close the other ends of the pipe since they were dupped to 1.
475488
posix_spawn_file_actions_addclose(&fileActions, outputPipe[0])
476489
posix_spawn_file_actions_addclose(&fileActions, outputPipe[1])
477490

@@ -483,7 +496,7 @@ public final class Process: ObjectIdentifierProtocol {
483496
try open(pipe: &stderrPipe)
484497
posix_spawn_file_actions_adddup2(&fileActions, stderrPipe[1], 2)
485498

486-
// Close the other ends of the pipe.
499+
// Close the other ends of the pipe since they were dupped to 2.
487500
posix_spawn_file_actions_addclose(&fileActions, stderrPipe[0])
488501
posix_spawn_file_actions_addclose(&fileActions, stderrPipe[1])
489502
}
@@ -500,11 +513,14 @@ public final class Process: ObjectIdentifierProtocol {
500513
throw SystemError.posix_spawn(rv, arguments)
501514
}
502515

516+
// Close the local read end of the input pipe.
517+
try close(fd: stdinPipe[0])
518+
503519
if outputRedirection.redirectsOutput {
504520
let outputClosures = outputRedirection.outputClosures
505521

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

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

518534
// Only schedule a thread for stderr if no redirect was requested.
519535
if !outputRedirection.redirectStderr {
520-
// Close the write end of the stderr pipe.
521-
try close(fd: &stderrPipe[1])
536+
// Close the local write end of the stderr pipe.
537+
try close(fd: stderrPipe[1])
522538

523539
// Create a thread and start reading the stderr output on it.
524540
thread = Thread { [weak self] in
@@ -531,6 +547,8 @@ public final class Process: ObjectIdentifierProtocol {
531547
}
532548
}
533549
#endif // POSIX implementation
550+
551+
return stdinStream
534552
}
535553

536554
/// Blocks the calling process until the subprocess finishes execution.
@@ -731,11 +749,15 @@ private func open(pipe: inout [Int32]) throws {
731749
}
732750

733751
/// 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)
752+
private func close(fd: Int32) throws {
753+
func innerClose(_ fd: inout Int32) throws {
754+
let rv = TSCLibc.close(fd)
755+
guard rv == 0 else {
756+
throw SystemError.close(rv)
757+
}
738758
}
759+
var innerFd = fd
760+
try innerClose(&innerFd)
739761
}
740762

741763
extension Process.Error: CustomStringConvertible {
@@ -788,3 +810,23 @@ extension ProcessResult.Error: CustomStringConvertible {
788810
}
789811
}
790812
#endif
813+
814+
#if os(Windows)
815+
extension FileHandle: ProcessStdinStream {
816+
public var position: Int {
817+
return Int(offsetInFile)
818+
}
819+
820+
public func write(_ byte: UInt8) {
821+
write(Data([byte]))
822+
}
823+
824+
public func write<C: Collection>(_ bytes: C) where C.Element == UInt8 {
825+
write(Data(bytes))
826+
}
827+
828+
public func flush() {
829+
synchronizeFile()
830+
}
831+
}
832+
#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)