The opinions expressed on this blog are purely mine

Signal capture and graceful shutdown in Swift

2022-10-06

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: "com.company.signalQueue")  // 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() {
          stopSubProcess()
          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)
      signal.resume()
      return signal
  }
}
                    
                
1
Setting up a "custom" DispatchQueue, so the app doesn't listen for signals on the main queue
2
When SIGINT was captured, the handler calls NSApp.terminate(self) to initiate the termination process.
3
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.
4
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
          }
      }
  }
}

...

                  
                
5
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!