From 0b9d87216377650cc84f3b748fdf60463a633297 Mon Sep 17 00:00:00 2001 From: Sergio Campama Date: Fri, 4 Sep 2020 22:38:51 -0700 Subject: [PATCH] Add support for Process stdin stream --- Sources/TSCBasic/Process.swift | 86 ++++++++++++++++----- Tests/TSCBasicTests/ProcessTests.swift | 17 ++++ Tests/TSCBasicTests/processInputs/in-to-out | 5 ++ 3 files changed, 87 insertions(+), 21 deletions(-) create mode 100755 Tests/TSCBasicTests/processInputs/in-to-out diff --git a/Sources/TSCBasic/Process.swift b/Sources/TSCBasic/Process.swift index b4723fb1..ed8bf092 100644 --- a/Sources/TSCBasic/Process.swift +++ b/Sources/TSCBasic/Process.swift @@ -331,8 +331,11 @@ public final class Process: ObjectIdentifierProtocol { } } - /// Launch the subprocess. - public func launch() throws { + /// Launch the subprocess. Returns a WritableByteStream object that can be used to communicate to the process's + /// stdin. If needed, the stream can be closed using the close() API. Otherwise, the stream will be closed + /// automatically. + @discardableResult + public func launch() throws -> WritableByteStream { precondition(arguments.count > 0 && !arguments[0].isEmpty, "Need at least one argument to launch the process.") precondition(!launched, "It is not allowed to launch the same process object again.") @@ -351,12 +354,15 @@ public final class Process: ObjectIdentifierProtocol { throw Process.Error.missingExecutableProgram(program: executable) } - #if os(Windows) + #if os(Windows) _process = Foundation.Process() _process?.arguments = Array(arguments.dropFirst()) // Avoid including the executable URL twice. _process?.executableURL = executablePath.asURL _process?.environment = environment + let stdinPipe = Pipe() + _process?.standardInput = stdinPipe + if outputRedirection.redirectsOutput { let stdoutPipe = Pipe() let stderrPipe = Pipe() @@ -379,6 +385,8 @@ public final class Process: ObjectIdentifierProtocol { } try _process?.run() + + return stdinPipe.fileHandleForWriting #else // Initialize the spawn attributes. #if canImport(Darwin) || os(Android) @@ -453,14 +461,17 @@ public final class Process: ObjectIdentifierProtocol { #endif } - // Workaround for https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=89e435f3559c53084498e9baad22172b64429362 - // Change allowing for newer version of glibc - guard let devNull = strdup("/dev/null") else { - throw SystemError.posix_spawn(0, arguments) - } - defer { free(devNull) } - // Open /dev/null as stdin. - posix_spawn_file_actions_addopen(&fileActions, 0, devNull, O_RDONLY, 0) + var stdinPipe: [Int32] = [-1, -1] + try open(pipe: &stdinPipe) + + let stdinStream = try LocalFileOutputByteStream(filePointer: fdopen(stdinPipe[1], "wb"), closeOnDeinit: true) + + // Dupe the read portion of the remote to 0. + posix_spawn_file_actions_adddup2(&fileActions, stdinPipe[0], 0) + + // Close the other side's pipe since it was dupped to 0. + posix_spawn_file_actions_addclose(&fileActions, stdinPipe[0]) + posix_spawn_file_actions_addclose(&fileActions, stdinPipe[1]) var outputPipe: [Int32] = [-1, -1] var stderrPipe: [Int32] = [-1, -1] @@ -471,7 +482,7 @@ public final class Process: ObjectIdentifierProtocol { // Open the write end of the pipe. posix_spawn_file_actions_adddup2(&fileActions, outputPipe[1], 1) - // Close the other ends of the pipe. + // Close the other ends of the pipe since they were dupped to 1. posix_spawn_file_actions_addclose(&fileActions, outputPipe[0]) posix_spawn_file_actions_addclose(&fileActions, outputPipe[1]) @@ -483,7 +494,7 @@ public final class Process: ObjectIdentifierProtocol { try open(pipe: &stderrPipe) posix_spawn_file_actions_adddup2(&fileActions, stderrPipe[1], 2) - // Close the other ends of the pipe. + // Close the other ends of the pipe since they were dupped to 2. posix_spawn_file_actions_addclose(&fileActions, stderrPipe[0]) posix_spawn_file_actions_addclose(&fileActions, stderrPipe[1]) } @@ -500,11 +511,14 @@ public final class Process: ObjectIdentifierProtocol { throw SystemError.posix_spawn(rv, arguments) } + // Close the local read end of the input pipe. + try close(fd: stdinPipe[0]) + if outputRedirection.redirectsOutput { let outputClosures = outputRedirection.outputClosures - // Close the write end of the output pipe. - try close(fd: &outputPipe[1]) + // Close the local write end of the output pipe. + try close(fd: outputPipe[1]) // Create a thread and start reading the output on it. var thread = Thread { [weak self] in @@ -517,8 +531,8 @@ public final class Process: ObjectIdentifierProtocol { // Only schedule a thread for stderr if no redirect was requested. if !outputRedirection.redirectStderr { - // Close the write end of the stderr pipe. - try close(fd: &stderrPipe[1]) + // Close the local write end of the stderr pipe. + try close(fd: stderrPipe[1]) // Create a thread and start reading the stderr output on it. thread = Thread { [weak self] in @@ -530,6 +544,8 @@ public final class Process: ObjectIdentifierProtocol { self.stderr.thread = thread } } + + return stdinStream #endif // POSIX implementation } @@ -731,11 +747,15 @@ private func open(pipe: inout [Int32]) throws { } /// Close the given fd. -private func close(fd: inout Int32) throws { - let rv = TSCLibc.close(fd) - guard rv == 0 else { - throw SystemError.close(rv) +private func close(fd: Int32) throws { + func innerClose(_ fd: inout Int32) throws { + let rv = TSCLibc.close(fd) + guard rv == 0 else { + throw SystemError.close(rv) + } } + var innerFd = fd + try innerClose(&innerFd) } extension Process.Error: CustomStringConvertible { @@ -788,3 +808,27 @@ extension ProcessResult.Error: CustomStringConvertible { } } } + +#if os(Windows) +extension FileHandle: WritableByteStream { + public var position: Int { + return Int(offsetInFile) + } + + public func write(_ byte: UInt8) { + write(Data([byte])) + } + + public func write(_ bytes: C) where C.Element == UInt8 { + write(Data(bytes)) + } + + public func flush() { + synchronizeFile() + } + + public func close() throws { + closeFile() + } +} +#endif diff --git a/Tests/TSCBasicTests/ProcessTests.swift b/Tests/TSCBasicTests/ProcessTests.swift index efccea0d..2b3d6f84 100644 --- a/Tests/TSCBasicTests/ProcessTests.swift +++ b/Tests/TSCBasicTests/ProcessTests.swift @@ -186,6 +186,23 @@ class ProcessTests: XCTestCase { XCTAssertEqual(result2, "hello\n") } + func testStdin() throws { + var stdout = [UInt8]() + let process = Process(args: script("in-to-out"), outputRedirection: .stream(stdout: { stdoutBytes in + stdout += stdoutBytes + }, stderr: { _ in })) + let stdinStream = try process.launch() + + stdinStream.write("hello\n") + stdinStream.flush() + + try stdinStream.close() + + try process.waitUntilExit() + + XCTAssertEqual(String(decoding: stdout, as: UTF8.self), "hello\n") + } + func testStdoutStdErr() throws { // A simple script to check that stdout and stderr are captured separatly. do { diff --git a/Tests/TSCBasicTests/processInputs/in-to-out b/Tests/TSCBasicTests/processInputs/in-to-out new file mode 100755 index 00000000..3b86f0d9 --- /dev/null +++ b/Tests/TSCBasicTests/processInputs/in-to-out @@ -0,0 +1,5 @@ +#!/usr/bin/env python + +import sys + +sys.stdout.write(sys.stdin.readline())