// // P4Connection.m // Perforce // // Created by Adam Czubernat on 04/10/2013. // Copyright (c) 2013 Perforce Software, Inc. All rights reserved. // #import "P4Connection.h" #import "clientapi.h" #import "clientprog.h" #import "errornum.h" #import "i18napi.h" NSString * const P4SessionExpiredNotification = @"P4SessionExpiredNotification"; NSString * const P4Domain = @"com.perforce"; NSString * NSStringFromStrPtr(const StrPtr *str); NSDictionary * NSDictionaryFromStrDict(const StrDict *dict); NSError * NSErrorFromError(const Error *error); class P4ClientApi; class P4Client; class P4Progress; #pragma mark - Private Interfaces - #pragma mark P4Operation @interface P4Operation () { @protected P4Connection *connection; P4ReceiveBlock_t receiveBlock; P4ResponseBlock_t responseBlock; NSMutableArray *response; NSMutableArray *errors; } - (id)initWithConnection:(P4Connection *)connection receive:(P4ReceiveBlock_t)receiveBlock response:(P4ResponseBlock_t)responseBlock; - (void)execute; // Override - (void)output:(id)value; - (void)error:(NSError *)error; - (void)update:(long)update total:(long)updateTotal; - (void)updateDescription:(NSString *)description units:(NSString *)updateUnits; @end #pragma mark P4Connection @interface P4Connection () { @protected NSOperationQueue *queue; NSString *name; } @property (nonatomic, assign) BOOL connected; @property (nonatomic, assign) P4ClientApi *clientApi; @end #pragma mark P4ClientApi class P4ClientApi : public ClientApi { }; #pragma mark P4Client class P4Client : public ClientUser, public KeepAlive { P4CommandOperation *operation; P4Progress *progressObject = nil; public: P4Client(P4CommandOperation *operation); ~P4Client(); virtual void InputData(StrBuf *, Error *); virtual void HandleError(Error *err); virtual void Message(Error *err); virtual void OutputError(const char *errBuf); virtual void OutputInfo(char level, const char *data); virtual void OutputBinary(const char *data, int length); virtual void OutputText(const char *data, int length); virtual void OutputStat(StrDict *varList); virtual void Prompt(const StrPtr &msg, StrBuf &rsp, int noEcho, Error *e); virtual void Prompt(const StrPtr &msg, StrBuf &rsp, int noEcho, int noOutput, Error *e); virtual void ErrorPause(char *errBuf, Error *e); virtual void Edit(FileSys *f1, Error *e); virtual void Diff(FileSys *f1, FileSys *f2, int doPage, char *diffFlags, Error *e); virtual void Diff(FileSys *f1, FileSys *f2, FileSys *fout, int doPage, char *diffFlags, Error *e); virtual void Merge(FileSys *base, FileSys *leg1, FileSys *leg2, FileSys *result, Error *e); virtual int Resolve(ClientMerge *m, Error *e); virtual int Resolve(ClientResolveA *r, int preview, Error *e); virtual void Help(const char *const *help); virtual ClientProgress *CreateProgress(int type); virtual int ProgressIndicator(); virtual void Finished(); // KeepAlive virtual int IsAlive(); }; #pragma mark P4Progress class P4Progress : public ClientProgress { P4Operation *operation; public: P4Progress(P4Operation *operation, int type); ~P4Progress(); void Description(const StrPtr *description, int units); void Total(long); int Update(long); void Done(int fail); }; #pragma mark - Implementation #pragma mark - P4Operation @implementation P4Operation @synthesize response, errors; @synthesize progress, progressDescription, completed, total, units; - (NSError *)error { if (!errors.count) return nil; NSArray *descriptions = [errors valueForKeyPath:@"localizedDescription"]; NSError *error = [NSError errorWithFormat:@"%@", [descriptions componentsJoinedByString:@"\n"]]; return error; } - (NSArray *)errorsWithCode:(P4Error)errorCode { NSIndexSet *indexes = [errors indexesOfObjectsPassingTest: ^BOOL(NSError *error, NSUInteger idx, BOOL *stop) { return error.code == errorCode; }]; return indexes.count ? [errors objectsAtIndexes:indexes] : nil; } - (void)ignoreErrors:(NSArray *)array { [errors removeObjectsInArray:array]; if (!errors.count) errors = nil; } - (void)ignoreErrorsWithCode:(P4Error)errorCode { NSIndexSet *indexes = [errors indexesOfObjectsPassingTest: ^BOOL(NSError *error, NSUInteger idx, BOOL *stop) { return error.code == errorCode; }]; [errors removeObjectsAtIndexes:indexes]; if (!errors.count) errors = nil; } - (id)init { self = [super init]; PSInstanceCreated([self class]); return self; } - (id)initWithConnection:(P4Connection *)conn receive:(P4ReceiveBlock_t)rcvBlock response:(P4ResponseBlock_t)rspBlock { self = [self init]; if (self) { connection = conn; receiveBlock = [rcvBlock copy]; responseBlock = [rspBlock copy]; } return self; } - (void)main { PSLog(@"P4 > %@", self); // Execute concrete command [self execute]; // Launch completion block on main thread dispatch_sync(dispatch_get_main_queue(), ^{ if (responseBlock) responseBlock(self, response); // Log unhandled errors for (NSError *error in errors) PSLog(@"Error > %ld \"%@\"", error.code, error.localizedDescription); }); } - (void)execute { NSAssert(0, @"Not implemented"); } - (void)output:(id)value { if (!response) response = [NSMutableArray array]; [response addObject:value]; } - (void)error:(NSError *)error { NSAssert(error, @"Assigning nil error"); if (error.code == P4ErrorSessionExpired) [[NSNotificationCenter defaultCenter] postNotificationName:P4SessionExpiredNotification object:error]; if (!errors) errors = [NSMutableArray array]; [errors addObject:error]; } - (void)update:(long)update total:(long)updateTotal { if (updateTotal) { total = updateTotal; progress = completed / (float)total; PSLog(@"Progress total: %ld", total); } if (update) { completed += update; progress = completed / (float)total; PSLog(@"Progress update: %5.2f %7ld/%-8ld %@", progress, completed, total, units); } [[NSOperationQueue mainQueue] addOperationWithBlock:^{ if (receiveBlock) receiveBlock(self); }]; } - (void)updateDescription:(NSString *)description units:(NSString *)updateUnits { if (description) progressDescription = description; if (updateUnits) units = updateUnits; PSLog(@"Progress description: %@ units: %@", description, updateUnits); [[NSOperationQueue mainQueue] addOperationWithBlock:^{ if (receiveBlock) receiveBlock(self); }]; } - (void)dealloc { PSInstanceDeallocated([self class]); } @end @implementation P4ConnectOperation @synthesize host, username; - (NSString *)description { return [NSString stringWithFormat:@"connect %@ %@@%@", connection.name, username, host]; } - (void)execute { if (!host.length || !username.length) { [self error:[NSError errorWithFormat:@"Invalid credentials"]]; return; } Error internalError; P4ClientApi *clientApi = connection.clientApi; if (clientApi) { clientApi->Final(&internalError); internalError.Clear(); delete clientApi; } clientApi = new P4ClientApi(); clientApi->SetProtocol("tag", ""); clientApi->SetProtocol("enableStreams",""); /* Unicode servers clientApi->SetTrans(CharSetApi::CharSet::UTF_8); */ clientApi->SetPort(host.UTF8String); clientApi->SetUser(username.UTF8String); // Connect with server clientApi->Init(&internalError); // Check for connection error if (internalError.IsError()) [self error:NSErrorFromError(&internalError)]; // Assign values to connection connection.connected = !errors; connection.clientApi = clientApi; } @end @implementation P4CommandOperation @synthesize command, arguments, prompt, input; - (NSString *)description { NSMutableString *string = command.mutableCopy; if (arguments.count) [string appendFormat:@" %@", [arguments componentsJoinedByString:@" "]]; if (input) [string appendFormat:@" : %@", input]; return string; } - (void)execute { if (![connection isConnected]) { [self error:[NSError errorWithFormat:@"Not connected"]]; return; } NSInteger retries = 1; Error internalError; // Make arguments array char **argv = (char **)malloc(sizeof(char *) * arguments.count); for (NSUInteger idx=0; idxDropped()) { retries--; // Finalize connection clientApi->Final(&internalError); internalError.Clear(); errors = nil; // Reconnect PSLog(@"Reconnecting > %@", self); clientApi->Init(&internalError); // Check for errors if (internalError.IsError()) { [self error:NSErrorFromError(&internalError)]; continue; } } // Run command clientApi->SetArgv((int)arguments.count, argv); clientApi->SetBreak(client); clientApi->Run([command UTF8String], client); } while (clientApi->Dropped() && !self.isCancelled && retries); // Cleanup clientApi->SetBreak(NULL); delete client; free(argv); // Handle unknown errors if (!response && !errors) { if (self.isCancelled) [self error:[NSError errorWithFormat:@"Cancelled"]]; else if (retries < 0) [self error:[NSError errorWithFormat:@"Connection dropped"]]; } } @end @implementation P4ThreadOperation @synthesize block; - (NSString *)description { return @"Thread operation"; } - (void)execute { if (block) block(self); block = nil; } - (void)addError:(NSError *)error { [self error:error]; } @end #pragma mark - P4Connection @implementation P4Connection @synthesize connected, clientApi; - (NSString *)description { return name; } - (id)initWithName:(NSString *)aName { self = [super init]; if (self) { name = aName; PSInstanceCreated([self class]); } return self; } - (id)initWithConnection:(P4Connection *)connection name:(NSString *)aName { self = [self initWithName:aName]; if (self) { [self connectWithHost:NSStringFromStrPtr(&connection.clientApi->GetPort()) username:NSStringFromStrPtr(&connection.clientApi->GetUser()) response:^(P4Operation *operation, NSArray *response) { [self setWorkspace:NSStringFromStrPtr(&connection.clientApi->GetClient()) root:NSStringFromStrPtr(&connection.clientApi->GetCwd())]; }]; } return self; } - (id)init { return [self initWithName:@"default"]; } - (void)dealloc { PSInstanceDeallocated([self class]); } - (void)connectWithHost:(NSString *)host username:(NSString *)username response:(P4ResponseBlock_t)responseBlock { [queue cancelAllOperations]; queue = [[NSOperationQueue alloc] init]; queue.name = @"com.perforce.connection"; queue.maxConcurrentOperationCount = 1; P4ConnectOperation *operation = [[P4ConnectOperation alloc] initWithConnection:self receive:nil response:responseBlock]; operation.host = host; operation.username = username; [queue addOperation:operation]; } - (BOOL)isConnected { return connected; } - (void)disconnect { Error internalError; [queue cancelAllOperations]; queue = nil; if (clientApi) clientApi->Final(&internalError); if (internalError.IsError()) { NSError *error = NSErrorFromError(&internalError); PSLog(@"Disconnect error %ld %@", error.code, error.localizedDescription); } delete clientApi; clientApi = NULL; connected = NO; } - (void)setWorkspace:(NSString *)workspace root:(NSString *)path { NSAssert(connected, @"Setting workspace of disconnected connection"); clientApi->SetClient(workspace.UTF8String); clientApi->SetCwd(path.UTF8String); } - (NSString *)ticket { const StrPtr &ticket = clientApi->GetPassword(); if (!ticket.Length()) return nil; return [NSString stringWithUTF8String:ticket.Text()]; } - (void)setTicket:(NSString *)ticket { clientApi->SetPassword([ticket UTF8String]); } - (P4Operation *)run:(NSString *)command response:(P4ResponseBlock_t)responseBlock { NSArray *params = [command arrayOfArguments]; NSString *cmd = [params objectAtIndex:0]; NSArray *arguments = [params subarrayWithRange:NSMakeRange(1, params.count-1)]; return [self run:cmd arguments:arguments prompt:nil input:nil receive:nil response:responseBlock]; } - (P4Operation *)run:(NSString *)command arguments:(NSArray *)args response:(P4ResponseBlock_t)responseBlock { NSMutableArray *arguments = [NSMutableArray arrayWithCapacity:args.count]; for (id arg in args) { if ([arg isKindOfClass:[NSString class]]) [arguments addObject:arg]; else if ([arg isKindOfClass:[NSArray class]]) [arguments addObjectsFromArray:arg]; else [arguments addObject:[arg description]]; } return [self run:command arguments:arguments prompt:nil input:nil receive:nil response:responseBlock]; } - (P4Operation *)run:(NSString *)command arguments:(NSArray *)args prompt:(NSString *)prompt input:(NSDictionary *)input receive:(P4ReceiveBlock_t)receiveBlock response:(P4ResponseBlock_t)responseBlock { P4CommandOperation *operation = [[P4CommandOperation alloc] initWithConnection:self receive:receiveBlock response:responseBlock]; operation.command = command; operation.arguments = args; operation.prompt = prompt; operation.input = input; [queue addOperation:operation]; return operation; } - (void)runOperation:(NSOperation *)operation { [queue addOperation:operation]; } - (void)runBlock:(void (^)(P4ThreadOperation *))block { P4ThreadOperation *operation = [[P4ThreadOperation alloc] init]; [operation setBlock:block]; [queue addOperation:operation]; } - (NSString *)name { return name; } - (NSArray *)operations { return queue.operations; } - (void)cancelAllOperations { [queue cancelAllOperations]; } - (void)setNextOperation:(NSOperation *)operation { [queue setSuspended:YES]; NSArray *operations = [queue operations]; if (operations.count > 1) { NSOperation *next = [operations objectAtIndex:1]; if (next != operation) [next addDependency:operation]; } [queue setSuspended:NO]; } - (void)waitUntilAllOperationsAreFinished { [queue waitUntilAllOperationsAreFinished]; } - (void)setSuspended:(BOOL)flag { [queue setSuspended:flag]; } - (BOOL)isSuspended { return queue.isSuspended; } @end #pragma mark - P4Client P4Client::P4Client(P4CommandOperation *op) : operation(op) { } P4Client::~P4Client() { } void P4Client::InputData(StrBuf *buf, Error *err) { NSDictionary *input = operation.input; if (!input) return; NSMutableArray *specs = [NSMutableArray arrayWithCapacity:input.count]; [input enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { if ([value isKindOfClass:[NSString class]]) { // Escape newlines inside string with newline and tab value = [value stringByReplacingOccurrencesOfString:@"\n" withString:@"\n\t" options:kNilOptions range:(NSRange) { 0, [value length]-1 }]; NSString *spec = [NSString stringWithFormat:@"%@:\t%@", key, value]; [specs addObject:spec]; } else if ([value isKindOfClass:[NSNumber class]]) { // Append numeric value NSString *spec = [NSString stringWithFormat:@"%@:\t%@", key, value]; [specs addObject:spec]; } else if ([value isKindOfClass:[NSArray class]]) { // Separate components by newline with tab NSString *components = [value componentsJoinedByString:@"\n\t"]; NSString *spec = [NSString stringWithFormat:@"%@:\n\t%@", key, components]; [specs addObject:spec]; } }]; NSString *specsString = [specs componentsJoinedByString:@"\n"]; const char *specsCstring = [specsString UTF8String]; PSLog(@"Spec string: \n'%s'", specsCstring); buf->Set(specsCstring); } void P4Client::HandleError(Error *error) { [operation error:NSErrorFromError(error)]; } void P4Client::Message(Error *error) { ErrorId *errorId = error->GetId(0); NSInteger severity = error->GetSeverity(); NSInteger code = errorId->UniqueCode(); NSInteger subsystem = errorId->Subsystem(); if (severity > E_INFO || (subsystem == ES_DM && code != 6370)) { // Failure HandleError(error); } else { // Info output StrBuf buffer; error->Fmt(&buffer, EF_PLAIN); char *cstring = buffer.Text(); NSString *string = [NSString stringWithUTF8String:cstring]; if (string.length) [operation output:string]; } } void P4Client::OutputError(const char *errBuf) { NSCAssert(0, @"Not implemented"); } void P4Client::OutputInfo(char level, const char *data) { NSCAssert(0, @"Not implemented"); } void P4Client::OutputBinary(const char *data, int length) { NSCAssert(0, @"Not implemented"); } void P4Client::OutputText(const char *data, int length) { NSCAssert(0, @"Not implemented"); } void P4Client::OutputStat(StrDict *varList) { NSDictionary *dictionary = NSDictionaryFromStrDict(varList); if (dictionary) [operation output:dictionary]; } void P4Client::Prompt(const StrPtr &msg, StrBuf &response, int noEcho, Error *e) { response.Set(operation.prompt.UTF8String); } #pragma mark Unused overrides void P4Client::Prompt(const StrPtr &msg, StrBuf &rsp, int noEcho, int noOutput, Error *e) { NSCAssert(0, @"Not implemented"); } void P4Client::ErrorPause(char *errBuf, Error *e) { NSCAssert(0, @"Not implemented"); } void P4Client::Edit(FileSys *f1, Error *e) { NSCAssert(0, @"Not implemented"); } void P4Client::Diff(FileSys *f1, FileSys *f2, int doPage, char *diffFlags, Error *e) { NSCAssert(0, @"Not implemented"); } void P4Client::Diff(FileSys *f1, FileSys *f2, FileSys *fout, int doPage, char *diffFlags, Error *e) { NSCAssert(0, @"Not implemented"); } void P4Client::Merge(FileSys *base, FileSys *leg1, FileSys *leg2, FileSys *result, Error *e) { NSCAssert(0, @"Not implemented"); } int P4Client::Resolve(ClientMerge *m, Error *e) { NSCAssert(0, @"Not implemented"); return 0; } int P4Client::Resolve(ClientResolveA *r, int preview, Error *e) { NSCAssert(0, @"Not implemented"); return 0; } void P4Client::Help(const char *const *help) { NSCAssert(0, @"Not implemented"); } #pragma mark Progress ClientProgress * P4Client::CreateProgress(int type) { progressObject = new P4Progress(operation, type); return progressObject; } int P4Client::ProgressIndicator() { return 1; } void P4Client::Finished() { } #pragma mark KeepAlive int P4Client::IsAlive() { if (operation.isCancelled) { PSLog(@"Canceling operation %@", operation); return 0; } return 1; } #pragma mark - P4Progress P4Progress::P4Progress(P4Operation *op, int code) : operation(op) { int type = code; const char *name = NULL; if (type == CPT_SENDFILE) // Files sent to server name = "Sending"; else if (type == CPT_RECVFILE) // Files received from server name = "Receiving"; else if (type == CPT_FILESTRANS) // Files transmitted name = "Transmitted"; else if (type == CPT_COMPUTATION) // Computation performed server-side name = "Computation"; PSLog(@"Progress creating: %s", name); } P4Progress::~P4Progress() { PSLog(@"Progress dealloc"); } void P4Progress::Description(const StrPtr *str, int code) { const char *units = NULL; if (code == CPU_UNSPECIFIED) units = "None"; else if (code == CPU_PERCENT) units = "%"; else if (code == CPU_FILES) units = "files"; else if (code == CPU_KBYTES) units = "KB"; else if (code == CPU_MBYTES) units = "MB"; NSString *description = NSStringFromStrPtr(str); NSString *unitsString = [NSString stringWithUTF8String:units]; [operation updateDescription:description units:unitsString]; } void P4Progress::Total(long value) { [operation update:0 total:value]; } int P4Progress::Update(long update) { [operation update:update total:0]; if (operation.isCancelled) { PSLog(@"Progress cancelling..."); return 1; } return 0; } void P4Progress::Done(int fail) { PSLog(@"Progress done: %@", fail ? @"Fail" : @"Success"); } #pragma mark - C functions NSString * NSStringFromStrPtr(const StrPtr *str) { if (!str) return nil; NSString *string = [NSString stringWithUTF8String:str->Text()]; return string ?: [NSData dataWithBytes:str->Text() length:str->Length()]; } NSDictionary * NSDictionaryFromStrDict(const StrDict *dict) { StrRef var, val; NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; for (int i = 0; ((StrDict *)dict)->GetVar(i, var, val); i++) { if (var == P4Tag::v_specFormatted || var == P4Tag::v_func) continue; id value, key = NSStringFromStrPtr(&var); if (val.IsNumeric()) { if (var == P4Tag::v_time || var == "Update" || var == "Access") value = [NSDate dateWithTimeIntervalSince1970:val.Atoi()]; else if (var == "openattr-tags" || var == "attr-tags") value = NSStringFromStrPtr(&val); else value = @(val.Atoi()); } else { value = NSStringFromStrPtr(&val); } [dictionary setObject:value forKey:key]; } return dictionary; } NSError * NSErrorFromError(const Error *error) { // Get error description StrBuf buffer; error->Fmt(&buffer, EF_PLAIN); char *cstring = buffer.Text(); NSString *errorString = [NSString stringWithUTF8String:cstring]; // Get error code NSInteger code = error->GetId(0)->UniqueCode(); NSCAssert(errorString && code, @"Failed to create NSError from Error"); // Get error arguments StrDict *dict = ((Error *)error)->GetDict(); StrPtr *argc = dict->GetVar("argc") ? : dict->GetVar("depotFile"); NSString *errorArgs = NSStringFromStrPtr(argc); // Create info NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys: errorString, NSLocalizedDescriptionKey, errorArgs, NSLocalizedFailureReasonErrorKey, // Could be nil nil]; return [NSError errorWithDomain:P4Domain code:code userInfo:info]; }