OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Chrome remote inspector utility for pyauto tests. | 6 """Chrome remote inspector utility for pyauto tests. |
7 | 7 |
8 This script provides a python interface that acts as a front-end for Chrome's | 8 This script provides a python interface that acts as a front-end for Chrome's |
9 remote inspector module, communicating via sockets to interact with Chrome in | 9 remote inspector module, communicating via sockets to interact with Chrome in |
10 the same way that the Developer Tools does. This -- in theory -- should allow | 10 the same way that the Developer Tools does. This -- in theory -- should allow |
(...skipping 23 matching lines...) Expand all Loading... |
34 at a time. If a second instance is instantiated, a RuntimeError will be raised. | 34 at a time. If a second instance is instantiated, a RuntimeError will be raised. |
35 RemoteInspectorClient could be made into a singleton in the future if the need | 35 RemoteInspectorClient could be made into a singleton in the future if the need |
36 for it arises. | 36 for it arises. |
37 """ | 37 """ |
38 | 38 |
39 import asyncore | 39 import asyncore |
40 import datetime | 40 import datetime |
41 import logging | 41 import logging |
42 import optparse | 42 import optparse |
43 import pprint | 43 import pprint |
| 44 import re |
44 import simplejson | 45 import simplejson |
45 import socket | 46 import socket |
46 import sys | 47 import sys |
47 import threading | 48 import threading |
48 import time | 49 import time |
49 import urllib2 | 50 import urllib2 |
50 import urlparse | 51 import urlparse |
51 | 52 |
52 | 53 |
53 class _DevToolsSocketRequest(object): | 54 class _DevToolsSocketRequest(object): |
(...skipping 219 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
273 class _RemoteInspectorThread(threading.Thread): | 274 class _RemoteInspectorThread(threading.Thread): |
274 """Manages communication using Chrome's remote inspector protocol. | 275 """Manages communication using Chrome's remote inspector protocol. |
275 | 276 |
276 This class works in conjunction with the _DevToolsSocketClient class to | 277 This class works in conjunction with the _DevToolsSocketClient class to |
277 communicate with a remote Chrome instance following the remote inspector | 278 communicate with a remote Chrome instance following the remote inspector |
278 communication protocol in WebKit. This class performs the higher-level work | 279 communication protocol in WebKit. This class performs the higher-level work |
279 of managing request and reply messages, whereas _DevToolsSocketClient handles | 280 of managing request and reply messages, whereas _DevToolsSocketClient handles |
280 the lower-level work of socket communication. | 281 the lower-level work of socket communication. |
281 """ | 282 """ |
282 | 283 |
283 def __init__(self, tab_index, tab_filter, verbose, show_socket_messages): | 284 def __init__(self, url, tab_index, tab_filter, verbose, show_socket_messages): |
284 """Initialize. | 285 """Initialize. |
285 | 286 |
286 Args: | 287 Args: |
| 288 url: The base URL to connent to. |
287 tab_index: The integer index of the tab in the remote Chrome instance to | 289 tab_index: The integer index of the tab in the remote Chrome instance to |
288 use for snapshotting. | 290 use for snapshotting. |
289 tab_filter: When specified, is run over tabs of the remote Chrome | 291 tab_filter: When specified, is run over tabs of the remote Chrome |
290 instances to choose which one to connect to. | 292 instances to choose which one to connect to. |
291 verbose: A boolean indicating whether or not to use verbose logging. | 293 verbose: A boolean indicating whether or not to use verbose logging. |
292 show_socket_messages: A boolean indicating whether or not to show the | 294 show_socket_messages: A boolean indicating whether or not to show the |
293 socket messages sent/received when communicating with the remote | 295 socket messages sent/received when communicating with the remote |
294 Chrome instance. | 296 Chrome instance. |
295 """ | 297 """ |
296 threading.Thread.__init__(self) | 298 threading.Thread.__init__(self) |
297 self._logger = logging.getLogger('_RemoteInspectorThread') | 299 self._logger = logging.getLogger('_RemoteInspectorThread') |
298 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) | 300 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) |
299 | 301 |
300 self._killed = False | 302 self._killed = False |
301 self._requests = [] | 303 self._requests = [] |
302 self._action_queue = [] | 304 self._action_queue = [] |
303 self._action_queue_condition = threading.Condition() | 305 self._action_queue_condition = threading.Condition() |
304 self._action_specific_callback = None # Callback only for current action. | 306 self._action_specific_callback = None # Callback only for current action. |
305 self._action_specific_callback_lock = threading.Lock() | 307 self._action_specific_callback_lock = threading.Lock() |
306 self._general_callbacks = [] # General callbacks that can be long-lived. | 308 self._general_callbacks = [] # General callbacks that can be long-lived. |
307 self._general_callbacks_lock = threading.Lock() | 309 self._general_callbacks_lock = threading.Lock() |
308 self._condition_to_wait = None | 310 self._condition_to_wait = None |
309 | 311 |
310 # Create a DevToolsSocket client and wait for it to complete the remote | 312 # Create a DevToolsSocket client and wait for it to complete the remote |
311 # debugging protocol handshake with the remote Chrome instance. | 313 # debugging protocol handshake with the remote Chrome instance. |
312 result = self._IdentifyDevToolsSocketConnectionInfo(tab_index, tab_filter) | 314 result = self._IdentifyDevToolsSocketConnectionInfo( |
| 315 url, tab_index, tab_filter) |
313 self._client = _DevToolsSocketClient( | 316 self._client = _DevToolsSocketClient( |
314 verbose, show_socket_messages, result['host'], result['port'], | 317 verbose, show_socket_messages, result['host'], result['port'], |
315 result['path']) | 318 result['path']) |
316 self._client.inspector_thread = self | 319 self._client.inspector_thread = self |
317 while asyncore.socket_map: | 320 while asyncore.socket_map: |
318 if self._client.handshake_done or self._killed: | 321 if self._client.handshake_done or self._killed: |
319 break | 322 break |
320 asyncore.loop(timeout=1, count=1, use_poll=True) | 323 asyncore.loop(timeout=1, count=1, use_poll=True) |
321 | 324 |
322 def ClientSocketExceptionOccurred(self): | 325 def ClientSocketExceptionOccurred(self): |
(...skipping 181 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
504 def _FillInParams(self, request): | 507 def _FillInParams(self, request): |
505 """Fills in parameters for requests as necessary before the request is sent. | 508 """Fills in parameters for requests as necessary before the request is sent. |
506 | 509 |
507 Args: | 510 Args: |
508 request: The _DevToolsSocketRequest object associated with a request | 511 request: The _DevToolsSocketRequest object associated with a request |
509 message that is about to be sent. | 512 message that is about to be sent. |
510 """ | 513 """ |
511 if request.method == 'Profiler.takeHeapSnapshot': | 514 if request.method == 'Profiler.takeHeapSnapshot': |
512 # We always want detailed v8 heap snapshot information. | 515 # We always want detailed v8 heap snapshot information. |
513 request.params = {'detailed': True} | 516 request.params = {'detailed': True} |
514 elif request.method == 'Profiler.getProfile': | 517 elif request.method == 'Profiler.getHeapSnapshot': |
515 # To actually request the snapshot data from a previously-taken snapshot, | 518 # To actually request the snapshot data from a previously-taken snapshot, |
516 # we need to specify the unique uid of the snapshot we want. | 519 # we need to specify the unique uid of the snapshot we want. |
517 # The relevant uid should be contained in the last | 520 # The relevant uid should be contained in the last |
518 # 'Profiler.takeHeapSnapshot' request object. | 521 # 'Profiler.takeHeapSnapshot' request object. |
519 last_req = self._GetLatestRequestOfType(request, | 522 last_req = self._GetLatestRequestOfType(request, |
520 'Profiler.takeHeapSnapshot') | 523 'Profiler.takeHeapSnapshot') |
521 if last_req and 'uid' in last_req.results: | 524 if last_req and 'uid' in last_req.results: |
| 525 request.params = {'uid': last_req.results['uid']} |
| 526 elif request.method == 'Profiler.getProfile': |
| 527 # TODO(eustas): Remove this case after M27 is released. |
| 528 last_req = self._GetLatestRequestOfType(request, |
| 529 'Profiler.takeHeapSnapshot') |
| 530 if last_req and 'uid' in last_req.results: |
522 request.params = {'type': 'HEAP', 'uid': last_req.results['uid']} | 531 request.params = {'type': 'HEAP', 'uid': last_req.results['uid']} |
523 | 532 |
524 @staticmethod | 533 @staticmethod |
525 def _IdentifyDevToolsSocketConnectionInfo(tab_index, tab_filter): | 534 def _IdentifyDevToolsSocketConnectionInfo(url, tab_index, tab_filter): |
526 """Identifies DevToolsSocket connection info from a remote Chrome instance. | 535 """Identifies DevToolsSocket connection info from a remote Chrome instance. |
527 | 536 |
528 Args: | 537 Args: |
| 538 url: The base URL to connent to. |
529 tab_index: The integer index of the tab in the remote Chrome instance to | 539 tab_index: The integer index of the tab in the remote Chrome instance to |
530 which to connect. | 540 which to connect. |
531 tab_filter: When specified, is run over tabs of the remote Chrome instance | 541 tab_filter: When specified, is run over tabs of the remote Chrome instance |
532 to choose which one to connect to. | 542 to choose which one to connect to. |
533 | 543 |
534 Returns: | 544 Returns: |
535 A dictionary containing the DevToolsSocket connection info: | 545 A dictionary containing the DevToolsSocket connection info: |
536 { | 546 { |
537 'host': string, | 547 'host': string, |
538 'port': integer, | 548 'port': integer, |
539 'path': string, | 549 'path': string, |
540 } | 550 } |
541 | 551 |
542 Raises: | 552 Raises: |
543 RuntimeError: When DevToolsSocket connection info cannot be identified. | 553 RuntimeError: When DevToolsSocket connection info cannot be identified. |
544 """ | 554 """ |
545 try: | 555 try: |
546 # TODO(dennisjeffrey): Do not assume port 9222. The port should be passed | 556 f = urllib2.urlopen(url + '/json') |
547 # as input to this function. | |
548 f = urllib2.urlopen('http://localhost:9222/json') | |
549 result = f.read() | 557 result = f.read() |
550 logging.debug(result) | 558 logging.debug(result) |
551 result = simplejson.loads(result) | 559 result = simplejson.loads(result) |
552 except urllib2.URLError, e: | 560 except urllib2.URLError, e: |
553 raise RuntimeError( | 561 raise RuntimeError( |
554 'Error accessing Chrome instance debugging port: ' + str(e)) | 562 'Error accessing Chrome instance debugging port: ' + str(e)) |
555 | 563 |
556 if tab_filter: | 564 if tab_filter: |
557 connect_to = filter(tab_filter, result)[0] | 565 connect_to = filter(tab_filter, result)[0] |
558 else: | 566 else: |
(...skipping 278 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
837 | 845 |
838 logging.basicConfig() | 846 logging.basicConfig() |
839 self._logger = logging.getLogger('RemoteInspectorClient') | 847 self._logger = logging.getLogger('RemoteInspectorClient') |
840 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) | 848 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) |
841 | 849 |
842 # Creating _RemoteInspectorThread might raise an exception. This prevents an | 850 # Creating _RemoteInspectorThread might raise an exception. This prevents an |
843 # AttributeError in the destructor. | 851 # AttributeError in the destructor. |
844 self._remote_inspector_thread = None | 852 self._remote_inspector_thread = None |
845 self._remote_inspector_driver_thread = None | 853 self._remote_inspector_driver_thread = None |
846 | 854 |
| 855 # TODO(dennisjeffrey): Do not assume port 9222. The port should be passed |
| 856 # as input to this function. |
| 857 url = 'http://localhost:9222' |
| 858 |
| 859 self._webkit_version = self._GetWebkitVersion(url) |
| 860 |
847 # Start up a thread for long-term communication with the remote inspector. | 861 # Start up a thread for long-term communication with the remote inspector. |
848 self._remote_inspector_thread = _RemoteInspectorThread( | 862 self._remote_inspector_thread = _RemoteInspectorThread( |
849 tab_index, tab_filter, verbose, show_socket_messages) | 863 url, tab_index, tab_filter, verbose, show_socket_messages) |
850 self._remote_inspector_thread.start() | 864 self._remote_inspector_thread.start() |
851 # At this point, a connection has already been made to the remote inspector. | 865 # At this point, a connection has already been made to the remote inspector. |
852 | 866 |
853 # This thread calls asyncore.loop, which activates the channel service. | 867 # This thread calls asyncore.loop, which activates the channel service. |
854 self._remote_inspector_driver_thread = _RemoteInspectorDriverThread() | 868 self._remote_inspector_driver_thread = _RemoteInspectorDriverThread() |
855 self._remote_inspector_driver_thread.start() | 869 self._remote_inspector_driver_thread.start() |
856 | 870 |
857 def __del__(self): | 871 def __del__(self): |
858 """Called on destruction of this object.""" | 872 """Called on destruction of this object.""" |
859 self.Stop() | 873 self.Stop() |
(...skipping 16 matching lines...) Expand all Loading... |
876 snapshot that was taken. | 890 snapshot that was taken. |
877 { | 891 { |
878 'url': string, # URL of the webpage that was snapshotted. | 892 'url': string, # URL of the webpage that was snapshotted. |
879 'raw_data': string, # The raw data as JSON string. | 893 'raw_data': string, # The raw data as JSON string. |
880 'total_v8_node_count': integer, # Total number of nodes in the v8 heap. | 894 'total_v8_node_count': integer, # Total number of nodes in the v8 heap. |
881 # Only if |include_summary| is True. | 895 # Only if |include_summary| is True. |
882 'total_heap_size': integer, # Total v8 heap size (number of bytes). | 896 'total_heap_size': integer, # Total v8 heap size (number of bytes). |
883 # Only if |include_summary| is True. | 897 # Only if |include_summary| is True. |
884 } | 898 } |
885 """ | 899 """ |
| 900 # TODO(eustas): Remove this hack after M27 is released. |
| 901 if self._IsWebkitVersionNotOlderThan(537, 27): |
| 902 get_heap_snapshot_method = 'Profiler.getHeapSnapshot' |
| 903 else: |
| 904 get_heap_snapshot_method = 'Profiler.getProfile' |
| 905 |
886 HEAP_SNAPSHOT_MESSAGES = [ | 906 HEAP_SNAPSHOT_MESSAGES = [ |
887 ('Page.getResourceTree', {}), | 907 ('Page.getResourceTree', {}), |
888 ('Debugger.enable', {}), | 908 ('Debugger.enable', {}), |
889 ('Profiler.clearProfiles', {}), | 909 ('Profiler.clearProfiles', {}), |
890 ('Profiler.takeHeapSnapshot', {}), | 910 ('Profiler.takeHeapSnapshot', {}), |
891 ('Profiler.getProfile', {}), | 911 (get_heap_snapshot_method, {}), |
892 ] | 912 ] |
893 | 913 |
894 self._current_heap_snapshot = [] | 914 self._current_heap_snapshot = [] |
895 self._url = '' | 915 self._url = '' |
896 self._collected_heap_snapshot_data = {} | 916 self._collected_heap_snapshot_data = {} |
897 | 917 |
898 done_condition = threading.Condition() | 918 done_condition = threading.Condition() |
899 | 919 |
900 def HandleReply(reply_dict): | 920 def HandleReply(reply_dict): |
901 """Processes a reply message received from the remote Chrome instance. | 921 """Processes a reply message received from the remote Chrome instance. |
(...skipping 299 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1201 | 1221 |
1202 Returns: | 1222 Returns: |
1203 A human-readable string representation of the given number of bytes. | 1223 A human-readable string representation of the given number of bytes. |
1204 """ | 1224 """ |
1205 if num_bytes < 1024: | 1225 if num_bytes < 1024: |
1206 return '%d B' % num_bytes | 1226 return '%d B' % num_bytes |
1207 elif num_bytes < 1048576: | 1227 elif num_bytes < 1048576: |
1208 return '%.2f KB' % (num_bytes / 1024.0) | 1228 return '%.2f KB' % (num_bytes / 1024.0) |
1209 else: | 1229 else: |
1210 return '%.2f MB' % (num_bytes / 1048576.0) | 1230 return '%.2f MB' % (num_bytes / 1048576.0) |
| 1231 |
| 1232 @staticmethod |
| 1233 def _GetWebkitVersion(endpoint): |
| 1234 """Fetches Webkit version information from a remote Chrome instance. |
| 1235 |
| 1236 Args: |
| 1237 endpoint: The base URL to connent to. |
| 1238 |
| 1239 Returns: |
| 1240 A dictionary containing Webkit version information: |
| 1241 { |
| 1242 'major': integer, |
| 1243 'minor': integer, |
| 1244 } |
| 1245 |
| 1246 Raises: |
| 1247 RuntimeError: When Webkit version info can't be fetched or parsed. |
| 1248 """ |
| 1249 try: |
| 1250 f = urllib2.urlopen(endpoint + '/json/version') |
| 1251 result = f.read(); |
| 1252 result = simplejson.loads(result) |
| 1253 except urllib2.URLError, e: |
| 1254 raise RuntimeError( |
| 1255 'Error accessing Chrome instance debugging port: ' + str(e)) |
| 1256 |
| 1257 if 'WebKit-Version' not in result: |
| 1258 raise RuntimeError('WebKit-Version is not specified.') |
| 1259 |
| 1260 parsed = re.search('^(\d+)\.(\d+)', result['WebKit-Version']) |
| 1261 if parsed is None: |
| 1262 raise RuntimeError('WebKit-Version cannot be parsed.') |
| 1263 |
| 1264 try: |
| 1265 info = { |
| 1266 'major': int(parsed.group(1)), |
| 1267 'minor': int(parsed.group(2)), |
| 1268 } |
| 1269 except ValueError: |
| 1270 raise RuntimeError('WebKit-Version cannot be parsed.') |
| 1271 |
| 1272 return info |
| 1273 |
| 1274 def _IsWebkitVersionNotOlderThan(self, major, minor): |
| 1275 """Compares remote Webkit version with specified one. |
| 1276 |
| 1277 Args: |
| 1278 major: Major Webkit version. |
| 1279 minor: Minor Webkit version. |
| 1280 |
| 1281 Returns: |
| 1282 True if remote Webkit version is same or newer than specified, |
| 1283 False otherwise. |
| 1284 |
| 1285 Raises: |
| 1286 RuntimeError: If remote Webkit version hasn't been fetched yet. |
| 1287 """ |
| 1288 if not hasattr(self, '_webkit_version'): |
| 1289 raise RuntimeError('WebKit version has not been fetched yet.') |
| 1290 version = self._webkit_version |
| 1291 |
| 1292 if version['major'] < major: |
| 1293 return False |
| 1294 elif version['major'] == major and version['minor'] < minor: |
| 1295 return False |
| 1296 else: |
| 1297 return True |
OLD | NEW |