Created
November 11, 2017 07:53
-
-
Save norio-nomura/90a374d33c5178fdbc336b1e50cf74dc to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
enum Error: CustomStringConvertible, Swift.Error { | |
case failed(subcommand: String, message: String, status: Int32) | |
var description: String { | |
switch self { | |
case let .failed(subcommand: subcommand, message: message, status: status): | |
return "`\(subcommand)` failed with status: \(status)\n\(message)" | |
} | |
} | |
} | |
private final class Pipe { | |
let fileHandleForReading: FileHandle | |
let fileHandleForWriting: FileHandle | |
let readingSemaphore = DispatchSemaphore(value: 1) | |
init() throws { | |
var fileDescriptors: [Int32] = [0, 0] | |
guard 0 == pipe(&fileDescriptors) else { | |
throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno)) | |
} | |
fileHandleForReading = .init(fileDescriptor: fileDescriptors[0], closeOnDealloc: true) | |
fileHandleForWriting = .init(fileDescriptor: fileDescriptors[1], closeOnDealloc: true) | |
} | |
convenience init(readabilityHandler: @escaping (Data) -> Void) throws { | |
try self.init() | |
var error: Int32 = 0 | |
let channel = DispatchIO(type: .stream, | |
fileDescriptor: fileHandleForReading.fileDescriptor, | |
queue: .global()) { error = $0 } | |
guard error == 0 else { | |
throw NSError(domain: NSPOSIXErrorDomain, code: Int(error)) | |
} | |
channel.setLimit(lowWater: 1) | |
channel.read(offset: 0, length: .max, queue: .global()) { done, data, error in | |
if let data = data { | |
data.withUnsafeBytes { | |
readabilityHandler(.init(bytes: $0, count: data.count)) | |
} | |
} | |
if error != 0 { | |
fatalError("\(NSError(domain: NSPOSIXErrorDomain, code: Int(error)))") | |
} | |
if done { | |
channel.close() | |
self.readingSemaphore.signal() | |
} | |
} | |
} | |
func waitUntilEndOfFile() { | |
readingSemaphore.wait() | |
} | |
} | |
@discardableResult | |
func execute(_ arguments: [String], | |
at url: URL? = nil, | |
environment: [String: String]? = nil, | |
input: Data? = nil, | |
verbose: Bool = false) throws -> String { | |
if verbose { | |
print("- " + arguments.joined(separator: " ")) | |
} | |
let process = Process() | |
process.launchPath = "/usr/bin/env" | |
process.arguments = arguments | |
if let url = url { | |
process.currentDirectoryURL = url | |
} | |
if let environment = environment { | |
process.environment = ProcessInfo.processInfo.environment.merging(environment) { | |
(_, new) in new | |
} | |
} | |
var stdoutData = Data() | |
let stdoutPipe = try Pipe() { data in | |
if verbose { | |
FileHandle.standardOutput.write(data) | |
} | |
stdoutData.append(data) | |
} | |
var stderrData = Data() | |
let stderrPipe = try Pipe() { data in | |
if verbose { | |
FileHandle.standardError.write(data) | |
} | |
stderrData.append(data) | |
} | |
process.standardOutput = stdoutPipe.fileHandleForWriting | |
process.standardError = stderrPipe.fileHandleForWriting | |
if let input = input { | |
let stdinPipe = try Pipe() | |
process.standardInput = stdinPipe.fileHandleForReading | |
process.launch() | |
stdinPipe.fileHandleForWriting.write(input) | |
stdinPipe.fileHandleForWriting.closeFile() | |
} else { | |
process.launch() | |
} | |
process.waitUntilExit() | |
stdoutPipe.waitUntilEndOfFile() | |
stderrPipe.waitUntilEndOfFile() | |
if process.terminationStatus != 0 { | |
throw Error.failed(subcommand: arguments.joined(separator: " "), | |
message: String(data: stderrData, encoding: .utf8) ?? "", | |
status: process.terminationStatus) | |
} | |
return String(data: stdoutData, encoding: .utf8) ?? "" | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment