Permalink
Browse files

DaemonController, an exercise in refactoring

  • Loading branch information...
1 parent 99143f3 commit 1a5c2499963b0cbc3385c5721bbd1923b1d8a60d @mxcl committed Nov 22, 2009
Showing with 274 additions and 3 deletions.
  1. +25 −0 DaemonController.h
  2. +238 −0 DaemonController.m
  3. +11 −3 Playdar.prefPane.xcodeproj/project.pbxproj
View
@@ -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
View
@@ -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
@@ -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 */; };
@@ -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 */; };
@@ -31,7 +33,7 @@
isa = PBXContainerItemProxy;
containerPortal = 089C1669FE841209C02AAC07 /* Project object */;
proxyType = 1;
- remoteGlobalIDString = 8D202CE80486D31800D8A456 /* Playdar.prefPane */;
+ remoteGlobalIDString = 8D202CE80486D31800D8A456;
remoteInfo = Playdar.prefPane;
};
/* End PBXContainerItemProxy section */
@@ -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>"; };
@@ -189,6 +193,8 @@
children = (
32DBCFA20370C41700C91783 /* pc.h */,
638168B110B0D5AA00CADBD5 /* erlexec_playdar */,
+ 63E814A310B8D6AB0094A6E6 /* DaemonController.h */,
+ 63E814A410B8D6AB0094A6E6 /* DaemonController.m */,
);
name = "Other Sources";
sourceTree = "<group>";
@@ -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;
};
@@ -390,6 +397,7 @@
buildActionMask = 2147483647;
files = (
8D202CF10486D31800D8A456 /* main.m in Sources */,
+ 63E814A510B8D6AB0094A6E6 /* DaemonController.m in Sources */,
639969C3108E401800F7CE1F /* MBSliderButton.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -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;

0 comments on commit 1a5c249

Please sign in to comment.