Detecting Launch At Login Revisited

In the past, I presented two approaches for a sandboxed OS X application to check whether it was launched at login via a helper app or not. Both of them don’t work anymore on OS X 10.8 and newer, but I will present them nevertheless for better understanding, before turning to a new approach that works well on OS X 10.8 and 10.9.

If you need instructions on the basic setup of how to launch a sandboxed app meant for Mac App Store distribution at login, please read my blog post about the Launch At Login Sandbox Project. This post focuses on the aspect of detecting if an application has been launched at login via a helper app that is contained in the same application bundle as the main app.

Counting Arguments (not working anymore)

My first approach was to include the following code snippet in the main app:

BOOL autolaunched = ([[[NSProcessInfo processInfo] arguments] count]);

This worked under older versions of OS X, as the number of arguments returned by [NSProcessInfo processInfo] was different when an application was launched at login, but this wasn’t documented and also doesn’t work anymore.

Passing On Arguments (not working anymore)

In a second approach, I thought of passing on a specific launch argument to the main application when it is launched by the helper app. This looked like:

NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:[NSArray arrayWithObject:@"launchAtLogin"], NSWorkspaceLaunchConfigurationArguments, nil]; // launch argument
[[NSWorkspace sharedWorkspace] launchApplicationAtURL:[NSURL fileURLWithPath:newPath]
                                              options:NSWorkspaceLaunchWithoutActivation
                                        configuration:dict
                                                error:nil];

While this approached also worked for some time, so that the main application could resolve the launch argument passed to it by the helper app, it silently fails on OS X 10.9. I haven’t found the reason but think that this is a sandbox related issue where the helper app is not allowed to pass on launch arguments to the main application.

A New Detection Approach

So, as I needed another new approach, I came up with an quite as simple idea (thanks to @radexp for pointing me in the right direction), which was easy enough to implement and works fine. The core of it is to not terminate the helper app at login after it has launched the main application, so that the main application can check if the helper app is still running. If it is, the main app has been launched at login.

In the helper app, delete the termination code you will likely have in it and which should look like this:

In the main application’s -applicationDidFinishLaunching: method, add a few lines of code to check if the helper app is running:

#define helperAppBundleIdentifier @"com.mybundle.helperIdentifier" // change as appropriate to help app bundle identifier
BOOL startedAtLogin = NO;
NSArray *apps = [[NSWorkspace sharedWorkspace] runningApplications];
for (NSRunningApplication *app in apps) {
    if ([app.bundleIdentifier isEqualToString:helperAppBundleIdentifier]) startedAtLogin = YES;
}

Now, if the main application knows it has been launched at login, and the helper app is still running, we may want to terminate the helper app, as it is not needed anymore. The main application cannot, as it is sandboxed, directly terminate the helper app, but it can send the helper app a command to terminate, via the NSDistributedNotificationCenter class:

#define terminateNotification @"TERMINATEHELPER" // can be basically any string
if (startedAtLogin) {
    [[NSDistributedNotificationCenter defaultCenter] postNotificationName:terminateNotification
                                                                   object:[[NSBundle mainBundle] bundleIdentifier]];
}

It doesn’t really matter which strings we pass on in this distributed notification as notification name and notification object, but they should be unique so that the helper app is not terminated by some other random distributed notification it may receive.

In the helper app, we have to add some code to listen to this termination notification:

#define terminateNotification @"TERMINATEHELPER" // must be same string as in main application
#define mainAppBundleIdentifier @"com.mybundle.identifier" // change as appropriate to main application bundle identifier
[[NSDistributedNotificationCenter defaultCenter] addObserver:self
                                                    selector:@selector(killApp)
                                                        name:terminateNotification
                                                      object:mainAppBundleIdentifier];

This observer should be added before the main application is launched, naturally, and the termination method -killApp could simply look like this:

-(void)killApp
{
    [NSApp terminate:nil];
}

In short, this is it. However, we have to take care of one more situation, as there is one situation where the helper app will be launched not during login, but in another situation, that is when the main application schedules itself for launch at login via the SMLoginItemSetEnabled function, which will launch the helper app. In such a situation, the helper app doesn’t need to launch the main application, of course, and doesn’t also need to register an observer, but instead can terminate immediately after finishing launching.