blog@timschroeder.net

The Launch At Login Sandbox Project

Sometimes it is useful for an app to launch at login. While a user can always accomplish this in the system preferences, having the possibility to turn on auto-launching inside the app is better. While in the glorious past this could be implemented in a number of ways, now in the age of the sandbox it is quite tricky to achieve. This tutorial will show how. It is a more comprehensive sequel to Delite Studio’s tutorial from October 2011 and /dev/random’s tutorial from January 2012.

Starting with official Apple documentation, the App Sandbox Design Guide tells us:

To create a login item for your sandboxed app, use the SMLoginItemSetEnabled function (declared in ServiceManagement/SMLoginItem.h) as described in “Adding Login Items Using the Service Management Framework” in Daemons and Services Programming Guide.

(With App Sandbox, you cannot create a login item using functions in the LSSharedFileList.h header file. For example, you cannot use the function LSSharedFileListInsertItemURL. Nor can you manipulate the state of launch services, such as by using the function LSRegisterURL.)

This is at least something. Turning to the Daemons and Services Programming Guide, it give us some more hints:

Applications can contain a helper application as a full application bundle, stored inside the main application bundle in the Contents/Library/LoginItems directory. Set either the LSUIElement or LSBackgroundOnly key in the Info.plist file of the helper application’s bundle.

Use the SMLoginItemSetEnabled function (available in Mac OS X v10.6.6 and later) to enable a helper application. It takes two arguments, a CFStringRef containing the bundle identifier of the helper application, and a Boolean specifying the desired state. Pass true to start the helper application immediately and indicate that it should be started every time the user logs in. Pass false to terminate the helper application and indicate that it should no longer be launched when the user logs in. This function returns true if the requested change has taken effect; otherwise, it returns false. This function can be used to manage any number of helper applications.

If multiple applications (for example, several applications from the same company) contain a helper application with the same bundle identifier, only the one with the greatest bundle version number is launched. Any of the applications that contain a copy of the helper application can enable and disable it.

So as we need to have a helper app in order to launch the main app at login, all we have to do is to create a helper app, a main app and to add some specific code to both of them. Doesn’t sound too difficult. The following instructions have been tested with Xcode 4.3 on OS X 10.7, use ARC and are Mac App Store approved. You can download the tutorial source code, if you want.

Prepare the Workspace

First, create a new empty main app (the name of the example app used in this tutorial is LaunchAtLoginApp) and close its window. Then do the same with the helper app (its name being LaunchAtLoginHelperApp here). To make things easier to maintain, we’ll then create a Xcode workspace and add the two apps to it. Make sure that you add both apps’ Xcode project files (.xcodeproj extension) to the workspace and not the project folders. Then, drag the helper app’s icon beneath the main app’s icon in the Xcode navigator area, as this will lead to your helper app being added to the main app project file. Your workspace’s navigator area should then look like this:

Prepare the navigator area

Tamper with the Main App

Our main app will need a number of tweaks to work. First, we’ll have to add a new build phase so that the helper app will be part of the main app’s bundle. In the main app’s target (not project) build settings tab, add a new copy files build phase, set the destination to ‘Wrapper’, the Subpath to ‘Contents/Library/LoginItems’ and drag&drop the helper app’s executable file into the list beneath it.

Add the helper app

It should then look like this:

New build phase

Next, in the main app’s target (not project) summary tab, we enable the sandbox and code-signing and add the ServiceManagement.framework to our frameworks. This should look like this:

Edit build settings

Then we have to modify the main app’s target (not project) build settings by changing the ‘Strip Debug Symbols During Copy’ setting, as follows:

Edit more build settings

Now it’s time for some code. In the main app’s AppDelegate.h file, add these lines to the class declaration:

-(IBAction)toggleLaunchAtLogin:(id)sender;
@property (assign) IBOutlet NSSegmentedControl *launchAtLoginButton;

In the main app’s AppDelegate.m file, add this line of code to the import section:

#import 

And add these lines to the implementation section:

@synthesize launchAtLoginButton;
-(IBAction)toggleLaunchAtLogin:(id)sender
{
    int clickedSegment = [sender selectedSegment];
    if (clickedSegment == 0) { // ON
        // Turn on launch at login
        if (!SMLoginItemSetEnabled ((__bridge CFStringRef)@"com.timschroeder.LaunchAtLoginHelperApp", YES)) {
            NSAlert *alert = [NSAlert alertWithMessageText:@"An error ocurred" 
                                            defaultButton:@"OK" 
                                          alternateButton:nil 
                                              otherButton:nil 
                                informativeTextWithFormat:@"Couldn't add Helper App to launch at login item list."];
            [alert runModal];
        }
    }
    if (clickedSegment == 1) { // OFF
        // Turn off launch at login
        if (!SMLoginItemSetEnabled ((__bridge CFStringRef)@"com.timschroeder.LaunchAtLoginHelperApp", NO)) {
           NSAlert *alert = [NSAlert alertWithMessageText:@"An error ocurred" 
                                             defaultButton:@"OK" 
                                           alternateButton:nil 
                                               otherButton:nil 
                                 informativeTextWithFormat:@"Couldn't remove Helper App from launch at login item list."];
           [alert runModal];
        }
    }
}

The -toggleLaunchAtLogin method will turn on and off auto-launching and is quite straightforward. Obviously, we should really save the state of auto-launch in our user defaults, but I’ve skipped that here. Please note that you’ve to give your helper app’s bundle identifier string to the SMLoginItemSetEnabled function.

In your main app’s MainMenu.xib file, add a NSSegmentedControl to the Window, name it’s segments like shown below, connect the AppDelegate’s outlet to it and its action method to AppDelegate’s -toggleLaunchAtLogin method. Your main app’s window could now look like this:

Launch At Login App

Tamper with the Helper App

Let’s now turn to our helper app. It has only one purpose: Launching the main app. It doesn’t need an interface, so in its MainMenu.xib just delete its main window. In its Info.plist file, add a new line to make the helper app background only:

Helper App info.plist

In the helper app’s target (not project) summary tab enable the sandbox and code-signing, just like we did with the main app. In the helper app’s target (not project) build settings tab set the ‘Skip Install’ option to YES, as otherwise we would end up with too many bundles in our final app archive and get a validation error:

Helper App build settings

Finally, it’s again time for some code. In the helper app’s AppDelegate.m file, add these lines:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    // Check if main app is already running; if yes, do nothing and terminate helper app
    BOOL alreadyRunning = NO;
    NSArray *running = [[NSWorkspace sharedWorkspace] runningApplications];
    for (NSRunningApplication *app in running) {
        if ([[app bundleIdentifier] isEqualToString:@"com.timschroeder.LaunchAtLoginApp"]) {
            alreadyRunning = YES;
        }
    }

    if (!alreadyRunning) {
        NSString *path = [[NSBundle mainBundle] bundlePath];
        NSArray *p = [path pathComponents];
        NSMutableArray *pathComponents = [NSMutableArray arrayWithArray:p];
        [pathComponents removeLastObject];
        [pathComponents removeLastObject];
        [pathComponents removeLastObject];
        [pathComponents addObject:@"MacOS"];
        [pathComponents addObject:@"LaunchAtLoginApp"];
        NSString *newPath = [NSString pathWithComponents:pathComponents];
        [[NSWorkspace sharedWorkspace] launchApplication:newPath];
    }
    [NSApp terminate:nil];
}

This should be more or less self-explanatory. We need to check if the main app is already running because the helper app will be launched everytime we set the auto-launch to ON via the SMLoginItemSetEnabled function in the main app. The way I construct the file path of the main application is not especially nice, but it works. Homer Wang has suggested an improvement to this code snippet which you can find on Stack Overflow.

Wrap Up

Please make sure that you’ve entered the correct developer certificate information for both the helper app and the main app. This should typically look like this:

Code Signing

Now build your main app (that will build the helper app, too) and right click on the product to show the build project in Finder. Go to Finder, copy your main app’s executable file and paste it to your /Applications folder. Start the main app, turn on launch at login and log out. Then, a deep breath later, log in again, and your main app will be launched in a most mysterious way. In your main app’s code, you can even check if the main app was launched at login. (Please see below for update).

Though this will work, there are some caveats:

  • Please be advised that this approach will only work if your app is located either in /Applications or ~/Applications, so for testing you’ll have to paste or move your build to this location.
  • If you want to distribute your app via the Mac App Store, don’t enable the launch at login option by default.
  • Some people say that launch at login won’t work if Spotlight is disabled on your system. I haven’t checked that, but it should really work independently of Spotlight.

Addendum (July 30, 2012): Check for Launch at Login in Main App

If you want to check in your main app if it has been launched by the helper app at login, please read the Detecting Launch At Login Revisited blog post.

Another Addendum (November 16, 2012): Fixing codesigning issues on OS X 10.8.2

You may have noticed that beginning with OS X 10.8.2 the console will show some strange log statements when the user toggles the launch at login setting. The log statements look like this:

lsboxd[4886]: Not allowing process 5010 to launch “/Applications/LaunchAtLoginApp.app/Contents/Library/LoginItems/LaunchAtLoginApp Helper.app” because the security assessment verdict was denied.

lsboxd[4886]: Not allowing process 5010 to register app “/Applications/LaunchAtLoginApp.app/Contents/Library/LoginItems/LaunchAtLoginApp Helper.app” for launch.

After some research, I noticed that everything will still work in spite of these statements. However, I managed to get rid of them by modifying the codesigning process of the main app: The log statements will not appear if the main app bundle is signed with the developer’s Developer ID certificate.

The reason for this dependence on a specific certificate is, as far as I know, not documented, but it seems that in addition to the requirement that an app has to be located in the /Applications or ~/Applications folder in order to be able to schedule launch at login (see above), in OS X 10.8.2 it also has to be signed with an Apple distribution certificate. A distribution certificate is issued to you by Apple when you are a paying member of the Mac Developer Program, and typically, you’ll have four of them:

  • two (application + installer) distribution certificates to sign apps for Mac App Store distribution, also known as 3rd Party Mac Developer certificates,
  • two (application + installer) distribution certificates to sign apps for distribution outside of the Mac App Store, also known as Developer ID certificates.

In addition, you’ll typically have one development certificate to sign apps for internal development and testing purposes, especially if developing an app that uses push notifications or iCloud. If you run an app from within Xcode, it will be signed with this last type of certificate if you use the sample code I’ve provided. Trying to change the launch at login setting of the app when the app is signed with this type of certificate will lead to the log statements shown above. The log statements will also be shown if you archive the app, export it as an (not specifically signed) application within from the Xcode Organizer and then run it, as the app is then, at least if you use my sample code, signed with the 3rd Party Mac Developer certificate meant for Mac App Store distribution (but this will work fine if you then actually submit the app to the Mac App Store and download it from the Mac App Store).

The right way for testing your app locally is to have your main app signed with this type of certificate:

Certificate

This is the application distribution certificate meant for distribution outside the Mac App Store. To sign your app with this certificate, archive your app and select this option in the Xcode Organizer:

Export

The resulting app bundle will be signed with the right certificate and if you run it from the right location (see above), there won’t be that log statements.