The opinions expressed on this blog are purely mine

Signal capture and graceful shutdown in Swift


I am knee-deep in macOS systems programming lately, so I solved an interesting problem the other day: gracefully terminating a proper MacOS application on SIGINT signal.

Based on this website, SIGINT is: sent to a process by its controlling terminal when a user wishes to interrupt the process. This is typically initiated by pressing Ctrl-C, but on some systems, the "delete" character or "break" key can be used.

SIGTERM would have been a more natural choice, but it was already taken for something else. Anyways, they are almost identical in meaning.

What I did, is the following:

// appDelegate.swift

class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
  private var signal: DispatchSourceSignal?

  let queue = DispatchQueue(label: "")  // 1

  func applicationDidFinishLaunching(_ notification: Notification) {
      // Catch SIGINT signal and stop subprocess gracefully
      self.signal = handleSigint {
          NSApp.terminate(self) // 2

      // Other stuff in applicationDidFinishLaunching...


  // Gracefully stop subprocess before application termination
  func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
      if subprocessIsRunning() {
          return .terminateLater // 3
      return .terminateNow // 4

extension AppDelegate {
  // Catch and handle SIGINT
  func handleSigint(handler: @escaping DispatchSourceProtocol.DispatchSourceHandler) -> DispatchSourceSignal {
      Darwin.signal(SIGINT, SIG_IGN)
      let signal = DispatchSource.makeSignalSource(signal: SIGINT, queue: self.queue)

      signal.setEventHandler(handler: handler)
      return signal
Setting up a "custom" DispatchQueue, so the app doesn't listen for signals on the main queue
When SIGINT was captured, the handler calls NSApp.terminate(self) to initiate the termination process.
The previous terminate(_:) call has invoked applicationShouldTerminate(_:), where the application can control whether it is ready to terminate or not. In my example, the code checks if a certain subprocess is running: If it does, the app tries to stop the subprocess and returns a .terminateLater reply. (signaling that the app is not ready yet to exit). Now it is stopSubProcess()'s responsibility to signal whether it is safe to exit the application.
If the subprocess is not running, applicationShouldTerminate(_:) returns a .terminateNow reply. (signaling that the application can safely stop)
// subprocess.swift


func stopSubProcess() {
  DispatchQueue.main.async {
      self.stop { result in
          defer {
              NSApp.reply(toApplicationShouldTerminate: true) // 5
          if let error = result {
              // handle error
          } else {
             // success


When the completion handle finishes, it invokes NSApp.reply(toApplicationShouldTerminate: true) to signal the termination process that it can safely exit the application.
The examples were built on Ventura beta 10 | xcode 14.1 Beta 3 | Swift 5.7

That's all folks, see you later!