import Foundation #if !PMKCocoaPods import PromiseKit #endif #if os(macOS) /** To import the `Process` category: use_frameworks! pod "PromiseKit/Foundation" Or `Process` is one of the categories imported by the umbrella pod: use_frameworks! pod "PromiseKit" And then in your sources: import PromiseKit */ extension Process { /** Launches the receiver and resolves when it exits. let proc = Process() proc.launchPath = "/bin/ls" proc.arguments = ["/bin"] proc.launch(.promise).compactMap { std in String(data: std.out.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) }.then { stdout in print(str) } */ public func launch(_: PMKNamespacer) -> Promise<(out: Pipe, err: Pipe)> { let (stdout, stderr) = (Pipe(), Pipe()) do { standardOutput = stdout standardError = stderr #if swift(>=4.0) if #available(OSX 10.13, *) { try run() } else if let path = launchPath, FileManager.default.isExecutableFile(atPath: path) { launch() } else { throw PMKError.notExecutable(launchPath) } #else guard let path = launchPath, FileManager.default.isExecutableFile(atPath: path) else { throw PMKError.notExecutable(launchPath) } launch() #endif } catch { return Promise(error: error) } var q: DispatchQueue { if #available(macOS 10.10, iOS 8.0, tvOS 9.0, watchOS 2.0, *) { return DispatchQueue.global(qos: .default) } else { return DispatchQueue.global(priority: .default) } } return Promise { seal in q.async { self.waitUntilExit() guard self.terminationReason == .exit, self.terminationStatus == 0 else { 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: Process, standardOutput: String?, standardError: String?) } } extension Process.PMKError: LocalizedError { public var errorDescription: String? { switch self { case .notExecutable(let path?): return "File not executable: \(path)" case .notExecutable(nil): return "No launch path specified" case .execution(process: let task, standardOutput: _, standardError: _): return "Failed executing: `\(task)` (\(task.terminationStatus))." } } } public extension Promise where T == (out: Pipe, err: Pipe) { func print() -> Promise { return tap { result in switch result { case .fulfilled(let raw): let stdout = String(data: raw.out.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) let stderr = String(data: raw.err.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) Swift.print("stdout: `\(stdout ?? "")`") Swift.print("stderr: `\(stderr ?? "")`") case .rejected(let err): Swift.print(err) } } } } extension Process { /// Provided because Foundation’s is USELESS open override var description: String { let launchPath = self.launchPath ?? "$0" var args = [launchPath] arguments.flatMap{ args += $0 } return args.map { arg in let contains: Bool #if swift(>=3.2) contains = arg.contains(" ") #else contains = arg.characters.contains(" ") #endif if contains { return "\"\(arg)\"" } else if arg == "" { return "\"\"" } else { return arg } }.joined(separator: " ") } } #endif