Skip to content
This repository

Bonjour support #24

Merged
merged 13 commits into from over 1 year ago

3 participants

Jean Regisser Mike Lewis Mathieu Ravaux
Jean Regisser

This adds support for auto discovery of the ponyd server via Bonjour.

So instead of specifying a host to connect to in the client code, you can just do:

[debugger autoConnect];

Let me know what you think.

Note: I've opened this new pull request because I had inadvertently specified a sha1 in #19 instead of a branch.

ponyd/gateway.py
@@ -233,6 +240,8 @@ def __call__(self):
233 240
 
234 241
         print "PonyGateway starting. Listening on %s:%s" % (self.listen_interface, self.listen_port)
235 242
 
  243
+        thread.start_new_thread(bonjour.register_service, (self.bonjour_name, "_ponyd._tcp", self.listen_port))
2
Mike Lewis Owner

Is there a way we can do this without using threads? If not, that's OK, but, the server uses Tornado which is event driven. I just checked out pybonjour and it looks like it can work with select (which is how the ioloop in tornado works internally)

Please check out http://www.tornadoweb.org/documentation/ioloop.html

Here's what I think you'll need to do

io_loop.add_handler(service,  some_callback_to_dnsserviceprocessresult, io_loop.READ)

This means you won't have to run a select loop yourself.

Ok I'll see what I can do with the ioloop. I just went with the simplest way to add this feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ObjC/PonyDebugger/PDDebugger.m
@@ -177,6 +182,71 @@ - (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error;
177 182
     _socket = nil;
178 183
 }
179 184
 
  185
+#pragma mark - NSNetServiceBrowserDelegate
  186
+
  187
+- (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didFindService:(NSNetService*)service moreComing:(BOOL)moreComing;
  188
+{
  189
+    if (_bonjourServiceName
  190
+        && NSOrderedSame != [_bonjourServiceName compare:service.name
  191
+                                                 options:NSCaseInsensitiveSearch | NSNumericSearch | NSDiacriticInsensitiveSearch]) {
  192
+        return;
  193
+    }
2
Mike Lewis Owner

Please format to something like

const NSStringCompareOptions compareOptions = NSCaseInsensitiveSearch | NSNumericSearch | NSDiacriticInsensitiveSearch;
if (_bonjourServiceName  != nil  && [_bonjourServiceName compare:service.name options:compareOptions] != NSOrderedSame) {

Apologies for not having a style guide.

But also, why NSNumericSearch?

You're right NSNumericSearch is not needed, I don't remember why I put it ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Examples/PDTwitterTest/PDTwitterTest/PDAppDelegate.m
@@ -46,8 +46,12 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
46 46
     [debugger enableCoreDataDebugging];
47 47
     [debugger addManagedObjectContext:self.managedObjectContext withName:@"Twitter Test MOC"];
48 48
     
49  
-    // Connect on launch.
50  
-    [debugger connectToURL:[NSURL URLWithString:@"ws://localhost:9000/device"]];
  49
+    // Auto connect via bonjour discovery
  50
+    [debugger autoConnect];
  51
+    // Or to a specific ponyd bonjour service
  52
+    //[debugger autoConnectToBonjourServiceNamed:@"MY PONY"];
1
Mike Lewis Owner

Would prefer to leave the the default to the direct hostname for now. Would like to think more about security implications, etc, and having it a bit battle tested before changing the defaults.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Mike Lewis mikelikespie commented on the diff October 25, 2012
ObjC/PonyDebugger/PDDebugger.m
((30 lines not shown))
  211
+    NSUInteger serviceIndex = [_bonjourServices indexOfObject:service];
  212
+    if (NSNotFound != serviceIndex) {
  213
+        [_bonjourServices removeObjectAtIndex:serviceIndex];
  214
+        NSLog(@"Removed ponyd bonjour service: %@", service);
  215
+        
  216
+        // Try next one
  217
+        if (!_currentService && _bonjourServices.count){
  218
+            NSNetService* nextService = [_bonjourServices objectAtIndex:(serviceIndex % _bonjourServices.count)];
  219
+            [self _resolveService:nextService];
  220
+        }
  221
+    }
  222
+}
  223
+
  224
+#pragma mark - NSNetServiceDelegate
  225
+
  226
+- (void)netService:(NSNetService *)service didNotResolve:(NSDictionary *)errorDict;
2
Mike Lewis Owner

Will this keep stop after exhaustively trying all the services, or keep looping? (just need clarification, no fixes)

It will loop on all services until one of them can be resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Mike Lewis mikelikespie commented on the diff October 25, 2012
ponyd/bonjour.py
... ...
@@ -0,0 +1,27 @@
  1
+import select
  2
+import pybonjour
  3
+import logging
  4
+
  5
+logger = logging.getLogger('bonjour')
  6
+
  7
+def register_service(name, regtype, port):
1
Mike Lewis Owner

See comment below re: threads

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ponyd/bonjour.py
... ...
@@ -0,0 +1,27 @@
  1
+import select
  2
+import pybonjour
  3
+import logging
  4
+
  5
+logger = logging.getLogger('bonjour')
  6
+
  7
+def register_service(name, regtype, port):
  8
+    def register_callback(sdRef, flags, errorCode, name, regtype, domain):
  9
+        if errorCode == pybonjour.kDNSServiceErr_NoError:
  10
+			logger.debug('Registered bonjour service %s.%s', name, regtype)
  11
+
  12
+    service = pybonjour.DNSServiceRegister(name = name,
1
Mike Lewis Owner

For named arguments, I prefer foo=bar. This is consistent with PEP8 (even though not really following it for everything)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ponyd/bonjour.py
((2 lines not shown))
  2
+import pybonjour
  3
+import logging
  4
+
  5
+logger = logging.getLogger('bonjour')
  6
+
  7
+def register_service(name, regtype, port):
  8
+    def register_callback(sdRef, flags, errorCode, name, regtype, domain):
  9
+        if errorCode == pybonjour.kDNSServiceErr_NoError:
  10
+			logger.debug('Registered bonjour service %s.%s', name, regtype)
  11
+
  12
+    service = pybonjour.DNSServiceRegister(name = name,
  13
+                                           regtype = regtype,
  14
+                                           port = port,
  15
+                                           callBack = register_callback)
  16
+
  17
+    try:
1
Mike Lewis Owner

Even though I think this will be going away to use the ioloop, you could combine the two tries without restructuring much.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Mike Lewis
Owner

@jeanregisser Thanks for the pull request! It mostly looks good aside from a few comments. Once they're resolved, I will get it merged in.

As an aside, I think a nice addition would be a delegate that you could hook up to the resolver that will allow one to easily hook it up to a UITableView for an endpoint selection menu. It will be hard to make it secure without a UI to validate who you're connecting to. It may not be a concern now, but I have plans on adding remote execution via JSCocoa eventually.

Jean Regisser

Thanks for the feedback!
I'll make the necessary changes in the coming days.

Jean Regisser

I've finished the necessary changes.
Any chance to merge this soon?

Mathieu Ravaux

+1 this is really convenient

Mike Lewis
Owner

Cool! This looks much better (python runloop stuff in particular). Sorry for the delay getting it merged.

Mike Lewis mikelikespie merged commit 137cdf4 into from January 12, 2013
Mike Lewis mikelikespie closed this January 12, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
6  Examples/PDTwitterTest/PDTwitterTest/PDAppDelegate.m
@@ -46,8 +46,12 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
46 46
     [debugger enableCoreDataDebugging];
47 47
     [debugger addManagedObjectContext:self.managedObjectContext withName:@"Twitter Test MOC"];
48 48
     
49  
-    // Connect on launch.
  49
+    // Connect to a specific host
50 50
     [debugger connectToURL:[NSURL URLWithString:@"ws://localhost:9000/device"]];
  51
+    // Or auto connect via bonjour discovery
  52
+    //[debugger autoConnect];
  53
+    // Or to a specific ponyd bonjour service
  54
+    //[debugger autoConnectToBonjourServiceNamed:@"MY PONY"];
51 55
     
52 56
 #endif
53 57
     
4  ObjC/PonyDebugger/PDDebugger.h
@@ -25,6 +25,10 @@
25 25
 - (void)sendEventWithName:(NSString *)string parameters:(id)params;
26 26
 
27 27
 // Connect/Disconnect
  28
+- (void)autoConnect; // Connect to any ponyd service found via Bonjour
  29
+// Only connect to the specified Bonjour service name, this makes things easier in a teamwork
  30
+// environment where multiple instances of ponyd may run on the same network
  31
+- (void)autoConnectToBonjourServiceNamed:(NSString*)serviceName;
28 32
 - (void)connectToURL:(NSURL *)url;
29 33
 - (BOOL)isConnected;
30 34
 - (void)disconnect;
116  ObjC/PonyDebugger/PDDebugger.m
@@ -25,10 +25,11 @@
25 25
 
26 26
 
27 27
 static NSString *const PDClientIDKey = @"com.squareup.PDDebugger.clientID";
  28
+static NSString *const PDBonjourServiceType = @"_ponyd._tcp";
28 29
 
  30
+@interface PDDebugger () <SRWebSocketDelegate, NSNetServiceBrowserDelegate, NSNetServiceDelegate>
29 31
 
30  
-@interface PDDebugger () <SRWebSocketDelegate>
31  
-
  32
+- (void)_resolveService:(NSNetService*)service;
32 33
 - (void)_addController:(PDDomainController *)controller;
33 34
 - (NSString *)_domainNameForController:(PDDomainController *)controller;
34 35
 - (BOOL)_isTrackingDomainController:(PDDomainController *)controller;
@@ -37,6 +38,10 @@ - (BOOL)_isTrackingDomainController:(PDDomainController *)controller;
37 38
 
38 39
 
39 40
 @implementation PDDebugger {
  41
+    NSString *_bonjourServiceName;
  42
+    NSNetServiceBrowser *_bonjourBrowser;
  43
+    NSMutableArray *_bonjourServices;
  44
+    NSNetService *_currentService;
40 45
     NSMutableDictionary *_domains;
41 46
     NSMutableDictionary *_controllers;
42 47
     __strong SRWebSocket *_socket;
@@ -177,6 +182,70 @@ - (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error;
177 182
     _socket = nil;
178 183
 }
179 184
 
  185
+#pragma mark - NSNetServiceBrowserDelegate
  186
+
  187
+- (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didFindService:(NSNetService*)service moreComing:(BOOL)moreComing;
  188
+{
  189
+    const NSStringCompareOptions compareOptions = NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch;
  190
+    if (_bonjourServiceName != nil && [_bonjourServiceName compare:service.name options:compareOptions] != NSOrderedSame) {
  191
+        return;
  192
+    }
  193
+    
  194
+    NSLog(@"Found ponyd bonjour service: %@", service);
  195
+    [_bonjourServices addObject:service];
  196
+    
  197
+    if (!_currentService) {
  198
+        [self _resolveService:service];
  199
+    }
  200
+}
  201
+
  202
+- (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didRemoveService:(NSNetService*)service moreComing:(BOOL)moreComing;
  203
+{
  204
+    if ([service isEqual:_currentService]) {
  205
+        [_currentService stop];
  206
+        _currentService.delegate = nil;
  207
+        _currentService = nil;
  208
+    }
  209
+    
  210
+    NSUInteger serviceIndex = [_bonjourServices indexOfObject:service];
  211
+    if (NSNotFound != serviceIndex) {
  212
+        [_bonjourServices removeObjectAtIndex:serviceIndex];
  213
+        NSLog(@"Removed ponyd bonjour service: %@", service);
  214
+        
  215
+        // Try next one
  216
+        if (!_currentService && _bonjourServices.count){
  217
+            NSNetService* nextService = [_bonjourServices objectAtIndex:(serviceIndex % _bonjourServices.count)];
  218
+            [self _resolveService:nextService];
  219
+        }
  220
+    }
  221
+}
  222
+
  223
+#pragma mark - NSNetServiceDelegate
  224
+
  225
+- (void)netService:(NSNetService *)service didNotResolve:(NSDictionary *)errorDict;
  226
+{
  227
+    NSAssert([service isEqual:_currentService], @"Did not resolve incorrect service!");
  228
+    _currentService.delegate = nil;
  229
+    _currentService = nil;
  230
+    
  231
+    // Try next one, we may retry the same one if there's only 1 service in _bonjourServices
  232
+    NSUInteger serviceIndex = [_bonjourServices indexOfObject:service];
  233
+    if (NSNotFound != serviceIndex) {
  234
+        if (_bonjourServices.count){
  235
+            NSNetService* nextService = [_bonjourServices objectAtIndex:((serviceIndex + 1) % _bonjourServices.count)];
  236
+            [self _resolveService:nextService];
  237
+        }
  238
+    }
  239
+}
  240
+
  241
+
  242
+- (void)netServiceDidResolveAddress:(NSNetService *)service;
  243
+{
  244
+    NSAssert([service isEqual:_currentService], @"Resolved incorrect service!");
  245
+
  246
+    [self connectToURL:[NSURL URLWithString:[NSString stringWithFormat:@"ws://%@:%d/device", [service hostName], [service port]]]];
  247
+}
  248
+
180 249
 #pragma mark - Public Methods
181 250
 
182 251
 - (id)domainForName:(NSString *)name;
@@ -198,8 +267,34 @@ - (void)sendEventWithName:(NSString *)methodName parameters:(id)params;
198 267
 
199 268
 #pragma mark Connect / Disconnect
200 269
 
  270
+- (void)autoConnect;
  271
+{
  272
+    // Connect to any bonjour service
  273
+    [self autoConnectToBonjourServiceNamed:nil];
  274
+}
  275
+
  276
+- (void)autoConnectToBonjourServiceNamed:(NSString*)serviceName;
  277
+{
  278
+    if (_bonjourBrowser) {
  279
+        return;
  280
+    }
  281
+    
  282
+    _bonjourServiceName = serviceName;
  283
+    _bonjourServices = [NSMutableArray array];
  284
+    _bonjourBrowser = [[NSNetServiceBrowser alloc] init];
  285
+    [_bonjourBrowser setDelegate:self];
  286
+    
  287
+    if (_bonjourServiceName) {
  288
+        NSLog(@"Waiting for ponyd bonjour service '%@'...", _bonjourServiceName);
  289
+    } else {
  290
+        NSLog(@"Waiting for ponyd bonjour service...");
  291
+    }
  292
+    [_bonjourBrowser searchForServicesOfType:PDBonjourServiceType inDomain:@""];
  293
+}
  294
+
201 295
 - (void)connectToURL:(NSURL *)url;
202 296
 {
  297
+    NSLog(@"Connecting to %@", url);
203 298
     [_socket close];
204 299
     _socket.delegate = nil;
205 300
     
@@ -215,6 +310,15 @@ - (BOOL)isConnected;
215 310
 
216 311
 - (void)disconnect;
217 312
 {
  313
+    [_bonjourBrowser stop];
  314
+    _bonjourBrowser.delegate = nil;
  315
+    _bonjourBrowser = nil;
  316
+    _bonjourServiceName = nil;
  317
+    _bonjourServices = nil;
  318
+    [_currentService stop];
  319
+    _currentService.delegate = nil;
  320
+    _currentService = nil;
  321
+    
218 322
     [_socket close];
219 323
     _socket.delegate = nil;
220 324
     _socket = nil;
@@ -265,6 +369,14 @@ - (void)removeManagedObjectContext:(NSManagedObjectContext *)context;
265 369
 
266 370
 #pragma mark - Private Methods
267 371
 
  372
+- (void)_resolveService:(NSNetService*)service;
  373
+{
  374
+    NSLog(@"Resolving %@", service);
  375
+    _currentService = service;
  376
+    _currentService.delegate = self;
  377
+    [_currentService resolveWithTimeout:10.f];
  378
+}
  379
+
268 380
 - (NSString *)_domainNameForController:(PDDomainController *)controller;
269 381
 {
270 382
     Class cls = [[controller class] domainClass];
8  README.md
Source Rendered
@@ -127,7 +127,13 @@ PonyDebugger's main entry points exist in the `PDDebugger` singleton.
127 127
 PDDebugger *debugger = [PDDebugger defaultInstance];
128 128
 ```
129 129
 
130  
-To open the connection to `ws://localhost:9000/device`:
  130
+To connect automatically to the PonyGateway on your LAN (via Bonjour):
  131
+
  132
+``` objective-c
  133
+[debugger autoConnect];
  134
+```
  135
+
  136
+Or to open the connection to a specific host, for instance `ws://localhost:9000/device`:
131 137
 
132 138
 ``` objective-c
133 139
 [debugger connectToURL:[NSURL URLWithString:@"ws://localhost:9000/device"]];
22  ponyd/bonjour.py
... ...
@@ -0,0 +1,22 @@
  1
+import functools
  2
+from tornado import ioloop
  3
+import pybonjour
  4
+import logging
  5
+
  6
+logger = logging.getLogger('bonjour')
  7
+
  8
+def register_service(name, regtype, port):
  9
+    def connection_ready(service, fd, events):
  10
+        pybonjour.DNSServiceProcessResult(service)
  11
+
  12
+    def register_callback(sdRef, flags, errorCode, name, regtype, domain):
  13
+        if errorCode == pybonjour.kDNSServiceErr_NoError:
  14
+            logger.debug('Registered bonjour service %s.%s', name, regtype)
  15
+
  16
+    service = pybonjour.DNSServiceRegister(name=name,
  17
+                                           regtype=regtype,
  18
+                                           port=port,
  19
+                                           callBack=register_callback)
  20
+    io_loop = ioloop.IOLoop.instance()
  21
+    callback = functools.partial(connection_ready, service)
  22
+    io_loop.add_handler(service.fileno(), callback, io_loop.READ)
8  ponyd/gateway.py
@@ -14,6 +14,8 @@
14 14
 
15 15
 import argparse
16 16
 
  17
+import bonjour
  18
+
17 19
 import logging
18 20
 
19 21
 from ponyd.constants import DEFAULT_DEVTOOLS_PATH
@@ -213,6 +215,10 @@ class Gateway(PonydCommand):
213 215
                            default='127.0.0.1',
214 216
                            metavar='IFACE')
215 217
 
  218
+    bonjour_name = Arg('-b', '--bonjour-name',
  219
+                       help='name of the bonjour service. [default: %(default)s]',
  220
+                       default='Pony Gateway')
  221
+
216 222
     def __call__(self):
217 223
         if not os.path.exists(self.devtools_path):
218 224
             print "Error: devtools directory %s does not exist. Use ponydownloader to download a compatible version of Chrome Developer Tools." % self.devtools_path
@@ -233,6 +239,8 @@ def __call__(self):
233 239
 
234 240
         print "PonyGateway starting. Listening on %s:%s" % (self.listen_interface, self.listen_port)
235 241
 
  242
+        bonjour.register_service(self.bonjour_name, "_ponyd._tcp", self.listen_port)
  243
+
236 244
         application.listen(self.listen_port, self.listen_interface)
237 245
         tornado.ioloop.IOLoop.instance().start()
238 246
 
2  setup.py
@@ -24,7 +24,7 @@ def read(fname):
24 24
     author_email='eng@squareup.com',
25 25
     url='https://github.com/square/PonyDebugger',
26 26
     license='Apache Licence 2.0',
27  
-    install_requires=['tornado'],
  27
+    install_requires=['tornado', 'pybonjour'],
28 28
     packages=['ponyd'],
29 29
     include_package_data=True,
30 30
     zip_safe=False,
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.