Bonjour/MYBonjourService.m
author Jens Alfke <jens@mooseyard.com>
Sat May 16 14:10:15 2009 -0700 (2009-05-16)
changeset 47 60f2b46d9a3b
parent 43 aab592ac36fc
child 50 63baa74c903f
permissions -rw-r--r--
* Fixed #9: compilation error with iPhone 3.0 SDK.
* MYBonjourRegistration now allows you to set a TXT dictionary with non-NSData key values; they'll be translated to UTF-8 object descriptions. Useful for NSStrings and NSNumbers.
     1 //
     2 //  MYBonjourService.m
     3 //  MYNetwork
     4 //
     5 //  Created by Jens Alfke on 1/22/08.
     6 //  Copyright 2008 Jens Alfke. All rights reserved.
     7 //
     8 
     9 #import "MYBonjourService.h"
    10 #import "MYBonjourQuery.h"
    11 #import "MYAddressLookup.h"
    12 #import "IPAddress.h"
    13 #import "ConcurrentOperation.h"
    14 #import "Test.h"
    15 #import "Logging.h"
    16 #import "ExceptionUtils.h"
    17 #import <dns_sd.h>
    18 
    19 
    20 NSString* const kBonjourServiceResolvedAddressesNotification = @"BonjourServiceResolvedAddresses";
    21 
    22 
    23 @interface MYBonjourService ()
    24 @end
    25 
    26 
    27 @implementation MYBonjourService
    28 
    29 
    30 - (id) initWithName: (NSString*)serviceName
    31                type: (NSString*)type
    32              domain: (NSString*)domain
    33           interface: (UInt32)interfaceIndex
    34 {
    35     Assert(serviceName);
    36     Assert(type);
    37     Assert(domain);
    38     self = [super init];
    39     if (self != nil) {
    40         _name = [serviceName copy];
    41         _type = [type copy];
    42         _domain = [domain copy];
    43         _fullName = [[[self class] fullNameOfService: _name ofType: _type inDomain: _domain] retain];
    44         _interfaceIndex = interfaceIndex;
    45     }
    46     return self;
    47 }
    48 
    49 - (void) dealloc {
    50     [_txtQuery stop];
    51     [_txtQuery release];
    52     [_addressLookup stop];
    53     [_addressLookup release];
    54     [_name release];
    55     [_type release];
    56     [_domain release];
    57     [_fullName release];
    58     [_hostname release];
    59     [super dealloc];
    60 }
    61 
    62 
    63 @synthesize name=_name, type=_type, domain=_domain, fullName=_fullName, interfaceIndex=_interfaceIndex;
    64 
    65 
    66 - (NSString*) description {
    67     return $sprintf(@"%@[%@]", self.class,self.fullName);
    68 }
    69 
    70 
    71 - (NSComparisonResult) compare: (id)obj {
    72     return [_name caseInsensitiveCompare: [obj name]];
    73 }
    74 
    75 - (BOOL) isEqual: (id)obj {
    76     if ([obj isKindOfClass: [MYBonjourService class]]) {
    77         MYBonjourService *service = obj;
    78         return [_name caseInsensitiveCompare: [service name]] == 0
    79             && $equal(_type, service->_type)
    80             && $equal(_domain, service->_domain)
    81             && _interfaceIndex == service->_interfaceIndex;
    82     } else {
    83         return NO;
    84     }
    85 }
    86 
    87 - (NSUInteger) hash {
    88     return _name.hash ^ _type.hash ^ _domain.hash;
    89 }
    90 
    91 
    92 - (void) added {
    93     LogTo(Bonjour,@"Added %@",self);
    94 }
    95 
    96 - (void) removed {
    97     LogTo(Bonjour,@"Removed %@",self);
    98     [self stop];
    99     
   100     [_txtQuery stop];
   101     [_txtQuery release];
   102     _txtQuery = nil;
   103     
   104     [_addressLookup stop];
   105 }
   106 
   107 
   108 - (void) priv_finishResolve {
   109     // If I haven't finished my resolve yet, run it *synchronously* now so I can return a valid value:
   110     if (!_startedResolve )
   111         [self start];
   112     if (self.serviceRef)
   113         [self waitForReply];
   114 }    
   115 
   116 - (NSString*) hostname {
   117     if (!_hostname) [self priv_finishResolve];
   118     return _hostname;
   119 }
   120 
   121 - (UInt16) port {
   122     if (!_port) [self priv_finishResolve];
   123     return _port;
   124 }
   125 
   126 
   127 #pragma mark -
   128 #pragma mark TXT RECORD:
   129 
   130 
   131 - (NSDictionary*) txtRecord {
   132     if (!_txtQuery) {
   133         _txtQuery = [[MYBonjourQuery alloc] initWithBonjourService: self 
   134                                                         recordType: kDNSServiceType_TXT];
   135         _txtQuery.continuous = YES;
   136         [_txtQuery start];
   137     }
   138     return _txtRecord;
   139 }
   140 
   141 - (void) txtRecordChanged {
   142     // no-op (this is here for subclassers to override)
   143 }
   144 
   145 - (NSString*) txtStringForKey: (NSString*)key {
   146     NSData *value = [self.txtRecord objectForKey: key];
   147     if( ! value )
   148         return nil;
   149     if( ! [value isKindOfClass: [NSData class]] ) {
   150         Warn(@"TXT dictionary has unexpected value type: %@",value.class);
   151         return nil;
   152     }
   153     NSString *str = [[NSString alloc] initWithData: value encoding: NSUTF8StringEncoding];
   154     if( ! str )
   155         str = [[NSString alloc] initWithData: value encoding: NSWindowsCP1252StringEncoding];
   156     return [str autorelease];
   157 }
   158 
   159 - (void) setTxtData: (NSData*)txtData {
   160     NSDictionary *txtRecord = txtData ?[NSNetService dictionaryFromTXTRecordData: txtData] :nil;
   161     if (!$equal(txtRecord,_txtRecord)) {
   162         LogTo(Bonjour,@"%@ TXT = %@", self,txtRecord);
   163         [self willChangeValueForKey: @"txtRecord"];
   164         setObj(&_txtRecord, txtRecord);
   165         [self didChangeValueForKey: @"txtRecord"];
   166         [self txtRecordChanged];
   167     }
   168 }
   169 
   170 
   171 - (void) queryDidUpdate: (MYBonjourQuery*)query {
   172     if (query==_txtQuery)
   173         [self setTxtData: query.recordData];
   174 }
   175 
   176 
   177 #pragma mark -
   178 #pragma mark HOSTNAME/PORT RESOLUTION:
   179 
   180 
   181 - (void) priv_resolvedHostname: (NSString*)hostname
   182                           port: (uint16_t)port
   183                      txtRecord: (NSData*)txtData
   184 {
   185     LogTo(Bonjour, @"%@: hostname=%@, port=%u, txt=%u bytes", 
   186           self, hostname, port, txtData.length);
   187 
   188     // Don't call a setter method to set these properties: the getters are synchronous, so
   189     // I might already be blocked in a call to one of them, in which case creating a KV
   190     // notification could cause trouble...
   191     _hostname = hostname.copy;
   192     _port = port;
   193     
   194     // TXT getter is async, though, so I can use a setter to announce the data's availability:
   195     [self setTxtData: txtData];
   196 }
   197 
   198 
   199 static void resolveCallback(DNSServiceRef                       sdRef,
   200                             DNSServiceFlags                     flags,
   201                             uint32_t                            interfaceIndex,
   202                             DNSServiceErrorType                 errorCode,
   203                             const char                          *fullname,
   204                             const char                          *hosttarget,
   205                             uint16_t                            port,
   206                             uint16_t                            txtLen,
   207                             const unsigned char                 *txtRecord,
   208                             void                                *context)
   209 {
   210     MYBonjourService *service = context;
   211     @try{
   212         //LogTo(Bonjour, @"resolveCallback for %@ (err=%i)", service,errorCode);
   213         if (!errorCode) {
   214             NSData *txtData = nil;
   215             if (txtRecord)
   216                 txtData = [NSData dataWithBytes: txtRecord length: txtLen];
   217             [service priv_resolvedHostname: [NSString stringWithUTF8String: hosttarget]
   218                                       port: ntohs(port)
   219                                  txtRecord: txtData];
   220         }
   221     }catchAndReport(@"MYBonjourResolver query callback");
   222     [service gotResponse: errorCode];
   223 }
   224 
   225 
   226 - (DNSServiceErrorType) createServiceRef: (DNSServiceRef*)sdRefPtr {
   227     _startedResolve = YES;
   228     return DNSServiceResolve(sdRefPtr,
   229                              kDNSServiceFlagsShareConnection,
   230                              _interfaceIndex, 
   231                              _name.UTF8String, _type.UTF8String, _domain.UTF8String,
   232                              &resolveCallback, self);
   233 }
   234 
   235 
   236 - (MYAddressLookup*) addressLookup {
   237     if (!_addressLookup) {
   238         // Create the lookup the first time this is called:
   239         _addressLookup = [[MYAddressLookup alloc] initWithHostname: self.hostname];
   240         _addressLookup.port = _port;
   241         _addressLookup.interfaceIndex = _interfaceIndex;
   242     }
   243     // (Re)start the lookup if it's expired:
   244     if (_addressLookup && _addressLookup.timeToLive <= 0.0)
   245         [_addressLookup start];
   246     return _addressLookup;
   247 }
   248 
   249 
   250 - (MYBonjourQuery*) queryForRecord: (UInt16)recordType {
   251     MYBonjourQuery *query = [[[MYBonjourQuery alloc] initWithBonjourService: self recordType: recordType]
   252                                  autorelease];
   253     return [query start] ?query :nil;
   254 }
   255 
   256 
   257 @end
   258 
   259 
   260 
   261 /*
   262  Copyright (c) 2008-2009, Jens Alfke <jens@mooseyard.com>. All rights reserved.
   263  
   264  Redistribution and use in source and binary forms, with or without modification, are permitted
   265  provided that the following conditions are met:
   266  
   267  * Redistributions of source code must retain the above copyright notice, this list of conditions
   268  and the following disclaimer.
   269  * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
   270  and the following disclaimer in the documentation and/or other materials provided with the
   271  distribution.
   272  
   273  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
   274  IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
   275  FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI-
   276  BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
   277  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
   278   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
   279  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
   280  THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   281  */