// // PLSOverseer.m // Pulse // // Created by Matt Attaway on 1/20/14. // Copyright (c) 2014 Zen of the Monkey. All rights reserved. // #import "PLSOverseer.h" #import "P4Message.h" #import "P4Connection.h" #import "PLSFileEvent.h" @interface PLSOverseer() @property (strong) FileSystemWatcher* watcher; @property (strong) NSTimer* timer; @property (strong) P4Connection* p4connection; @end @implementation PLSOverseer @synthesize path = _path; @synthesize port = _port; @synthesize user = _user; @synthesize client = _client; @synthesize charset = _charset; - (id) init { NSString* user = NSUserName(); return [self initWithSettings:@"perforce:1666" user:user client:[user stringByAppendingString:@"-pulse"] path:[[NSArray arrayWithObjects:@"/Users", user, @"perforce", nil] componentsJoinedByString:@"/"] highChange:0 charset:@"none" enableAutoSubmit:false disabled:false]; } - (id)initWithDictionary:(NSDictionary *)dict { return [self initWithSettings:dict[@"port"] user:dict[@"user"] client:dict[@"client"] path:dict[@"path"] highChange:[dict[@"highChange"] integerValue] charset:dict[@"charset"] enableAutoSubmit:[dict[@"enableAutoSubmit"] boolValue] disabled:[dict[@"disabled"] boolValue]]; } - (id)initWithSettings:(NSString*)port user:(NSString*)user client:(NSString*)client path:(NSString*)path highChange:(NSInteger)highChange charset:(NSString*)charset enableAutoSubmit:(BOOL)enableAutoSubmit disabled:(BOOL)disabled { self = [super init]; if (self) { self.port = port; self.client = client; self.user = user; self.path = path; self.charset = charset; self.highChange = highChange; self.enableAutoSubmit = enableAutoSubmit ? enableAutoSubmit : false; self.disabled = disabled ? disabled : false; self.needsLogin = false; self.connectionDown = false; self.syncing = false; self.reconciling = false; self.lookingForConnection = false; self.lookingForTicket = false; self.watcher = [[FileSystemWatcher alloc] init]; [self.watcher watchPath:self.path target:self]; [[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate:self]; self.p4connection = [[P4Connection alloc] initWithSettings:self.port user:self.user client:self.client path:self.path charset:self.charset]; } return self; } - (BOOL)userNotificationCenter:(NSUserNotificationCenter*)center shouldPresentNotification:(NSUserNotification*)notification { return YES; } - (void)checkLoginLauncher:(NSTimer*)theTimer { if(!self.lookingForTicket) { [self performSelectorInBackground:@selector(checkLogin) withObject:nil]; } else { NSLog(@"IM BUSY WAITING FOR GODOT"); } } - (void)checkConnectionLauncher:(NSTimer*)theTimer { if(!self.lookingForConnection) { [self performSelectorInBackground:@selector(checkConnection) withObject:nil]; } else { NSLog(@"IM BUSY LOOKING FOR HOME"); } } - (void)checkForUpdatesLauncher:(NSTimer *)theTimer { if(!self.syncing) { [self performSelectorInBackground:@selector(checkForUpdates:) withObject:theTimer]; } else { NSLog(@"IM BUSY SINKING"); } } - (void)fileEvent:(PLSFileEvent*)event { if(!self.reconciling) { [self performSelectorInBackground:@selector(scanForChanges:) withObject:event]; } else { NSLog(@"IM BUSY LOOKIN"); } } // // methods that look for recovery from server communication problems // -(void)checkLogin { self.lookingForTicket = true; NSLog (@"Checking for ticket for %@", self.port); if([self.p4connection isLoggedIn]) { NSLog(@"IM IN LIKE FLYNN!!!"); dispatch_async(dispatch_get_main_queue(), ^{ [self handleLoggedin]; }); } else { NSLog(@"STILL LOOKING FOR THE KEYMASTER"); } self.lookingForTicket = false;; } - (void)checkConnection { self.lookingForConnection = true; NSLog (@"Checking connection for %@", self.port); if([self.p4connection isConnected]) { NSLog(@"IT'S GO TIME!!!"); dispatch_async(dispatch_get_main_queue(), ^{ [self handleReconnected]; }); self.lookingForConnection = false; } else { NSLog(@"NAH BRAH, STILL DOWN"); } self.lookingForConnection = false; } // // methods that do the majority of the real work of syncing and reconciling // - (void)checkForUpdates:(NSTimer *)theTimer { self.syncing = true; NSLog (@"Checking for updates against %@", self.port); // check to see if new changes have been submitted P4Message* results = [self.p4connection getLatestChangeInClient]; if([results isConnectionError]) { self.syncing = false; dispatch_async(dispatch_get_main_queue(), ^{ [self handleConnectionError:results]; }); return; } // let the app know we need a password someday if([results isLoginError]) { dispatch_async(dispatch_get_main_queue(), ^{ [self handleLoginError:results now:NO]; }); self.syncing = false; return; } // change the charset if appropriate and bail for this cycle if([self handleUnicodeError:results]) { self.syncing = false; return; } // if latest change is higher than stored change then sync to that change, otherwise bail NSInteger highchange = [[results.results objectForKey:@"change"] integerValue]; if( highchange <= self.highChange) { self.syncing = false; return; } NSLog (@"Highest change is now %li", (long)highchange); P4Message* syncRes = [self.p4connection syncFiles:[NSString stringWithFormat:@"%@/...", self.path]]; if(self.highChange == 0) { NSUserNotification *notification = [[NSUserNotification alloc] init]; notification.title = @"Pulse is ready to rock!"; notification.informativeText = @"All of your files are up-to-date."; notification.soundName = NSUserNotificationDefaultSoundName; [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification]; } else { // get the list of changes that have sync between stored change and latest change results = [self.p4connection getChanges:highchange low:self.highChange]; NSInteger changesCount = [results.list count] - 1; // summarize activity and pop up a notification if( ![syncRes.message isEqual: @""] ) { NSUserNotification *notification = [[NSUserNotification alloc] init]; if(changesCount == 1 ) { notification.title = [[NSArray arrayWithObjects: @"Synced change", results.list[0][@"change"], @"by", results.list[0][@"user"], nil] componentsJoinedByString:@" "]; } else { notification.title = [NSString stringWithFormat: @"%li changes synced", [results.list count] - 1]; } notification.informativeText = results.list[0][@"desc"]; [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification]; } self.highChange = 1; } // update stored change to latest change self.highChange = highchange; self.syncing = false; } - (void)scanForChanges:(PLSFileEvent*)event { self.reconciling = true; NSMutableArray* files = [[NSMutableArray alloc] init]; NSUInteger fileActions = 0; // quick sanity check; skip hidden files for(int i=0; i < [event.files count]; i++) { NSString* file = [event.files objectAtIndex:i]; NSArray *pathComponents = [file pathComponents]; for( NSString* p in pathComponents) { if([p hasPrefix:@"."]) { NSLog(@"%@ is a hidden path. Ignoring.", file); continue; } } // made it through the path check, add it to the list of files to work on [files addObject:file]; fileActions = fileActions | [[event.fileActions objectAtIndex:i] unsignedIntValue]; } // if there are no files in the array then everything was a .file. Run away! if([files count] == 0) { self.reconciling = false; return; } // see if we can optimize our reconcile a bit; don't look for modified files if watcher says they // were all adds and deletes NSUInteger opts = P4ConnectionReconcileAddOption | P4ConnectionReconcileDeleteOption; if(fileActions & kFSEventStreamEventFlagItemModified) { opts = opts | P4ConnectionReconcileEditOption; } // time to dance; if we aren't submitting all the time we need to run // revert -k on the files we're operating on in case their state has changed. // for instance, a deleted file is restored, or added file deleted P4Message* results = nil; if(self.enableAutoSubmit) { results = [self.p4connection reconcilePaths:event.files options:opts]; } else { results = [self.p4connection revertPaths:event.files options:P4ConnectionRevertServerOnlyOption]; } // we're always looking for connection errors if([results isConnectionError]) { self.reconciling = false; dispatch_async(dispatch_get_main_queue(), ^{ [self handleConnectionError:results]; }); return; } // change the charset if needed and re-run the command // there's no way to put events back on the queue, so we have to try as // hard as we can to succeed here if([self handleUnicodeError:results]) { if(self.enableAutoSubmit) { results = [self.p4connection reconcilePaths:event.files options:opts]; } else { results = [self.p4connection revertPaths:event.files options:P4ConnectionRevertServerOnlyOption]; } } // let the app know we need a password now if([results isLoginError]) { dispatch_async(dispatch_get_main_queue(), ^{ [self handleLoginError:results now:YES]; }); self.reconciling = false; return; } // if we're not submitting automatically that means we've been reverting stuff above // now actually check out the files if(!self.enableAutoSubmit) { results = [self.p4connection reconcilePaths:event.files options:opts]; } else { NSString* description = @"Change auto-submitted by Pulse"; results = [self.p4connection submit:description]; } NSLog (@"File event results\n%@", results.message); self.reconciling = false; } -(P4Message*)login:(NSString*)password { P4Message* msg = [self.p4connection login:password]; if([self handleConnectionError:msg]) { return msg; } if(![msg.error isEqual: @""]) { self.needsLogin = true; } else { self.needsLogin = false; [self.delegate loggedIn]; } return msg; } // if we get a unicode error flip the state to make it go away // this method is safe to call in a thread - (BOOL)handleUnicodeError:(P4Message*)msg { if([msg isUnicodeError]) { if([self.charset isEqual:@"none"]) { NSLog(@"SWAPPING TO UTF8"); self.charset = @"utf8"; } else { NSLog(@"SWAPPING TO NONE"); self.charset = @"none"; } return true; } return false; } // if we have a connection error shut it all down and start testing for a live connection // this should be called on the main thread so that notifications are routed correctly - (BOOL)handleConnectionError:(P4Message*)msg { if([msg isConnectionError]) { self.connectionDown = true; [self.delegate connectionDown:self]; [self pauseTracking]; [self startConnectionTestTimer]; return true; } return false; } // if we have a login error flag the controller of the urgency and shut down the loop // this should be called on the main thread so that notifications are routed correctly - (BOOL)handleLoginError:(P4Message*)msg now:(BOOL)now { if([msg isLoginError]) { if(now) { [self.delegate needPasswordNow:self]; } else { [self.delegate needPassword:self]; } self.needsLogin = true; [self pauseTracking]; [self startLoginTestTimer]; return true; } return false; } // called when we identify that the Overseer has reconnected to the Perforce server // TODO: should we look for file operations we missed while down? maybe cache them? - (void)handleReconnected { self.connectionDown = false; [self.delegate connectionUp:self]; [self stopConnectionTestTimer]; [self startTracking]; } // called when we see a valid ticket appear - (void)handleLoggedin { self.needsLogin = false; [self.delegate loggedIn]; [self stopLoginTestTimer]; [self startTracking]; } - (void)startLoginTestTimer { [self.timer invalidate]; self.timer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(checkLoginLauncher:) userInfo:nil repeats:YES]; } - (void)stopLoginTestTimer { [self.timer invalidate]; } - (void)startConnectionTestTimer { [self.timer invalidate]; self.timer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(checkConnectionLauncher:) userInfo:nil repeats:YES]; } - (void)stopConnectionTestTimer { [self.timer invalidate]; } - (void)startSyncTimer { [self.timer invalidate]; self.timer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(checkForUpdatesLauncher:) userInfo:nil repeats:YES]; } - (void)stopSyncTimer { [self.timer invalidate]; } - (void)startTracking { // nope, not starting if we're disabled if(self.disabled) { return; } // start the appropriate timer depending on the connection state if(self.connectionDown) { [self startConnectionTestTimer]; } else if(self.needsLogin) { [self startLoginTestTimer]; } else { [self startSyncTimer]; if(self.watcher) { [self.watcher startWatching]; } } } // not sure what's going to happen if I pause tracking while disabled. need to test. -(void)pauseTracking { if(self.watcher) { [self.watcher pauseWatching]; } [self stopSyncTimer]; } - (void)setPath:(NSString *)thepath { if(![_path isEqual: thepath]) { _path = thepath; self.p4connection = [[P4Connection alloc] initWithSettings:self.port user:self.user client:self.client path:self.path charset:self.charset]; // if the path changed we need to reset the watcher [self.watcher stopWatching]; [self.watcher watchPath:_path target:self]; } } - (NSString*)path { return _path; } - (void)setPort:(NSString *)theport { // if there's no port on the port set the port (if that terrible joke was confusing, run for away from this code) NSError *error = NULL; NSRegularExpression* regex = [NSRegularExpression regularExpressionWithPattern:@":\\d+$" options:NSRegularExpressionCaseInsensitive error:&error]; NSString* p = nil; if([regex numberOfMatchesInString:theport options:0 range:NSMakeRange(0, [theport length])] == 0) { p = [theport stringByAppendingString: @":1666"]; } else { p = theport; } if(![_port isEqual: p]) { _port = p; self.p4connection = [[P4Connection alloc] initWithSettings:self.port user:self.user client:self.client path:self.path charset:self.charset]; } } - (NSString*)port { return _port; } - (void)setUser:(NSString *)theuser { if(![_user isEqual: theuser]) { _user = theuser; self.p4connection = [[P4Connection alloc] initWithSettings:self.port user:self.user client:self.client path:self.path charset:self.charset]; } } - (NSString*)user { return _user; } - (void)setClient:(NSString *)theclient { if(![_client isEqual: theclient]) { _client = theclient; self.p4connection = [[P4Connection alloc] initWithSettings:self.port user:self.user client:self.client path:self.path charset:self.charset]; } } - (NSString*)client { return _client; } - (void)setCharset:(NSString *)thecharset { if(![_charset isEqual: thecharset]) { _charset = thecharset; self.p4connection = [[P4Connection alloc] initWithSettings:self.port user:self.user client:self.client path:self.path charset:self.charset]; } } - (NSString*)charset { return _charset; } - (NSDictionary*)getSettings { NSDictionary* settings = [[NSDictionary alloc] initWithObjectsAndKeys: self.port, @"port", self.user, @"user", self.path, @"path", self.client, @"client", [NSString stringWithFormat: @"%ld",(long)self.highChange], @"highChange", self.charset, @"charset", [NSString stringWithFormat:@"%d",self.enableAutoSubmit], @"enableAutoSubmit", [NSString stringWithFormat:@"%d",self.disabled], @"disabled", nil]; return settings; } @end