The Launchd Nightmare

My Mac application, MailSteward, has always had a feature allowing the user to schedule the app to archive their email in a database. To do this MailSteward uses an old standby in the UNIX system, called crontab, which has been around since the beginning of time, i.e., the creation of UNIX.

Now, running the beta of Mojave, MacOS 10.14, it doesn’t work anymore. So I am switching over to launchd, the Apple system for scheduling daemons and agents. What a nightmare! The crontab is simple and elegant. Launchd is incredibly complicated and has some of the worst documentation I have ever seen.

If you google something like “macOS cocoa schedule application with launchd”, you will get a list of links to very frustrated developers. I never was able to find a clear explanation of how the Hell to do it. There were a few quite confident how tos, but they used old commands that Apple has since deprecated and which don’t work anymore.

So I had to use the traditional computer science technique, trial and error. It only took a couple of days. Here is the code it takes to set up a simple schedule in launchd:

– (IBAction)schedule:(id)sender {
int rc;
NSRange aRange;
NSTask *task1;
NSTask *task2;
NSTask *task3;
NSTask *task4;
NSPipe *stdOutPipe = [ NSPipe pipe ];
NSFileHandle *stdOutReadHandle = [ stdOutPipe fileHandleForReading ];
NSMutableData *idData;
NSMutableArray *args = [ NSMutableArray array ];
NSFileManager *manager = [NSFileManager defaultManager];
NSMutableString *idString = [NSMutableString string];
NSMutableString *guiString = [NSMutableString string];
NSMutableString *dictAll = [NSMutableString string];
NSMutableString *dictWeekday = [NSMutableString string];
NSMutableString *junk = [NSMutableString string];
NSString *plistPath = [[NSBundle mainBundle] pathForResource:@”ms.schedule” ofType:@”plist”];
NSMutableString *plistFile = [NSMutableString stringWithContentsOfFile:plistPath encoding:NSUTF8StringEncoding error:NULL];
[dictAll setString:@”\nHour\n8\nMinute\n45\n\n”];
[dictWeekday setString:@”\nHour\n8\nMinute\n45\nWeekday\n1\n\n”];

[self savePrefButton:self];
[self getPrefs];

task1 = [[NSTask alloc] init];
[args removeAllObjects];
[args addObject:@”-u”];
[ task1 setCurrentDirectoryPath: @”.” ];
[ task1 setLaunchPath:@”/usr/bin/id”];
[ task1 setArguments: args ];
[ task1 setStandardOutput: stdOutPipe ];
[ task1 launch ];
[ task1 waitUntilExit ];
idData = [ [ NSMutableData alloc ] initWithData :[ stdOutReadHandle readDataToEndOfFile ] ];
[stdOutReadHandle closeFile];
idString = [[ NSMutableString alloc ] initWithData: idData encoding: [ NSString defaultCStringEncoding ]];
[task1 release];

if ( [schedAll state] ) {
aRange = [plistFile rangeOfString:@”StartCalendarInterval“];
aRange.location += 1 + aRange.length;
[plistFile insertString:dictAll atIndex:aRange.location];
[junk setString:@”Hour\n“];
[junk appendString:[NSString stringWithFormat:@”%d”,schedHour]];
rc = [plistFile replaceOccurrencesOfString:@”Hour\n8″ withString:junk options:NSBackwardsSearch range:NSMakeRange(0, [plistFile length])];
[junk setString:@”Minute\n“];
[junk appendString:[NSString stringWithFormat:@”%d”,schedMin]];
rc = [plistFile replaceOccurrencesOfString:@”Minute\n45″ withString:junk options:NSBackwardsSearch range:NSMakeRange(0, [plistFile length])];
//NSLog(@”plistFile = %@”,plistFile);
} else {

}
[junk setString:@”~/Library/LaunchAgents/mailsteward.schedule.plist”];
[junk setString:[junk stringByExpandingTildeInPath]];
if ( [manager fileExistsAtPath:junk] ) {
[manager removeItemAtPath:junk error:nil];
}
[manager createFileAtPath:junk contents:[NSData dataWithBytes:[plistFile UTF8String] length:[plistFile length]] attributes:nil];

task4 = [[NSTask alloc] init];
[guiString setString:@”gui/”];
[guiString appendString:idString];
[args removeAllObjects];
[args addObject:@”bootout”];
[args addObject:guiString];
[args addObject:junk];
[task4 setLaunchPath:launchctlCommand];
[task4 setArguments:args];
[task4 launch];
[task4 waitUntilExit];
[task4 release];

task2 = [[NSTask alloc] init];
[guiString setString:@”gui/”];
[guiString appendString:idString];
[args removeAllObjects];
[args addObject:@”bootstrap”];
[args addObject:guiString];
[args addObject:junk];
[task2 setLaunchPath:launchctlCommand];
[task2 setArguments:args];
[task2 launch];
[task2 waitUntilExit];
[task2 release];

[guiString appendString:@”/mailsteward.schedule”];
task3 = [[NSTask alloc] init];
[args removeAllObjects];
[args addObject:@”enable”];
[args addObject:guiString];
[task3 setLaunchPath:launchctlCommand];
[task3 setArguments:args];
[task3 launch];
[task3 waitUntilExit];
[task3 release];

}

Leave a Reply

Your email address will not be published. Required fields are marked *