carton/Sources/CartonHelpers/ProcessRunner.swift

158 lines
4.5 KiB
Swift

// Copyright 2020 Carton contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Dispatch
import Foundation
#if canImport(Combine)
import Combine
#else
import OpenCombine
#endif
import TSCBasic
public extension Subscribers.Completion {
var result: Result<(), Failure> {
switch self {
case let .failure(error):
return .failure(error)
case .finished:
return .success(())
}
}
}
struct ProcessRunnerError: Error, CustomStringConvertible {
let description: String
}
public final class ProcessRunner {
public let publisher: AnyPublisher<String, Error>
private var subscription: AnyCancellable?
// swiftlint:disable:next function_body_length
public init(
_ arguments: [String],
loadingMessage: String = "Running...",
parser: ProcessOutputParser? = nil,
_ terminal: InteractiveWriter
) {
let subject = PassthroughSubject<String, Error>()
var tmpOutput = ""
publisher = subject
.handleEvents(
receiveSubscription: { _ in
terminal.clearLine()
terminal.write("\(loadingMessage)\n", inColor: .yellow)
},
receiveOutput: {
if parser != nil {
// Aggregate this for formatting later
tmpOutput += $0
} else {
terminal.write($0)
}
}, receiveCompletion: {
switch $0 {
case .finished:
let processName = arguments[0].first == "/" ?
AbsolutePath(arguments[0]).basename : arguments[0]
terminal.write("\n")
if let parser = parser {
if parser.parsingConditions.contains(.success) {
parser.parse(tmpOutput, terminal)
}
} else {
terminal.write(tmpOutput)
}
terminal.write(
"\n`\(processName)` process finished successfully\n",
inColor: .green,
bold: false
)
case let .failure(error):
let errorString = String(describing: error)
if errorString.isEmpty {
terminal.clearLine()
terminal.write(
"Compilation failed.\n\n",
inColor: .red
)
if let parser = parser {
if parser.parsingConditions.contains(.failure) {
parser.parse(tmpOutput, terminal)
}
} else {
terminal.write(tmpOutput)
}
} else {
terminal.write(
"\nProcess failed and produced following output: \n",
inColor: .red
)
print(error)
}
}
}
)
.eraseToAnyPublisher()
DispatchQueue.global().async {
let stdout: TSCBasic.Process.OutputClosure = {
guard let string = String(data: Data($0), encoding: .utf8) else { return }
subject.send(string)
}
var stderrBuffer = [UInt8]()
let stderr: TSCBasic.Process.OutputClosure = {
stderrBuffer.append(contentsOf: $0)
}
let process = Process(
arguments: arguments,
outputRedirection: .stream(stdout: stdout, stderr: stderr),
verbose: true,
startNewProcessGroup: true
)
let result = Result<ProcessResult, Error> {
try process.launch()
return try process.waitUntilExit()
}
switch result.map(\.exitStatus) {
case .success(.terminated(code: EXIT_SUCCESS)):
subject.send(completion: .finished)
case let .failure(error):
subject.send(completion: .failure(error))
default:
let errorDescription = String(data: Data(stderrBuffer), encoding: .utf8) ?? ""
return subject
.send(completion: .failure(ProcessRunnerError(description: errorDescription)))
}
}
}
public func waitUntilFinished() throws {
try tsc_await { completion in
subscription = publisher
.sink(
receiveCompletion: { completion($0.result) },
receiveValue: { _ in }
)
}
}
}