Monotone-Parent: e67291cb30df49fcf6c20e3c285bfc306157d7c5

Monotone-Revision: 7693a13871fea2b066790d804159529866a7e4c8

Monotone-Author: wsourdeau@inverse.ca
Monotone-Date: 2008-12-12T17:32:14
Monotone-Branch: ca.inverse.sogo
maint-2.0.2
Wolfgang Sourdeau 2008-12-12 17:32:14 +00:00
parent b6b1069fb7
commit 5e8f6e38ad
1 changed files with 395 additions and 336 deletions

View File

@ -30,95 +30,105 @@
/* /*
TODO: TODO:
- implemented pooling - implemented pooling
- auto-close channels which are very old?! - auto-close channels which are very old?!
(eg missing release due to an exception) (eg missing release due to an exception)
*/ */
@interface GCSChannelHandle : NSObject @interface GCSChannelHandle : NSObject
{ {
@public @public
NSURL *url; NSURL *url;
EOAdaptorChannel *channel; EOAdaptorChannel *channel;
NSDate *creationTime; NSDate *creationTime;
NSDate *lastReleaseTime; NSDate *lastReleaseTime;
NSDate *lastAcquireTime; NSDate *lastAcquireTime;
} }
- (EOAdaptorChannel *)channel; - (EOAdaptorChannel *) channel;
- (BOOL)canHandleURL:(NSURL *)_url; - (BOOL) canHandleURL: (NSURL *) _url;
- (NSTimeInterval)age; - (NSTimeInterval) age;
@end @end
@implementation GCSChannelManager @implementation GCSChannelManager
static BOOL debugOn = NO; static BOOL debugOn = NO;
static BOOL debugPools = NO; static BOOL debugPools = NO;
static int ChannelExpireAge = 180; static int ChannelExpireAge = 180;
static NSTimeInterval ChannelCollectionTimer = 5 * 60; static NSTimeInterval ChannelCollectionTimer = 5 * 60;
+ (void)initialize { + (void) initialize
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
debugOn = [ud boolForKey:@"GCSChannelManagerDebugEnabled"]; debugOn = [ud boolForKey: @"GCSChannelManagerDebugEnabled"];
debugPools = [ud boolForKey:@"GCSChannelManagerPoolDebugEnabled"]; debugPools = [ud boolForKey: @"GCSChannelManagerPoolDebugEnabled"];
ChannelExpireAge = [[ud objectForKey:@"GCSChannelExpireAge"] intValue]; ChannelExpireAge = [[ud objectForKey: @"GCSChannelExpireAge"] intValue];
if (ChannelExpireAge < 1) if (ChannelExpireAge < 1)
ChannelExpireAge = 180; ChannelExpireAge = 180;
ChannelCollectionTimer = ChannelCollectionTimer =
[[ud objectForKey:@"GCSChannelCollectionTimer"] intValue]; [[ud objectForKey: @"GCSChannelCollectionTimer"] intValue];
if (ChannelCollectionTimer < 1) if (ChannelCollectionTimer < 1)
ChannelCollectionTimer = 5*60; ChannelCollectionTimer = 5*60;
} }
+ (NSString *)adaptorNameForURLScheme:(NSString *)_scheme { + (NSString *) adaptorNameForURLScheme: (NSString *) _scheme
// TODO: map scheme to adaptors (eg 'postgresql://' to PostgreSQL {
// TODO: map scheme to adaptors (eg 'postgresql: //' to PostgreSQL
return @"PostgreSQL"; return @"PostgreSQL";
} }
+ (id)defaultChannelManager { + (id) defaultChannelManager
{
static GCSChannelManager *cm = nil; static GCSChannelManager *cm = nil;
if (cm == nil)
cm = [[self alloc] init]; if (!cm)
cm = [self new];
return cm; return cm;
} }
- (id)init { - (id) init
if ((self = [super init])) { {
self->urlToAdaptor = [[NSMutableDictionary alloc] initWithCapacity:4]; if ((self = [super init]))
self->availableChannels = [[NSMutableArray alloc] initWithCapacity:16]; {
self->busyChannels = [[NSMutableArray alloc] initWithCapacity:16]; urlToAdaptor = [[NSMutableDictionary alloc] initWithCapacity: 4];
availableChannels = [[NSMutableArray alloc] initWithCapacity: 16];
busyChannels = [[NSMutableArray alloc] initWithCapacity: 16];
gcTimer = [[NSTimer scheduledTimerWithTimeInterval:
ChannelCollectionTimer
target: self selector: @selector (_garbageCollect: )
userInfo: nil repeats: YES] retain];
}
self->gcTimer = [[NSTimer scheduledTimerWithTimeInterval:
ChannelCollectionTimer
target:self selector:@selector(_garbageCollect:)
userInfo:nil repeats:YES] retain];
}
return self; return self;
} }
- (void)dealloc { - (void) dealloc
if (self->gcTimer) [self->gcTimer invalidate]; {
[self->gcTimer release]; if (gcTimer)
[gcTimer invalidate];
[self->busyChannels release]; [busyChannels release];
[self->availableChannels release]; [availableChannels release];
[self->urlToAdaptor release]; [urlToAdaptor release];
[super dealloc]; [super dealloc];
} }
/* DB key */ /* DB key */
- (NSString *)databaseKeyForURL:(NSURL *)_url { - (NSString *) databaseKeyForURL: (NSURL *) _url
{
/* /*
We need to build a proper key that omits passwords and URL path components We need to build a proper key that omits passwords and URL path components
which are not required. which are not required.
*/ */
NSString *key; NSString *key;
key = [NSString stringWithFormat:@"%@\n%@\n%@\n%@", key = [NSString stringWithFormat: @"%@\n%@\n%@\n%@",
[_url host], [_url port], [_url host], [_url port],
[_url user], [_url gcsDatabaseName]]; [_url user], [_url gcsDatabaseName]];
return key; return key;
@ -126,307 +136,343 @@ static NSTimeInterval ChannelCollectionTimer = 5 * 60;
/* adaptors */ /* adaptors */
- (NSDictionary *)connectionDictionaryForURL:(NSURL *)_url { - (NSDictionary *) connectionDictionaryForURL: (NSURL *) _url
{
NSMutableDictionary *md; NSMutableDictionary *md;
id tmp; id tmp;
md = [NSMutableDictionary dictionaryWithCapacity:4];
if ((tmp = [_url host]) != nil) md = [NSMutableDictionary dictionaryWithCapacity: 4];
[md setObject:tmp forKey:@"hostName"];
if ((tmp = [_url port]) != nil) if ((tmp = [_url host]))
[md setObject:tmp forKey:@"port"]; [md setObject: tmp forKey: @"hostName"];
if ((tmp = [_url user]) != nil) if ((tmp = [_url port]))
[md setObject:tmp forKey:@"userName"]; [md setObject: tmp forKey: @"port"];
if ((tmp = [_url password]) != nil) if ((tmp = [_url user]))
[md setObject:tmp forKey:@"password"]; [md setObject: tmp forKey: @"userName"];
if ((tmp = [_url password]))
if ((tmp = [_url gcsDatabaseName]) != nil) [md setObject: tmp forKey: @"password"];
[md setObject:tmp forKey:@"databaseName"];
if ((tmp = [_url gcsDatabaseName]))
[self debugWithFormat:@"build connection dictionary for URL %@: %@", [md setObject: tmp forKey: @"databaseName"];
[self debugWithFormat: @"build connection dictionary for URL %@: %@",
[_url absoluteString], md]; [_url absoluteString], md];
return md; return md;
} }
- (EOAdaptor *)adaptorForURL:(NSURL *)_url { - (EOAdaptor *) adaptorForURL: (NSURL *) _url
{
EOAdaptor *adaptor; EOAdaptor *adaptor;
NSString *key; NSString *key;
NSString *adaptorName;
if (_url == nil) NSDictionary *condict;
return nil;
if ((key = [self databaseKeyForURL:_url]) == nil) adaptor = nil;
return nil;
if ((adaptor = [self->urlToAdaptor objectForKey:key]) != nil) { if (_url)
[self debugWithFormat:@"using cached adaptor: %@", adaptor]; {
return adaptor; /* cached :-) */ if ((key = [self databaseKeyForURL: _url]))
} {
adaptor = [urlToAdaptor objectForKey: key];
[self debugWithFormat:@"creating new adaptor for URL: %@", _url]; if (adaptor)
[self debugWithFormat: @"using cached adaptor: %@", adaptor];
if ([EOAdaptor respondsToSelector:@selector(adaptorForURL:)]) { else
adaptor = [EOAdaptor adaptorForURL:_url]; {
} [self debugWithFormat: @"creating new adaptor for URL: %@", _url];
else {
NSString *adaptorName; if ([EOAdaptor respondsToSelector: @selector (adaptorForURL: )])
NSDictionary *condict; adaptor = [EOAdaptor adaptorForURL: _url];
else
adaptorName = [[self class] adaptorNameForURLScheme:[_url scheme]]; {
if ([adaptorName length] == 0) { adaptorName = [[self class]
[self errorWithFormat:@"cannot handle URL: %@", _url]; adaptorNameForURLScheme: [_url scheme]];
return nil; if ([adaptorName length])
{
condict = [self connectionDictionaryForURL: _url];
adaptor = [EOAdaptor adaptorWithName: adaptorName];
if (adaptor)
[adaptor setConnectionDictionary: condict];
else
[self errorWithFormat:
@"did not find adaptor '%@' for URL: %@",
adaptorName, _url];
}
else
[self errorWithFormat: @"cannot handle URL: %@", _url];
}
[urlToAdaptor setObject: adaptor forKey: key];
}
}
} }
condict = [self connectionDictionaryForURL:_url];
if ((adaptor = [EOAdaptor adaptorWithName:adaptorName]) == nil) {
[self errorWithFormat:@"did not find adaptor '%@' for URL: %@",
adaptorName, _url];
return nil;
}
[adaptor setConnectionDictionary:condict];
}
[self->urlToAdaptor setObject:adaptor forKey:key];
return adaptor; return adaptor;
} }
/* channels */ /* channels */
- (GCSChannelHandle *)findBusyChannelHandleForChannel:(EOAdaptorChannel *)_ch { - (GCSChannelHandle *)
findBusyChannelHandleForChannel: (EOAdaptorChannel *) _ch
{
NSEnumerator *e; NSEnumerator *e;
GCSChannelHandle *handle; GCSChannelHandle *handle, *currentHandle;
e = [self->busyChannels objectEnumerator]; handle = NULL;
while ((handle = [e nextObject])) {
if ([handle channel] == _ch) e = [busyChannels objectEnumerator];
return handle; while (!handle && (currentHandle = [e nextObject]))
} if ([currentHandle channel] == _ch)
return nil; handle = currentHandle;
}
- (GCSChannelHandle *)findAvailChannelHandleForURL:(NSURL *)_url { return handle;
NSEnumerator *e;
GCSChannelHandle *handle;
e = [self->availableChannels objectEnumerator];
while ((handle = [e nextObject])) {
if ([handle canHandleURL:_url])
return handle;
if (debugPools) {
[self logWithFormat:@"DBPOOL: cannot use handle (%@ vs %@)",
[_url absoluteString], [handle->url absoluteString]];
}
}
return nil;
} }
- (EOAdaptorChannel *)_createChannelForURL:(NSURL *)_url { - (GCSChannelHandle *) findAvailChannelHandleForURL: (NSURL *) _url
EOAdaptor *adaptor; {
NSEnumerator *e;
GCSChannelHandle *handle, *currentHandle;
handle = nil;
e = [availableChannels objectEnumerator];
while (!handle && (currentHandle = [e nextObject]))
if ([currentHandle canHandleURL: _url])
handle = currentHandle;
else if (debugPools)
[self logWithFormat: @"DBPOOL: cannot use handle (%@ vs %@) ",
[_url absoluteString], [handle->url absoluteString]];
return handle;
}
- (EOAdaptorChannel *) _createChannelForURL: (NSURL *) _url
{
EOAdaptor *adaptor;
EOAdaptorContext *adContext; EOAdaptorContext *adContext;
EOAdaptorChannel *adChannel; EOAdaptorChannel *adChannel;
if ((adaptor = [self adaptorForURL:_url]) == nil) adChannel = nil;
return nil;
adaptor = [self adaptorForURL: _url];
if ((adContext = [adaptor createAdaptorContext]) == nil) { if (adaptor)
[self errorWithFormat:@"could not create adaptor context!"]; {
return nil; adContext = [adaptor createAdaptorContext];
} if (adContext)
if ((adChannel = [adContext createAdaptorChannel]) == nil) { {
[self errorWithFormat:@"could not create adaptor channel!"]; adChannel = [adContext createAdaptorChannel];
return nil; if (!adChannel)
} [self errorWithFormat: @"could not create adaptor channel!"];
}
else
[self errorWithFormat: @"could not create adaptor context!"];
}
return adChannel; return adChannel;
} }
- (EOAdaptorChannel *)acquireOpenChannelForURL:(NSURL *)_url { - (EOAdaptorChannel *) acquireOpenChannelForURL: (NSURL *) _url
{
// TODO: naive implementation, add pooling! // TODO: naive implementation, add pooling!
EOAdaptorChannel *channel; EOAdaptorChannel *channel;
GCSChannelHandle *handle; GCSChannelHandle *handle;
NSCalendarDate *now; NSCalendarDate *now;
channel = nil;
now = [NSCalendarDate date]; now = [NSCalendarDate date];
/* look for cached handles */ /* look for cached handles */
if ((handle = [self findAvailChannelHandleForURL:_url]) != nil) {
// TODO: check age?
[self->busyChannels addObject:handle];
[self->availableChannels removeObject:handle];
ASSIGN(handle->lastAcquireTime, now);
if (debugPools)
[self logWithFormat:@"DBPOOL: reused cached DB channel!"];
return [[handle channel] retain];
}
if (debugPools) { handle = [self findAvailChannelHandleForURL: _url];
[self logWithFormat:@"DBPOOL: create new DB channel for URL: %@", if (handle)
[_url absoluteString]]; {
} // TODO: check age?
[busyChannels addObject: handle];
/* create channel */ [availableChannels removeObject: handle];
ASSIGN (handle->lastAcquireTime, now);
if ((channel = [self _createChannelForURL:_url]) == nil)
return nil; channel = [handle channel];
if (debugPools)
if ([channel isOpen]) [self logWithFormat: @"DBPOOL: reused cached DB channel! (%p)",
; channel];
else if (![channel openChannel]) { }
[self errorWithFormat:@"could not open channel %@ for URL: %@", else
channel, [_url absoluteString]]; {
return nil; if (debugPools)
} {
[self logWithFormat: @"DBPOOL: create new DB channel for URL: %@",
/* create handle for channel */ [_url absoluteString]];
}
handle = [[GCSChannelHandle alloc] init];
handle->url = [_url retain]; /* create channel */
handle->channel = [channel retain]; channel = [self _createChannelForURL: _url];
handle->creationTime = [now retain]; if (channel)
handle->lastAcquireTime = [now retain]; {
if ([channel isOpen]
[self->busyChannels addObject:handle]; || [channel openChannel])
[handle release]; {
/* create handle for channel */
return [channel retain];
handle = [[GCSChannelHandle alloc] init];
handle->url = [_url retain];
handle->channel = [channel retain];
handle->creationTime = [now retain];
handle->lastAcquireTime = [now retain];
[busyChannels addObject: handle];
[handle release];
}
else
[self errorWithFormat: @"could not open channel %@ for URL: %@",
channel, [_url absoluteString]];
}
}
return channel;
} }
- (void)releaseChannel:(EOAdaptorChannel *)_channel {
- (void) releaseChannel: (EOAdaptorChannel *) _channel
{
GCSChannelHandle *handle; GCSChannelHandle *handle;
if ((handle = [self findBusyChannelHandleForChannel:_channel]) != nil) {
NSCalendarDate *now;
now = [NSCalendarDate date]; handle = [self findBusyChannelHandleForChannel: _channel];
if (handle)
handle = [handle retain]; {
ASSIGN(handle->lastReleaseTime, now); handle = [handle retain];
[self->busyChannels removeObject:handle]; ASSIGN (handle->lastReleaseTime, [NSCalendarDate date]);
[busyChannels removeObject: handle];
if ([[handle channel] isOpen] && [handle age] < ChannelExpireAge) {
// TODO: consider age if ([_channel isOpen] && [handle age] < ChannelExpireAge)
[self->availableChannels addObject:handle]; {
if (debugPools) { // TODO: consider age
[availableChannels addObject: handle];
if (debugPools)
[self logWithFormat:
@"DBPOOL: keeping channel (age %ds, #%d, %p) : %@",
(int)
[handle age], [availableChannels count],
[handle->url absoluteString],
_channel];
}
else if (debugPools)
[self logWithFormat: [self logWithFormat:
@"DBPOOL: keeping channel (age %ds, #%d): %@", @"DBPOOL: freeing old channel (age %ds, %p) ", (int)
(int)[handle age], [self->availableChannels count], [handle age], _channel];
[handle->url absoluteString]];
}
[_channel release];
[handle release]; [handle release];
return;
} }
else
{
if ([_channel isOpen])
[_channel closeChannel];
if (debugPools) { [_channel release];
[self logWithFormat:
@"DBPOOL: freeing old channel (age %ds)", (int)[handle age]];
} }
/* not reusing channel */
[handle release]; handle = nil;
}
if ([_channel isOpen])
[_channel closeChannel];
[_channel release];
} }
/* checking for tables */ /* checking for tables */
- (BOOL)canConnect:(NSURL *)_url { - (BOOL) canConnect: (NSURL *) _url
/* {
this can check for DB connect as well as for table URLs (whether a table /*
exists) this can check for DB connect as well as for table URLs (whether a table
exists)
*/ */
EOAdaptorChannel *channel; EOAdaptorChannel *channel;
NSString *table; NSString *table;
BOOL result; BOOL result;
if ((channel = [self acquireOpenChannelForURL:_url]) == nil) { channel = [self acquireOpenChannelForURL: _url];
if (debugOn) [self debugWithFormat:@"could not acquire channel: %@", _url]; if (channel)
return NO; {
} if (debugOn)
if (debugOn) [self debugWithFormat:@"acquired channel: %@", channel]; [self debugWithFormat: @"acquired channel: %@", channel];
result = YES; /* could open channel */
/* check whether table exists */
/* check whether table exists */ table = [_url gcsTableName];
if ([table length] > 0)
table = [_url gcsTableName]; result = [channel tableExistsWithName: table];
if ([table length] > 0) else
result = [channel tableExistsWithName:table]; result = YES; /* could open channel */
/* release channel */ /* release channel */
[self releaseChannel: channel];
[self releaseChannel:channel]; channel = nil; }
else
{
if (debugOn)
[self debugWithFormat: @"could not acquire channel: %@", _url];
result = NO;
}
return result; return result;
} }
/* collect old channels */ /* collect old channels */
- (void)_garbageCollect:(NSTimer *)_timer { - (void) _garbageCollect: (NSTimer *) _timer
{
NSMutableArray *handlesToRemove; NSMutableArray *handlesToRemove;
unsigned i, count; unsigned i, count;
GCSChannelHandle *handle;
if ((count = [self->availableChannels count]) == 0)
/* no available channels */
return;
/* collect channels to expire */ count = [availableChannels count];
if (count)
handlesToRemove = [[NSMutableArray alloc] initWithCapacity:4]; {
for (i = 0; i < count; i++) { /* collect channels to expire */
GCSChannelHandle *handle;
handlesToRemove = [[NSMutableArray alloc] initWithCapacity: count];
handle = [self->availableChannels objectAtIndex:i]; for (i = 0; i < count; i++)
if (![[handle channel] isOpen]) { {
[handlesToRemove addObject:handle]; handle = [availableChannels objectAtIndex: i];
continue; if ([[handle channel] isOpen])
{
if ([handle age] > ChannelExpireAge)
[handlesToRemove addObject: handle];
}
else
[handlesToRemove addObject: handle];
}
/* remove channels */
count = [handlesToRemove count];
if (debugPools)
[self logWithFormat: @"DBPOOL: garbage collecting %d channels.", count];
for (i = 0; i < count; i++)
{
handle = [handlesToRemove objectAtIndex: i];
[handle retain];
[availableChannels removeObject: handle];
if ([[handle channel] isOpen])
[[handle channel] closeChannel];
[handle release];
}
[handlesToRemove release];
} }
if ([handle age] > ChannelExpireAge) {
[handlesToRemove addObject:handle];
continue;
}
}
/* remove channels */
count = [handlesToRemove count];
if (debugPools)
[self logWithFormat:@"DBPOOL: garbage collecting %d channels.", count];
for (i = 0; i < count; i++) {
GCSChannelHandle *handle;
handle = [[handlesToRemove objectAtIndex:i] retain];
[self->availableChannels removeObject:handle];
if ([[handle channel] isOpen])
[[handle channel] closeChannel];
[handle release];
}
[handlesToRemove release];
} }
/* debugging */ /* debugging */
- (BOOL)isDebuggingEnabled { - (BOOL) isDebuggingEnabled
{
return debugOn; return debugOn;
} }
/* description */ /* description */
- (NSString *)description { - (NSString *) description
{
NSMutableString *ms; NSMutableString *ms;
ms = [NSMutableString stringWithCapacity:256]; ms = [NSMutableString stringWithCapacity: 256];
[ms appendFormat:@"<0x%p[%@]:", self, NSStringFromClass([self class])]; [ms appendFormat: @"<0x%p[%@]: ", self, NSStringFromClass ([self class])];
[ms appendFormat:@" #adaptors=%d", [self->urlToAdaptor count]]; [ms appendFormat: @" #adaptors=%d", [urlToAdaptor count]];
[ms appendString:@">"]; [ms appendString: @">"];
return ms; return ms;
} }
@ -434,82 +480,95 @@ static NSTimeInterval ChannelCollectionTimer = 5 * 60;
@implementation GCSChannelHandle @implementation GCSChannelHandle
- (void)dealloc { - (void) dealloc
[self->channel release]; {
[self->creationTime release]; [channel release];
[self->lastReleaseTime release]; [creationTime release];
[self->lastAcquireTime release]; [lastReleaseTime release];
[lastAcquireTime release];
[super dealloc]; [super dealloc];
} }
/* accessors */ /* accessors */
- (EOAdaptorChannel *)channel { - (EOAdaptorChannel *) channel
return self->channel; {
return channel;
} }
- (BOOL)canHandleURL:(NSURL *)_url { - (BOOL) canHandleURL: (NSURL *) _url
BOOL isSQLite; {
BOOL result;
if (_url == nil) {
[self logWithFormat:@"MISMATCH: no url .."];
return NO;
}
if (_url == self->url)
return YES;
isSQLite = [[_url scheme] isEqualToString:@"sqlite"]; result = NO;
if (!isSQLite && ![[self->url host] isEqual:[_url host]]) { if (_url)
[self logWithFormat:@"MISMATCH: different host (%@ vs %@)", {
[self->url host], [_url host]]; if (_url == url
return NO; || [[_url scheme] isEqualToString: @"sqlite"])
} result = YES;
if (![[self->url gcsDatabaseName] isEqualToString:[_url gcsDatabaseName]]) { else if ([[url host] isEqual: [_url host]])
[self logWithFormat:@"MISMATCH: different db .."]; {
return NO; if ([[url gcsDatabaseName]
} isEqualToString: [_url gcsDatabaseName]])
if (!isSQLite) { {
if (![[self->url user] isEqual:[_url user]]) { if ([[url user] isEqual: [_url user]])
[self logWithFormat:@"MISMATCH: different user .."]; {
return NO; if ([[url port] intValue] == [[_url port] intValue])
result = YES;
else
[self logWithFormat:
@"MISMATCH: different port (%@ vs %@) ..",
[url port], [_url port]];
}
else
[self logWithFormat: @"MISMATCH: different user .."];
}
else
[self logWithFormat: @"MISMATCH: different db .."];
}
else
[self logWithFormat: @"MISMATCH: different host (%@ vs %@) ",
[url host], [_url host]];
} }
if ([[self->url port] intValue] != [[_url port] intValue]) { else
[self logWithFormat:@"MISMATCH: different port (%@ vs %@) ..", [self logWithFormat: @"MISMATCH: no url .."];
[self->url port], [_url port]];
return NO; return result;
}
}
return YES;
} }
- (NSTimeInterval)age { - (NSTimeInterval) age
return [[NSCalendarDate calendarDate] {
timeIntervalSinceDate:self->creationTime]; return [[NSCalendarDate calendarDate]
timeIntervalSinceDate: creationTime];
} }
/* NSCopying */ /* NSCopying */
- (id)copyWithZone:(NSZone *)_zone { - (id) copyWithZone: (NSZone *) _zone
{
return [self retain]; return [self retain];
} }
/* description */ /* description */
- (NSString *)description { - (NSString *) description
{
NSMutableString *ms; NSMutableString *ms;
ms = [NSMutableString stringWithCapacity:256]; ms = [NSMutableString stringWithCapacity: 256];
[ms appendFormat:@"<0x%p[%@]:", self, NSStringFromClass([self class])]; [ms appendFormat: @"<0x%p[%@]: ", self, NSStringFromClass ([self class])];
[ms appendFormat:@" channel=0x%p", self->channel]; [ms appendFormat: @" channel=0x%p", channel];
if (self->creationTime) [ms appendFormat:@" created=%@", self->creationTime]; if (creationTime)
if (self->lastReleaseTime) [ms appendFormat: @" created=%@", creationTime];
[ms appendFormat:@" last-released=%@", self->lastReleaseTime]; if (lastReleaseTime)
if (self->lastAcquireTime) [ms appendFormat: @" last-released=%@", lastReleaseTime];
[ms appendFormat:@" last-acquired=%@", self->lastAcquireTime]; if (lastAcquireTime)
[ms appendFormat: @" last-acquired=%@", lastAcquireTime];
[ms appendString:@">"];
[ms appendString: @">"];
return ms; return ms;
} }