Skip to content

Instantly share code, notes, and snippets.

@bhargavg
Last active April 21, 2022 17:51
Show Gist options
  • Save bhargavg/26049391c5c6692e3ae4b1b80f66d24c to your computer and use it in GitHub Desktop.
Save bhargavg/26049391c5c6692e3ae4b1b80f66d24c to your computer and use it in GitHub Desktop.
An interactive process
import Foundation
import Result
func launchProcess(
path: String,
args: [String],
options: LaunchProcessArgs
) -> Result<(stdOut: Data, stdErr: Data), ProcessError> {
let process = Process()
process.launchPath = path
process.arguments = args
let group = DispatchGroup()
let queue = DispatchQueue(label: "com.bhargavg.process.queue")
let stdInChannel: DispatchIO?
if options.contains(.attachStdIn) {
let channel = DispatchIO(
type: .stream,
fileDescriptor: STDIN_FILENO,
queue: queue,
cleanupHandler: { (fd) in }
)
channel.setLimit(lowWater: 1)
stdInChannel = channel
} else {
stdInChannel = nil
}
let execInPipe = Pipe()
process.standardInput = execInPipe.fileHandleForReading
let execInChannel = DispatchIO(
type: .stream,
fileDescriptor: execInPipe.fileHandleForWriting.fileDescriptor,
queue: queue,
cleanupHandler: { (fd) in }
)
execInChannel.setLimit(lowWater: 1)
stdInChannel?.read(
offset: 0,
length: .max,
queue: queue
) { (isDone, mayBeDispatchData, errorCode) in
let result: Result<DispatchData, ProcessError> = Result(mayBeDispatchData, failWith: .error(errorCode))
switch result {
case let .success(data):
execInChannel.write(
offset: 0,
data: data,
queue: queue,
ioHandler: { _ in }
)
case .failure:
/// Ignoring stdin read errors
break
}
if isDone {
stdInChannel?.close()
}
}
let stdOutChannel: DispatchIO?
if options.contains(.attachStdOut) {
let channel = DispatchIO(
type: .stream,
fileDescriptor: STDOUT_FILENO,
queue: queue,
cleanupHandler: { (fd) in }
)
channel.setLimit(lowWater: 1)
stdOutChannel = channel
} else {
stdOutChannel = nil
}
var execOutData: DispatchData? = nil
let execOutPipe = Pipe()
process.standardOutput = execOutPipe.fileHandleForWriting
let execOutChannel = DispatchIO(
type: .stream,
fileDescriptor: execOutPipe.fileHandleForReading.fileDescriptor,
queue: queue,
cleanupHandler: { (fd) in }
)
execOutChannel.setLimit(lowWater: 1)
execOutChannel.setInterval(interval: .milliseconds(500), flags: .strictInterval)
execOutChannel.read(
offset: 0,
length: .max,
queue: queue
) { (isDone, mayBeDispatchData, errorCode) in
let result: Result<DispatchData, ProcessError> = Result(mayBeDispatchData, failWith: .error(errorCode))
switch result {
case let .success(data):
if var execOutData = execOutData {
execOutData.append(data)
} else {
execOutData = data
}
stdOutChannel?.write(
offset: 0,
data: data,
queue: queue,
ioHandler: { _ in
/// Ignoring console write errors
}
)
case .failure:
/// Ignoring read errors,
/// hoping these will trigger process to fail, thereby
/// caught in termination handler
break;
}
if isDone {
execOutChannel.close()
}
}
let stdErrChannel: DispatchIO?
if options.contains(.attachStdErr) {
let channel = DispatchIO(
type: .stream,
fileDescriptor: STDOUT_FILENO,
queue: queue,
cleanupHandler: { (fd) in }
)
channel.setLimit(lowWater: 1)
stdErrChannel = channel
} else {
stdErrChannel = nil
}
var execErrData: DispatchData? = nil
let execErrPipe = Pipe()
process.standardError = execErrPipe.fileHandleForWriting
let execErrChannel = DispatchIO(
type: .stream,
fileDescriptor: execErrPipe.fileHandleForReading.fileDescriptor,
queue: queue,
cleanupHandler: { (fd) in }
)
execErrChannel.setLimit(lowWater: 1)
execErrChannel.read(
offset: 0,
length: .max,
queue: queue
) { (isDone, mayBeDispatchData, errorCode) in
let result: Result<DispatchData, ProcessError> = Result(mayBeDispatchData, failWith: .error(errorCode))
switch result {
case let .success(data):
if var execErrData = execErrData {
execErrData.append(data)
} else {
execErrData = data
}
stdErrChannel?.write(
offset: 0,
data: data,
queue: queue,
ioHandler: { _ in
/// Ignoring console write errors
}
)
case .failure:
/// Ignoring read errors,
/// hoping these will trigger process to fail, thereby
/// caught in termination handler
break;
}
if isDone {
execErrChannel.close()
}
}
var result: Result<(stdOut: Data, stdErr: Data), ProcessError> = .failure(.error(-1))
process.terminationHandler = { (process) in
if process.terminationStatus == EXIT_SUCCESS {
result = .success((stdOut: execOutData?.asData ?? Data(), stdErr: execErrData?.asData ?? Data()))
} else {
result = .failure(.error(process.terminationStatus))
}
group.leave()
}
group.enter()
process.launch()
group.wait()
stdOutChannel?.close()
return result
}
extension DispatchData {
var asData: Data {
let bytes = UnsafeMutablePointer<UInt8>.allocate(capacity: count)
copyBytes(to: bytes, count: count)
let data = Data(bytes: bytes, count: count)
bytes.deinitialize(count: count)
bytes.deallocate(capacity: count)
return data
}
}
import Foundation
struct LaunchProcessArgs: OptionSet {
let rawValue: Int
static let attachStdIn = LaunchProcessArgs(rawValue: 1 << 0)
static let attachStdOut = LaunchProcessArgs(rawValue: 1 << 1)
static let attachStdErr = LaunchProcessArgs(rawValue: 1 << 2)
static let interactive: LaunchProcessArgs = [.attachStdIn, .attachStdOut, .attachStdErr]
}
import Result
let result = launchProcess(
path: "/usr/local/bin/npm",
args: ["init"],
options: .interactive
)
print(result)
// swift-tools-version:3.1
import PackageDescription
let package = Package(
name: "testprocess",
dependencies: [
.Package(url: "https://github.com/antitypical/Result", majorVersion: 3, minor: 2)
]
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment