Sunday, March 22, 2015

Implementing "run on login" for your (node-webkit) app in OS X

I spent a good chunk of today trying to figure out how to implement a "run on login" option for my pet project, sleep.  It's a little node-webkit application with a toolbar/statusbar tray icon UI which attempts to answer the age-old question of "ugh, when did I fall asleep last night?" by telling you when your MacBook went to sleep, presumably because you closed the lid, or because it was idle for a while.  As a person who often finds myself waking up with my face planted in my laptop or with my laptop on my chest, lid closed, this is handy for determining whether or not I got enough sleep the night before.

A quick Google search reveals some ways to achieve this with the Cocoa framework.  One of the answers to a StackOverflow thread (incidentally authored by the then-lead-developer of Growl) talks about using the LSSharedFileList API.

Not wishing to wrap a Cocoa API, I found an easier solution: using launchd / launchctl.  It turns out, you can easily create a launchd LaunchAgent which will run your app when the user logs in.  I was pleasantly surprised that launch jobs can be created by a user, rather than requiring root.  I haven't run into any permissions issues yet.

our gameplan

On a high level, here's our approach:

  1. Write a simple launchd job, which comes in the form of a .plist (XML) file. 
  2. Then, run some command line arguments to move it to ~/Library/LaunchAgents/, and use launchctl to "load" the job.  
  3. (optionally) We can easily disable the job by running `launchctl unload` at a later point.  
  4. (optionally) We can also check to see if our job is currently active by doing `launchctl list` and grep-ing our job name (technically, we see if it's "loaded", which doesn't necessarily mean it's not disabled, but for our purposes).

let's write some XML (it won't hurt, I promise)

It's always lovely to find a website dedicated to explaining and documenting things for developers like, a site dedicated simply to presenting a table of Python's strftime placeholders. I was thrilled to come across this super helpful guide to launchd.  Skimming through quickly, we learn what launchd is, what a daemon and agent are, and that all we need is a file as simple as this:

defining a launchd job

First, we need to give our job a label.  According to the guide, the convention is to use reverse domain notation, so I'm using "com.capablemonkey.sleepApp".   I've chosen my handle as the 'vendor' name which typically follows the domain ('com' in this case).  sleepApp is the name of my application.

Next, we'll describe the program to be run.  In my case, I want to run a .app package, so I'll be using the nifty `open` OS X command which knows how to execute .app packages.  To set this in our config file, we'll specify a new field called ProgramArguments which is an array of strings: the command/program, followed by any arguments.  The last argument is the location of the .app package: /Applications/

Lastly, we'll include the RunAtLoad flag which will cause the job to be "run" when it's "loaded" (a job can be loaded, but not run immediately).

putting things in motion

'enabling' the job

Now that we've described the job, we need to place the file in ~/Library/LaunchAgents in order for the job to be run when the user logs in:

cp com.capablemonkey.sleepApp.plist ~/Library/LaunchAgents/

Then, we'll ask launchctl to load the job:

launchctl load ~/Library/LaunchAgents/com.capablemonkey.sleepApp.plist

In my node-webkit app, I can accomplish this by running those commands with `child_process.exec`:

disabling the job

Should the user decide to disable running on login, our application can do:

launchctl unload ~/Library/LaunchAgents/com.capablemonkey.sleepApp.plist

checking to see if job is enabled

Our application can check to see if running on login is enabled:

launchctl list | grep com.capablemonkey.sleepApp

grep will return an error code of 1 and stdout will be empty if the job is not loaded.  Otherwise, we'll see our job and some information in stdout.

that's all, folks!

Pretty straightforward stuff.  Not sure if this is the best way to accomplish this, but it works well.  I didn't find any good resources on programmatically implementing "run on login", short of asking the user to add the app to their Login Items list or writing an AppleScript that does that.  Hope this comes in handy for someone!