It’s been a while since the last post, but I’ve finally gotten past the boring parts of the new Klicko System Preference pane & background process. The latter is actually a per-user Launch Agent. I’d used launchd before in the last Quay implementation, but didn’t fully understand what I was doing.
Well, I still can’t claim I fully understand all, but I’m definitely getting better at this. As soon as Klicko 1.2 is out and working (hm, maybe that should be the other way around?) I’ll back-port everything I learned to Quay.
One important part of messing around with launchd agents is that you have to set everything up just so – permissions of the executable and of its containing folders, as well as of the controlling plist if it’s a global agent; and you have to run launchctl to load (register) the controlling plist with launchd. Only then, and if the conditions inside the plist are satisfied, launchd will run your agent for you.
Uninstalling your agent, usually for putting an updated version in its place, must also be done carefully. You have to unload it if it’s loaded (even though it may not be running at the time) before swapping in the new executable and plist. Ideally, to avoid wasting time, there should be an easy way to test if your agent is properly loaded – the user might have unloaded it manually, or whatever. Unfortunately, the only “official” way to do so is to run launchctl again, passing in the “list” parameter, and parsing the output; not ideal.
However, there’s a primitive API in place in <launch.h>, and in fact launchctl uses this API itself to communicate with launchd. Still, there’s no documentation about it; only some sample code. Also, there’s this recent reply on the launchd-dev mailing list:
Right now the <launch.h> API is only rated for
daemons checking in with launchd (ala SampleD).
It is not really designed for job management.
Rather, we recommend that folks do their job
management by fork/exec of launchctl.
…
Quinn “The Eskimo!”
Still, I thought it would be interesting to use this API to check on the agent; after all, if something fails you’ll get an error back, and nothing will be messed up. So here’s the code for doing so:
#include <launch.h>
static id GetFromLaunchData(launch_data_t obj);
static void Launch_data_iterate(launch_data_t obj, const char *key, void* dict) {
if (obj) {
id value = GetFromLaunchData(obj);
if(value) {
[(NSMutableDictionary*)dict setObject:value forKey:[NSString stringWithUTF8String:key]];
}
}
}
static NSDictionary* GetFromLaunchDictionary(launch_data_t dict) {
NSMutableDictionary* result = NULL;
if (launch_data_get_type(dict)==LAUNCH_DATA_DICTIONARY) {
result = [NSMutableDictionary dictionary];
launch_data_dict_iterate(dict, Launch_data_iterate, result);
}
return result;
}
static NSArray* GetFromLaunchArray(launch_data_t arr) {
NSMutableArray* result = NULL;
if (launch_data_get_type(arr)==LAUNCH_DATA_ARRAY) {
size_t count = launch_data_array_get_count(arr);
result = [NSMutableArray arrayWithCapacity:count];
for (size_t i=0;i<count;i++) {
id obj = GetFromLaunchData(launch_data_array_get_index(arr, i));
if (obj) {
[result addObject:obj];
}
}
}
return result;
}
static id GetFromLaunchData(launch_data_t obj) {
switch (launch_data_get_type(obj)) {
case LAUNCH_DATA_STRING:
return [NSString stringWithUTF8String:launch_data_get_string(obj)];
case LAUNCH_DATA_INTEGER:
return [NSNumber numberWithLongLong:launch_data_get_integer(obj)];
case LAUNCH_DATA_REAL:
return [NSNumber numberWithDouble:launch_data_get_real(obj)];
case LAUNCH_DATA_BOOL:
return [NSNumber numberWithBool:launch_data_get_bool(obj)?YES:NO];
case LAUNCH_DATA_ARRAY:
return GetFromLaunchArray(obj);
case LAUNCH_DATA_DICTIONARY:
return GetFromLaunchDictionary(obj);
case LAUNCH_DATA_FD:
return [NSNumber numberWithInt:launch_data_get_fd(obj)];
case LAUNCH_DATA_MACHPORT:
return [NSNumber numberWithInt:launch_data_get_machport(obj)];
default:
break;
}
return nil;
}
static NSDictionary* GetFromJobLabel(NSString* job) {
NSDictionary* result = nil;
launch_data_t msg = launch_data_alloc(LAUNCH_DATA_DICTIONARY);
if (msg&&launch_data_dict_insert(msg, launch_data_new_string([job fileSystemRepresentation]), LAUNCH_KEY_GETJOB)) {
launch_data_t response = launch_msg(msg);
launch_data_free(msg);
if (response&&(launch_data_get_type(response)==LAUNCH_DATA_DICTIONARY)) {
result = GetFromLaunchDictionary(response);
}
launch_data_free(response);
}
return result;
}
To use it, just call the last function like this:
NSDictionary* dictionary = GetFromJobLabel(@"com.yourcompany.agentlabel");
and examine the dictionary. If it’s nil, some error happened, or your agent isn’t loaded into launchd.
If you do get a dictionary back, it will be autoreleased, and the “Label” key’s object should be equal to the string you passed in. There are two more keys that might be interesting to check: “PID” will contain your agent’s pid if it’s running, and will be missing if it’s not; and “LastExitStatus” will show you what your agent’s main() returned on its last run.
Note that most of the code above is actually based on the launchd source, converted to use Foundation objects instead of CFTypes.