//
// 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 <sys/socket.h>
#import <netinet/in.h>
#import <netinet6/in6.h>
#import <arpa/inet.h>
#import <ifaddrs.h>
#include <netdb.h>
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;
}