Terminate subprocesses of macOS command line tool in Swift
Here is what we did in order to react on interrupt (CTRL-C
) when using two piped subprocesses.
Idea behind: Blocking waitUntilExit()
call replaced with async terminationHandler
. Infinite loop dispatchMain()
used to serve dispatch events. On receiving Interrupt
signal we calling interrupt()
on subprocesses.
Example class which incapsulates subprocess launch and interrupt logic:
class AppTester: Builder {
private var processes: [Process] = [] // Keeps references to launched processes.
func test(completion: @escaping (Int32) -> Void) {
let xcodebuildProcess = Process(executableName: "xcodebuild", arguments: ...)
let xcprettyProcess = Process(executableName: "xcpretty", arguments: ...)
// Organising pipe between processes. Like `xcodebuild ... | xcpretty` in shell
let pipe = Pipe()
xcodebuildProcess.standardOutput = pipe
xcprettyProcess.standardInput = pipe
// Assigning `terminationHandler` for needed subprocess.
processes.append(xcodebuildProcess)
xcodebuildProcess.terminationHandler = { process in
completion(process.terminationStatus)
}
xcodebuildProcess.launch()
xcprettyProcess.launch()
// Note. We should not use blocking `waitUntilExit()` call.
}
func interrupt() {
// Interrupting running processes (if any).
processes.filter { $0.isRunning }.forEach { $0.interrupt() }
}
}
Usage (i.e. main.swift
):
let tester = AppTester(...)
tester.test(....) {
if $0 == EXIT_SUCCESS {
// Do some other work.
} else {
exit($0)
}
}
// Making Interrupt signal listener.
let source = DispatchSource.makeSignalSource(signal: SIGINT)
source.setEventHandler {
tester.interrupt() // Will interrupt running processes (if any).
exit(SIGINT)
}
source.resume()
dispatchMain() // Starting dispatch loop. This function never returns.
Example output in shell:
...
▸ Running script 'Run Script: Verify Sources'
▸ Processing Framework-Info.plist
▸ Running script 'Run Script: Verify Sources'
▸ Linking AppTestability
^C** BUILD INTERRUPTED **
Swift Command Line Tool utilizing Process() and multithreading crashes after a certain number of execution rounds (~3148)
I have found a fix while researching this bug. It seems that, despite what the documentation claims, Pipe
will not automatically close its reading filehandle.
So if you add a try outputPipe.fileHandleForReading.close()
after reading from it, that will fix the issue.
How to get the launch arguments while making a command line tool with swift
See the linked question, there’s the answer How do I access program arguments in Swift?
import Foundation
let args = NSProcessInfo.processInfo().arguments;
println(args);
Trapping signals in a Swift command line application
Dispatch Sources
can be used to monitor UNIX signals.
Here is a simple example, a Swift 3 translation of the C code in the
"Monitoring Signals" section from the Concurrency Programming Guide.
import Dispatch // or Foundation
signal(SIGINT, SIG_IGN) // // Make sure the signal does not terminate the application.
let sigintSrc = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main)
sigintSrc.setEventHandler {
print("Got SIGINT")
// ...
exit(0)
}
sigintSrc.resume()
Note that this requires an active GCD event loop, e.g. with
dispatchMain()
in a command-line program.
Get terminal output after a command swift
NSTask
is class to run another program as a subprocess. You can
capture the program's output, error output, exit status and much more.
Expanding on my answer to xcode 6 swift system() command,
here is a simple utility function to run a command synchronously,
and return the output, error output and exit code (now updated for Swift 2):
func runCommand(cmd : String, args : String...) -> (output: [String], error: [String], exitCode: Int32) {
var output : [String] = []
var error : [String] = []
let task = NSTask()
task.launchPath = cmd
task.arguments = args
let outpipe = NSPipe()
task.standardOutput = outpipe
let errpipe = NSPipe()
task.standardError = errpipe
task.launch()
let outdata = outpipe.fileHandleForReading.readDataToEndOfFile()
if var string = String.fromCString(UnsafePointer(outdata.bytes)) {
string = string.stringByTrimmingCharactersInSet(NSCharacterSet.newlineCharacterSet())
output = string.componentsSeparatedByString("\n")
}
let errdata = errpipe.fileHandleForReading.readDataToEndOfFile()
if var string = String.fromCString(UnsafePointer(errdata.bytes)) {
string = string.stringByTrimmingCharactersInSet(NSCharacterSet.newlineCharacterSet())
error = string.componentsSeparatedByString("\n")
}
task.waitUntilExit()
let status = task.terminationStatus
return (output, error, status)
}
Sample usage:
let (output, error, status) = runCommand("/usr/bin/git", args: "status")
print("program exited with status \(status)")
if output.count > 0 {
print("program output:")
print(output)
}
if error.count > 0 {
print("error output:")
print(error)
}
Or, if you are only interested in the output, but not in
the error messages or exit code:
let output = runCommand("/usr/bin/git", args: "status").output
Output and error output are returned as an array of strings, one
string for each line.
The first argument to runCommand()
must be the full path to an
executable, such as "/usr/bin/git"
. You can start the program using a shell (which is what system()
also does):
let (output, error, status) = runCommand("/bin/sh", args: "-c", "git status")
The advantage is that the "git" executable is automatically found
via the default search path. The disadvantage is that you have to
quote/escape arguments correctly if they contain spaces or other
characters which have a special meaning in the shell.
Update for Swift 3:
func runCommand(cmd : String, args : String...) -> (output: [String], error: [String], exitCode: Int32) {
var output : [String] = []
var error : [String] = []
let task = Process()
task.launchPath = cmd
task.arguments = args
let outpipe = Pipe()
task.standardOutput = outpipe
let errpipe = Pipe()
task.standardError = errpipe
task.launch()
let outdata = outpipe.fileHandleForReading.readDataToEndOfFile()
if var string = String(data: outdata, encoding: .utf8) {
string = string.trimmingCharacters(in: .newlines)
output = string.components(separatedBy: "\n")
}
let errdata = errpipe.fileHandleForReading.readDataToEndOfFile()
if var string = String(data: errdata, encoding: .utf8) {
string = string.trimmingCharacters(in: .newlines)
error = string.components(separatedBy: "\n")
}
task.waitUntilExit()
let status = task.terminationStatus
return (output, error, status)
}
Related Topics
Can/How to Replace My Kvo Stuff with Rc3
Check If Variable Is a Block/Function/Callable in Swift
Variable P Passed by Reference Before Being Initialized
Swift3:How to Handle Precedencegroup Now Operator Should Be Declare with a Body
Making Cocoa Application Scriptable Swift
Swift: How to Open File with Associated Application
How to Handle Touch Gestures in Swiftui in Swift Uikit Map Component
Cannot Form Weak Reference to Instance of Class Nstextview
Format String with Trailing Zeros Removed for X Decimal Places in Swift
Segue Not Getting Selected Row Number
Sliding One Swiftui View Out from Underneath Another
Adding Nscoding as an Extension
Swift: How to Animate the Rowheight of a Uitableview
Get Path to Swift Script from Within Script
Using Uiactivityindicatorview with Uiwebview in Swift
Getting Timed Metadata in Swift iOS 8 from M3U8 Streaming Video