//
// 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","true");
/*
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; idx<arguments.count; idx++) {
NSString *arg = [arguments objectAtIndex:idx];
argv[idx] = (char *)[arg UTF8String];
}
// Create new client instance
P4Client *client = new P4Client(self);
P4ClientApi *clientApi = connection.clientApi;
do {
// Reconnect if connection dropped
if (clientApi->Dropped()) {
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];
}