// // PLSConnection.m // Pulse // // Created by Matt Attaway on 2/1/14. // Copyright (c) 2014 Zen of the Monkey. All rights reserved. // #import "P4Connection.h" #import "P4Message.h" @implementation P4Connection - (id) init { self = [super init]; if(self) { self.p4path = @"/usr/local/bin/p4"; } return self; } - (id) initWithSettings:(NSString*)port user:(NSString*)user client:(NSString*)client path:(NSString*)path charset:(NSString*)charset { self = [super init]; if(self) { self.path = path; self.port = port; self.user = user; self.client = client; self.p4path = @"/usr/local/bin/p4"; self.charset = charset; } return self; } - (P4Message*)revertPath:(NSString*)path options:(NSUInteger)opts { NSArray* paths = [[NSArray alloc] initWithObjects:path, nil]; return [self revertPaths:paths options:opts]; } - (P4Message*)revertPaths:(NSArray *)paths options:(NSUInteger)opts { // bail if no path has been provided. it makes Perforce sad if([paths count] == 0) { return [[P4Message alloc] initWithError:@"Did not run command. Need a path for revert"]; } NSTask* task = [[NSTask alloc] init]; task.launchPath = self.p4path; task.currentDirectoryPath = self.path; NSMutableArray* args = [[NSMutableArray alloc] initWithArray: @[@"-p", self.port, @"-u", self.user, @"-c", self.client, @"-d", self.path, @"-C", self.charset, @"revert"]]; // handle the command options if(opts & P4ConnectionRevertServerOnlyOption) { [args addObject:@"-k"]; } task.arguments = [args arrayByAddingObjectsFromArray:paths]; NSPipe* pipe = [NSPipe pipe]; [task setStandardOutput: pipe]; NSFileHandle* tfile = [pipe fileHandleForReading]; [task launch]; [task waitUntilExit]; NSData* data = [tfile readDataToEndOfFile]; P4Message* results = [[P4Message alloc] init]; results.message = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding]; return results; } - (P4Message*)submit:(NSString*)description { NSTask* task = [[NSTask alloc] init]; task.launchPath = self.p4path; task.currentDirectoryPath = self.path; NSString* command = @"submit"; task.arguments = @[@"-p", self.port, @"-u", self.user, @"-c", self.client, @"-d", self.path, @"-C", self.charset, command, @"-d", description]; NSPipe* pipe = [NSPipe pipe]; [task setStandardOutput: pipe]; NSFileHandle* tfile = [pipe fileHandleForReading]; [task launch]; [task waitUntilExit]; NSData* data = [tfile readDataToEndOfFile]; P4Message* results = [[P4Message alloc] init]; results.message = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding]; return results; } - (P4Message*) syncFiles:(NSString *)path { NSTask* task = [[NSTask alloc] init]; task.launchPath = self.p4path; task.currentDirectoryPath = self.path; NSString* command = @"sync"; task.arguments = @[@"-p", self.port, @"-u", self.user, @"-c", self.client, @"-d", self.path, @"-C", self.charset, command, path]; NSPipe* out = [NSPipe pipe]; [task setStandardOutput: out]; NSFileHandle* ofile = [out fileHandleForReading]; // NSPipe* err = [NSPipe pipe]; // [task setStandardError: err]; // NSFileHandle* efile = [err fileHandleForReading]; [task launch]; [task waitUntilExit]; NSData* odata = [ofile readDataToEndOfFile]; //NSData* edata = [efile readDataToEndOfFile]; P4Message* results = [[P4Message alloc] init]; results.message = [[NSString alloc] initWithData: odata encoding: NSUTF8StringEncoding]; return results; } - (P4Message*) reconcilePath:(NSString *)path options:(NSUInteger)opts { NSArray* paths = [[NSArray alloc] initWithObjects:path, nil]; return [self reconcilePaths:paths options:opts]; } - (P4Message*) reconcilePaths:(NSArray *)paths options:(NSUInteger)opts { NSTask* task = [[NSTask alloc] init]; task.launchPath = self.p4path; task.currentDirectoryPath = self.path; // XXX: hardcoded .p4ignore for now NSDictionary* env = [[NSProcessInfo processInfo] environment]; [env setValue:@".p4ignore" forKey:@"P4IGNORE"]; [task setEnvironment:env]; NSMutableArray* args = [[NSMutableArray alloc] initWithArray: @[@"-p", self.port, @"-u", self.user, @"-c", self.client, @"-d", self.path, @"-C", self.charset, @"reconcile", @"-f"]]; // handle the command options if(opts & P4ConnectionReconcileAddOption) { [args addObject:@"-a"]; } if(opts & P4ConnectionReconcileDeleteOption) { [args addObject:@"-d"]; } if(opts & P4ConnectionReconcileEditOption) { [args addObject:@"-e"]; } task.arguments = [args arrayByAddingObjectsFromArray:paths]; NSPipe* out = [NSPipe pipe]; [task setStandardOutput: out]; NSFileHandle* ofile = [out fileHandleForReading]; NSPipe* err = [NSPipe pipe]; [task setStandardError: err]; NSFileHandle* efile = [err fileHandleForReading]; [task launch]; [task waitUntilExit]; NSData* odata = [ofile readDataToEndOfFile]; NSData* edata = [efile readDataToEndOfFile]; P4Message* results = [[P4Message alloc] init]; results.message = [[NSString alloc] initWithData: odata encoding: NSUTF8StringEncoding]; results.error = [[NSString alloc] initWithData: edata encoding: NSUTF8StringEncoding]; return results; } - (P4Message*)getLatestChangeInClient { P4Message* results = [[P4Message alloc] init]; NSTask* task = [[NSTask alloc] init]; task.launchPath = self.p4path; task.currentDirectoryPath = self.path; NSDictionary* env = [[NSProcessInfo processInfo] environment]; [env setValue:@".p4ignore" forKey:@"P4IGNORE"]; [task setEnvironment:env]; NSString* clientPath = [NSString stringWithFormat:@"//%@/...", self.client ]; NSString* command = @"changes"; task.arguments = @[@"-ztag", @"-p", self.port, @"-u", self.user, @"-c", self.client, @"-d", self.path, @"-C", self.charset, command, @"-m1", clientPath ]; NSPipe* out = [NSPipe pipe]; [task setStandardOutput: out]; NSFileHandle* ofile = [out fileHandleForReading]; NSPipe* err = [NSPipe pipe]; [task setStandardError: err]; NSFileHandle* efile = [err fileHandleForReading]; [task launch]; int count = 0; while([task isRunning]) { [NSThread sleepForTimeInterval:1.0f]; count++; if(count > 10) { NSLog(@"I GOT TIRED OF WAITING"); results.error = @"Operation timed out for reasons I don't fully understand"; return results; } } NSData* odata = [ofile readDataToEndOfFile]; NSData* edata = [efile readDataToEndOfFile]; NSString* string = [[NSString alloc] initWithData: odata encoding: NSUTF8StringEncoding]; NSArray* lines = [string componentsSeparatedByString:@"\n"]; results.error = [[NSString alloc] initWithData: edata encoding: NSUTF8StringEncoding]; for( NSString* line in lines) { NSArray* words = [line componentsSeparatedByString:@" "]; if ([words count] > 1) { NSString* value = [[words subarrayWithRange:NSMakeRange(2, [words count] - 2)] componentsJoinedByString:@" "]; [results.results setObject:value forKey:[words objectAtIndex:1]]; } } return results; } - (P4Message*)getChanges:(NSInteger)high low:(NSInteger)low { NSTask* task = [[NSTask alloc] init]; task.launchPath = self.p4path; task.currentDirectoryPath = self.path; NSString* clientPath = [NSString stringWithFormat:@"//%@/...@%li,%li", self.client, low, high ]; NSString* command = @"changes"; task.arguments = @[@"-ztag", @"-p", self.port, @"-u", self.user, @"-c", self.client, @"-d", self.path, @"-C", self.charset, command, clientPath ]; NSPipe* out = [NSPipe pipe]; [task setStandardOutput: out]; NSFileHandle* ofile = [out fileHandleForReading]; NSPipe* err = [NSPipe pipe]; [task setStandardError: err]; NSFileHandle* efile = [err fileHandleForReading]; [task launch]; [task waitUntilExit]; NSData* odata = [ofile readDataToEndOfFile]; NSData* edata = [efile readDataToEndOfFile]; NSString* string = [[NSString alloc] initWithData: odata encoding: NSUTF8StringEncoding]; NSArray* lines = [string componentsSeparatedByString:@"\n"]; NSMutableDictionary* change = nil; P4Message* results = [[P4Message alloc] init]; results.error = [[NSString alloc] initWithData: edata encoding: NSUTF8StringEncoding]; for( NSString* line in lines) { NSArray* words = [line componentsSeparatedByString:@" "]; if ([words count] > 2 && [[words objectAtIndex:0] isEqualToString:@"..."]) { // create our dictionary if needs be if ([[words objectAtIndex:1] isEqual: @"change"]) { if(change) { [results.list addObject:change]; } change = [[NSMutableDictionary alloc] init]; } NSString* value = [[words subarrayWithRange:NSMakeRange(2, [words count] - 2)] componentsJoinedByString:@" "]; [change setObject:value forKey:[words objectAtIndex:1]]; } } if(change) { [results.list addObject:change]; } return results; } - (P4Message*)login:(NSString*)password { NSTask* task = [[NSTask alloc] init]; task.launchPath = self.p4path; task.currentDirectoryPath = self.path; NSString* command = @"login"; task.arguments = @[@"-p", self.port, @"-u", self.user, @"-c", self.client, @"-d", self.path, @"-C", self.charset, command]; NSPipe* out = [NSPipe pipe]; [task setStandardOutput: out]; NSFileHandle* ofile = [out fileHandleForReading]; NSPipe* err = [NSPipe pipe]; [task setStandardError: err]; NSFileHandle* efile = [err fileHandleForReading]; NSPipe* in = [NSPipe pipe]; [task setStandardInput: in]; NSFileHandle* ifile = [in fileHandleForWriting]; [task launch]; [ifile writeData: [password dataUsingEncoding: NSUTF8StringEncoding]]; [ifile closeFile]; [task waitUntilExit]; NSData* odata = [ofile readDataToEndOfFile]; NSData* edata = [efile readDataToEndOfFile]; P4Message* results = [[P4Message alloc] init]; results.error = [[NSString alloc] initWithData: edata encoding: NSUTF8StringEncoding]; results.message = [[NSString alloc] initWithData: odata encoding: NSUTF8StringEncoding]; return results; } - (BOOL)isConnected { NSTask* task = [[NSTask alloc] init]; task.launchPath = self.p4path; task.currentDirectoryPath = self.path; NSString* command = @"help"; task.arguments = @[@"-ztag", @"-p", self.port, @"-u", self.user, @"-c", self.client, @"-d", self.path, @"-C", self.charset, command]; NSPipe* err = [NSPipe pipe]; [task setStandardError: err]; NSFileHandle* efile = [err fileHandleForReading]; [task launch]; [task waitUntilExit]; NSData* edata = [efile readDataToEndOfFile]; P4Message* results = [[P4Message alloc] init]; results.error = [[NSString alloc] initWithData: edata encoding: NSUTF8StringEncoding]; return ![results isConnectionError]; } - (BOOL)isLoggedIn { NSTask* task = [[NSTask alloc] init]; task.launchPath = self.p4path; task.currentDirectoryPath = self.path; NSString* command = @"login"; task.arguments = @[@"-ztag", @"-p", self.port, @"-u", self.user, @"-c", self.client, @"-d", self.path, @"-C", self.charset, command, @"-s"]; NSPipe* err = [NSPipe pipe]; [task setStandardError: err]; NSFileHandle* efile = [err fileHandleForReading]; [task launch]; [task waitUntilExit]; NSData* edata = [efile readDataToEndOfFile]; P4Message* results = [[P4Message alloc] init]; results.error = [[NSString alloc] initWithData: edata encoding: NSUTF8StringEncoding]; return ![results isLoginError]; } @end
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#14 | 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. |
||
#13 | 8759 | Matt Attaway |
Move off of waitUntilExit when polling for changes One of the great moments in coding where I don’t entirely understand why this fixes things, but so far it appears using isRunning checks prevents the stalled processes I was seeing with waitUntilExit User visible bug fix |
||
#12 | 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 |
||
#11 | 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) |
||
#10 | 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 |
||
#9 | 8698 | Matt Attaway |
Add a method to run revert -k. Will hook up later into the reconcile code |
||
#8 | 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 |
||
#7 | 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 |
||
#6 | 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 |
||
#5 | 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 |
||
#4 | 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. |
||
#3 | 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. |
||
#2 | 8570 | Matt Attaway |
Start capturing and returning stderr as well as stdout In preparation for handlilng loggin in, start capturing and passing stderr to the calling method in the error portion of the P4Message. |
||
#1 | 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. |
||
//guest/matt_attaway/Pulse/Pulse/PLSConnection.m | |||||
#3 | 8544 | Matt Attaway | Hardcode P4IGNORE for now to properly ignore files taht should be ignored | ||
#2 | 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. |
||
#1 | 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. |