Trapping Signals in a Swift Command Line Application

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.

Trap SIGINT in Cocoa macos application

Charles's answer is correct, but his caveat ("Be sure only to call reentrant functions from within the handler") is an extreme limitation. It's possible to redirect handling of a signal to a safer environment using kqueue and CFFileDescriptor.

Technical Note TN2050: Observing Process Lifetimes Without Polling is on a different subject but illustrates the technique. There, Apple describes Charles's caveat this way:

Listening for a signal can be tricky because of the wacky execution
environment associated with signal handlers. Specifically, if you
install a signal handler (using signal or sigaction), you must be very
careful about what you do in that handler. Very few functions are safe
to call from a signal handler. For example, it is not safe to allocate
memory using malloc!

The functions that are safe from a signal handler (the async-signal
safe
functions) are listed on the sigaction man page.

In most cases you must take extra steps to redirect incoming signals
to a more sensible environment.

I've taken the code illustration from there and modified it for handling SIGINT. Sorry, it's Objective-C. Here's the one-time setup code:

// Ignore SIGINT so it doesn't terminate the process.

signal(SIGINT, SIG_IGN);

// Create the kqueue and set it up to watch for SIGINT. Use the
// EV_RECEIPT flag to ensure that we get what we expect.

int kq = kqueue();

struct kevent changes;
EV_SET(&changes, SIGINT, EVFILT_SIGNAL, EV_ADD | EV_RECEIPT, NOTE_EXIT, 0, NULL);
(void) kevent(kq, &changes, 1, &changes, 1, NULL);

// Wrap the kqueue in a CFFileDescriptor. Then create a run-loop source
// from the CFFileDescriptor and add that to the runloop.

CFFileDescriptorContext context = { 0, self, NULL, NULL, NULL };
CFFileDescriptorRef kqRef = CFFileDescriptorCreate(NULL, kq, true, sigint_handler, &context);
CFRunLoopSourceRef rls = CFFileDescriptorCreateRunLoopSource(NULL, kqRef, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode);
CFRelease(rls);

CFFileDescriptorEnableCallBacks(kqRef, kCFFileDescriptorReadCallBack);
CFRelease(kqRef);

And here's how you would implement the sigint_handler callback referenced above:

static void sigint_handler(CFFileDescriptorRef f,  CFOptionFlags callBackTypes, void *info)
{
struct kevent event;

(void) kevent(CFFileDescriptorGetNativeDescriptor(f), NULL, 0, &event, 1, NULL);
CFFileDescriptorEnableCallBacks(f, kCFFileDescriptorReadCallBack);

// You've been notified!
}

Note that this technique requires that you run the setup code on a thread which will live as long as you're interested in handling SIGINT (perhaps the app's lifetime) and service/run its run loop. Threads created by the system for its own purposes, such as those which service Grand Central Dispatch queues, are not suitable for this purpose.

The main thread of the app will work and you can use it. However, if the main thread locks up or become unresponsive, then it's not servicing its run loop and the SIGINT handler won't be called. Since SIGINT is often used to interrupt exactly such a stuck process, the main thread may not be suitable.

So, you may want to spawn a thread of your own just to monitor this signal. It should do nothing else, because anything else might cause it to get stuck, too. Even there, though, there are issues. Your handler function will be called on this background thread of yours and the main thread may still be locked up. There's a lot of stuff that is main-thread-only in the system libraries and you won't be able to do any of that. But you'll have much greater flexibility than in a POSIX-style signal handler.

I should add that GCD's dispatch sources can also monitor UNIX signals and are easier to work with, especially from Swift. However, they won't have a dedicated thread pre-created to run the handler. The handler will be submitted to a queue. Now, you may designate a high-priority/high-QOS queue, but I'm not entirely certain that the handler will get run if the process has numerous runaway threads already running. That is, a task that's actually running on a high-priority queue will take precedence over lower-priority threads or queues, but starting a new task might not. I'm not sure.

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.

Swift: command line tool exit callback

You can install a signal handler with Swift. For example:

import Foundation

let startTime = Date()
var signalReceived: sig_atomic_t = 0

signal(SIGINT) { signal in signalReceived = 1 }

var i = 0
while true {
if signalReceived == 1 { break }
usleep(500_000)
if signalReceived == 1 { break }
i += 1
print(i)
}

let endTime = Date()
print("Program has run for \(endTime.timeIntervalSince(startTime)) seconds")

Modified from this gist.

Swift GCD: Why signal handler doesn't work in function

DispatchSource is a class, i.e. a reference type. In your second example the dispatch source is stored in a local variable of the function. As soon as the function returns, no reference exists to the dispatch source, so that it is canceled and deallocated.

You need to store the dispatch source somewhere where it lives as long as the program runs, e.g. in a global variable:

public func registerSigint() -> DispatchSourceSignal {
signal(SIGINT, SIG_IGN)
let sigintSrc = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main)
sigintSrc.setEventHandler {
print("Got SIGINT")
exit(0)
}
sigintSrc.resume()
return sigintSrc
}

let source = registerSigint()
dispatchMain()

Determine viewport size (in characters) from command line app in Swift?

The C code from Getting terminal width in C? is easily translated to Swift:

import Darwin

var w = winsize()
if ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0 {
print("rows:", w.ws_row, "cols", w.ws_col)
}

(For some reason, this does not work in the Xcode debugger console,
you have to call the executable in the Terminal window.)

Alternatively, using the ncurses library (from Getting terminal width in C?):

import Darwin.ncurses

initscr()
let s = "rows: \(LINES), cols: \(COLS)"
mvaddstr(1, 1, s);
refresh();
getch()
endwin()

To keep track of window resize events you have to handle the
SIGWINCH signal, compare Trapping signals in a Swift command line application:

import Darwin
import Dispatch

var w = winsize()
if ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0 {
print("rows:", w.ws_row, "cols", w.ws_col)
}

let sigwinchSrc = DispatchSource.makeSignalSource(signal: SIGWINCH, queue: .main)
sigwinchSrc.setEventHandler {
if ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0 {
print("rows:", w.ws_row, "cols", w.ws_col)
}
}
sigwinchSrc.resume()

dispatchMain()


Related Topics



Leave a reply



Submit