Skip to content

Commit

Permalink
DaemonController, an exercise in refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
mxcl committed Nov 23, 2009
1 parent 99143f3 commit 1a5c249
Show file tree
Hide file tree
Showing 3 changed files with 274 additions and 3 deletions.
25 changes: 25 additions & 0 deletions DaemonController.h
@@ -0,0 +1,25 @@
#import <Cocoa/Cocoa.h>

@interface DaemonController : NSObject
{
NSString* root;
id delegate;

NSTask* daemon_task;
NSTimer* poll_timer;
NSTimer* check_startup_status_timer;
pid_t pid;
}

-(id)initWithDelegate:(id)delegate andRootDir:(NSString*)path;

-(void)start;
-(void)startInTerminal;
-(void)stop;

-(bool)isRunning;

-(NSString*)playdarctl;
-(int)numFiles;

@end
238 changes: 238 additions & 0 deletions DaemonController.m
@@ -0,0 +1,238 @@
#import "DaemonController.h"
#import <sys/sysctl.h>

/** returns the pid of the running playdar instance, or 0 if not found */
static pid_t playdar_pid()
{
int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0 };
struct kinfo_proc *info;
size_t N;
pid_t pid = 0;

if(sysctl(mib, 3, NULL, &N, NULL, 0) < 0)
return 0; //wrong but unlikely
if(!(info = NSZoneMalloc(NULL, N)))
return 0; //wrong but unlikely
if(sysctl(mib, 3, info, &N, NULL, 0) < 0)
goto end;

N = N / sizeof(struct kinfo_proc);
for(size_t i = 0; i < N; i++)
if(strcmp(info[i].kp_proc.p_comm, "playdar.smp") == 0)
{ pid = info[i].kp_proc.p_pid; break; }
end:
NSZoneFree(NULL, info);
return pid;
}

static void kqueue_termination_callback(CFFileDescriptorRef f, CFOptionFlags callBackTypes, void* self)
{
[(id)self performSelector:@selector(daemonTerminated:) withObject:nil];
}

static inline void kqueue_watch_pid(pid_t pid, id self)
{
int kq;
struct kevent changes;
CFFileDescriptorContext context = { 0, self, NULL, NULL, NULL };
CFRunLoopSourceRef rls;

// Create the kqueue and set it up to watch for SIGCHLD. Use the
// new-in-10.5 EV_RECEIPT flag to ensure that we get what we expect.

kq = kqueue();

EV_SET(&changes, pid, EVFILT_PROC, EV_ADD | EV_RECEIPT, NOTE_EXIT, 0, NULL);
(void) kevent(kq, &changes, 1, &changes, 1, NULL);

// Wrap the kqueue in a CFFileDescriptor (new in Mac OS X 10.5!). Then
// create a run-loop source from the CFFileDescriptor and add that to the
// runloop.

CFFileDescriptorRef ref;
ref = CFFileDescriptorCreate(NULL, kq, true, kqueue_termination_callback, &context);
rls = CFFileDescriptorCreateRunLoopSource(NULL, ref, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode);
CFRelease(rls);

CFFileDescriptorEnableCallBacks(ref, kCFFileDescriptorReadCallBack);
}

#define START_POLL poll_timer = [NSTimer scheduledTimerWithTimeInterval:0.33 target:self selector:@selector(poll:) userInfo:nil repeats:true];



@implementation DaemonController

-(id)initWithDelegate:(id)d andRootDir:(NSString*)r
{
delegate = [d retain];
root = [r retain];

if(pid = playdar_pid())
kqueue_watch_pid(pid, self); // watch the pid for termination
else
START_POLL;

return self;
}

-(bool)isRunning
{
return daemon_task && [daemon_task isRunning] || (pid = playdar_pid());
}

-(NSString*)erlexec_playdar
{
return [root stringByAppendingPathComponent:@"Contents/MacOS/erlexec_playdar"];
}

-(NSString*)playdarctl
{
return [root stringByAppendingPathComponent:@"bin/playdarctl"];
}

-(void)daemonTerminated:(NSNotification*)note
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSTaskDidTerminateNotification object:daemon_task];

daemon_task = nil;
pid = 0;

START_POLL;

[delegate performSelector:@selector(playdarStopped)];
}

-(void)stop
{
NSTask* task = [[NSTask alloc] init];
task.launchPath = [self playdarctl];
task.arguments = [NSArray arrayWithObjects:@"stop", nil];

[delegate performSelector:@selector(playdarIsStopping)];

[task launch];
[task waitUntilExit];

if (task.terminationStatus == 0)
// the kqueue event will tell us when the process exits
return;

// playdarctl failed to stop the daemon, so it's time to be more forceful
if(daemon_task)
[daemon_task terminate];
else if (pid = playdar_pid() == 0){
// actually we weren't even running in the first place
START_POLL
[delegate performSelector:@selector(playdarStopped)];
}else if (kill(pid, SIGTERM) == -1 && errno != ESRCH)
[delegate performSelector:@selector(playdarFailedToStop) withObject:@"Perhaps you don't have the right permissions?"];
}

-(void)initDaemonTask
{
[poll_timer invalidate];
poll_timer = nil;

[delegate performSelector:@selector(playdarIsStarting)];

daemon_task = [[NSTask alloc] init];
}

-(void)failedToStartDaemonTask
{
NSMutableString* msg = [@"The file at “" mutableCopy];
[msg appendString:daemon_task.launchPath];
[msg appendString:@"” could not be executed."];

[delegate performSelector:@selector(playdarFailedToStart:) withObject:msg];

daemon_task = nil;
}

-(void)start
{
@try {
[self initDaemonTask];
daemon_task.launchPath = [self erlexec_playdar];
daemon_task.arguments = [NSArray arrayWithObject:@"-d"];

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(daemonTerminated:)
name:NSTaskDidTerminateNotification
object:daemon_task];
[daemon_task launch];
pid = daemon_task.processIdentifier;

#define CHECK_READY_FOR_SCAN \
[self performSelector:@selector(checkReadyForScan) withObject:nil afterDelay:0.2];

CHECK_READY_FOR_SCAN
}
@catch (NSException* e) {
[self failedToStartDaemonTask];
}
}

-(void)startInTerminal
{
@try {
[self initDaemonTask];
[daemon_task setLaunchPath:@"/usr/bin/open"];
[daemon_task setArguments:[NSArray arrayWithObjects:[self erlexec_playdar], @"-aTerminal", nil]];
[daemon_task launch];
[daemon_task waitUntilExit];

daemon_task = nil;

CHECK_READY_FOR_SCAN
}
@catch (NSException* e) {
[self failedToStartDaemonTask];
}
}

-(void)checkReadyForScan
{
int const n = [self numFiles];

if (n < 0)
CHECK_READY_FOR_SCAN
else {
if (!pid) // started via Terminal route perhaps
kqueue_watch_pid(pid = playdar_pid(), self);
[delegate performSelector:@selector(playdarStarted:) withObject:[NSNumber numberWithInt:n]];
}
}

-(void)poll:(NSTimer*)t
{
if (pid = playdar_pid() == 0) return;

[delegate performSelector:@selector(playdarIsStarting)];
[poll_timer invalidate];
poll_timer = nil;
kqueue_watch_pid(pid, self);
[self checkReadyForScan];
}

/////////////////////////////////////////////////////////////////////////// misc
-(int)numFiles
{
NSTask* task = [[NSTask alloc] init];
[task setLaunchPath:[self playdarctl]];
[task setArguments:[NSArray arrayWithObject:@"numfiles"]];
[task setStandardOutput:[NSPipe pipe]];
[task launch];
[task waitUntilExit];

// if not zero then we library module isn't ready yet
if (task.terminationStatus != 0)
return -1;

NSData* data = [[[task standardOutput] fileHandleForReading] readDataToEndOfFile];
return [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] intValue];
}

@end
14 changes: 11 additions & 3 deletions Playdar.prefPane.xcodeproj/project.pbxproj
Expand Up @@ -11,6 +11,7 @@
639969C3108E401800F7CE1F /* MBSliderButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 639969C2108E401800F7CE1F /* MBSliderButton.m */; };
639969CA108E40B300F7CE1F /* button_surround.png in Resources */ = {isa = PBXBuildFile; fileRef = 639969C8108E40B300F7CE1F /* button_surround.png */; };
639969CB108E40B300F7CE1F /* button_knob.png in Resources */ = {isa = PBXBuildFile; fileRef = 639969C9108E40B300F7CE1F /* button_knob.png */; };
63A9BCE210B8FE5B0015077D /* DaemonController.m in Sources */ = {isa = PBXBuildFile; fileRef = 63E814A410B8D6AB0094A6E6 /* DaemonController.m */; };
63ADA832108B6447004FFC0C /* main.xib in Resources */ = {isa = PBXBuildFile; fileRef = 63A9EFAE108B605C0055B9DA /* main.xib */; };
63ADA849108B69AE004FFC0C /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63ADA848108B69AE004FFC0C /* Sparkle.framework */; };
63ADA855108B6AB7004FFC0C /* Sparkle.framework in Copy Sparkle.framework */ = {isa = PBXBuildFile; fileRef = 63ADA848108B69AE004FFC0C /* Sparkle.framework */; };
Expand All @@ -20,6 +21,7 @@
63E80EF410B87B8E0094A6E6 /* StatusItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = 63E80EE910B87B6D0094A6E6 /* StatusItem.xib */; };
63E80EF510B87B990094A6E6 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 63E80ED510B87AC10094A6E6 /* main.m */; };
63E80EF710B87B990094A6E6 /* StatusItemController.m in Sources */ = {isa = PBXBuildFile; fileRef = 63E80ED710B87AC10094A6E6 /* StatusItemController.m */; };
63E814A510B8D6AB0094A6E6 /* DaemonController.m in Sources */ = {isa = PBXBuildFile; fileRef = 63E814A410B8D6AB0094A6E6 /* DaemonController.m */; };
8D202CEE0486D31800D8A456 /* playdar.icns in Resources */ = {isa = PBXBuildFile; fileRef = F506C040013D9D8001CA16C8 /* playdar.icns */; };
8D202CF10486D31800D8A456 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = F506C03D013D9D7901CA16C8 /* main.m */; };
8D202CF30486D31800D8A456 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */; };
Expand All @@ -31,7 +33,7 @@
isa = PBXContainerItemProxy;
containerPortal = 089C1669FE841209C02AAC07 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 8D202CE80486D31800D8A456 /* Playdar.prefPane */;
remoteGlobalIDString = 8D202CE80486D31800D8A456;
remoteInfo = Playdar.prefPane;
};
/* End PBXContainerItemProxy section */
Expand Down Expand Up @@ -82,6 +84,8 @@
63E80EE710B87B6D0094A6E6 /* on.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = on.png; path = StatusItem/on.png; sourceTree = "<group>"; };
63E80EE810B87B6D0094A6E6 /* pressed.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = pressed.png; path = StatusItem/pressed.png; sourceTree = "<group>"; };
63E80EE910B87B6D0094A6E6 /* StatusItem.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = StatusItem.xib; path = StatusItem/StatusItem.xib; sourceTree = "<group>"; };
63E814A310B8D6AB0094A6E6 /* DaemonController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DaemonController.h; sourceTree = "<group>"; };
63E814A410B8D6AB0094A6E6 /* DaemonController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DaemonController.m; sourceTree = "<group>"; };
8D202CF70486D31800D8A456 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
8D202CF80486D31800D8A456 /* Playdar.prefPane */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Playdar.prefPane; sourceTree = BUILT_PRODUCTS_DIR; };
F506C035013D953901CA16C8 /* PreferencePanes.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PreferencePanes.framework; path = /System/Library/Frameworks/PreferencePanes.framework; sourceTree = "<absolute>"; };
Expand Down Expand Up @@ -189,6 +193,8 @@
children = (
32DBCFA20370C41700C91783 /* pc.h */,
638168B110B0D5AA00CADBD5 /* erlexec_playdar */,
63E814A310B8D6AB0094A6E6 /* DaemonController.h */,
63E814A410B8D6AB0094A6E6 /* DaemonController.m */,
);
name = "Other Sources";
sourceTree = "<group>";
Expand Down Expand Up @@ -380,8 +386,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
63E80EF510B87B990094A6E6 /* main.m in Sources */,
63E80EF710B87B990094A6E6 /* StatusItemController.m in Sources */,
63A9BCE210B8FE5B0015077D /* DaemonController.m in Sources */,
63E80EF510B87B990094A6E6 /* main.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -390,6 +397,7 @@
buildActionMask = 2147483647;
files = (
8D202CF10486D31800D8A456 /* main.m in Sources */,
63E814A510B8D6AB0094A6E6 /* DaemonController.m in Sources */,
639969C3108E401800F7CE1F /* MBSliderButton.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -464,7 +472,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
ONLY_ACTIVE_ARCH = YES;
PREBINDING = NO;
SDKROOT = macosx10.6;
SDKROOT = macosx10.5;
VALID_ARCHS = "i386 ppc x86_64";
};
name = Debug;
Expand Down

0 comments on commit 1a5c249

Please sign in to comment.