Enable, disable and start services programmatically in macOS
Asked Answered
R

3

6

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?

Raddy answered 10/8, 2017 at 7:32 Comment(2)
developer.apple.com/library/content/documentation/MacOSX/… did you check that whether that fulfils your needs?Corpus
It does not. I need it for a LaunchDaemon, not a LaunchAgent.Raddy
B
4

Apple has a deprecated C API for starting, stopping, and enabling launchd services in launch.h. The source code for the API is on their opensource site: https://opensource.apple.com/source/launchd/launchd-442.26.2/liblaunch/

Here's some sample code that asks launchd to start the LittleSnitchUIAgent service:

#include <launch.h>

int main(int argc, const char * argv[]) {
    const char *job = "at.obdev.LittleSnitchUIAgent";
    launch_data_t resp, msg;
    msg = launch_data_alloc(LAUNCH_DATA_DICTIONARY);
    launch_data_dict_insert(
        msg, launch_data_new_string(job), LAUNCH_KEY_STARTJOB);
    resp = launch_msg(msg);
    launch_data_free(msg);
    return 0;
}

The LittleSnitchUIAgent isn't signification -- I chose it at random from my local list of services. I left error checking out of the sample to keep it straight forward.

If you haven't already I would recommend giving the launchd man pages and the Daemons and Services Programming Guide a very close study. Launchd can start your processes in response to almost anything: a timer, a socket connection, a device being added to the system, among many others. It's rare that you actually need to manage your own services. I haven't been able to confirm this but I suspect that's why they've deprecated the API.

Bashuk answered 20/8, 2017 at 17:27 Comment(3)
Thanks you very much for this kind of information. I really did it now by opening an XPC connection to the service and launchd starts it on demand on incomming connection.Raddy
but how to install the .plist on the first-place? when I try to save/copy/stream the .plist file into /Library/LaunchDaemon from ObjC code, It always fails - even if I give my App "Full Disk Access" --- to programmatically install a new daemon (with user consent of course) you MUST at least copy a .plist, then give it the first kick somehow. How to do that?Englut
The open liblaunch code looks VERY old, and at least by some code comments, I can say it's from the time each user had their own 'launchd' except for the first one of the system. Do you know if that code is usable as is, or have too many things change since then in launchd ? in other words: is it really usable?Englut
N
1

Looks like there is a ServiceManagement API/Framework used to load LaunchAgents. Looks SMJobBless is the only method available

Nanny answered 5/1, 2019 at 22:14 Comment(0)
E
0

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.

Englut answered 24/3, 2022 at 21:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.