Bonjour/MYBonjourService.m
changeset 29 59689fbdcf77
parent 26 cb9cdf247239
child 31 1d6924779df7
     1.1 --- a/Bonjour/MYBonjourService.m	Wed Apr 22 16:45:39 2009 -0700
     1.2 +++ b/Bonjour/MYBonjourService.m	Tue Apr 28 10:36:28 2009 -0700
     1.3 @@ -7,84 +7,120 @@
     1.4  //
     1.5  
     1.6  #import "MYBonjourService.h"
     1.7 +#import "MYBonjourQuery.h"
     1.8 +#import "MYAddressLookup.h"
     1.9  #import "IPAddress.h"
    1.10  #import "ConcurrentOperation.h"
    1.11  #import "Test.h"
    1.12  #import "Logging.h"
    1.13 +#import "ExceptionUtils.h"
    1.14 +#import <dns_sd.h>
    1.15  
    1.16  
    1.17  NSString* const kBonjourServiceResolvedAddressesNotification = @"BonjourServiceResolvedAddresses";
    1.18  
    1.19  
    1.20  @interface MYBonjourService ()
    1.21 -@property (copy) NSSet* addresses;
    1.22  @end
    1.23  
    1.24 -@interface MYBonjourResolveOperation ()
    1.25 -@property (assign) MYBonjourService *service;
    1.26 -@property (retain) NSSet *addresses;
    1.27 -@end
    1.28 -
    1.29 -
    1.30  
    1.31  @implementation MYBonjourService
    1.32  
    1.33  
    1.34 -- (id) initWithNetService: (NSNetService*)netService
    1.35 +- (id) initWithName: (NSString*)serviceName
    1.36 +               type: (NSString*)type
    1.37 +             domain: (NSString*)domain
    1.38 +          interface: (uint32)interfaceIndex
    1.39  {
    1.40      self = [super init];
    1.41      if (self != nil) {
    1.42 -        _netService = [netService retain];
    1.43 -        _netService.delegate = self;
    1.44 +        _name = [serviceName copy];
    1.45 +        _type = [type copy];
    1.46 +        _domain = [domain copy];
    1.47 +        _interfaceIndex = interfaceIndex;
    1.48      }
    1.49      return self;
    1.50  }
    1.51  
    1.52 -- (void) dealloc
    1.53 -{
    1.54 -    Log(@"DEALLOC %@",self);
    1.55 -    _netService.delegate = nil;
    1.56 -    [_netService release];
    1.57 -    [_txtRecord release];
    1.58 -    [_addresses release];
    1.59 +- (void) dealloc {
    1.60 +    [_name release];
    1.61 +    [_type release];
    1.62 +    [_domain release];
    1.63 +    [_hostname release];
    1.64 +    [_txtQuery stop];
    1.65 +    [_txtQuery release];
    1.66 +    [_addressLookup stop];
    1.67 +    [_addressLookup release];
    1.68      [super dealloc];
    1.69  }
    1.70  
    1.71  
    1.72 -- (NSString*) description
    1.73 -{
    1.74 -    return $sprintf(@"%@['%@'.%@%@]", self.class,self.name,_netService.type,_netService.domain);
    1.75 +@synthesize name=_name, type=_type, domain=_domain, interfaceIndex=_interfaceIndex;
    1.76 +
    1.77 +
    1.78 +- (NSString*) description {
    1.79 +    return $sprintf(@"%@['%@'.%@%@]", self.class,_name,_type,_domain);
    1.80  }
    1.81  
    1.82  
    1.83 -- (NSComparisonResult) compare: (id)obj
    1.84 -{
    1.85 -    return [self.name caseInsensitiveCompare: [obj name]];
    1.86 +- (NSComparisonResult) compare: (id)obj {
    1.87 +    return [_name caseInsensitiveCompare: [obj name]];
    1.88  }
    1.89  
    1.90 -
    1.91 -- (NSNetService*) netService        {return _netService;}
    1.92 -- (BOOL) isEqual: (id)obj           {return [obj isKindOfClass: [MYBonjourService class]] && [_netService isEqual: [obj netService]];}
    1.93 -- (NSUInteger) hash                 {return _netService.hash;}
    1.94 -- (NSString*) name                  {return _netService.name;}
    1.95 -
    1.96 -
    1.97 -- (void) added
    1.98 -{
    1.99 -    LogTo(Bonjour,@"Added %@",_netService);
   1.100 +- (BOOL) isEqual: (id)obj {
   1.101 +    if ([obj isKindOfClass: [MYBonjourService class]]) {
   1.102 +        MYBonjourService *service = obj;
   1.103 +        return [_name caseInsensitiveCompare: [service name]] == 0
   1.104 +            && $equal(_type, service->_type)
   1.105 +            && $equal(_domain, service->_domain)
   1.106 +            && _interfaceIndex == service->_interfaceIndex;
   1.107 +    } else {
   1.108 +        return NO;
   1.109 +    }
   1.110  }
   1.111  
   1.112 -- (void) removed
   1.113 -{
   1.114 -    LogTo(Bonjour,@"Removed %@",_netService);
   1.115 -    [_netService stopMonitoring];
   1.116 -    _netService.delegate = nil;
   1.117 +- (NSUInteger) hash {
   1.118 +    return _name.hash ^ _type.hash ^ _domain.hash;
   1.119 +}
   1.120 +
   1.121 +
   1.122 +- (void) added {
   1.123 +    LogTo(Bonjour,@"Added %@",self);
   1.124 +}
   1.125 +
   1.126 +- (void) removed {
   1.127 +    LogTo(Bonjour,@"Removed %@",self);
   1.128 +    [self stop];
   1.129      
   1.130 -    if( _resolveOp ) {
   1.131 -        [_resolveOp cancel];
   1.132 -        [_resolveOp release];
   1.133 -        _resolveOp = nil;
   1.134 -    }
   1.135 +    [_txtQuery stop];
   1.136 +    [_txtQuery release];
   1.137 +    _txtQuery = nil;
   1.138 +    
   1.139 +    [_addressLookup stop];
   1.140 +}
   1.141 +
   1.142 +
   1.143 +- (void) priv_finishResolve {
   1.144 +    // If I haven't finished my resolve yet, run it synchronously now so I can return a valid value:
   1.145 +    if (!_startedResolve )
   1.146 +        [self start];
   1.147 +    if (self.serviceRef)
   1.148 +        [self waitForReply];
   1.149 +}    
   1.150 +
   1.151 +- (NSString*) fullName {
   1.152 +    if (!_fullName) [self priv_finishResolve];
   1.153 +    return _fullName;
   1.154 +}
   1.155 +
   1.156 +- (NSString*) hostname {
   1.157 +    if (!_hostname) [self priv_finishResolve];
   1.158 +    return _hostname;
   1.159 +}
   1.160 +
   1.161 +- (UInt16) port {
   1.162 +    if (!_port) [self priv_finishResolve];
   1.163 +    return _port;
   1.164  }
   1.165  
   1.166  
   1.167 @@ -92,19 +128,18 @@
   1.168  #pragma mark TXT RECORD:
   1.169  
   1.170  
   1.171 -- (NSDictionary*) txtRecord
   1.172 -{
   1.173 -    [_netService startMonitoring];
   1.174 +- (NSDictionary*) txtRecord {
   1.175 +    // If I haven't started my resolve yet, start it now. (_txtRecord will be nil till it finishes.)
   1.176 +    if (!_startedResolve)
   1.177 +        [self start];
   1.178      return _txtRecord;
   1.179  }
   1.180  
   1.181 -- (void) txtRecordChanged
   1.182 -{
   1.183 +- (void) txtRecordChanged {
   1.184      // no-op (this is here for subclassers to override)
   1.185  }
   1.186  
   1.187 -- (NSString*) txtStringForKey: (NSString*)key
   1.188 -{
   1.189 +- (NSString*) txtStringForKey: (NSString*)key {
   1.190      NSData *value = [self.txtRecord objectForKey: key];
   1.191      if( ! value )
   1.192          return nil;
   1.193 @@ -118,95 +153,112 @@
   1.194      return [str autorelease];
   1.195  }
   1.196  
   1.197 -
   1.198 -- (void)netService:(NSNetService *)sender didUpdateTXTRecordData:(NSData *)data
   1.199 -{
   1.200 -    NSDictionary *txtDict = [NSNetService dictionaryFromTXTRecordData: data];
   1.201 -    if( ! $equal(txtDict,_txtRecord) ) {
   1.202 -        LogTo(Bonjour,@"%@ got TXT record (%u bytes)",self,data.length);
   1.203 +- (void) setTxtData: (NSData*)txtData {
   1.204 +    NSDictionary *txtRecord = txtData ?[NSNetService dictionaryFromTXTRecordData: txtData] :nil;
   1.205 +    if (!$equal(txtRecord,_txtRecord)) {
   1.206 +        LogTo(Bonjour,@"%@ TXT = %@", self,txtRecord);
   1.207          [self willChangeValueForKey: @"txtRecord"];
   1.208 -        setObj(&_txtRecord,txtDict);
   1.209 +        setObj(&_txtRecord, txtRecord);
   1.210          [self didChangeValueForKey: @"txtRecord"];
   1.211          [self txtRecordChanged];
   1.212      }
   1.213  }
   1.214  
   1.215  
   1.216 -#pragma mark -
   1.217 -#pragma mark ADDRESS RESOLUTION:
   1.218 -
   1.219 -
   1.220 -#define kAddressResolveTimeout      10.0
   1.221 -#define kAddressExpirationInterval  60.0
   1.222 -#define kAddressErrorRetryInterval   5.0
   1.223 -
   1.224 -
   1.225 -- (NSSet*) addresses
   1.226 -{
   1.227 -    if( _addresses && CFAbsoluteTimeGetCurrent() >= _addressesExpireAt ) {
   1.228 -        setObj(&_addresses,nil);            // eww, toss 'em and get new ones
   1.229 -        [self resolve];
   1.230 -    }
   1.231 -    return _addresses;
   1.232 +- (void) queryDidUpdate: (MYBonjourQuery*)query {
   1.233 +    if (query==_txtQuery)
   1.234 +        [self setTxtData: query.recordData];
   1.235  }
   1.236  
   1.237  
   1.238 -- (MYBonjourResolveOperation*) resolve
   1.239 +#pragma mark -
   1.240 +#pragma mark FULLNAME/HOSTNAME/PORT RESOLUTION:
   1.241 +
   1.242 +
   1.243 +- (void) priv_resolvedFullName: (NSString*)fullName
   1.244 +                      hostname: (NSString*)hostname
   1.245 +                          port: (uint16_t)port
   1.246 +                     txtRecord: (NSData*)txtData
   1.247  {
   1.248 -    if( ! _resolveOp ) {
   1.249 -        LogTo(Bonjour,@"Resolving %@",self);
   1.250 -        _resolveOp = [[MYBonjourResolveOperation alloc] init];
   1.251 -        _resolveOp.service = self;
   1.252 -        [_resolveOp start];
   1.253 -        Assert(_netService);
   1.254 -        Assert(_netService.delegate=self);
   1.255 -        [_netService resolveWithTimeout: kAddressResolveTimeout];
   1.256 -    }
   1.257 -    return _resolveOp;
   1.258 +    LogTo(Bonjour, @"%@: fullname='%@', hostname=%@, port=%u, txt=%u bytes", 
   1.259 +          self, fullName, hostname, port, txtData.length);
   1.260 +
   1.261 +    // Don't call a setter method to set these properties: the getters are synchronous, so
   1.262 +    // I might already be blocked in a call to one of them, in which case creating a KV
   1.263 +    // notification could cause trouble...
   1.264 +    _fullName = fullName.copy;
   1.265 +    _hostname = hostname.copy;
   1.266 +    _port = port;
   1.267 +    
   1.268 +    // TXT getter is async, though, so I can use a setter to announce the data's availability:
   1.269 +    [self setTxtData: txtData];
   1.270 +    
   1.271 +    // Now that I know my full name, I can start a persistent query to track the TXT:
   1.272 +    _txtQuery = [[MYBonjourQuery alloc] initWithBonjourService: self 
   1.273 +                                                    recordType: kDNSServiceType_TXT];
   1.274 +    _txtQuery.continuous = YES;
   1.275 +    [_txtQuery start];
   1.276  }
   1.277  
   1.278 -- (void) setAddresses: (NSSet*)addresses
   1.279 +
   1.280 +static void resolveCallback(DNSServiceRef                       sdRef,
   1.281 +                            DNSServiceFlags                     flags,
   1.282 +                            uint32_t                            interfaceIndex,
   1.283 +                            DNSServiceErrorType                 errorCode,
   1.284 +                            const char                          *fullname,
   1.285 +                            const char                          *hosttarget,
   1.286 +                            uint16_t                            port,
   1.287 +                            uint16_t                            txtLen,
   1.288 +                            const unsigned char                 *txtRecord,
   1.289 +                            void                                *context)
   1.290  {
   1.291 -    setObj(&_addresses,addresses);
   1.292 +    MYBonjourService *service = context;
   1.293 +    @try{
   1.294 +        //LogTo(Bonjour, @"resolveCallback for %@ (err=%i)", service,errorCode);
   1.295 +        if (errorCode) {
   1.296 +            [service setError: errorCode];
   1.297 +        } else {
   1.298 +            NSData *txtData = nil;
   1.299 +            if (txtRecord)
   1.300 +                txtData = [NSData dataWithBytes: txtRecord length: txtLen];
   1.301 +            [service priv_resolvedFullName: [NSString stringWithUTF8String: fullname]
   1.302 +                                  hostname: [NSString stringWithUTF8String: hosttarget]
   1.303 +                                      port: ntohs(port)
   1.304 +                                 txtRecord: txtData];
   1.305 +        }
   1.306 +    }catchAndReport(@"MYBonjourResolver query callback");
   1.307  }
   1.308  
   1.309  
   1.310 -- (void) _finishedResolving: (NSSet*)addresses expireIn: (NSTimeInterval)expirationInterval
   1.311 -{
   1.312 -    _addressesExpireAt = CFAbsoluteTimeGetCurrent() + expirationInterval;
   1.313 -    self.addresses = addresses;
   1.314 -    _resolveOp.addresses = addresses;
   1.315 -    [_resolveOp finish];
   1.316 -    [_resolveOp release];
   1.317 -    _resolveOp = nil;
   1.318 +- (DNSServiceRef) createServiceRef {
   1.319 +    _startedResolve = YES;
   1.320 +    DNSServiceRef serviceRef = NULL;
   1.321 +    self.error = DNSServiceResolve(&serviceRef, 0,
   1.322 +                                   _interfaceIndex, 
   1.323 +                                   _name.UTF8String, _type.UTF8String, _domain.UTF8String,
   1.324 +                                   &resolveCallback, self);
   1.325 +    return serviceRef;
   1.326  }
   1.327  
   1.328  
   1.329 -- (void)netServiceDidResolveAddress:(NSNetService *)sender
   1.330 -{
   1.331 -    // Convert the raw sockaddrs into IPAddress objects:
   1.332 -    NSMutableSet *addresses = [NSMutableSet setWithCapacity: 2];
   1.333 -    for( NSData *rawAddr in _netService.addresses ) {
   1.334 -        IPAddress *addr = [[IPAddress alloc] initWithSockAddr: rawAddr.bytes];
   1.335 -        if( addr ) {
   1.336 -            [addresses addObject: addr];
   1.337 -            [addr release];
   1.338 -        }
   1.339 +- (MYAddressLookup*) addressLookup {
   1.340 +    if (!_addressLookup) {
   1.341 +        // Create the lookup the first time this is called:
   1.342 +        _addressLookup = [[MYAddressLookup alloc] initWithHostname: self.hostname];
   1.343 +        _addressLookup.port = _port;
   1.344 +        _addressLookup.interfaceIndex = _interfaceIndex;
   1.345      }
   1.346 -    LogTo(Bonjour,@"Resolved %@: %@",self,addresses);
   1.347 -    [self _finishedResolving: addresses expireIn: kAddressExpirationInterval];
   1.348 +    // (Re)start the lookup if it's expired:
   1.349 +    if (_addressLookup && _addressLookup.timeToLive <= 0.0)
   1.350 +        [_addressLookup start];
   1.351 +    return _addressLookup;
   1.352  }
   1.353  
   1.354 -- (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict
   1.355 -{
   1.356 -    LogTo(Bonjour,@"Error resolving %@ -- %@",self,errorDict);
   1.357 -    [self _finishedResolving: [NSArray array] expireIn: kAddressErrorRetryInterval];
   1.358 -}
   1.359  
   1.360 -- (void)netServiceDidStop:(NSNetService *)sender
   1.361 -{
   1.362 -    LogTo(Bonjour,@"Resolve stopped for %@",self);
   1.363 -    [self _finishedResolving: [NSArray array] expireIn: kAddressErrorRetryInterval];
   1.364 +- (MYBonjourQuery*) queryForRecord: (UInt16)recordType {
   1.365 +    MYBonjourQuery *query = [[[MYBonjourQuery alloc] initWithBonjourService: self recordType: recordType]
   1.366 +                                 autorelease];
   1.367 +    return [query start] ?query :nil;
   1.368  }
   1.369  
   1.370  
   1.371 @@ -214,20 +266,6 @@
   1.372  
   1.373  
   1.374  
   1.375 -
   1.376 -@implementation MYBonjourResolveOperation
   1.377 -
   1.378 -@synthesize service=_service, addresses=_addresses;
   1.379 -
   1.380 -- (void) dealloc
   1.381 -{
   1.382 -    [_addresses release];
   1.383 -    [super dealloc];
   1.384 -}
   1.385 -
   1.386 -@end
   1.387 -
   1.388 -
   1.389  /*
   1.390   Copyright (c) 2008-2009, Jens Alfke <jens@mooseyard.com>. All rights reserved.
   1.391