Enable, disable and start services programmatically in macOS

Viewed 2344

I am writing a program that comes with a service. What I did so far is to create a helper tool that can run elevated tasks for my process and can communicate via XPC.

My program comes bundled with a service and I want to use the helper tool to install and start/stop this service so my program can have a checkbox "start service with system" in the settings.

I can successfully copy the plist for the service, but I cannot find any way to enable, disable, start or stop the service programmatically. I think the solution to call system("launchctl load /path/to/service.plist"); pretty ugly. Is there any mechanism in objective C to accomplish this task and get a success or failed result?

3 Answers

From ObjC you could always use the NSTask to manipulate daemons and agents using the launchctl command-line.

Examples:

- (BOOL)isDaemonLoaded:(NSString *)daemonLabel {
    NSTask *task = [NSTask new];
    [task setLaunchPath:@"/bin/launchctl"];
    [task setArguments:@[@"list", daemonLabel]];
    [task launch];
    [task waitUntilExit];
    // return code 0 is for loaded daemon, and much info is in stdout, and 113 or other nonzero value when daemon is not loaded
    return ([task terminationStatus] == 0);
}

or, starting/stopping a launchd global daemon:

typedef NS_ENUM(BOOL, DaemonState)
{
    DaemonStateOff = NO,
    DaemonStateOn = YES
};
- (BOOL)switchGlobalDaemon:(NSString *)daemonLabel 
                     state:(DaemonState)newState {
    NSString *command = (newState == OITStateOn) ? @"load" : @"unload";
    NSString *daemonPath = [[@"/Library/LaunchDaemons" stringByAppendingPathComponent: daemonLabel] stringByAppendingPathExtension:@"plist"];
    if (NO == [[NSFileManager defaultManager] fileExistsAtPath:daemonPath])
        return NO;

    NSTask *task = [NSTask new];
    [task setLaunchPath:@"/bin/launchctl"];
    [task setArguments:@[command, daemonPath]];
    [task launch];
    [task waitUntilExit];
    return ([task terminationStatus] == 0);
}

Of course any launchctl command can be wrapped like this. It looks silly to do this, but since Apple removed programmatic APIs and left us with launchctl...

Anyways, there are quite a few ways to manage your daemons and agents the "straight" way - by setting the rules in the .plist, and letting launchd apply the rules.

Only sometimes there are business logic consideration outside the MacOS domain (say - a notification from a remote server, via push-notifications, telling you to turn off some service, because the customer stopped paying...) or to change configuration of a running service - but that won't take effect until you relaunched it. So it may be still useful to know this technique.

Related