diff --git a/code/cli/munki/shared/admin/munkiimportlib.swift b/code/cli/munki/shared/admin/munkiimportlib.swift index b6dfbfe6..7e910ec8 100644 --- a/code/cli/munki/shared/admin/munkiimportlib.swift +++ b/code/cli/munki/shared/admin/munkiimportlib.swift @@ -579,8 +579,8 @@ func extractAndCopyIcon(_ repo: Repo, installerItem: String, pkginfo: PlistDict, return [String]() } -/// A subclass of AsyncProcessRunner to create disk images -class HdiUtilCreateFromFolderRunner: AsyncProcessRunner { +/// A subclass of ProcessRunner to create disk images +class HdiUtilCreateFromFolderRunner: ProcessRunner { init(sourceDir: String, outputPath: String) { let tool = "/usr/bin/hdiutil" let arguments = ["create", "-fs", "HFS+", "-srcfolder", sourceDir, outputPath] @@ -602,8 +602,8 @@ class HdiUtilCreateFromFolderRunner: AsyncProcessRunner { /// Wraps dirPath (generally an app bundle or bundle-style pkg into a disk image. /// Returns path to the created dmg file -/// async because it can take a while, depending on the size of the item -func makeDmg(_ dirPath: String) async -> String { +/// It can take a while, depending on the size of the item +func makeDmg(_ dirPath: String) -> String { let itemname = (dirPath as NSString).lastPathComponent print("Making disk image containing \(itemname)...") let dmgName = (itemname as NSString).deletingPathExtension + ".dmg" @@ -613,7 +613,7 @@ func makeDmg(_ dirPath: String) async -> String { } let dmgPath = (tmpDir as NSString).appendingPathComponent(dmgName) let dmgCreator = HdiUtilCreateFromFolderRunner(sourceDir: dirPath, outputPath: dmgPath) - await dmgCreator.run() + dmgCreator.run() if dmgCreator.results.exitcode != 0 { printStderr("Disk image creation failed.") return "" diff --git a/code/cli/munki/shared/utils/cliutils.swift b/code/cli/munki/shared/utils/cliutils.swift index 874d6cd5..76d168c3 100644 --- a/code/cli/munki/shared/utils/cliutils.swift +++ b/code/cli/munki/shared/utils/cliutils.swift @@ -36,6 +36,158 @@ struct CLIResults { var error: String = "" } +/// A class to run processes synchronously +class ProcessRunner { + let task = Process() + var results = CLIResults() + // var delegate: ProcessDelegate? + + init(_ tool: String, + arguments: [String] = [], + environment: [String: String] = [:], + stdIn: String = "") + { + task.executableURL = URL(fileURLWithPath: tool) + task.arguments = arguments + if !environment.isEmpty { + task.environment = environment + } + + // set up input pipe + let inPipe = Pipe() + task.standardInput = inPipe + // set up our stdout and stderr pipes and handlers + let outputPipe = Pipe() + outputPipe.fileHandleForReading.readabilityHandler = { fh in + let data = fh.availableData + if data.isEmpty { // EOF on the pipe + outputPipe.fileHandleForReading.readabilityHandler = nil + } else { + self.processOutput(String(data: data, encoding: .utf8)!) + } + } + let errorPipe = Pipe() + errorPipe.fileHandleForReading.readabilityHandler = { fh in + let data = fh.availableData + if data.isEmpty { // EOF on the pipe + errorPipe.fileHandleForReading.readabilityHandler = nil + } else { + self.processError(String(data: data, encoding: .utf8)!) + } + } + let inputPipe = Pipe() + inputPipe.fileHandleForWriting.writeabilityHandler = { fh in + if !stdIn.isEmpty { + if let data = stdIn.data(using: .utf8) { + fh.write(data) + } + } + fh.closeFile() + inputPipe.fileHandleForWriting.writeabilityHandler = nil + } + task.standardOutput = outputPipe + task.standardError = errorPipe + task.standardInput = inputPipe + } + + deinit { + // make sure the task gets terminated + cancel() + } + + func cancel() { + task.terminate() + } + + func run() { + if !task.isRunning { + do { + try task.run() + } catch { + // task didn't start + displayError("error running \(task.executableURL?.path ?? "")") + displayError(error.localizedDescription) + results.exitcode = -1 + // delegate?.processUpdated() + return + } + // delegate?.processUpdated() + } + // task.waitUntilExit() + while task.isRunning { + // loop until process exits + usleep(10000) + } + + while (task.standardOutput as? Pipe)?.fileHandleForReading.readabilityHandler != nil || + (task.standardError as? Pipe)?.fileHandleForReading.readabilityHandler != nil + { + // loop until stdout and stderr pipes close + usleep(10000) + } + + results.exitcode = Int(task.terminationStatus) + // delegate?.processUpdated() + } + + // making this a seperate method so the non-timeout calls + // don't need to worry about catching exceptions + // NOTE: the timeout here is _not_ an idle timeout; + // it's the maximum time the process can run + func run(timeout: Int = -1) throws { + var deadline: Date? + if !task.isRunning { + do { + if timeout > 0 { + deadline = Date().addingTimeInterval(TimeInterval(timeout)) + } + try task.run() + } catch { + // task didn't start + displayError("ERROR running \(task.executableURL?.path ?? "")") + displayError(error.localizedDescription) + results.exitcode = -1 + // delegate?.processUpdated() + return + } + // delegate?.processUpdated() + } + // task.waitUntilExit() + while task.isRunning { + // loop until process exits + if let deadline { + if Date() >= deadline { + displayError("ERROR: \(task.executableURL?.path ?? "") timed out after \(timeout) seconds") + task.terminate() + results.exitcode = Int.max // maybe we should define a specific code + throw ProcessError.timeout + } + } + usleep(10000) + } + + while (task.standardOutput as? Pipe)?.fileHandleForReading.readabilityHandler != nil || + (task.standardError as? Pipe)?.fileHandleForReading.readabilityHandler != nil + { + // loop until stdout and stderr pipes close + usleep(10000) + } + + results.exitcode = Int(task.terminationStatus) + // delegate?.processUpdated() + } + + func processOutput(_ str: String) { + // can be overridden by subclasses + results.output.append(str) + } + + func processError(_ str: String) { + // can be overridden by subclasses + results.error.append(str) + } +} + /// Runs a command line tool synchronously, returns CLIResults /// this implementation attempts to handle scenarios in which a large amount of stdout /// or sterr output is generated