diff --git a/Sources/Process+Promise.swift b/Sources/Process+Promise.swift index 0448475..03cab3c 100644 --- a/Sources/Process+Promise.swift +++ b/Sources/Process+Promise.swift @@ -72,20 +72,64 @@ extension Process { self.waitUntilExit() guard self.terminationReason == .exit, self.terminationStatus == 0 else { - return seal.reject(PMKError.execution(self)) + let stdoutData = try? self.readDataFromPipe(stdout) + let stderrData = try? self.readDataFromPipe(stderr) + + let stdoutString = stdoutData.flatMap { (data: Data) -> String? in String(data: data, encoding: .utf8) } + let stderrString = stderrData.flatMap { (data: Data) -> String? in String(data: data, encoding: .utf8) } + + return seal.reject(PMKError.execution(process: self, standardOutput: stdoutString, standardError: stderrString)) } seal.fulfill((stdout, stderr)) } } } + private func readDataFromPipe(_ pipe: Pipe) throws -> Data { + let handle = pipe.fileHandleForReading + defer { handle.closeFile() } + + // Someday, NSFileHandle will probably be updated with throwing equivalents to its read and write methods, + // as NSTask has, to avoid raising exceptions and crashing the app. + // Unfortunately that day has not yet come, so use the underlying BSD calls for now. + + let fd = handle.fileDescriptor + + let bufsize = 1024 * 8 + let buf = UnsafeMutablePointer.allocate(capacity: bufsize) + + #if swift(>=4.1) + defer { buf.deallocate() } + #else + defer { buf.deallocate(capacity: bufsize) } + #endif + + var data = Data() + + while true { + let bytesRead = read(fd, buf, bufsize) + + if bytesRead == 0 { + break + } + + if bytesRead < 0 { + throw POSIXError.Code(rawValue: errno).map { POSIXError($0) } ?? CocoaError(.fileReadUnknown) + } + + data.append(buf, count: bytesRead) + } + + return data + } + /** The error generated by PromiseKit’s `Process` extension */ public enum PMKError { /// NOT AVAILABLE ON 10.13 and above because Apple provide this error handling themselves case notExecutable(String?) - case execution(Process) + case execution(process: Process, standardOutput: String?, standardError: String?) } } @@ -97,7 +141,7 @@ extension Process.PMKError: LocalizedError { return "File not executable: \(path)" case .notExecutable(nil): return "No launch path specified" - case .execution(let task): + case .execution(process: let task, standardOutput: _, standardError: _): return "Failed executing: `\(task)` (\(task.terminationStatus))." } } diff --git a/Tests/TestNSTask.swift b/Tests/TestNSTask.swift index 0ed49b7..8d94c48 100644 --- a/Tests/TestNSTask.swift +++ b/Tests/TestNSTask.swift @@ -32,14 +32,12 @@ class NSTaskTests: XCTestCase { }.catch { err in do { throw err - } catch Process.PMKError.execution(let proc) { - let expectedStderrData = "ls: \(dir): No such file or directory\n".data(using: .utf8, allowLossyConversion: false)! - let stdout = (proc.standardOutput as! Pipe).fileHandleForReading.readDataToEndOfFile() - let stderr = (proc.standardError as! Pipe).fileHandleForReading.readDataToEndOfFile() + } catch Process.PMKError.execution(let proc, let stdout, let stderr) { + let expectedStderr = "ls: \(dir): No such file or directory\n" - XCTAssertEqual(stderr, expectedStderrData) + XCTAssertEqual(stderr, expectedStderr) XCTAssertEqual(proc.terminationStatus, 1) - XCTAssertEqual(stdout.count, 0) + XCTAssertEqual(stdout?.count ?? 0, 0) } catch { XCTFail() }