PDA

View Full Version : Creating Login Item




dwstudio
May 20, 2012, 08:33 PM
Hi all,
I've created a "dockless" app and I need to add a checkmark in my preference pane that allows the user to configure the ability to start the app on login.

How would I do this with sandboxing enabled?



displaced
May 21, 2012, 06:02 AM
Hi all,
I've created a "dockless" app and I need to add a checkmark in my preference pane that allows the user to configure the ability to start the app on login.

How would I do this with sandboxing enabled?

I'm currently working on the same thing and the answer seems to be 'Painfully'.

Here's the full guide I followed: http://www.delitestudio.com/2011/10/25/start-dockless-apps-at-login-with-app-sandbox-enabled/

It's more work than you'd expect!

Chris

ArtOfWarfare
May 21, 2012, 11:54 AM
Nuts!

And here I was going to use this as an excuse for not getting in the sandbox.

*Sigh* - guess I need to get this all working in the sandbox and then also make sure nothing else breaks when I make my app sandbox enabled.

displaced
May 21, 2012, 12:34 PM
Isn't it just!

Requiring that a specific API is used is one thing... But needing a helper app at a specific location within the main bundle seems like a wind-up.

I developed my app with sandboxing on from the outset. This lead to many calls failing silently or acting odd without any guidance as to why. Only googling the problem implicated an interaction with the sandbox being the cause.

Worth it in the end, I suppose!

ArtOfWarfare
May 21, 2012, 11:05 PM
I found this source, it looks pretty good:
http://blog.mcohen.me/2012/01/12/login-items-in-the-sandbox/

Then there's also this:
http://blog.mcohen.me/2012/05/20/sandboxing-revisited/

But one thing I can't figure out is what should my helper app's code look like?

Right now I have this:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[[NSWorkspace sharedWorkspace] launchApplication:@"Battery Status"];
}

I've placed this in the proper directory, but when I attempt to launch it I get this message (accessed in the Console app,)
5/21/12 11:29:36.833 PM Battery Status Helper: LSOpenFromURLSpec() returned -10827 for application Battery Status path (null).

I've also attempted this:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[[NSWorkspace sharedWorkspace] launchApplication:@"com.<My Name>.BatteryStatus"];
}

This didn't result in anything being printed to the console, but was equally unsuccessful.

If I simply don't sandbox the helper, the first code snippet works... but that defeats the entire purpose of sandboxing an app. Could someone please explain exactly what the helper should contain for code? (And for what it's worth, the app bundle name is "Battery Status.app", with a space. The bundle ID does not have a space, or anything else to separate "Battery" from "Status".)

Edit
This looks really helpful... I think I'll just use it:
https://github.com/tcurdt/TCLoginItemHelper/tree/master/Sources

2X Edit
Question: Will this method of making login items work on computers running Snow Leopard? (Namely those running versions with the App Store?) I'm going to have to wait until next week until I have access to a computer running Snow Leopard to test on (all other features were already tested last week under Snow Leopard), so if someone else could confirm for me that it does work, that'd be great...

displaced
May 22, 2012, 02:10 PM
This looks really helpful... I think I'll just use it:
https://github.com/tcurdt/TCLoginItemHelper/tree/master/Sources


Yeah, I think that's what I based mine on (not on my dev machine at the moment to check!).


Question: Will this method of making login items work on computers running Snow Leopard? (Namely those running versions with the App Store?) I'm going to have to wait until next week until I have access to a computer running Snow Leopard to test on (all other features were already tested last week under Snow Leopard), so if someone else could confirm for me that it does work, that'd be great...

Off the top of my head, I think SMLoginItemSetEnabled is supported on Snow Leopard. For info I found a web page (that I've since lost) which described how this method works. It actually sets a Spotlight metadata flag against the helper bundle to indicate that it should be launched at login. A launchd job runs a metadata query at login to find all the items with the flag set and executes them. Seems convoluted, but I'm sure there's method to the madness. Incidentally, this raises a support issue in that some 'power users' like doing things such as disabling Spotlight and metadata support. Without the metadata daemons running, I'd imagine that login items set via SMLoginItemSetEnabled would fail to run.

ArtOfWarfare
May 22, 2012, 02:34 PM
:-/

I'm getting this error message in Console:

5/22/12 4:32:28.650 PM com.apple.AppSandboxSMLoginItemEnabler: FAILURE: Job com.TaylorMarks.BatteryStatusHelper is not loaded in launchd.

Any idea why it's happening or how to fix it?

Incidentally, this raises a support issue in that some 'power users' like doing things such as disabling Spotlight and metadata support. Without the metadata daemons running, I'd imagine that login items set via SMLoginItemSetEnabled would fail to run.

Would I be considered a "power user"? I disabled the desktop so that I couldn't be tempted to put random items on my desktop. It forces me to actually come up with a location that makes sense for the item.

ArtOfWarfare
May 29, 2012, 07:48 PM
Bump...

I'm still having the issue that each time I try to set my app as a login item, I get this message (accessed via Console.)

5/22/12 4:32:28.650 PM com.apple.AppSandboxSMLoginItemEnabler: FAILURE: Job com.TaylorMarks.BatteryStatusHelper is not loaded in launchd

A checkbox is cocoa bound to a BOOL property... when it's clicked, it calls this:

- (void)setLaunchOnLogin:(BOOL)value
{
if (value == [self launchOnLogin])
{
NSLog(@"Don't need to change anything.");
return;
}
if (!value) {
NSLog(@"Will attempt to add to login items.");
[self addLoginItem];
} else {
NSLog(@"Will attempt to remove from login items.");
[self removeLoginItem];
}
}

When the checkbox is being checked, it calls [self addLoginItem], which is given below:
- (void)addLoginItem {
NSString *ref = @"com.TaylorMarks.BatteryStatusHelper";
if (!SMLoginItemSetEnabled((CFStringRef)ref, true)) {
NSLog(@"SMLoginItemSetEnabled failed to add.");
}

else {
NSLog(@"SMLoginItemSetEnabled succeeded in adding!");
}
}

Has anyone gotten autolaunch to work in a sandboxed environment?

Is anyone willing to just share their entire project so we can see what they've done? (The github project seemed to only have the helper, not the actual app, in it...)

ArtOfWarfare
Jun 6, 2012, 05:18 PM
Alright, I've managed to make a test app that works. Here is the unabridged process:

Make a new Cocoa App (your main app)
- In your target's summary tab, check off "Enable Entitlements", "Code Sign Application", and "Enable Entitlements"
(These gray steps are just to reproduce exactly what I did)
- Delete the window and menu bar from your XIB.
- Remove references to the window in the header and implementation file.
- In your -info.plist file, add LSUIElement and set it to YES.
- Add to the top of your implementation file:
#import <ServiceManagement/ServiceManagement.h>
- In Target->Build Phases->Link Binary with Frameworks, add ServiceManagement.framework

Here is all the code for the implementation file... all the sample app is is a menu bar item with the name "SimpleStatusItem". Clicking on it presents a menu with two options: toggle the autolaunch, and quit it. It also lets you know whether it's set to autolaunch or not.
- (void)dealloc
{
[super dealloc];
}

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength] retain];
statusItem.title = @"SimpleStatusItem";
statusItem.highlightMode = YES;

NSMenu* statusMenu = [[NSMenu alloc] init];
statusMenu.delegate = self;
statusItem.menu = statusMenu;

NSMenuItem* launchItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:@"Autolaunch" action:@selector(toggle:) keyEquivalent:@""];
[statusMenu addItem:launchItem];
[launchItem release];

NSMenuItem* quitItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:@"Quit" action:@selector(terminate:) keyEquivalent:@""];
[statusMenu addItem:quitItem];
[quitItem release];

[statusMenu release];
}

- (void)menuNeedsUpdate:(NSMenu*)menu
{
NSString* bundleID = @"com.taylormarks.loginhelper";
NSArray* jobDicts = nil;
jobDicts = (NSArray*)SMCopyAllJobDictionaries(kSMDomainUserLaunchd);

[[menu itemWithTitle:@"Autolaunch"] setState:NSOffState];

if ((jobDicts != nil) && [jobDicts count] > 0)
{
for (NSDictionary* job in jobDicts)
{
if ([bundleID isEqualToString:[job objectForKey:@"Label"]])
{
if ([[job objectForKey:@"OnDemand"] boolValue])
{
[[menu itemWithTitle:@"Autolaunch"] setState:NSOnState];
}
break;
}
}

CFRelease((CFDictionaryRef)jobDicts);
jobDicts = nil;
}
}

- (void)toggle:(NSMenuItem*)item
{
NSString *ref = @"com.taylormarks.loginhelper";
if (item.state == NSOffState)
{
if (!SMLoginItemSetEnabled((CFStringRef)ref, true))
{
NSLog(@"SMLoginItemSetEnabled failed to enable.");
}
}

else
{
if (!SMLoginItemSetEnabled((CFStringRef)ref, false))
{
NSLog(@"SMLoginItemSetEnabled failed to disable.");
}
}
}

- (void)applicationWillTerminate:(NSNotification*)notification
{
statusItem.menu.delegate = nil;
[statusItem.menu removeAllItems];
statusItem.menu = nil;
[statusItem release];
}

Make a new Cocoa App (the autolaunch helper app)
(If this looks familiar, it's because it starts out the exact same process that was just done for the main app.)
- In your target's summary tab, check off "Enable Entitlements", "Code Sign Application", and "Enable Entitlements"
(You'll almost definitely want to do these with the autolaunch helper app, even if you didn't want them with the main app)
- Delete the window and menu bar from your XIB.
- Remove references to the window in the header and implementation file.
- In your -info.plist file, add LSUIElement and set it to YES.
- This is the code for the implementation of the helper:
- (void)dealloc
{
[super dealloc];
}

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSString* appPath = [[[[[[NSBundle mainBundle] bundlePath] // .app/Contents/Library/LoginItems/.app
stringByDeletingLastPathComponent] // .app/Contents/Library/LoginItems
stringByDeletingLastPathComponent] // .app/Contents/Library
stringByDeletingLastPathComponent] // .app/Contents
stringByDeletingLastPathComponent];// .app

[[NSWorkspace sharedWorkspace] launchApplication:appPath];

[NSApp terminate:nil];
}

@end


- Archive the helper app
- Distribute->Export Developer ID-signed Application (save it in your main app's project directory)

Back to the Main App
- Target -> Build Phases -> Add Build Phase -> Add Copy Files
- Destination: Wrapper, Subpath "Contents/Library/LoginItems".
- Add the app bundle
- Add Build Phase -> Add Run Script:
#!/bin/sh
PATH=${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Contents/Library/LoginItems/loginhelper.app/Contents/embedded.provisionprofile
if cat PATH
then rm PATH
fi
- Set Strip Debug Symbols During Copy in Main App's Target Build Settings to NO for both Debug and Release
- Distribute with Export Developer ID-signed Application (Save in Applications)

I verified the functionality of this test app with this process:
Testing...
- Launch App, Verify it shouldn't autolaunch, terminate app, login/out, shouldn't be running
- Launch App, set it to autolaunch, verify, terminate app, launch, verify, terminate, login/out, should be running
- Turn off autolaunch, verify, terminate app, launch, verify, login/out, shouldn't be running
- Launch app, set it to autolaunch, login/out, should be running*

* It took several minutes before this login finished and the app appeared in my menu bar. I don't know what the hold up was, but I almost gave up and set about trying to find out what was wrong before it suddenly popped up... not particularly desirable.

Edit:
Apple evidently didn't like my code signature for my helper app. I fixed it by doing this in terminal:
codesign -f -v -s "3rd Party Mac Developer Application:" loginhelper.app

Assuming you only have a single 3rd Party Mac Developer Application certificate, that should work without modification. loginhelper.app should be replaced with whatever the name of your helper app is. I was in the directory where the helper app's archive had been saved when I did it.

Maybe in the future I can make a script for the helper app that automatically does this when it's archived...