// // 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 P4ErrorDomain = @"P4ErrorDomain"; NSString * const P4SessionExpiredNotification = @"P4SessionExpiredNotification"; 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)receive; - (void)finish; @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; int units = 0; long total = 0; long completed = 0; public: P4Progress(P4Operation *op, int code) : operation(op) { }; 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, timestamp, completed, total, parentOperation, name; - (void)setParentOperation:(P4Operation *)operation { if (parentOperation) [self removeDependency:parentOperation]; parentOperation = operation; [self addDependency:operation]; } - (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 *)aConnection receive:(P4ReceiveBlock_t)aReceiveBlock response:(P4ResponseBlock_t)aResponseBlock { self = [self init]; if (self) { connection = aConnection; receiveBlock = [aReceiveBlock copy]; responseBlock = [aResponseBlock copy]; } return self; } - (void)main { if (parentOperation.errors || parentOperation.isCancelled) return [self cancel], PSLog(@"P4 > cancelling > %@", self); PSLog(@"P4 > %@", self); timestamp = PSTimeStampMake(); // Launch receive to indicate start if (receiveBlock) [self performSelectorOnMainThread:@selector(receive) withObject:nil waitUntilDone:YES]; // Execute concrete command [self execute]; // Launch completion on main thread [self performSelectorOnMainThread:@selector(finish) withObject:nil waitUntilDone:YES]; } - (void)cancel { [super cancel]; if (!self.isExecuting) { receiveBlock = nil; responseBlock = nil; } } - (void)execute { NSAssert(0, @"Not implemented"); } - (void)output:(id)value { if (!response) response = [NSMutableArray array]; else if (receiveBlock) // Report incremental response as progress [self performSelectorOnMainThread:@selector(receive) withObject:nil waitUntilDone:YES]; [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)receive { P4ReceiveBlock_t block = receiveBlock; receiveBlock = nil; // Prevent recursive block(self); receiveBlock = block; } - (void)finish { if (responseBlock) responseBlock(self, response); receiveBlock = nil; responseBlock = nil; // Log unhandled errors for (NSError *error in errors) PSLog(@"Error > %ld \"%@\"", error.code, error.localizedDescription); } - (void)setTotal:(long)value { if (total == value || value < 0) return; total = value; PSLog(@"Progress total: %ld", total); [self setCompleted:completed]; } - (void)setCompleted:(long)value { completed = value; if (total < completed) total = completed; CGFloat updated = total ? completed / (CGFloat)total : 0.0f; if (fabs(updated - progress) < 0.01f) // 1% markup return; progress = updated; PSLog(@"Progress update: %5.2f %7ld/%-8ld MB", progress, completed /1024/1024, total /1024/1024); if (receiveBlock) [self performSelectorOnMainThread:@selector(receive) withObject:nil waitUntilDone:YES]; } - (void)setDescription:(NSString *)description { PSLog(@"Progress description: %@", description); if (receiveBlock) [self performSelectorOnMainThread:@selector(receive) withObject:nil waitUntilDone:YES]; } - (void)addDependency:(NSOperation *)operation { if (operation) // Escape nil [super addDependency:operation]; } - (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",""); clientApi->SetPort([host UTF8String]); clientApi->SetUser([username UTF8String]); // Set username and host permanently to p4 config clientApi->DefineUser([username UTF8String], NULL); clientApi->DefinePort([host UTF8String], NULL); // 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 = [NSMutableString string]; if (self.name) [string appendFormat:@"[%@] ", self.name]; [string appendString:command]; for (id arg in arguments) { [string appendFormat:@" %@", [arg isKindOfClass:[NSArray class]] ? [arg componentsJoinedByString:@" "] : arg]; } if (input) [string appendFormat:@" : { %ld values }", input.count]; return string; } - (void)execute { if (![connection isConnected]) { [self error:[NSError errorWithFormat:@"Not connected"]]; return; } NSInteger retries = 1; Error internalError; // Unwrap arguments NSMutableArray *args = [NSMutableArray arrayWithCapacity:arguments.count]; for (id arg in arguments) { if ([arg isKindOfClass:[NSString class]]) [args addObject:arg]; else if ([arg isKindOfClass:[NSArray class]]) [args addObjectsFromArray:arg]; else [args addObject:[arg description]]; } // Make arguments array char **argv = (char **)malloc(sizeof(char *) * args.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)args.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:@"Operation was cancelled"]]; else if (retries < 0) [self error:[NSError errorWithFormat:@"Connection dropped"]]; } } @end @implementation P4ThreadOperation @synthesize block; - (NSString *)description { return [NSString stringWithFormat:@"[%@]", self.name ?: @"thread operation"]; } - (void)execute { if (block) block(self); block = nil; } - (void)setResponseBlock:(P4ResponseBlock_t)aBlock { responseBlock = aBlock; } - (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 setCharset:NSStringFromStrPtr(&connection.clientApi->GetCharset())]; [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]); // Set client permanently to p4 config clientApi->DefineClient([workspace UTF8String], 0); } - (void)setCharset:(NSString *)set { clientApi->SetCharset(set.length ? [set UTF8String]: ""); clientApi->SetTrans(set.length ? CharSetApi::Lookup([set UTF8String]) : CharSetApi::NOCONV); } - (void)setIgnoreFile:(NSString *)path { clientApi->SetIgnoreFile(path.UTF8String); } - (NSString *)ticket { const StrPtr &ticket = clientApi->GetPassword(); if (!ticket.Length()) return nil; return NSStringFromStrPtr(&ticket); } - (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 { return [self run:command arguments:args 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]; } - (P4Operation *)runBlock:(void (^)(P4ThreadOperation *))block { P4ThreadOperation *operation = [[P4ThreadOperation alloc] init]; [operation setBlock:block]; [queue addOperation:operation]; return 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); NSString *string = NSStringFromStrPtr(&buffer); 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 void P4Progress::Description(const StrPtr *str, int code) { if (code == CPU_KBYTES) units = 1024; else if (code == CPU_MBYTES) units = 1024 * 1024; operation.description = NSStringFromStrPtr(str); } void P4Progress::Total(long value) { if (units) value *= units; operation.total += value; total = value; } int P4Progress::Update(long value) { if (units) value *= units; operation.completed += value - completed; completed = value; if (operation.isCancelled) { PSLog(@"Progress cancelling..."); return 1; } return 0; } #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); NSString *errorString = NSStringFromStrPtr(&buffer) ?: @""; // Get error code NSInteger code = error->GetId(0)->UniqueCode(); // 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:P4ErrorDomain code:code userInfo:info]; }