// // 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" @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 { self = [super init]; if (self) { self.port = @"perforce:1666"; self.user = NSUserName(); self.client = [self.user stringByAppendingString:@"-pulse"]; self.path = [[NSArray arrayWithObjects:@"/Users", self.user, @"perforce", nil] componentsJoinedByString:@"/"]; self.highChange = 0; self.charset = @"none"; self.needsLogin = 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; } - (id)initWithDictionary:(NSDictionary *)dict { self = [super init]; if (self) { self.port = dict[@"port"]; self.client = dict[@"client"]; self.user = dict[@"user"]; self.path = dict[@"path"]; self.charset = dict[@"charset"]; self.highChange = [dict[@"highChange"] integerValue]; self.needsLogin = 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; } - (id)initWithSettings:(NSString*)port user:(NSString*)user client:(NSString*)client path:(NSString*)path highChange:(NSInteger)highChange charset:(NSString*)charset { self = [super init]; if (self) { self.port = port; self.client = client; self.user = user; self.path = path; self.charset = charset; self.highChange = highChange; self.needsLogin = 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)checkForUpdates:(NSTimer *)theTimer { 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.connectionDown = true; [self.delegate connectionDown:self]; [self pauseTracking]; return; } // let the app know we need a password someday if([results isLoginError]) { self.needsLogin = true; [self.delegate needPassword:self]; [self pauseTracking]; return; } // change the charset if appropriate and bail for this cycle if([self handleUnicodeError:results]) { 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) { return; } NSLog (@"Highest change is now %li", (long)highchange); P4Message* syncRes = [self.p4connection syncFiles:nil]; 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"] ; notification.soundName = NSUserNotificationDefaultSoundName; [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification]; } self.highChange = 1; } // update stored change to latest change self.highChange = highchange; } // if we get a unicode error flip the state to make it go away - (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; } - (void)fileEvent:(NSString*)file { // quick sanity check; bail if these are hidden files NSArray *pathComponents = [file pathComponents]; for( NSString* p in pathComponents) { if([p hasPrefix:@"."]) { NSLog(@"%@ is a hidden path. Ignoring.", file); return; } } P4Message* results = [self.p4connection reconcile:nil]; if([results isConnectionError]) { self.connectionDown = true; [self.delegate connectionDown:self]; [self pauseTracking]; return; } // change the charset if appropriate and re-run the command if([self handleUnicodeError:results]) { results = [self.p4connection reconcile:nil]; } // let the app know we need a password now if([results isLoginError]) { self.needsLogin = true; [self pauseTracking]; [self.delegate needPasswordNow:self]; return; } NSLog (@"woot! Just saw\n%@", results.message); } -(P4Message*)login:(NSString*)password { P4Message* msg = [self.p4connection login:password]; if([msg isConnectionError]) { self.connectionDown = true; [self.delegate connectionDown:self]; [self pauseTracking]; return msg; } if(![msg.error isEqual: @""]) { self.needsLogin = true; } else { self.needsLogin = false; } return msg; } - (void)startTimer { [self.timer invalidate]; self.timer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(checkForUpdates:) userInfo:nil repeats:YES]; } - (void)stopTimer { [self.timer invalidate]; } - (void)startTracking { if(self.watcher) { [self.watcher startWatching]; } [self startTimer]; } -(void)pauseTracking { if(self.watcher) { [self.watcher pauseWatching]; } [self stopTimer]; } - (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(![_port isEqual: theport]) { _port = theport; 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", nil]; return settings; } @end
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#38 | 8777 | Matt Attaway |
Try to figure out weird command hang in Pulse. For the time being we just kill the connection if NSTask hangs. I just wish I knew why it was hanging. |
||
#37 | 8760 | Matt Attaway |
Remove sound when sync is complete I left it for now for the initial sync and login notifications, but it is annoying for normal syncs. User visible change |
||
#36 | 8723 | Matt Attaway |
Continuously update state on opened files Automatically checking out files is all well and good, but there’s a problem; frequently in the course of working on files, before you commit, files will go through a number of different states. You may edit a file, then delete it, and then re-add it unchanged all before trying to commit. At the end of the process in the case above nothing should happen, but if you edit a file and never revisit it you are stuck with that initial edit. Pulse now runs a non-destructive revert on files when it receives an event notification. This resets the working state so that if there is a major change (from edit -> delete) we can properly catch it and tell the system. It also is handy for dealing with added files that you then delete. There are edge cases with this code that I’m confident are not handled correctly. The good news is nothing should be destructive; the worse that happens is you revert -k everything and re-run reconcile. User visible change |
||
#35 | 8722 | Matt Attaway |
Optimize reconcile calls to minimize file scans The FSEvents API gives wonderfully detailed information which allows us to be a bit more precise in how we run reconcile. Now we only look for edited files if one of the files was flagged as modified. No user visible change (fairly subtle optimization) |
||
#34 | 8721 | Matt Attaway |
Move to using the file list provided by the file watcher for reconcile Instead of looking at the world all the time Pulse now selectively only reconciles the files that were changed. Prior users will note files don’t get accidentally swept up anytime some other file changes. User visible change |
||
#33 | 8720 | Matt Attaway |
Rework reconcile methods to handle file arguments Previously the only option was to blindly reconcile the world. Now reconciles can be more targeted, although there’s no code to actually do a targeted reconcile yet in the Overseer. Infrastructure only change |
||
#32 | 8706 | Matt Attaway |
Infrastructure work to more intelligently handle FSEvents Prior to this change all file system events were equal; chown-ing a file was considered the same as changing the content. With this change we: 1) bundle all of the file events into one operation instead or doing it one file at a time 2) ignore any file events that are not create/delete/modify/rename The interfaces are all in place, but the Overseers are still not taking advantage of this information, so there’s no user visible change at this time. (hopefully) Infrastructure only change |
||
#31 | 8702 | Matt Attaway |
Add the ability to pause updates on a connection After some fun today where I added a gazillion files while running automated tests I realized there is is much to be said for being able to pause your automated friend. This change adds the ability to pause updates on a connection. Syncs and file events are not tracked while updates are paused. Paused connections are also not used when figuring out what the state icon should be. Also fix a bug related to all of this where a server coming online while in the connection dialog would not cause the icon to update. We now do an icon state check after coming out of the connection dialog. In addition, we use the proper timer when coming out of the connection dialog, not always the sync timer. User visible change |
||
#30 | 8692 | Matt Attaway |
Add auto-submit capability to Pulse If you so choose you may now configure Pulse to automatically submit files as soon as they are changed. This is done as part of the file event watcher; files are opened with reconcile and if configured to do so immediately submitted with a boilerplate commit message. For obvious reasons auto-commit is off by default. User visible change |
||
#29 | 8690 | Matt Attaway |
Automatically append ":1666" to the end of a server if the user forgets User visible change |
||
#28 | 8684 | Matt Attaway |
First steps on the new connection dialog At this point it’s effectively a read only dialog. Changes made in the text fields are currently ignored. It does look much nicer though. To view it use the appropriately named ‘??????’ menu entry. User visible change |
||
#27 | 8657 | Matt Attaway |
Tidy up constructors and whitespace a bit Finally figured out the proper way to have a designated contructor in Obj-C. Hooray for progress. No user visible change. |
||
#26 | 8656 | Matt Attaway |
Move login check out to a thread too At this point the only Perforce command that is running in the main thread is login from the login dialog itself. This should help keep Pulse peppy. User visible change (less lag) |
||
#25 | 8654 | Matt Attaway |
Move connections checks into threads Getting everything slow off of the main thread. That means you connection checks. User visible change (in so much as it’s less laggy now) |
||
#24 | 8652 | Matt Attaway |
Rework login to take advantage of other clients logging in Previously the Overseer entirely shut down operations when it noticed that the user needed to login. This is all well and good if you only use Pulse, but less helpful if use other clients too. This change introduces a new timer that attempts ‘p4 login -s’ every few seconds to see if there is a valid ticket. This is not unlike the timer that gets enabled when a connection is lost. This change also includes some WIP on submit support that I was too lazy to pull out. User visible changelist |
||
#23 | 8635 | Matt Attaway |
Move reconcile and sync calls off into threads to improve UI performance Not shockingly, running a giant p4 sync in the main thread is a bad idea. With this change I put on my grown up pants and move all of the potentially long running commands into their own threads. User visible change |
||
#22 | 8597 | Matt Attaway |
Handle disconnected servers automatically If Pulse loses its connection with the Perforce server it now reports that the connection is down and starts a new timer to look for a live connection. The Overseer handles all of this on its own; unlike login there’s really nothing for the user to do. This change also fixes a bug where the login dialog was not being properly regenerated for connections after the first. User visible change |
||
#21 | 8595 | Matt Attaway |
WIP on handling connection errors With this change we report the errors to our delegate, but we don’t attempt to recover. That will come with the next checkin. This change does add in the protocol for handling these errors. User visible change |
||
#20 | 8594 | Matt Attaway | Custom message for completed sync after initial sync | ||
#19 | 8592 | Matt Attaway |
Add less aggressive notification when background processes detect need to login Instead of popping a dialog up right in your face we use a standard notification and change the status icon to indicate Pulse is unhappy. Using the login menu item will log the user is and restart the background processes. User visible change. |
||
#18 | 8591 | Matt Attaway |
Make the notification text for a single change more useful. Pulse now gives the change number, author, and some of the change description when syncing a single file. I think I might swap out the change number with the number of files changed, but that’s for later. User visible change. |
||
#17 | 8590 | Matt Attaway |
Add crude login capabilities This is the first step toward getting the login behavior I want. Right now anytime a Peforce command gets a login error the Overseer ion charge notifies its delegate, which is currently the AppDelegate. The AppDelegate then kicks off the requisite login dialog which gets the password and does the login. There’s no error handling or feedback and the dialog pops up even when it is a background process that hit the error. The next step is to implement the Login menu item and the ‘need login’ tool tip and status icon. User visible (and possibly user annoying) change |
||
#16 | 8575 | Matt Attaway |
Automatically handle Unicode servers; stub out login handling There's a lot going on in this change: * Fixed a bug where charset was always set to none * Added protocol on PLSOverseer so that something can properly handle login errors * PLSAppDelegate implements stubs for the PLSOverseer protocol * PLSOverseers automatically configure themselves for Unicode servers now * Ripped out handleCommonErrors: because it turns out I want to handle the same error in multiple ways. Next step is to properly handle 'p4 login' and request a password. User visible changes. |
||
#15 | 8571 | Matt Attaway |
Expand connection properties for charset; add method to crudely handle common errors This is the next step towards handling login and unicode errors. The interfaces have been widened to track the charset. I’m using a string at this point so that I can just pass in the character set without having to test it; ‘none’ works with non-Unicode servers. The handler just logs what it would do so that I can test the behavior. Next step is to actually ask the user for their password and run login. Still no real functional change. |
||
#14 | 8569 | Matt Attaway |
Rejigger P4Connection to use a standard return type The previous mechanism made it hard to return anything but error messages. With the P4Message object we can return dictionaries, lists, and messages with a single object. I can use the new object more intelligently, but that’s for another change. I also reorganized the code to separate the ‘p4’ layer off into its own potentially reusable bucket. No functional change. |
||
#13 | 8555 | Matt Attaway | Replace hardcoded defaultsfor new connections with user specific values. | ||
#12 | 8553 | Matt Attaway | Add a bit of extra logging while I work out this occasional hang | ||
#11 | 8552 | Matt Attaway |
Properly pluralize when handling just one changelist. I probably need to rethink the notification titles; changelist counts are less useful when most syncs are just one changelist. |
||
#10 | 8537 | Matt Attaway |
Rework Pulse startup to (sorta) support real users Instead of repopulating the system with my test data it now pops up the connection dialog if there are no existing connections. This change also fixes a bug where the PLSConnection object wasn’t being refreshed when settings were changed on an Overseer. |
||
#9 | 8527 | Matt Attaway |
Improve query efficiency and improve feedback to user This change adds a check for the highest synced changed. Instead of blindly running sync over and over Pulse now runs ‘p4 changes -m 1 //<client>/...' and if the number is higher than the stored value it runs sync and then stores the new highest change. Highest change numbers are written to the preferences to further reduce needless queries. With this change we also fetch the list of changes between the previous high change and the new one so that we can report the number of changes synced. |
||
#8 | 8525 | Matt Attaway |
Refactor the Perforce interaction out of the Overseer into a separate class No functional change, hopefully. This should just simplify the Overseer code and set me up to have a proper place to run Perforce commands. |
||
#7 | 8524 | Matt Attaway | Stop trying to add files that are hidden (start with ‘.’) It just ends in tears and revert -k’s. | ||
#6 | 8523 | Matt Attaway |
Prevent double start call to FSEvents This was a bit of shuffling, but the behavior is much tidier now. Overseers do not start overseeing until they are explicitly told to do so. This makes their behavior much more predictable. |
||
#5 | 8522 | Matt Attaway |
Clean up the behavior of the connections dialog All of the Overseers are now paused while the connection dialog is up so that the user can fiddle with their settings in peace. |
||
#4 | 8519 | Matt Attaway |
Finally add a connections dialog to manage connections It’s taken a week to figure out how to manage windows and tabelviews, but this change adds a crude connection dialog to add and remove connections as well as save them out to the preferences file. More to do to make it more sane, but it works for now! |
||
#3 | 8511 | Matt Attaway |
Get a very crude form of reconcile and sync working. Continuing in my purely experimental mode, this change adds rough versions of the core features: 1) Every 5 seconds a timer fires and runs sync to grab new files. The user is made aware of the synced files via an OS X notification. 2) When files are modified ‘p4 reconcile’ is kicked off on the entire workspace Of course there is no error or sanity checking, but it is technically usable for very rough definitions of usable. |
||
#2 | 8509 | Matt Attaway |
Add support for loading multiple connections The code is rough, but Pulse can now load up and start watching multiple paths, each associated with a separate Perforce server. Also cleaned up some white space and normalized the ‘*’ used to denote pointers. |
||
#1 | 8506 | Matt Attaway |
Pull model out of controller; disable dock icon No functional change, but the code is moderately more sane. This change introduces an Overseer object that is responsible for maintaining one path/Port/User/Client combination. All of the delegation responsibilities move to the Overseer, as does FileSystemWatcher configuration. Overall this just sets me up to actually monitor multiple paths. |