// // P4Server.m // p4scout // // Created by Work on 8/30/08. // Copyright 2008 Perforce Software, Inc. All rights reserved. // #import "P4Server.h" #import "P4DefaultsKeys.h" #import "P4ClientApi.h" #import "NSMutableArrayQueueAdditions.h" #import "errornum.h" #import #import #import #import #import #include static NSString * kPortKey = @"port"; static NSString * kUserKey = @"user"; static NSString * kLabelKey = @"label"; static NSString * kPasswordKey = @"password"; //static NSString * kReachableKey = @"connectionState"; static const NSString * const P4CommandFunctionKey = @"func"; static const NSString * const P4CommandArgumentsKey = @"args"; static const NSString * const P4CommandDelegateKey = @"delegate"; const NSDictionary * RetainedUserKeys = nil; NSString * P4ScoutErrorDomain = @"com.perforce.P4Scout"; NSString * P4ServerErrorReceivedNotification = @"P4ServerErrorReceived"; NSString * ActivePropertyKey = @"active"; NSString * InfoPropertyKey = @"info"; NSString * NumberOfUsersPropertyKey = @"numberOfUsers"; NSString * MonitorInfoPropertyKey = @"monitorInfo"; NSString * ConnectionStatePropertyKey = @"connectionState"; NSString * const LabelPropertyKey = @"label";; NSString * ReachablePropertyKey = @"reachable"; NSString * CurrentCommandPropertyKey = @"currentCommand"; // retrieved by looking at output of the error. Could also compare // the ErrorId objects in msgserver.h but I don't to include C++ if I don't have to. // For what it's worth, the code returned in the NSError is the code in the ErrorId // instances. int kP4TicketErrorCode = 322; const NSTimeInterval MonitorTaskTimeThreshold = (60 * 2); // 2 minutes static void ReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void *info); @interface P4Server () @property (readwrite, assign) BOOL reachable; @property (readwrite) ConnectionState connectionState; @property (readwrite) BOOL active; @property (readwrite) BOOL discovered; @property (readwrite) int numberOfUsers; @property (readwrite, copy) NSDictionary * currentCommand; @property (readwrite, copy) NSArray * monitorInfo; @property (readwrite, copy) NSDictionary * usersInMonitorTable; @property (readwrite) BOOL requiresTicket; @property (readonly) NSDate * expirationDate; // not adjusted for time-zone @property (readonly) NSDate * serverDate; // not adjusted for time-zone -(void)runNextOperation; -(void)connect; -(void)disconnect; -(void)startReachability; -(void)stopReachability; -(void)reachability:(SCNetworkReachabilityRef)reachability didUpdateWithFlags:(SCNetworkReachabilityFlags)flags; -(void)setCurrentCommand:(NSDictionary*)command; - (BOOL)isReachableWithoutRequiringConnection:(SCNetworkReachabilityFlags)flags; -(BOOL)getUserCount:(int *)userCount dateString:(NSString **)dateString fromLicenseString:(NSString *)license; @end @implementation P4Server @synthesize label, refreshRate, p4port, user, password, connectionState, numberOfUsers, currentUserExistsOnServer, lastError, requiresTicket, usersInMonitorTable = _usersInMonitorTable, reachable = _reachable, active = _active, currentCommand = _currentCommand, discovered = _discovered, expirationDate = _cachedExpirationDate, remoteHostname = _cachedRemoteHostName, monitorInfo = _cachedMonitorEntries; -(id)initAsDiscovered:(BOOL)discovered { if ( (self = [super init]) == nil ) return nil; cachedInfo = [[NSDictionary dictionary] retain]; commandQueue = [[NSMutableArray arrayWithCapacity:3] retain]; operationQueue = [[NSOperationQueue alloc] init]; _discovered = discovered; if ( discovered ) { _reachable = YES; } // Important!! We want to run ClientApi operations in another thread, but // only one at a time, otherwise, we'll be running multiple threads // against the same ClientApi. operationQueue.maxConcurrentOperationCount = 1; [self startReachability]; return self; } -(id)init { return [self initAsDiscovered:NO]; } -(id)initWithCoder:(NSCoder*)coder { if ( (self = [super init]) == nil ) return nil; cachedInfo = [[NSDictionary dictionary] retain]; commandQueue = [[NSMutableArray arrayWithCapacity:3] retain]; operationQueue = [[NSOperationQueue alloc] init]; // Important!! We want to run ClientApi operations in another thread, but // only one at a time, otherwise, we'll be running multiple threads // against the same ClientApi. operationQueue.maxConcurrentOperationCount = 1; self.label = [coder decodeObjectForKey:kLabelKey]; self.p4port = [coder decodeObjectForKey:kPortKey]; self.user = [coder decodeObjectForKey:kUserKey]; self.password = [coder decodeObjectForKey:kPasswordKey]; return self; } -(void)dealloc { if ( _reachabilityRef ) { SCNetworkReachabilityUnscheduleFromRunLoop( _reachabilityRef, CFRunLoopGetMain(), kCFRunLoopDefaultMode ); CFRelease( _reachabilityRef ); } [taggedConnection release]; [timer release]; [cachedInfo release]; [_cachedExpirationDate release]; [_cachedMonitorEntries release]; [_cachedRemoteHostName release]; [_usersInMonitorTable release]; [commandQueue release]; [operationQueue release]; [password release]; [label release]; [user release]; [p4port release]; [super dealloc]; } -(void)encodeWithCoder:(NSCoder*)coder { [coder encodeObject:self.label forKey:kLabelKey]; [coder encodeObject:self.p4port forKey:kPortKey]; [coder encodeObject:self.user forKey:kUserKey]; [coder encodeObject:self.password forKey:kPasswordKey]; } -(BOOL)queueFunction:(NSString *)function arguments:(NSArray *)args { if (function == nil) return NO; NSDictionary * command = [NSDictionary dictionaryWithObjectsAndKeys: function, P4CommandFunctionKey, args, P4CommandArgumentsKey, nil]; if ( !self.reachable ) { // NSLog( @"%@ is unreachable, ignoring command %@", self, command ); return NO; } [commandQueue enqueueObject:command]; // If the connection is up and running, we can just use it if (taggedConnection != nil && taggedConnection.connected ) { [self runNextOperation]; return YES; } [self connect]; return YES; } -(NSString*)currentCommandDescription { if ( connectionState == Connecting ) return @"Connecting..."; if ( !self.active && lastUpdate != 0 ) { static NSDateFormatter * dateFormatter = nil; if (!dateFormatter) { dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setDateStyle:NSDateFormatterShortStyle]; [dateFormatter setTimeStyle:NSDateFormatterShortStyle]; } return [NSString stringWithFormat:@"Last update: %@", [dateFormatter stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:lastUpdate]]]; } NSString * function = [self.currentCommand objectForKey:P4CommandFunctionKey]; if ( [function isEqualToString:@"users"] ) return @"Updating users..."; if ( [function isEqualToString:@"monitor"] ) return @"Updating monitor..."; if ( [function isEqualToString:@"info"] ) return @"Updating info..."; return nil; } -(void)runNextOperation { if ( self.currentCommand != nil ) { // NSLog(@"runNextOperation - waiting for the current command to complete: %@", self.currentCommand); return; } if ( [commandQueue count] == 0 ) { // NSLog(@"runNextOperation - no current operation!" ); return; } NSDictionary * currentCommand = [commandQueue dequeueObject]; NSString * function = [currentCommand objectForKey:P4CommandFunctionKey]; NSArray * arguments = [currentCommand objectForKey:P4CommandArgumentsKey]; // Skip all the monitor invocations if we can't run monitor or if we would create a new user by doing so. // We shouldn't run it if our current user doesn't exists otherwise we'd create a user while ( [function isEqualToString:@"monitor"] && (!self.monitorEnabled || !currentUserExistsOnServer) ) { currentCommand = [commandQueue dequeueObject]; function = [currentCommand objectForKey:P4CommandFunctionKey]; arguments = [currentCommand objectForKey:P4CommandArgumentsKey]; } [self setCurrentCommand:currentCommand]; if ( currentCommand == nil ) { // close the connection if it is open. // we've got nothing else to run [self disconnect]; return; } // NSLog(@"running: %@", self.currentCommand ); // In case we are now running the users command, reset the user data if ( [function isEqualToString:@"users"] ) { taggedConnection.userInfo = [NSMutableDictionary dictionaryWithCapacity:4]; newNumberOfUsers = 0; self.usersInMonitorTable = nil; currentUserExistsOnServer = NO; } else if ( [function isEqualToString:@"monitor"] ) { self.monitorInfo = nil; //[cachedMonitor removeAllObjects]; taggedConnection.userInfo = [NSMutableArray arrayWithCapacity:4]; } taggedConnection.programIdentifier = @"P4Scout"; NSInvocation * invocation = [taggedConnection invocationForRunCommand:function withArguments:arguments delegate:self]; NSInvocationOperation * operation = [[NSInvocationOperation alloc] initWithInvocation:invocation]; [operationQueue addOperation:operation]; [operation release]; } -(void)setRefreshRate:(NSTimeInterval)time { [timer release]; timer = nil; if ( time > 1.0 ) { if ( timer == nil ) timer = [[NSTimer scheduledTimerWithTimeInterval:time target:self selector:@selector(refresh:) userInfo:nil repeats:YES] retain]; refreshRate = time; } } -(NSString *)remoteHostname { if ( _cachedRemoteHostName ) return _cachedRemoteHostName; if ( !p4port ) return @"perforce"; NSRange r = [p4port rangeOfString:@":" options:NSBackwardsSearch]; if (r.location == NSNotFound) { // resolve? _cachedRemoteHostName = [p4port retain]; } else { _cachedRemoteHostName = [[p4port substringToIndex:r.location] retain]; } return _cachedRemoteHostName; } -(NSString*)description { return [NSString stringWithFormat:@"%@, %@", [self remoteHostname], self.user]; } -(void)setP4port:(NSString *)p { if ( [p4port isEqualToString:p] ) return; [_cachedRemoteHostName release]; _cachedRemoteHostName = nil; self.monitorInfo = nil; self.usersInMonitorTable = nil; [p4port autorelease]; p4port = [p copy] ; // restart reachability now that we have a new name [self stopReachability]; [self startReachability]; } -(void)startReachability { if (self.discovered) return; NSAssert(!_reachabilityRef, @"Reachability already running!"); const char * addressName = [self.remoteHostname UTF8String]; struct sockaddr_in sin; bzero(&sin, sizeof(sin)); sin.sin_len = sizeof(sin); sin.sin_family = AF_INET; int result; bool isNumericalIp = YES; result = inet_aton(addressName, &sin.sin_addr); if ( result == 1 ) { _reachabilityRef = SCNetworkReachabilityCreateWithAddress( kCFAllocatorDefault, (struct sockaddr *)&sin ); isNumericalIp = YES; } else { struct sockaddr_in6 sin6; bzero(&sin6, sizeof(sin6)); sin6.sin6_len = sizeof(sin6); sin6.sin6_family = AF_INET6; result = inet_pton(AF_INET6, addressName, &sin6.sin6_addr); if ( result == 1 ) { _reachabilityRef = SCNetworkReachabilityCreateWithAddress( kCFAllocatorDefault, (struct sockaddr *)&sin6 ); isNumericalIp = YES; } else { _reachabilityRef = SCNetworkReachabilityCreateWithName( kCFAllocatorDefault, [self.remoteHostname UTF8String] ); isNumericalIp = NO; } } SCNetworkReachabilityContext context; context.info = self; context.retain = CFRetain; context.release = CFRelease; SCNetworkReachabilitySetCallback( _reachabilityRef, ReachabilityCallback, &context ); SCNetworkReachabilityScheduleWithRunLoop( _reachabilityRef, CFRunLoopGetMain(), kCFRunLoopDefaultMode ); if (isNumericalIp) { SCNetworkReachabilityFlags flags; SCNetworkReachabilityGetFlags( _reachabilityRef, &flags ); [self reachability:_reachabilityRef didUpdateWithFlags:flags]; } } -(void)stopReachability { if (self.discovered) return; if ( !_reachabilityRef ) return; SCNetworkReachabilityUnscheduleFromRunLoop( _reachabilityRef, CFRunLoopGetMain(), kCFRunLoopDefaultMode ); CFRelease( _reachabilityRef ); _reachabilityRef = NULL; } - (BOOL)isReachableWithoutRequiringConnection:(SCNetworkReachabilityFlags)flags { // kSCNetworkReachabilityFlagsReachable indicates that the specified nodename or address can // be reached using the current network configuration. BOOL isReachable = flags & kSCNetworkReachabilityFlagsReachable; // This flag indicates that the specified nodename or address can // be reached using the current network configuration, but a // connection must first be established. // // If the flag is false, we don't have a connection. But because CFNetwork // automatically attempts to bring up a WWAN connection, if the WWAN reachability // flag is present, a connection is not required. BOOL noConnectionRequired = !(flags & kSCNetworkReachabilityFlagsConnectionRequired); if ((flags & kSCNetworkReachabilityFlagsIsWWAN)) { noConnectionRequired = YES; } return (isReachable && noConnectionRequired) ? YES : NO; } -(void)reachability:(SCNetworkReachabilityRef)reachability didUpdateWithFlags:(SCNetworkReachabilityFlags)flags { BOOL newReachability = [self isReachableWithoutRequiringConnection:flags]; if ( newReachability == _reachable ) return; self.reachable = newReachability; if ( _reachable ) [self refresh:self]; // NSLog( @"Reachability updated for %@. Status is %d", self, [self isReachableWithoutRequiringConnection:flags] ); } -(void)setLastError:(NSError *)e { [e retain]; [lastError release]; lastError = e; } -(NSString *)hostname { NSString * hn = nil; if (!taggedConnection) { // TODO: I should cache, or calculate the hostname from the p4port value P4ClientApi * api = [[P4ClientApi alloc] init]; hn = api.hostname; [api release]; } else { hn = [taggedConnection hostname]; } return hn; } -(BOOL)reachable { // servers discovered by Bonjour are considered reachable return _discovered || _reachable; } -(NSDictionary*)info { return cachedInfo; } -(BOOL)hasAlert { if (self.connectionState != Online ) return NO; if (self.hasLicense && self.remainingLicenseTime < (60*60*24*7)) return YES; if (self.lastError != nil) return YES; int max = self.maximumUserCount; int remainingUsers = 0; float progress = 0; if ( max ) { remainingUsers = max - self.numberOfUsers; progress = (float)self.numberOfUsers / (float)max; } // show users warning if ( progress > 0.95 && remainingUsers < 5 ) return YES; if ( [self.monitorInfo count] > 0 ) return YES; return NO; } -(BOOL)active { return _active; } -(BOOL)getUserCount:(int *)userCount dateString:(NSString **)dateString fromLicenseString:(NSString *)license { // ... serverLicense Perforce Public Depot 2000 users (expires 2010/01/30) if ( !license ) return NO; int savedUserCount = 0; NSString * savedDateString = nil; if ( [license isEqualToString:@"none"]) { savedUserCount = 2; goto end; } NSArray * licenseComponents = [license componentsSeparatedByString:@" "]; int i = 0; for (NSString * license in licenseComponents) { if ([license isEqualToString:@"users"]) break; i++; } // if "users" was not in the license string if ( i == 0 || i == [licenseComponents count] ) return NO; savedUserCount = [[licenseComponents objectAtIndex:i-1] intValue]; // find the date in the license. This is complicated because sometimes // the last character in the license might be a space in which case // licenseComponents contains an empty string as its final item int j = [licenseComponents count] - 1; for ( ; j >=0; j-- ) { NSString * license = [licenseComponents objectAtIndex:j]; if ( [license characterAtIndex:[license length] - 1] == ')' ) { savedDateString = license; break; } } if ( savedDateString == nil ) goto end; // savedDateString looks like this "2010/01/30)" // so tear off that last ')' // "2010/01/30" savedDateString = [savedDateString substringToIndex:[savedDateString length]-1]; end: if ( userCount ) *userCount = savedUserCount; if ( dateString ) *dateString = savedDateString; return YES; } -(int)maximumUserCount { // ... serverLicense Perforce Public Depot 2000 users (expires 2010/01/30) NSString * s = [self.info objectForKey:@"serverLicense"]; if ( !s ) return 0; int maxUserCount = 0; [self getUserCount:&maxUserCount dateString:NULL fromLicenseString:s]; return maxUserCount; } -(BOOL)hasLicense { NSString * s = [self.info objectForKey:@"serverLicense"]; if ( !s ) return NO; return ( ![s isEqualToString:@"none"] ); } -(NSTimeInterval)remainingLicenseTime { if ( !self.info ) return 0; if ( !self.hasLicense ) return 1000.0*365.0*24.0*60.0*60.0; // 1000 years NSDate * expDate = self.expirationDate; NSDate * serverDate = self.serverDate; return [expDate timeIntervalSinceDate:serverDate]; } -(NSDate*)expirationDate { if ( _cachedExpirationDate ) return _cachedExpirationDate; // ... serverLicense Perforce Public Depot 2000 users (expires 2010/01/30) NSString * s = [self.info objectForKey:@"serverLicense"]; if ( !s ) return nil; if ( [s isEqualToString:@"none"]) return nil; NSString * licenseDateString = nil; [self getUserCount:NULL dateString:&licenseDateString fromLicenseString:s]; if ( licenseDateString == nil ) return nil; // we could adjust for the server time zone here with the "serverDate" part of p4 info static NSDateFormatter * f = nil; if (!f) { f = [[NSDateFormatter alloc] init]; [f setDateFormat:@"yyyy/MM/dd"]; } _cachedExpirationDate = [[f dateFromString:licenseDateString] retain]; // if ( !cachedExpirationDate ) // NSLog(@"cachedExpirationDate is nil"); return _cachedExpirationDate; } -(NSDate*)serverDate { // ... serverDate 2008/12/05 16:10:43 -0800 PST NSString * s = [self.info objectForKey:@"serverDate"]; if ( !s ) return nil; static NSDateFormatter * f = nil; if ( !f ) { f = [[NSDateFormatter alloc] init]; [f setDateFormat:@"yyyy/MM/dd kk:mm:ss"]; } NSDate * d = [f dateFromString:s]; return d; } -(NSString*)simpleVersion { // ... serverVersion P4D/FREEBSD60X86_64/2008.1/168182 (2008/10/10) NSString * s = [self.info objectForKey:@"serverVersion"]; if ( !s ) return nil; NSArray * a = [s componentsSeparatedByString:@" "]; // NSString * versionDate = [a objectAtIndex:1]; a = [[a objectAtIndex:0] componentsSeparatedByString:@"/"]; return [NSString stringWithFormat:@"%@/%@", [a objectAtIndex:2], [a objectAtIndex:3]]; } -(NSString*)platform { // ... serverVersion P4D/FREEBSD60X86_64/2008.1/168182 (2008/10/10) NSString * s = [self.info objectForKey:@"serverVersion"]; if ( !s ) return nil; NSArray * a = [s componentsSeparatedByString:@" "]; a = [[a objectAtIndex:0] componentsSeparatedByString:@"/"]; return [NSString stringWithFormat:@"%@/%@", [a objectAtIndex:0], [a objectAtIndex:1]]; } -(BOOL)unicodeEnabled { NSString * s = [self.info objectForKey:@"unicode"]; return [s isEqualToString:@"enabled"]; } -(BOOL)monitorEnabled { NSString * s = [self.info objectForKey:@"monitor"]; return [s isEqualToString:@"enabled"]; } -(NSTimeInterval)runningTimeForMonitorTask:(NSDictionary*)task { // ... time 76:30:46 NSArray * timeComponents = [[task objectForKey:@"time"] componentsSeparatedByString:@":"]; if ( !timeComponents ) return -1; NSTimeInterval processTime = 0; processTime += [[timeComponents objectAtIndex:0] intValue] * 60*60; processTime += [[timeComponents objectAtIndex:1] intValue] * 60; processTime += [[timeComponents objectAtIndex:2] intValue]; return processTime; } -(NSInteger)numberofIdleProcesses { int num = 0; NSArray * a = self.monitorInfo; // NSLog( @"Monitor info for %@ --------", self ); for (NSDictionary * d in a ) { if ( [[d objectForKey:@"command"] isEqualToString:@"IDLE"] ) num++; // NSLog( @"%@", d ); } // NSLog( @"--------", self ); return num; } -(NSArray*)idleProcesses { NSMutableArray * a = [NSMutableArray arrayWithCapacity:1]; NSArray * mi = self.monitorInfo; for (NSDictionary * d in mi ) { if ( ![[d objectForKey:@"command"] isEqualToString:@"IDLE"] ) continue; [a addObject:d]; } return a; } -(NSArray*)activeProcesses { NSMutableArray * a = [NSMutableArray arrayWithCapacity:1]; NSArray * mi = self.monitorInfo; for (NSDictionary * d in mi ) { if ( [[d objectForKey:@"command"] isEqualToString:@"IDLE"] ) continue; [a addObject:d]; } return a; } +(NSString*)apiDefaultUser { P4ClientApi * api = [[P4ClientApi alloc] init]; NSString * defaultUser = api.user; [api release]; return defaultUser; } +(NSString*)defaultUser { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSString * defaultUser = [defaults stringForKey:kDefaultUserPreferenceKey]; if ( !defaultUser || [defaultUser length] == 0 ) defaultUser = [self apiDefaultUser]; NSAssert(defaultUser, @"P4ClientApi user didn't return anything" ); return defaultUser; } -(IBAction)refresh:(id)sender { self.lastError = nil; if ( !self.reachable ) return; [self updateInfo:sender]; [self updateUsers:sender]; [self updateMonitorInfo:sender]; } -(IBAction)updateInfo:(id)sender { [cachedInfo release]; cachedInfo = nil; [self queueFunction:@"info" arguments:nil]; } -(IBAction)updateMonitorInfo:(id)sender { [self queueFunction:@"monitor" arguments:[NSArray arrayWithObject:@"show"]]; } -(IBAction)updateUsers:(id)sender { [self queueFunction:@"users" arguments:nil]; } -(void)connect { // start the connection. When we see that it started, we'll // run the operation in clientApiDidConnect: if ( self.connectionState == Connecting ) return; NSAssert( taggedConnection == nil, @"starting a new connection when we haven't niled out taggedConnection" ); taggedConnection = [[P4ClientApi alloc] init]; if (self.user) taggedConnection.user = self.user; else taggedConnection.user = [P4Server defaultUser]; taggedConnection.password = self.password; taggedConnection.callbackThread = [NSThread currentThread]; // now that we've received info, we can check to see if we are unicode- // enabled. If so, we need to set the charset so we can continue // to talk to the server if ( self.unicodeEnabled ) taggedConnection.charset = @"utf8"; // NSLog(@"Connecting to %@...", self); self.connectionState = Connecting; self.active = YES; NSDictionary * protocol = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt:64], P4ProtocolVersion, nil]; NSInvocation * invocation = [taggedConnection invocationForConnectToPort:p4port withProtocol:protocol delegate:self]; NSInvocationOperation * connectOperation = [[NSInvocationOperation alloc] initWithInvocation:invocation]; [operationQueue addOperation:connectOperation]; [connectOperation release]; } -(void)disconnect { // nothing to reset if ( !taggedConnection ) return; NSInvocation * invocation = [taggedConnection invocationForDisconnectWithDelegate:self]; NSInvocationOperation * disconnectOperation = [[NSInvocationOperation alloc] initWithInvocation:invocation]; [operationQueue addOperation:disconnectOperation]; [disconnectOperation release]; } // we could't connect -(void)clientApi:(P4ClientApi *) api didFailToConnectWithError:(NSError*)s { NSAssert( api == taggedConnection, @"api in callback and our connection differ!" ); // NSLog(@"failed to connect to %@", self.p4port); [taggedConnection release]; taggedConnection = nil; // NSLog(@"disconnected"); if (self.connectionState != Offline) self.connectionState = Offline; self.active = NO; [commandQueue removeAllObjects]; self.currentCommand = nil; // NSLog(@"Connect ERROR to %@...", self); } // we connected -(void)clientApiDidConnect:(P4ClientApi*)api { NSAssert( api == taggedConnection, @"api in callback and our connection differ!" ); // NSLog(@"connected"); if ( self.connectionState != Online ) self.connectionState = Online; [self runNextOperation]; } -(void)clientApi:(P4ClientApi*)api didDisconnectWithError:(NSError*)err { // NSLog(@"released API. disconnected from %@", self.p4port ? self.p4port : @"perforce:1666"); NSAssert( api == taggedConnection, @"api in callback and our connection differ!" ); [taggedConnection release]; taggedConnection = nil; // we release in the callback once it's finished self.active = NO; } -(void)clientApi:(P4ClientApi*)api didReceiveSimpleServerMessage:(NSString*)s level:(char)l { NSAssert( api == taggedConnection, @"api in callback and our connection differ!" ); ; // no-op just to supress default output } -(void)clientApi:(P4ClientApi*)api didReceiveTaggedResponse:(NSDictionary*)stat { NSAssert( api == taggedConnection, @"api in callback and our connection differ!" ); NSString * function = api.currentCommand; if ( [function isEqualToString:@"info"] ) { BOOL wasUnicodeEnabled = self.unicodeEnabled; [self willChangeValueForKey:InfoPropertyKey]; [_cachedExpirationDate release]; _cachedExpirationDate = nil; [cachedInfo release]; cachedInfo = [stat copy]; [self didChangeValueForKey:InfoPropertyKey]; // if we were not unicode enabled, but this server clearly needs unicode // we need to disconnect from the server so the next connection will // set itself to unicode enabled. The same if the server is suddenly // not unicode enabled, but it was the last time we talked to it. // (very, very unlikely) if (wasUnicodeEnabled != self.unicodeEnabled) { if ( self.unicodeEnabled ) taggedConnection.charset = @"utf8"; // We do not yet handle the case where a server returns // to non-unicode mode. This is highly unlikely and // requires the connection to be torn down and a new // new connection to be started. } } else if ( [function isEqualToString:@"users"] ) { newNumberOfUsers++; if ((newNumberOfUsers % 10) == 0) // update every 10 users self.numberOfUsers = newNumberOfUsers; // save the user dictionary is in the usertable NSString * userName = [stat objectForKey:@"User"]; if ( !userName ) { NSLog(@"%@ has no \"User\" key in the dict %@", api.p4port, stat); return; } if ( [userName isEqualToString:api.user] ) currentUserExistsOnServer = YES; // Only keep the information for users that we really need for memory // efficiency NSMutableDictionary * trimmedUserInfo = [NSMutableDictionary dictionaryWithCapacity:3]; if ( RetainedUserKeys == nil ) RetainedUserKeys = [[NSArray arrayWithObjects:@"User", @"FullName", @"Email", nil] retain]; for (NSString * key in RetainedUserKeys) { id entry = [stat objectForKey:key]; if ( entry ) [trimmedUserInfo setObject:entry forKey:key]; } [api.userInfo setObject:[NSDictionary dictionaryWithDictionary:trimmedUserInfo] forKey:userName]; } else if ( [function isEqualToString:@"monitor"] ) { if ( [self runningTimeForMonitorTask:stat] < MonitorTaskTimeThreshold ) return; if ([[stat objectForKey:@"command"] isEqualToString:@"IDLE"]) return; [taggedConnection.userInfo addObject:[[stat copy] autorelease]]; } } -(void)clientApi:(P4ClientApi*)api didReceiveError:(NSError*)e { NSAssert( api == taggedConnection, @"api in callback and our connection differ!" ); int subsystem = [[[e userInfo] valueForKey:P4SubsystemErrorKey] intValue]; int subCode = [[[e userInfo] valueForKey:P4SubCodeErrorKey] intValue]; if ( subsystem == ES_SERVER && subCode == kP4TicketErrorCode ) { self.requiresTicket = YES; // we don't want to report the error so the server detail will display // the monitor information without the error return; } self.lastError = [e copy]; [[NSNotificationCenter defaultCenter] postNotificationName:P4ServerErrorReceivedNotification object:self]; } -(void)clientApiDidFinishCommand:(P4ClientApi*)api { lastUpdate = [NSDate timeIntervalSinceReferenceDate]; // NSLog(@"Finished %@", api.currentCommand); // NSAssert( [api.currentCommand isEqualToDictionary:self.currentCommand], @"current command differs from returned op. More than one at a time happened" ); self.currentCommand = nil; NSString * function = api.currentCommand; if ( [function isEqualToString:@"users"] ) { self.usersInMonitorTable = api.userInfo; self.numberOfUsers = newNumberOfUsers; newNumberOfUsers = 0; } else if ( [function isEqualToString:@"monitor"] ) { self.monitorInfo = api.userInfo; NSMutableDictionary * remainingUsers = [NSMutableDictionary dictionaryWithCapacity:[self.monitorInfo count]]; for (NSDictionary * monitorEntry in self.monitorInfo) { NSString * userName = [monitorEntry objectForKey:@"user"]; [remainingUsers setObject:[self.usersInMonitorTable objectForKey:userName] forKey:userName]; } self.usersInMonitorTable = remainingUsers; } api.userInfo = nil; if (commandQueue.count > 0) { [self runNextOperation]; return; } [self disconnect]; } void ReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void * info) { P4Server * server = (P4Server*)info; [server reachability:target didUpdateWithFlags:flags]; } @end NSTimeInterval TimeIntervalFromString( NSString * s ) { NSArray * times = [s componentsSeparatedByString:@":"]; if ( [times count] != 3 ) return -1; NSTimeInterval uptime = 0; uptime += [[times objectAtIndex:0] intValue] * 60 * 60; // hours uptime += [[times objectAtIndex:1] intValue] * 60; // minutes uptime += [[times objectAtIndex:2] intValue]; // seconds return uptime; }