PonyDebugger Tutorial: Debugging iOS Apps using Chrome

Pony debugger is a great debugging tool for iOS network traffic and beyond. I’ve met so many pitfalls to get it work, so here is a tutorial summarizing the steps.

Install:

curl -s https://cloud.github.com/downloads/square/PonyDebugger/bootstrap-ponyd.py \
  | python - --ponyd-symlink=/usr/local/bin/ponyd ~/Library/PonyDebugger
source ~/Library/PonyDebugger/bin/activate
pip install -U -e git+https://github.com/square/PonyDebugger.git#egg=ponydebugger \
  --allow-external pybonjour --allow-unverified pybonjour
ponyd update-devtools

Notes:

  • See https://github.com/square/PonyDebugger/blob/master/README_ponyd.rst

  • A copy of src will be at /Users/zxp/Library/PonyDebugger/src/ponydebugger/

  • The real server is at /Library/Python/2.7/site-packages/ponyd-1.0-py2.7.egg/ponyd/

  • ponyd update-devtools downloads http://storage.googleapis.com/chromium-browser-continuous/Mac/152100/devtools_frontend.zip to /usr/local/src/devtools. This will loaded by http://localhost:9000/devtools/devtools.html?host=localhost:9000&page=2

Run

  • Run ponyd serve -v, and then open http://localhost:9000.

  • Open the test app ~/Library/PonyDebugger/src/ponydebugger/Examples/PDTestApp/PDTestApp.xcodeproj

  • In Xcode choose scheme to ‘PDTestApp’, and run it.

  • Now you should see the app connected in http://localhost:9000.

  • Click it to open the debugger: http://localhost:9000/devtools/devtools.html?host=localhost:9000&page=1

Using PonyDebugger in your project

  • Install CocoaPod
$ sudo gem install cocoapods
$ pod setup
  • Create a ‘PodFile’ in the project folder, with following content:
platform :ios, '5.0'
pod 'PonyDebugger', '~> 0.4.0’ 
  • Run pod install

  • This creates a ‘xcworkspace’ file. Open this xcworkspace file, not the xcodeporj file!

[14:10:37] zxp@Xiangpengs-McAir-7:~/dev/iOS/Beginning-iOS6-Dev_sourcecode/Chapter_08/08 - Cells $ ll
total 32
drwxr-xr-x@  9 zxp  staff   306 11 16 14:10 .
drwx------@  5 zxp  staff   170  7  7 22:29 ..
-rw-r--r--@  1 zxp  staff  6148  7  7 23:39 .DS_Store
drwxr-xr-x@ 13 zxp  staff   442  7  7 22:39 Cells
drwxr-xr-x@  5 zxp  staff   170  7  7 22:38 Cells.xcodeproj
drwxr-xr-x   3 zxp  staff   102 11 16 14:10 Cells.xcworkspace
-rw-r--r--   1 zxp  staff    52 11 16 14:09 PodFile
-rw-r--r--   1 zxp  staff   275 11 16 14:10 Podfile.lock
drwxr-xr-x   9 zxp  staff   306 11 16 14:10 Pods

[14:11:29] zxp@Xiangpengs-McAir-7:~/dev/iOS/Beginning-iOS6-Dev_sourcecode/Chapter_08/08 - Cells $ cat PodFile 
platform :ios, '5.0'
pod 'PonyDebugger', '~> 0.4.0'
  • Now add these code to your AppDelegate.m, and enable ENABLE_PONYDEBUGGER as described in the comment. You have to do this. Uncomment the #if doesn’t work.

#if ENABLE_PONYDEBUGGER
#import <PonyDebugger/PonyDebugger.h>
#endif
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Don't use PonyDebugger unless we have ENABLE_PONYDEBUGGER enabled.
    // When ENABLE_PONYDEBUGGER is enabled -lSocketRocket -lPonyDebugger
    // should be added to "Other linker flags" settings.
    // Release builds should not use PonyDebugger

#if ENABLE_PONYDEBUGGER

    PDDebugger *debugger = [PDDebugger defaultInstance];

    // Enable Network debugging, and automatically track network traffic that comes through any classes that implement either NSURLConnectionDelegate, NSURLSessionTaskDelegate, NSURLSessionDataDelegate or NSURLSessionDataDelegate methods.
    [debugger enableNetworkTrafficDebugging];
    [debugger forwardAllNetworkTraffic];

    // Enable Core Data debugging, and broadcast the main managed object context.
    [debugger enableCoreDataDebugging];
    // zxp: Check example app to enable this properly
    // [debugger addManagedObjectContext:self.managedObjectContext withName:@"PonyDebugger Test App MOC"];

    // Enable View Hierarchy debugging. This will swizzle UIView methods to monitor changes in the hierarchy
    // Choose a few UIView key paths to display as attributes of the dom nodes
    [debugger enableViewHierarchyDebugging];
    [debugger setDisplayedViewAttributeKeyPaths:@[@"frame", @"hidden", @"alpha", @"opaque", @"accessibilityLabel", @"text"]];

    // Connect to a specific host
    [debugger connectToURL:[NSURL URLWithString:@"ws://localhost:9000/device"]];
    // Or auto connect via bonjour discovery
    //[debugger autoConnect];
    // Or to a specific ponyd bonjour service
    //[debugger autoConnectToBonjourServiceNamed:@"MY PONY"];

    // Enable remote logging to the DevTools Console via PDLog()/PDLogObjects().
    [debugger enableRemoteLogging];
#endif
  • Finally!

How it works

  • ponyd is a Tornado web server which uses WebSocket to communicate to the device and to Chrome. It registers itself as a Bonjour service ‘Pony Gateway’, so that an iOS device can easily discover the server. The server acts as a bridge between these two clients, forwarding messages back-and-forth.

    • Bonjour is a framework created by Apple for service discovery
    • WebSocket is a bi-directional communication protocol via JSON over HTTP
  • The device communicate to the server via ‘/device’ endpoint. Here is an example of network messages, and the definition can be found at https://developer.chrome.com/devtools/docs/protocol/0.1/network#events

    {"method":"Network.requestWillBeSent","params":{"documentURL":"http:\/\/corner.squareup.com\/images\/ponydebugger\/icon.png","loaderId":"","requestId":"A346E01D-D247-4D92-B551-0832F6C3DB51-2","frameId":"","request":{"url":"http:\/\/corner.squareup.com\/images\/ponydebugger\/icon.png","method":"GET","headers":{"Accept-Encoding":"gzip, deflate","Accept-Language":"en-us","Accept":"*\/*"}},"timestamp":1416210440.562284}}    

    {"method":"Network.responseReceived","params":{"loaderId":"","type":"Image","response":{"statusText":"no error","status":200,"requestHeaders":{"Accept-Encoding":"gzip, deflate","Accept-Language":"en-us","Accept":"*\/*"},"headers":{"Etag":"\"87f76cc58c10ba60a067bb8ce11e7bc1\"","Last-Modified":"Fri, 07 Feb 2014 20:22:54 GMT","x-amz-request-id":"FCA78F2A33F51058","Server":"AmazonS3","x-amz-id-2":"CmkoMPfxmlot6YYxPo9ZEXrR8jd5n\/DmII6Le6Trj+q3Xqzpcn1TbfbwNxY5qKuL","Content-Type":"image\/png","Content-Length":"17339","Date":"Sun, 16 Nov 2014 22:01:48 GMT"},"url":"http:\/\/corner.squareup.com\/images\/ponydebugger\/icon.png","mimeType":"image\/png"},"requestId":"A346E01D-D247-4D92-B551-0832F6C3DB51-2","frameId":"","timestamp":1416210440.562592}}    

    {"method":"Network.dataReceived","params":{"dataLength":17339,"requestId":"A346E01D-D247-4D92-B551-0832F6C3DB51-2","timestamp":1416210440.563106,"encodedDataLength":17339}}    

    {"method":"Network.loadingFinished","params":{"timestamp":1416210440.563705,"requestId":"A346E01D-D247-4D92-B551-0832F6C3DB51-2"}}  
  • On receiving device data, the server forwards it to Chrome via write_message() to ‘/devtools’ endpoint. Since WS protocol is bi-directional, the server can write to the client browser.

  • The server also receives Chrome’s message, e.g.

    {"method":"Debugger.supportsSeparateScriptCompilationAndExecution","id":2}  

Such messages will be forwarded to the device.

  • The device registration process:
    # Dev tools connecting to device 68B552FA-4A62-4B65-86E8-63272A3634D9   

    devtoolshandler {"method":"Debugger.causesRecompilation","id":1}    
    devtoolshandler {"method":"Debugger.supportsSeparateScriptCompilationAndExecution","id":2}  
    devtoolshandler {"method":"Profiler.causesRecompilation","id":3}    
    devtoolshandler {"method":"Profiler.isSampling","id":4} 
    devtoolshandler {"method":"Profiler.hasHeapProfiler","id":5}    
    devtoolshandler {"method":"Timeline.supportsFrameInstrumentation","id":6}   
    devtoolshandler {"method":"Page.canOverrideDeviceMetrics","id":7}   
    devtoolshandler {"method":"Page.canOverrideGeolocation","id":8} 
    devtoolshandler {"method":"Page.canOverrideDeviceOrientation","id":9}   
    devtoolshandler {"method":"CSS.getSupportedCSSProperties","id":10}  

    device! {"id":1,"error":"unknown domain Debugger"}  
    device! {"id":2,"error":"unknown domain Debugger"}  
    device! {"id":3,"error":"unknown domain Profiler"}  
    device! {"id":4,"error":"unknown domain Profiler"}  
    device! {"id":5,"error":"unknown domain Profiler"}  
    device! {"id":6,"error":"unknown domain Timeline"}  
    device! {"id":7,"error":"Error Domain=PDErrorDomain Code=100 \"Unknown or unimplemented method name: Page.canOverrideDeviceMetrics\" UserInfo=0x7a67dba0 {NSLocalizedDescription=Unknown or unimplemented method name: Page.canOverrideDeviceMetrics}"} 
    device! {"id":8,"error":"Error Domain=PDErrorDomain Code=100 \"Unknown or unimplemented method name: Page.canOverrideGeolocation\" UserInfo=0x7beca020 {NSLocalizedDescription=Unknown or unimplemented method name: Page.canOverrideGeolocation}"} 
    device! {"id":9,"error":"Error Domain=PDErrorDomain Code=100 \"Unknown or unimplemented method name: Page.canOverrideDeviceOrientation\" UserInfo=0x7a686fe0 {NSLocalizedDescription=Unknown or unimplemented method name: Page.canOverrideDeviceOrientation}"} 
    device! {"id":10,"error":"unknown domain CSS"}  

    devtoolshandler {"method":"Network.enable","id":11} 
    devtoolshandler {"method":"Page.enable","id":12}    
    devtoolshandler {"method":"Network.enable","id":13} 
    devtoolshandler {"method":"Page.getResourceTree","id":14}   
    devtoolshandler {"method":"CSS.enable","id":15} 

    device! {"id":11,"error":null}  
    device! {"id":12,"error":null}  
    device! {"id":13,"error":null}  
    device! {"id":14,"error":null,"result":{"frameTree":{"frame":{"name":"Root","id":"0","securityOrigin":"com.squareup.PDTestApp","loaderId":"0","url":"\/Users\/zxp\/Library\/Developer\/CoreSimulator\/Devices\/FC39E0A1-11D1-4475-B993-2D66CA5904EF\/data\/Containers\/Bundle\/Application\/E1F5A2F5-2BCB-4CEC-B8B9-4487B12E2D1D\/PDTestApp.app"}}}}    
    device! {"id":15,"error":"unknown domain CSS"}  

    devtoolshandler {"method":"Network.canClearBrowserCache","id":16}   
    devtoolshandler {"method":"Network.canClearBrowserCookies","id":17} 
    device! {"id":16,"error":null,"result":{"result":false}}    
    device! {"id":17,"error":null,"result":{"result":false}}    

    devtoolshandler {"method":"Worker.enable","id":18}  
    device! {"id":18,"error":"unknown domain Worker”} 

    devtoolshandler {"method":"Debugger.canSetScriptSource","id":19}    
    devtoolshandler {"method":"Debugger.enable","id":20}    
    device! {"id":19,"error":"unknown domain Debugger"} 
    device! {"id":20,"error":"unknown domain Debugger”}   

    devtoolshandler {"method":"Profiler.enable","id":21}    
    devtoolshandler {"method":"Console.enable","id":22} 
    devtoolshandler {"method":"Inspector.enable","id":23}   
    devtoolshandler {"method":"Database.enable","id":24}    
    device! {"id":21,"error":"unknown domain Profiler"} 
    device! {"id":22,"error":null}  
    device! {"id":23,"error":null}  
    device! {"id":24,"error":"unknown domain Database"} 

    devtoolshandler {"method":"DOMStorage.enable","id":25}  
    devtoolshandler {"method":"Page.setTouchEmulationEnabled","params":{"enabled":false},"id":26}   
    device! {"id":25,"error":"unknown domain DOMStorage"}   
    device! {"id":26,"error":"Error Domain=PDErrorDomain Code=100 \"Unknown or unimplemented method name: Page.setTouchEmulationEnabled\" UserInfo=0x7b97a1e0 {NSLocalizedDescription=Unknown or unimplemented method name: Page.setTouchEmulationEnabled}"}    
  • After the registration, the device can send the traffic data like the network messages above, following Chrome RDP.

  • To debug the process, add print() code in on_message() functions here:

     /Library/Python/2.7/site-packages/ponyd-1.0-py2.7.egg/ponyd $ sudo vi [gateway.py](http://gateway.py/) 

Chrome Remote Debugging Protocol

This entry was posted in Android & iOS. Bookmark the permalink.

发表评论