diff options
Diffstat (limited to 'mDNSResponder/Clients/DNSServiceBrowser.m')
-rwxr-xr-x | mDNSResponder/Clients/DNSServiceBrowser.m | 679 |
1 files changed, 679 insertions, 0 deletions
diff --git a/mDNSResponder/Clients/DNSServiceBrowser.m b/mDNSResponder/Clients/DNSServiceBrowser.m new file mode 100755 index 00000000..b4e04140 --- /dev/null +++ b/mDNSResponder/Clients/DNSServiceBrowser.m @@ -0,0 +1,679 @@ +/* -*- Mode: C; tab-width: 4 -*- + * + * Copyright (c) 2002-2003 Apple Computer, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Cocoa/Cocoa.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <net/if.h> +#include <arpa/inet.h> +#include <netdb.h> +#include <sys/select.h> +#include <netinet/in.h> +#include <unistd.h> +#include <dns_sd.h> + +@class ServiceController; // holds state corresponding to outstanding DNSServiceRef + +@interface BrowserController : NSObject +{ + IBOutlet id nameField; + IBOutlet id typeField; + + IBOutlet id serviceDisplayTable; + IBOutlet id typeColumn; + IBOutlet id nameColumn; + IBOutlet id serviceTypeField; + IBOutlet id serviceNameField; + + IBOutlet id hostField; + IBOutlet id ipAddressField; + IBOutlet id ip6AddressField; + IBOutlet id portField; + IBOutlet id interfaceField; + IBOutlet id textField; + + NSMutableArray *_srvtypeKeys; + NSMutableArray *_srvnameKeys; + NSMutableArray *_sortedServices; + NSMutableDictionary *_servicesDict; + + ServiceController *_serviceBrowser; + ServiceController *_serviceResolver; + ServiceController *_ipv4AddressResolver; + ServiceController *_ipv6AddressResolver; +} + +- (void)notifyTypeSelectionChange:(NSNotification*)note; +- (void)notifyNameSelectionChange:(NSNotification*)note; + +- (IBAction)connect:(id)sender; + +- (IBAction)handleTableClick:(id)sender; +- (IBAction)removeSelected:(id)sender; +- (IBAction)addNewService:(id)sender; + +- (IBAction)update:(NSString *)Type; + +- (void)updateBrowseWithName:(const char *)name type:(const char *)resulttype domain:(const char *)domain interface:(uint32_t)interface flags:(DNSServiceFlags)flags; +- (void)resolveClientWitHost:(NSString *)host port:(uint16_t)port interfaceIndex:(uint32_t)interface txtRecord:(const char*)txtRecord txtLen:(uint16_t)txtLen; +- (void)updateAddress:(uint16_t)rrtype addr:(const void *)buff addrLen:(uint16_t)addrLen host:(const char*)host interfaceIndex:(uint32_t)interface more:(boolean_t)moreToCome; + +- (void)_cancelPendingResolve; +- (void)_clearResolvedInfo; + +@end + +// The ServiceController manages cleanup of DNSServiceRef & runloop info for an outstanding request +@interface ServiceController : NSObject +{ + DNSServiceRef fServiceRef; + CFSocketRef fSocketRef; + CFRunLoopSourceRef fRunloopSrc; +} + +- (id)initWithServiceRef:(DNSServiceRef)ref; +- (void)addToCurrentRunLoop; +- (DNSServiceRef)serviceRef; +- (void)dealloc; + +@end // interface ServiceController + + +static void +ProcessSockData(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void *data, void *info) +{ + DNSServiceRef serviceRef = (DNSServiceRef)info; + DNSServiceErrorType err = DNSServiceProcessResult(serviceRef); + if (err != kDNSServiceErr_NoError) { + printf("DNSServiceProcessResult() returned an error! %d\n", err); + } +} + + +static void +ServiceBrowseReply(DNSServiceRef sdRef, DNSServiceFlags servFlags, uint32_t interfaceIndex, DNSServiceErrorType errorCode, + const char *serviceName, const char *regtype, const char *replyDomain, void *context) +{ + if (errorCode == kDNSServiceErr_NoError) { + [(BrowserController*)context updateBrowseWithName:serviceName type:regtype domain:replyDomain interface:interfaceIndex flags:servFlags]; + } else { + printf("ServiceBrowseReply got an error! %d\n", errorCode); + } +} + + +static void +ServiceResolveReply(DNSServiceRef sdRef, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode, + const char *fullname, const char *hosttarget, uint16_t port, uint16_t txtLen, const char *txtRecord, void *context) +{ + if (errorCode == kDNSServiceErr_NoError) { + [(BrowserController*)context resolveClientWitHost:[NSString stringWithUTF8String:hosttarget] port:port interfaceIndex:interfaceIndex txtRecord:txtRecord txtLen:txtLen]; + } else { + printf("ServiceResolveReply got an error! %d\n", errorCode); + } +} + + +static void +QueryRecordReply(DNSServiceRef DNSServiceRef, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode, + const char *fullname, uint16_t rrtype, uint16_t rrclass, uint16_t rdlen, const void *rdata, uint32_t ttl, void *context) +{ + if (errorCode == kDNSServiceErr_NoError) { + [(BrowserController*)context updateAddress:rrtype addr:rdata addrLen:rdlen host:fullname interfaceIndex:interfaceIndex more:(flags & kDNSServiceFlagsMoreComing)]; + } else { + printf("QueryRecordReply got an error! %d\n", errorCode); + } +} + + +static void +InterfaceIndexToName(uint32_t interface, char *interfaceName) +{ + assert(interfaceName); + + if (interface == kDNSServiceInterfaceIndexAny) { + // All active network interfaces. + strlcpy(interfaceName, "all", IF_NAMESIZE); + } else if (interface == kDNSServiceInterfaceIndexLocalOnly) { + // Only available locally on this machine. + strlcpy(interfaceName, "local", IF_NAMESIZE); + } else if (interface == kDNSServiceInterfaceIndexP2P) { + // Peer-to-peer. + strlcpy(interfaceName, "p2p", IF_NAMESIZE); + } else { + // Converts interface index to interface name. + if_indextoname(interface, interfaceName); + } +} + + +@implementation BrowserController //Begin implementation of BrowserController methods + +- (void)registerDefaults +{ + NSMutableDictionary *regDict = [NSMutableDictionary dictionary]; + + NSArray *typeArray = [NSArray arrayWithObjects:@"_afpovertcp._tcp", + @"_smb._tcp", + @"_rfb._tcp", + @"_ssh._tcp", + @"_ftp._tcp", + @"_http._tcp", + @"_printer._tcp", + @"_ipp._tcp", + @"_airport._tcp", + @"_presence._tcp", + @"_daap._tcp", + @"_dpap._tcp", + nil]; + + NSArray *nameArray = [NSArray arrayWithObjects:@"AppleShare Servers", + @"Windows Sharing", + @"Screen Sharing", + @"Secure Shell", + @"FTP Servers", + @"Web Servers", + @"LPR Printers", + @"IPP Printers", + @"AirPort Base Stations", + @"iChat Buddies", + @"iTunes Libraries", + @"iPhoto Libraries", + nil]; + + [regDict setObject:typeArray forKey:@"SrvTypeKeys"]; + [regDict setObject:nameArray forKey:@"SrvNameKeys"]; + + [[NSUserDefaults standardUserDefaults] registerDefaults:regDict]; +} + + +- (id)init +{ + self = [super init]; + if (self) { + _srvtypeKeys = nil; + _srvnameKeys = nil; + _serviceBrowser = nil; + _serviceResolver = nil; + _ipv4AddressResolver = nil; + _ipv6AddressResolver = nil; + _sortedServices = [[NSMutableArray alloc] init]; + _servicesDict = [[NSMutableDictionary alloc] init]; + } + return self; +} + + +- (void)awakeFromNib +{ + [typeField sizeLastColumnToFit]; + [nameField sizeLastColumnToFit]; + [nameField setDoubleAction:@selector(connect:)]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notifyTypeSelectionChange:) name:NSTableViewSelectionDidChangeNotification object:typeField]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notifyNameSelectionChange:) name:NSTableViewSelectionDidChangeNotification object:nameField]; + + _srvtypeKeys = [[[NSUserDefaults standardUserDefaults] arrayForKey:@"SrvTypeKeys"] mutableCopy]; + _srvnameKeys = [[[NSUserDefaults standardUserDefaults] arrayForKey:@"SrvNameKeys"] mutableCopy]; + + if (!_srvtypeKeys || !_srvnameKeys) { + [_srvtypeKeys release]; + [_srvnameKeys release]; + [self registerDefaults]; + _srvtypeKeys = [[[NSUserDefaults standardUserDefaults] arrayForKey:@"SrvTypeKeys"] mutableCopy]; + _srvnameKeys = [[[NSUserDefaults standardUserDefaults] arrayForKey:@"SrvNameKeys"] mutableCopy]; + } + + [typeField reloadData]; +} + + +- (void)dealloc +{ + [_srvtypeKeys release]; + [_srvnameKeys release]; + [_servicesDict release]; + [_sortedServices release]; + [super dealloc]; +} + + +-(void)tableView:(NSTableView *)theTableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(int)row +{ + if (row < 0) return; +} + + +- (int)numberOfRowsInTableView:(NSTableView *)theTableView //Begin mandatory TableView methods +{ + if (theTableView == typeField) { + return [_srvnameKeys count]; + } + if (theTableView == nameField) { + return [_servicesDict count]; + } + if (theTableView == serviceDisplayTable) { + return [_srvnameKeys count]; + } + return 0; +} + + +- (id)tableView:(NSTableView *)theTableView objectValueForTableColumn:(NSTableColumn *)theColumn row:(int)rowIndex +{ + if (theTableView == typeField) { + return [_srvnameKeys objectAtIndex:rowIndex]; + } + if (theTableView == nameField) { + return [[_servicesDict objectForKey:[_sortedServices objectAtIndex:rowIndex]] name]; + } + if (theTableView == serviceDisplayTable) { + if (theColumn == typeColumn) { + return [_srvtypeKeys objectAtIndex:rowIndex]; + } + if (theColumn == nameColumn) { + return [_srvnameKeys objectAtIndex:rowIndex]; + } + return nil; + } + + return nil; +} + + +- (void)notifyTypeSelectionChange:(NSNotification*)note +{ + [self _cancelPendingResolve]; + + int index = [[note object] selectedRow]; + if (index != -1) { + [self update:[_srvtypeKeys objectAtIndex:index]]; + } else { + [self update:nil]; + } +} + + +- (void)notifyNameSelectionChange:(NSNotification*)note +{ + [self _cancelPendingResolve]; + + int index = [[note object] selectedRow]; + if (index == -1) { + return; + } + + // Get the currently selected service + NSNetService *service = [_servicesDict objectForKey:[_sortedServices objectAtIndex:index]]; + + DNSServiceRef serviceRef; + DNSServiceErrorType err = DNSServiceResolve(&serviceRef, + (DNSServiceFlags)0, + kDNSServiceInterfaceIndexAny, + (const char *)[[service name] UTF8String], + (const char *)[[service type] UTF8String], + (const char *)[[service domain] UTF8String], + (DNSServiceResolveReply)ServiceResolveReply, + self); + + if (kDNSServiceErr_NoError == err) { + _serviceResolver = [[ServiceController alloc] initWithServiceRef:serviceRef]; + [_serviceResolver addToCurrentRunLoop]; + } +} + + +- (IBAction)update:(NSString *)theType +{ + [_servicesDict removeAllObjects]; + [_sortedServices removeAllObjects]; + [nameField reloadData]; + + // get rid of the previous browser if one exists + if (_serviceBrowser != nil) { + [_serviceBrowser release]; + _serviceBrowser = nil; + } + + if (theType) { + DNSServiceRef serviceRef; + DNSServiceErrorType err = DNSServiceBrowse(&serviceRef, (DNSServiceFlags)0, 0, [theType UTF8String], NULL, ServiceBrowseReply, self); + if (kDNSServiceErr_NoError == err) { + _serviceBrowser = [[ServiceController alloc] initWithServiceRef:serviceRef]; + [_serviceBrowser addToCurrentRunLoop]; + } + } +} + + +- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication +{ + return YES; +} + + +- (void)updateBrowseWithName:(const char *)name type:(const char *)type domain:(const char *)domain interface:(uint32_t)interface flags:(DNSServiceFlags)flags +{ + NSString *key = [NSString stringWithFormat:@"%s.%s%s%d", name, type, domain, interface]; + NSNetService *service = [[NSNetService alloc] initWithDomain:[NSString stringWithUTF8String:domain] type:[NSString stringWithUTF8String:type] name:[NSString stringWithUTF8String:name]]; + + if (flags & kDNSServiceFlagsAdd) { + [_servicesDict setObject:service forKey:key]; + } else { + [_servicesDict removeObjectForKey:key]; + } + + // If not expecting any more data, then reload (redraw) TableView with newly found data + if (!(flags & kDNSServiceFlagsMoreComing)) { + + // Save the current TableView selection + int index = [nameField selectedRow]; + NSString *selected = (index != -1) ? [[_sortedServices objectAtIndex:index] copy] : nil; + + [_sortedServices release]; + _sortedServices = [[_servicesDict allKeys] mutableCopy]; + [_sortedServices sortUsingSelector:@selector(caseInsensitiveCompare:)]; + [nameField reloadData]; + + // Restore the previous TableView selection + index = selected ? [_sortedServices indexOfObject:selected] : NSNotFound; + if (index != NSNotFound) { + [nameField selectRowIndexes:[NSIndexSet indexSetWithIndex:index] byExtendingSelection:NO]; + [nameField scrollRowToVisible:index]; + } + + [selected release]; + } + + [service release]; + + return; +} + + +- (void)resolveClientWitHost:(NSString *)host port:(uint16_t)port interfaceIndex:(uint32_t)interface txtRecord:(const char*)txtRecord txtLen:(uint16_t)txtLen +{ + DNSServiceRef serviceRef; + + if (_ipv4AddressResolver) { + [_ipv4AddressResolver release]; + _ipv4AddressResolver = nil; + } + + if (_ipv6AddressResolver) { + [_ipv6AddressResolver release]; + _ipv6AddressResolver = nil; + } + + // Start an async lookup for IPv4 addresses + DNSServiceErrorType err = DNSServiceQueryRecord(&serviceRef, (DNSServiceFlags)0, interface, [host UTF8String], kDNSServiceType_A, kDNSServiceClass_IN, QueryRecordReply, self); + if (err == kDNSServiceErr_NoError) { + _ipv4AddressResolver = [[ServiceController alloc] initWithServiceRef:serviceRef]; + [_ipv4AddressResolver addToCurrentRunLoop]; + } + + // Start an async lookup for IPv6 addresses + err = DNSServiceQueryRecord(&serviceRef, (DNSServiceFlags)0, interface, [host UTF8String], kDNSServiceType_AAAA, kDNSServiceClass_IN, QueryRecordReply, self); + if (err == kDNSServiceErr_NoError) { + _ipv6AddressResolver = [[ServiceController alloc] initWithServiceRef:serviceRef]; + [_ipv6AddressResolver addToCurrentRunLoop]; + } + + char interfaceName[IF_NAMESIZE]; + InterfaceIndexToName(interface, interfaceName); + + [hostField setStringValue:host]; + [interfaceField setStringValue:[NSString stringWithUTF8String:interfaceName]]; + [portField setIntValue:ntohs(port)]; + + // kind of a hack: munge txtRecord so it's human-readable + if (txtLen > 0) { + char *readableText = (char*) malloc(txtLen); + if (readableText != nil) { + ByteCount index, subStrLen; + memcpy(readableText, txtRecord, txtLen); + for (index=0; index < txtLen - 1; index += subStrLen + 1) { + subStrLen = readableText[index]; + readableText[index] = ' '; + } + [textField setStringValue:[NSString stringWithCString:&readableText[1] length:txtLen - 1]]; + free(readableText); + } + } +} + + +- (void)updateAddress:(uint16_t)rrtype addr:(const void *)buff addrLen:(uint16_t)addrLen host:(const char*) host interfaceIndex:(uint32_t)interface more:(boolean_t)moreToCome +{ + char addrBuff[256]; + + if (rrtype == kDNSServiceType_A) { + inet_ntop(AF_INET, buff, addrBuff, sizeof(addrBuff)); + if ([[ipAddressField stringValue] length] > 0) { + [ipAddressField setStringValue:[NSString stringWithFormat:@"%@, ", [ipAddressField stringValue]]]; + } + [ipAddressField setStringValue:[NSString stringWithFormat:@"%@%s", [ipAddressField stringValue], addrBuff]]; + + if (!moreToCome) { + [_ipv4AddressResolver release]; + _ipv4AddressResolver = nil; + } + } else if (rrtype == kDNSServiceType_AAAA) { + inet_ntop(AF_INET6, buff, addrBuff, sizeof(addrBuff)); + if ([[ip6AddressField stringValue] length] > 0) { + [ip6AddressField setStringValue:[NSString stringWithFormat:@"%@, ", [ip6AddressField stringValue]]]; + } + [ip6AddressField setStringValue:[NSString stringWithFormat:@"%@%s", [ip6AddressField stringValue], addrBuff]]; + + if (!moreToCome) { + [_ipv6AddressResolver release]; + _ipv6AddressResolver = nil; + } + } +} + + +- (void)connect:(id)sender +{ + NSString *host = [hostField stringValue]; + NSString *txtRecord = [textField stringValue]; + int port = [portField intValue]; + + int index = [nameField selectedRow]; + NSString *selected = (index >= 0) ? [_sortedServices objectAtIndex:index] : nil; + NSString *type = [[_servicesDict objectForKey:selected] type]; + + if ([type isEqual:@"_http._tcp."]) { + NSString *pathDelim = @"path="; + NSRange where; + + // If the TXT record specifies a path, extract it. + where = [txtRecord rangeOfString:pathDelim options:NSCaseInsensitiveSearch]; + if (where.length) { + NSRange targetRange = { where.location + where.length, [txtRecord length] - where.location - where.length }; + NSRange endDelim = [txtRecord rangeOfString:@"\n" options:kNilOptions range:targetRange]; + + if (endDelim.length) // if a delimiter was found, truncate the target range + targetRange.length = endDelim.location - targetRange.location; + + NSString *path = [txtRecord substringWithRange:targetRange]; + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%d%@", host, port, path]]]; + } else { + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%d", host, port]]]; + } + } + else if ([type isEqual:@"_ftp._tcp."]) [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"ftp://%@:%d/", host, port]]]; + else if ([type isEqual:@"_ssh._tcp."]) [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"ssh://%@:%d/", host, port]]]; + else if ([type isEqual:@"_afpovertcp._tcp."]) [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"afp://%@:%d/", host, port]]]; + else if ([type isEqual:@"_smb._tcp."]) [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"smb://%@:%d/", host, port]]]; + else if ([type isEqual:@"_rfb._tcp."]) [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"vnc://%@:%d/", host, port]]]; + + return; +} + + +- (IBAction)handleTableClick:(id)sender +{ + //populate the text fields +} + + +- (IBAction)removeSelected:(id)sender +{ + // remove the selected row and force a refresh + + int selectedRow = [serviceDisplayTable selectedRow]; + + if (selectedRow) { + + [_srvtypeKeys removeObjectAtIndex:selectedRow]; + [_srvnameKeys removeObjectAtIndex:selectedRow]; + + [[NSUserDefaults standardUserDefaults] setObject:_srvtypeKeys forKey:@"SrvTypeKeys"]; + [[NSUserDefaults standardUserDefaults] setObject:_srvnameKeys forKey:@"SrvNameKeys"]; + + [typeField reloadData]; + [serviceDisplayTable reloadData]; + } +} + + +- (IBAction)addNewService:(id)sender +{ + // add new entries from the edit fields to the arrays for the defaults + NSString *newType = [serviceTypeField stringValue]; + NSString *newName = [serviceNameField stringValue]; + + // 3282283: trim trailing '.' from service type field + if ([newType length] && [newType hasSuffix:@"."]) + newType = [newType substringToIndex:[newType length] - 1]; + + if ([newType length] && [newName length]) { + [_srvtypeKeys addObject:newType]; + [_srvnameKeys addObject:newName]; + + [[NSUserDefaults standardUserDefaults] setObject:_srvtypeKeys forKey:@"SrvTypeKeys"]; + [[NSUserDefaults standardUserDefaults] setObject:_srvnameKeys forKey:@"SrvNameKeys"]; + + [typeField reloadData]; + [serviceDisplayTable reloadData]; + } +} + + +- (void)_cancelPendingResolve +{ + [_ipv4AddressResolver release]; + _ipv4AddressResolver = nil; + + [_ipv6AddressResolver release]; + _ipv6AddressResolver = nil; + + [_serviceResolver release]; + _serviceResolver = nil; + + [self _clearResolvedInfo]; +} + + +- (void)_clearResolvedInfo +{ + [hostField setStringValue:@""]; + [ipAddressField setStringValue:@""]; + [ip6AddressField setStringValue:@""]; + [portField setStringValue:@""]; + [interfaceField setStringValue:@""]; + [textField setStringValue:@""]; +} + +@end // implementation BrowserController + + +@implementation ServiceController : NSObject +{ + DNSServiceRef fServiceRef; + CFSocketRef fSocketRef; + CFRunLoopSourceRef fRunloopSrc; +} + + +- (id)initWithServiceRef:(DNSServiceRef)ref +{ + self = [super init]; + if (self) { + fServiceRef = ref; + fSocketRef = NULL; + fRunloopSrc = NULL; + } + return self; +} + + +- (void)addToCurrentRunLoop +{ + CFSocketContext context = { 0, (void*)fServiceRef, NULL, NULL, NULL }; + + fSocketRef = CFSocketCreateWithNative(kCFAllocatorDefault, DNSServiceRefSockFD(fServiceRef), kCFSocketReadCallBack, ProcessSockData, &context); + if (fSocketRef) { + // Prevent CFSocketInvalidate from closing DNSServiceRef's socket. + CFOptionFlags sockFlags = CFSocketGetSocketFlags(fSocketRef); + CFSocketSetSocketFlags(fSocketRef, sockFlags & (~kCFSocketCloseOnInvalidate)); + fRunloopSrc = CFSocketCreateRunLoopSource(kCFAllocatorDefault, fSocketRef, 0); + } + if (fRunloopSrc) { + CFRunLoopAddSource(CFRunLoopGetCurrent(), fRunloopSrc, kCFRunLoopDefaultMode); + } else { + printf("Could not listen to runloop socket\n"); + } +} + + +- (DNSServiceRef)serviceRef +{ + return fServiceRef; +} + + +- (void)dealloc +{ + if (fSocketRef) { + CFSocketInvalidate(fSocketRef); // Note: Also closes the underlying socket + CFRelease(fSocketRef); + + // Workaround that gives time to CFSocket's select thread so it can remove the socket from its + // FD set before we close the socket by calling DNSServiceRefDeallocate. <rdar://problem/3585273> + usleep(1000); + } + + if (fRunloopSrc) { + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), fRunloopSrc, kCFRunLoopDefaultMode); + CFRelease(fRunloopSrc); + } + + DNSServiceRefDeallocate(fServiceRef); + + [super dealloc]; +} + + +@end // implementation ServiceController + +int main(int argc, const char *argv[]) +{ + return NSApplicationMain(argc, argv); +} |