Gracefully running Node as an NSTask in Cocoa

12th April, 2015 — Aral Balkan

Screenshot of Activity Viewer on OS X showing the sample app and its Node.js child process.
Graceful termination: when the parent process dies, the children should follow suit.

Update: 15th April, 2015

OK, this is weird: on further testing, it looks like I was confused about what was actually happening. It seems that (at least with the latest Xcode 6.3), the NSTask is killed properly when the Cocoa app terminates (even when stopped from within Xcode). However, the Node app itself spawns a child process for Pulse (the Go-based synchronisation engine), and it was that that wasn’t being cleaned up when the Cocoa app crashed or was stopped from within Xcode.

It seems, however, that monitoring the parent process and running cleanup (including killing Pulse, by sending it a SIGTERM) is the only reliable way of killing that child process. So, while the below should not be necessary for NSTasks, it is still useful for running cleanup of child tasks launched from your child taks.

Finally, I also added handlers in Node to call the clean up method for the various termination signals:

CoffeeScript: process.on 'exit', (code) =>  
  @cleanUp()

process.on 'uncaughtException', (code) =>
  cleanUp()

process.on 'SIGINT', (code) =>
  @cleanUp()

process.on 'SIGTERM', (code) =>
  @cleanUp()

Graceful termination is hard to do

You’d think that tasks created by your Cocoa app would automatically be terminated by the system when your app crashes or is killed from Xcode.

You’d be wrong. (See update.)

This tripped me up and apparently I’m not the only one (see this, this, and this, etc.)

Instead of gracefully terminating, if an NSTask is running when your app crashes (or is stopped from Xcode), the task will dangle. This zombie task is a memory and CPU leak and might cause your app to misbehave or crash the next time it is run.

What doesn’t work

Before showing you what does work, here (in hopes of saving you time) are a couple of things I tried that don’t work:

Basically, there doesn’t seem to be an easy way to manage child process cleanup from the Cocoa end of things.

What does work

Download source (GracefulNodeTask.zip, ~12MB)

The approach I finally took was based on the one used in the unfortunately-named Infanticide example by Alex Gray:

If we can’t monitor the child process and kill it, the child process can monitor the parent and kill itself when the parent dies. (This whole business is starting to sound rather more macarbre than I intended it to.)

I created a simple standalone example app that shows you how to run a Node.js app as an NSTask from within a sandboxed Cocoa app in a manner that gracefully handles the crashing or killing of the Cocoa app.

Let’s take a look at the relevant bits of the code to understand how it works.

Cocoa

First off, in the applicationDidFinishLaunching handler, we set up the paths we need for configuring the task:

Swift let mainBundle = NSBundle.mainBundle()
let pathToNodeApp = mainBundle.pathForResource("boot", ofType: "js", inDirectory: "js")!
let pathToNode = mainBundle.pathForResource("node", ofType: "")!
let pathToAppFolder = pathToNodeApp.stringByDeletingLastPathComponent

Then, we make a note of our own process ID:

Swift: let processIdentifier = NSProcessInfo.processInfo().processIdentifier

And, finally, we pass the process ID to the Node.js app as a commandline argument while creating the NSTask so that it knows which process to monitor:

Swift: self.nodeTask = NSTask()
self.nodeTask!.currentDirectoryPath = pathToAppFolder
self.nodeTask!.launchPath = pathToNode
self.nodeTask!.arguments = [pathToNodeApp, "\(processIdentifier)"]
self.nodeTask!.standardOutput.fileHandleForReading.readInBackgroundAndNotify()
self.nodeTask!.launch()

Those are the relevant bits on the Cocoa side of things.

(The sample project does a little bit more, like piping standard output from Node to the Cocoa app, etc., so do download the source and have a peek.)

Node

On the Node side of things, everything is inside a folder called js.

When dragging Node.js source folders like this into your Cocoa projects, make sure that the Copy items if needed and Create folder references options are checked to ensure that the folder hierarchy of your Node app is maintained.

Note also that I’ve dragged the Node.js binary into the project and that’s where we’re running it from.

As using CoffeeScript for this example, I’ve added it to our package.json.

We’re also using a module called is-running that checks whether a process is running in a manner that is compatible with Cocoa app sandboxing.

package.json {
  …
  "dependencies": {
  "coffee-script": "1.9.1",
  "is-running": "1.0.5"
  }
}
The boot.js script is the entrypoint of our app. It registers CoffeeScript and includes index.coffee:

boot.js: require('coffee-script/register');
require('./index.coffee');

In index.coffee, we first store the process ID passed from Cocoa:

CoffeeScript: parentProcessID = parseInt(process.argv[2])

Then., we start polling every second to see if the parent process is still alive. When the parent process dies, we gracefully terminate the Node process also:

CoffeeScript: parentProcessHealthCheckerIntervalID = setInterval ->
  isProcessRunning parentProcessID, (err, parentIsRunning) ->
    if err
      throw err
    if !parentIsRunning
      console.log 'Parent process died. Exiting gracefully…'
      @cleanUp()
      exit 0
, 1000

And that’s it.

Within a second of the parent process crashing or being otherwise killed, the Node process will gracefully shut itself down.

It took me a while to find this solution so I hope that it saves you some time in case you have the same problem.