Cocoa in the Shell

Coding an Apple compliant daemon

In my previous post I showed you how to prevent a disk from mounting on the filesystem, at the end of the post I said that it would probably be best if this kind of program could run as a daemon, so this is what I’ll show you in this post.

To begin let’s take a look at the precedent code :

#import <foundation/Foundation.h>
#import <diskArbitration/DiskArbitration.h>

DADissenterRef diskDidMount(DADiskRef dsk, void* context)
{
    DADissenterRef ref = DADissenterCreate(kCFAllocatorDefault, kDAReturnNotPermitted, NULL);
    return ref;
}

int main(int argc, const char* argv[])
{
    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
    DASessionRef session = DASessionCreate(kCFAllocatorDefault);
    DARegisterDiskMountApprovalCallback(session, NULL, diskDidMount, NULL);
    DASessionScheduleWithRunLoop(session, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
    CFRunLoopRun();
    [pool drain];
    return 0;
}

First, there are some things that you need to know, OS X is an UNIX system but since Mac OS X 10.4, daemons aren’t handled like the traditional BSD systems.

Since this release, daemons are managed by launchd.

OS X have two kinds of programs that run in background :

  • Daemons : Their owner is the system, they are launched even when nobody is logged and, a very important thing, they don’t have to launch any kind of GUI.

  • Agents : Their owner is a normal user, they are only launched when an user is logged and can display a GUI.

In order to make a difference between them, there are two distinct folders : /Library/LaunchDaemons and /Library/LaunchAgents.

Now let’s get working, there is nothing hard, first we will take a look at the Apple doc, here and here.
The second link is important because it shows what frameworks are daemon safe, and you can see that Disk Arbitration is daemon safe, so we can adapt our program without any problems.

Now look at the first link, it’s the man for launchd.plist, the first section that you must read is the EXPECTATIONS one, it tells you what you can do, what you can’t and what you must.

If you read correctly, you know that you need two fields at least in your plist :

  • Label

  • Program or ProgramArguments

You can take a look to the others flags too, some are interesting, like RunAtLoad which indicates that the program will run at login. Here is our finalized plist :

<!--?xml version="1.0" encoding="UTF-8"?-->
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.cocoabyss.diskblocker</string>
    <key>RunAtLoad</key>
    <true>
    <key>Program</key>
    <string>/tmp/DiskBlocker</string>
    <key>UserName</key>
    <string>Nyxouf</string>
</true></dict>
</plist>

The name of the file must follow this scheme : Label.plist, in my case I named it com.nyx0uf.diskblocker.plist.

Put the file in /Library/LaunchDaemons and you are done with the config file.

Now we need to add some code in our program to adapt it to run as a daemon. If you correctly read the doc, you know that you must catch the SIGTERM signal in order to terminate properly the daemon (free the memory etc..)
Again, it’s really easy, to do that we use the functions in signal.h, first we create a handler function :

void SIGTERM_handler(const int sigid)
{
    if (SIGTERM == sigid)
    {
        CFRunLoopStop(CFRunLoopGetCurrent());
    }
}

In the main function before the runloop, we call the signal function :

signal(SIGTERM, (sig_t)SIGTERM_handler);

Once the signal received, the runloop will stop, so we can properly end the program :

DASessionUnscheduleFromRunLoop(session, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
CFRelease(session);
[pool drain];
return 0;

Now, let’s compile

gcc DiskBlocker.m -o /tmp/DiskBlocker -framework DiskArbitration -framework Foundation

In the man, it’s written that the system daemon owner must be root, so let’s do a little chown.

sudo chown root:wheel /tmp/DiskBlocker

Last thing, launching the daemon, to do that we use launchctl

launchctl load /Library/LaunchDaemons/com.cocoabyss.diskblocker.plist

If like me, you specified the RunAtLoad flag, you can try to reboot to see if the daemon is launched.

You can get the code here.