From 1a5c2499963b0cbc3385c5721bbd1923b1d8a60d Mon Sep 17 00:00:00 2001 From: Max Howell Date: Sun, 22 Nov 2009 07:07:53 +0000 Subject: [PATCH] DaemonController, an exercise in refactoring --- DaemonController.h | 25 +++ DaemonController.m | 238 +++++++++++++++++++++ Playdar.prefPane.xcodeproj/project.pbxproj | 14 +- 3 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 DaemonController.h create mode 100644 DaemonController.m diff --git a/DaemonController.h b/DaemonController.h new file mode 100644 index 0000000..950ed37 --- /dev/null +++ b/DaemonController.h @@ -0,0 +1,25 @@ +#import + +@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 diff --git a/DaemonController.m b/DaemonController.m new file mode 100644 index 0000000..25ee966 --- /dev/null +++ b/DaemonController.m @@ -0,0 +1,238 @@ +#import "DaemonController.h" +#import + +/** 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 diff --git a/Playdar.prefPane.xcodeproj/project.pbxproj b/Playdar.prefPane.xcodeproj/project.pbxproj index 89cf4c6..475271d 100644 --- a/Playdar.prefPane.xcodeproj/project.pbxproj +++ b/Playdar.prefPane.xcodeproj/project.pbxproj @@ -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 = ""; }; 63E80EE810B87B6D0094A6E6 /* pressed.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = pressed.png; path = StatusItem/pressed.png; sourceTree = ""; }; 63E80EE910B87B6D0094A6E6 /* StatusItem.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = StatusItem.xib; path = StatusItem/StatusItem.xib; sourceTree = ""; }; + 63E814A310B8D6AB0094A6E6 /* DaemonController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DaemonController.h; sourceTree = ""; }; + 63E814A410B8D6AB0094A6E6 /* DaemonController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DaemonController.m; sourceTree = ""; }; 8D202CF70486D31800D8A456 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 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 = ""; }; @@ -189,6 +193,8 @@ children = ( 32DBCFA20370C41700C91783 /* pc.h */, 638168B110B0D5AA00CADBD5 /* erlexec_playdar */, + 63E814A310B8D6AB0094A6E6 /* DaemonController.h */, + 63E814A410B8D6AB0094A6E6 /* DaemonController.m */, ); name = "Other Sources"; sourceTree = ""; @@ -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;