11import Darwin
2+ import Dispatch
23import Foundation
34import Vapor
45
@@ -17,15 +18,28 @@ struct PosixSpawnResult {
1718}
1819
1920enum PosixSpawn {
21+ enum OutputStream {
22+ case stdout
23+ case stderr
24+ }
25+
2026 static func run(
2127 executablePath: String ,
2228 arguments: [ String ] ,
2329 workingDirectory: URL ,
2430 sandboxProfileURL: URL ? = nil ,
25- timeoutSeconds: Int ? = nil
31+ timeoutSeconds: Int ? = nil ,
32+ onOutputLine: ( ( OutputStream , String ) -> Void ) ? = nil
2633 ) throws -> PosixSpawnResult {
27- let stdoutURL = workingDirectory. appendingPathComponent ( " stdout.log " )
28- let stderrURL = workingDirectory. appendingPathComponent ( " stderr.log " )
34+ var stdoutPipe = try makePipe ( operation: " stdout pipe " )
35+ var stderrPipe = try makePipe ( operation: " stderr pipe " )
36+ defer {
37+ closeIfOpen ( & stdoutPipe. read)
38+ closeIfOpen ( & stdoutPipe. write)
39+ closeIfOpen ( & stderrPipe. read)
40+ closeIfOpen ( & stderrPipe. write)
41+ }
42+
2943 let launch = launchCommand (
3044 executablePath: executablePath,
3145 arguments: arguments,
@@ -39,8 +53,12 @@ enum PosixSpawn {
3953 #if !os(iOS)
4054 try throwIfFailed ( posix_spawn_file_actions_addchdir_np ( & actions, workingDirectory. path) , operation: " posix_spawn_file_actions_addchdir_np " )
4155 #endif
42- try throwIfFailed ( posix_spawn_file_actions_addopen ( & actions, STDOUT_FILENO, stdoutURL. path, O_WRONLY | O_CREAT | O_TRUNC, 0o644 ) , operation: " stdout redirect " )
43- try throwIfFailed ( posix_spawn_file_actions_addopen ( & actions, STDERR_FILENO, stderrURL. path, O_WRONLY | O_CREAT | O_TRUNC, 0o644 ) , operation: " stderr redirect " )
56+ try throwIfFailed ( posix_spawn_file_actions_adddup2 ( & actions, stdoutPipe. write, STDOUT_FILENO) , operation: " stdout redirect " )
57+ try throwIfFailed ( posix_spawn_file_actions_adddup2 ( & actions, stderrPipe. write, STDERR_FILENO) , operation: " stderr redirect " )
58+ try throwIfFailed ( posix_spawn_file_actions_addclose ( & actions, stdoutPipe. read) , operation: " stdout read close " )
59+ try throwIfFailed ( posix_spawn_file_actions_addclose ( & actions, stderrPipe. read) , operation: " stderr read close " )
60+ try throwIfFailed ( posix_spawn_file_actions_addclose ( & actions, stdoutPipe. write) , operation: " stdout write close " )
61+ try throwIfFailed ( posix_spawn_file_actions_addclose ( & actions, stderrPipe. write) , operation: " stderr write close " )
4462
4563 var attributes : posix_spawnattr_t ?
4664 try throwIfFailed ( posix_spawnattr_init ( & attributes) , operation: " posix_spawnattr_init " )
@@ -72,10 +90,58 @@ enum PosixSpawn {
7290 try throwIfFailed ( spawnStatus, operation: " posix_spawn \( launch. executablePath) " )
7391 #endif
7492
75- let waitStatus = try wait ( for: pid, timeoutSeconds: timeoutSeconds)
93+ closeIfOpen ( & stdoutPipe. write)
94+ closeIfOpen ( & stderrPipe. write)
95+
96+ let group = DispatchGroup ( )
97+ let outputQueue = DispatchQueue . global ( qos: . utility)
98+ var stdout = Data ( )
99+ var stderr = Data ( )
100+ var stdoutError : Error ?
101+ var stderrError : Error ?
102+
103+ group. enter ( )
104+ outputQueue. async {
105+ do {
106+ stdout = try readOutputPipe ( stdoutPipe. read, stream: . stdout, onOutputLine: onOutputLine)
107+ } catch {
108+ stdoutError = error
109+ }
110+ group. leave ( )
111+ }
112+
113+ group. enter ( )
114+ outputQueue. async {
115+ do {
116+ stderr = try readOutputPipe ( stderrPipe. read, stream: . stderr, onOutputLine: onOutputLine)
117+ } catch {
118+ stderrError = error
119+ }
120+ group. leave ( )
121+ }
76122
77- let stdout = try Data ( contentsOf: stdoutURL)
78- let stderr = try Data ( contentsOf: stderrURL)
123+ let waitStatus : Int32
124+ var waitError : Error ?
125+ do {
126+ waitStatus = try wait ( for: pid, timeoutSeconds: timeoutSeconds)
127+ } catch {
128+ waitError = error
129+ waitStatus = 0
130+ }
131+
132+ group. wait ( )
133+ closeIfOpen ( & stdoutPipe. read)
134+ closeIfOpen ( & stderrPipe. read)
135+
136+ if let waitError = waitError {
137+ throw waitError
138+ }
139+ if let stdoutError = stdoutError {
140+ throw stdoutError
141+ }
142+ if let stderrError = stderrError {
143+ throw stderrError
144+ }
79145 return PosixSpawnResult ( exitCode: exitCode ( from: waitStatus) , stdout: stdout, stderr: stderr)
80146 }
81147
@@ -115,6 +181,90 @@ enum PosixSpawn {
115181 }
116182 }
117183
184+ private static func throwIfErrnoFailed( _ status: Int32 , operation: String ) throws {
185+ guard status == 0 else {
186+ throw Abort ( . internalServerError, reason: " \( operation) failed: \( String ( cString: strerror ( errno) ) ) " )
187+ }
188+ }
189+
190+ private static func makePipe( operation: String ) throws -> ( read: Int32 , write: Int32 ) {
191+ var fds = [ Int32] ( repeating: - 1 , count: 2 )
192+ let status = fds. withUnsafeMutableBufferPointer { buffer in
193+ pipe ( buffer. baseAddress!)
194+ }
195+ try throwIfErrnoFailed ( status, operation: operation)
196+ return ( fds [ 0 ] , fds [ 1 ] )
197+ }
198+
199+ private static func closeIfOpen( _ fd: inout Int32 ) {
200+ guard fd >= 0 else {
201+ return
202+ }
203+ close ( fd)
204+ fd = - 1
205+ }
206+
207+ private static func readOutputPipe(
208+ _ fd: Int32 ,
209+ stream: OutputStream ,
210+ onOutputLine: ( ( OutputStream , String ) -> Void ) ?
211+ ) throws -> Data {
212+ var output = Data ( )
213+ var pendingLine = " "
214+ var buffer = [ UInt8] ( repeating: 0 , count: 4096 )
215+
216+ while true {
217+ let count = Darwin . read ( fd, & buffer, buffer. count)
218+ if count > 0 {
219+ let chunk = Data ( buffer [ 0 ..< count] )
220+ output. append ( chunk)
221+ emitCompleteLines ( from: chunk, pendingLine: & pendingLine, stream: stream, onOutputLine: onOutputLine)
222+ continue
223+ }
224+ if count == 0 {
225+ emitPendingLine ( pendingLine, stream: stream, onOutputLine: onOutputLine)
226+ return output
227+ }
228+ if errno == EINTR {
229+ continue
230+ }
231+ throw POSIXError ( POSIXErrorCode ( rawValue: errno) ?? . EIO)
232+ }
233+ }
234+
235+ private static func emitCompleteLines(
236+ from chunk: Data ,
237+ pendingLine: inout String ,
238+ stream: OutputStream ,
239+ onOutputLine: ( ( OutputStream , String ) -> Void ) ?
240+ ) {
241+ guard let onOutputLine = onOutputLine else {
242+ return
243+ }
244+
245+ pendingLine += String ( decoding: chunk, as: UTF8 . self)
246+ let parts = pendingLine. components ( separatedBy: . newlines)
247+ let endedWithNewline = pendingLine. unicodeScalars. last. map { CharacterSet . newlines. contains ( $0) } ?? false
248+ let completeLines = endedWithNewline ? parts : Array ( parts. dropLast ( ) )
249+
250+ for line in completeLines {
251+ emitPendingLine ( line, stream: stream, onOutputLine: onOutputLine)
252+ }
253+ pendingLine = endedWithNewline ? " " : parts. last ?? " "
254+ }
255+
256+ private static func emitPendingLine(
257+ _ line: String ,
258+ stream: OutputStream ,
259+ onOutputLine: ( ( OutputStream , String ) -> Void ) ?
260+ ) {
261+ let trimmed = line. trimmingCharacters ( in: . whitespacesAndNewlines)
262+ guard trimmed. isEmpty == false else {
263+ return
264+ }
265+ onOutputLine ? ( stream, trimmed)
266+ }
267+
118268 private static func wait( for pid: pid_t , timeoutSeconds: Int ? ) throws -> Int32 {
119269 var waitStatus : Int32 = 0
120270 guard let timeoutSeconds = timeoutSeconds else {
0 commit comments