Chromium Code Reviews| 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 216 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 270 | 271 |
| 271 class _RemoteInspectorThread(threading.Thread): | 272 class _RemoteInspectorThread(threading.Thread): |
| 272 """Manages communication using Chrome's remote inspector protocol. | 273 """Manages communication using Chrome's remote inspector protocol. |
| 273 | 274 |
| 274 This class works in conjunction with the _DevToolsSocketClient class to | 275 This class works in conjunction with the _DevToolsSocketClient class to |
| 275 communicate with a remote Chrome instance following the remote inspector | 276 communicate with a remote Chrome instance following the remote inspector |
| 276 communication protocol in WebKit. This class performs the higher-level work | 277 communication protocol in WebKit. This class performs the higher-level work |
| 277 of managing request and reply messages, whereas _DevToolsSocketClient handles | 278 of managing request and reply messages, whereas _DevToolsSocketClient handles |
| 278 the lower-level work of socket communication. | 279 the lower-level work of socket communication. |
| 279 """ | 280 """ |
| 280 def __init__(self, tab_index, verbose, show_socket_messages): | 281 def __init__(self, endpoint, tab_index, verbose, show_socket_messages): |
| 281 """Initialize. | 282 """Initialize. |
| 282 | 283 |
| 283 Args: | 284 Args: |
| 285 endpoint: The base URL to connent to. | |
|
yurys
2012/12/20 08:04:48
may be rename it to url?
eustas
2012/12/20 08:27:30
Done.
| |
| 284 tab_index: The integer index of the tab in the remote Chrome instance to | 286 tab_index: The integer index of the tab in the remote Chrome instance to |
| 285 use for snapshotting. | 287 use for snapshotting. |
| 286 verbose: A boolean indicating whether or not to use verbose logging. | 288 verbose: A boolean indicating whether or not to use verbose logging. |
| 287 show_socket_messages: A boolean indicating whether or not to show the | 289 show_socket_messages: A boolean indicating whether or not to show the |
| 288 socket messages sent/received when communicating with the remote | 290 socket messages sent/received when communicating with the remote |
| 289 Chrome instance. | 291 Chrome instance. |
| 290 """ | 292 """ |
| 291 threading.Thread.__init__(self) | 293 threading.Thread.__init__(self) |
| 292 self._logger = logging.getLogger('_RemoteInspectorThread') | 294 self._logger = logging.getLogger('_RemoteInspectorThread') |
| 293 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) | 295 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) |
| 294 | 296 |
| 295 self._killed = False | 297 self._killed = False |
| 296 self._requests = [] | 298 self._requests = [] |
| 297 self._action_queue = [] | 299 self._action_queue = [] |
| 298 self._action_queue_condition = threading.Condition() | 300 self._action_queue_condition = threading.Condition() |
| 299 self._action_specific_callback = None # Callback only for current action. | 301 self._action_specific_callback = None # Callback only for current action. |
| 300 self._action_specific_callback_lock = threading.Lock() | 302 self._action_specific_callback_lock = threading.Lock() |
| 301 self._general_callbacks = [] # General callbacks that can be long-lived. | 303 self._general_callbacks = [] # General callbacks that can be long-lived. |
| 302 self._general_callbacks_lock = threading.Lock() | 304 self._general_callbacks_lock = threading.Lock() |
| 303 self._condition_to_wait = None | 305 self._condition_to_wait = None |
| 304 | 306 |
| 305 # Create a DevToolsSocket client and wait for it to complete the remote | 307 # Create a DevToolsSocket client and wait for it to complete the remote |
| 306 # debugging protocol handshake with the remote Chrome instance. | 308 # debugging protocol handshake with the remote Chrome instance. |
| 307 result = self._IdentifyDevToolsSocketConnectionInfo(tab_index) | 309 result = self._IdentifyDevToolsSocketConnectionInfo( |
| 310 endpoint, tab_index) | |
| 308 self._client = _DevToolsSocketClient( | 311 self._client = _DevToolsSocketClient( |
| 309 verbose, show_socket_messages, result['host'], result['port'], | 312 verbose, show_socket_messages, result['host'], result['port'], |
| 310 result['path']) | 313 result['path']) |
| 311 self._client.inspector_thread = self | 314 self._client.inspector_thread = self |
| 312 while asyncore.socket_map: | 315 while asyncore.socket_map: |
| 313 if self._client.handshake_done or self._killed: | 316 if self._client.handshake_done or self._killed: |
| 314 break | 317 break |
| 315 asyncore.loop(timeout=1, count=1, use_poll=True) | 318 asyncore.loop(timeout=1, count=1, use_poll=True) |
| 316 | 319 |
| 317 def ClientSocketExceptionOccurred(self): | 320 def ClientSocketExceptionOccurred(self): |
| (...skipping 181 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 499 def _FillInParams(self, request): | 502 def _FillInParams(self, request): |
| 500 """Fills in parameters for requests as necessary before the request is sent. | 503 """Fills in parameters for requests as necessary before the request is sent. |
| 501 | 504 |
| 502 Args: | 505 Args: |
| 503 request: The _DevToolsSocketRequest object associated with a request | 506 request: The _DevToolsSocketRequest object associated with a request |
| 504 message that is about to be sent. | 507 message that is about to be sent. |
| 505 """ | 508 """ |
| 506 if request.method == 'Profiler.takeHeapSnapshot': | 509 if request.method == 'Profiler.takeHeapSnapshot': |
| 507 # We always want detailed v8 heap snapshot information. | 510 # We always want detailed v8 heap snapshot information. |
| 508 request.params = {'detailed': True} | 511 request.params = {'detailed': True} |
| 509 elif request.method == 'Profiler.getProfile': | 512 elif request.method == 'Profiler.getHeapSnapshot': |
| 510 # To actually request the snapshot data from a previously-taken snapshot, | 513 # To actually request the snapshot data from a previously-taken snapshot, |
| 511 # we need to specify the unique uid of the snapshot we want. | 514 # we need to specify the unique uid of the snapshot we want. |
| 512 # The relevant uid should be contained in the last | 515 # The relevant uid should be contained in the last |
| 513 # 'Profiler.takeHeapSnapshot' request object. | 516 # 'Profiler.takeHeapSnapshot' request object. |
| 514 last_req = self._GetLatestRequestOfType(request, | 517 last_req = self._GetLatestRequestOfType(request, |
| 515 'Profiler.takeHeapSnapshot') | 518 'Profiler.takeHeapSnapshot') |
| 516 if last_req and 'uid' in last_req.results: | 519 if last_req and 'uid' in last_req.results: |
| 520 request.params = {'uid': last_req.results['uid']} | |
| 521 elif request.method == 'Profiler.getProfile': | |
| 522 # TODO(eustas): Remove this case after M25 is released. | |
|
yurys
2012/12/20 08:04:48
This probably should be removed after M26 as M25 h
eustas
2012/12/20 08:27:30
Done.
| |
| 523 last_req = self._GetLatestRequestOfType(request, | |
| 524 'Profiler.takeHeapSnapshot') | |
| 525 if last_req and 'uid' in last_req.results: | |
| 517 request.params = {'type': 'HEAP', 'uid': last_req.results['uid']} | 526 request.params = {'type': 'HEAP', 'uid': last_req.results['uid']} |
| 518 | 527 |
| 519 @staticmethod | 528 @staticmethod |
| 520 def _IdentifyDevToolsSocketConnectionInfo(tab_index): | 529 def _IdentifyDevToolsSocketConnectionInfo(endpoint, tab_index): |
| 521 """Identifies DevToolsSocket connection info from a remote Chrome instance. | 530 """Identifies DevToolsSocket connection info from a remote Chrome instance. |
| 522 | 531 |
| 523 Args: | 532 Args: |
| 533 endpoint: The base URL to connent to. | |
| 524 tab_index: The integer index of the tab in the remote Chrome instance to | 534 tab_index: The integer index of the tab in the remote Chrome instance to |
| 525 which to connect. | 535 which to connect. |
| 526 | 536 |
| 527 Returns: | 537 Returns: |
| 528 A dictionary containing the DevToolsSocket connection info: | 538 A dictionary containing the DevToolsSocket connection info: |
| 529 { | 539 { |
| 530 'host': string, | 540 'host': string, |
| 531 'port': integer, | 541 'port': integer, |
| 532 'path': string, | 542 'path': string, |
| 533 } | 543 } |
| 534 | 544 |
| 535 Raises: | 545 Raises: |
| 536 RuntimeError: When DevToolsSocket connection info cannot be identified. | 546 RuntimeError: When DevToolsSocket connection info cannot be identified. |
| 537 """ | 547 """ |
| 538 try: | 548 try: |
| 539 # TODO(dennisjeffrey): Do not assume port 9222. The port should be passed | 549 f = urllib2.urlopen(endpoint + '/json') |
| 540 # as input to this function. | |
| 541 f = urllib2.urlopen('http://localhost:9222/json') | |
| 542 result = f.read(); | 550 result = f.read(); |
| 543 result = simplejson.loads(result) | 551 result = simplejson.loads(result) |
| 544 except urllib2.URLError, e: | 552 except urllib2.URLError, e: |
| 545 raise RuntimeError( | 553 raise RuntimeError( |
| 546 'Error accessing Chrome instance debugging port: ' + str(e)) | 554 'Error accessing Chrome instance debugging port: ' + str(e)) |
| 547 | 555 |
| 548 if tab_index >= len(result): | 556 if tab_index >= len(result): |
| 549 raise RuntimeError( | 557 raise RuntimeError( |
| 550 'Specified tab index %d doesn\'t exist (%d tabs found)' % | 558 'Specified tab index %d doesn\'t exist (%d tabs found)' % |
| 551 (tab_index, len(result))) | 559 (tab_index, len(result))) |
| (...skipping 266 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 818 | 826 |
| 819 logging.basicConfig() | 827 logging.basicConfig() |
| 820 self._logger = logging.getLogger('RemoteInspectorClient') | 828 self._logger = logging.getLogger('RemoteInspectorClient') |
| 821 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) | 829 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) |
| 822 | 830 |
| 823 # Creating _RemoteInspectorThread might raise an exception. This prevents an | 831 # Creating _RemoteInspectorThread might raise an exception. This prevents an |
| 824 # AttributeError in the destructor. | 832 # AttributeError in the destructor. |
| 825 self._remote_inspector_thread = None | 833 self._remote_inspector_thread = None |
| 826 self._remote_inspector_driver_thread = None | 834 self._remote_inspector_driver_thread = None |
| 827 | 835 |
| 836 # TODO(dennisjeffrey): Do not assume port 9222. The port should be passed | |
| 837 # as input to this function. | |
| 838 endpoint = 'http://localhost:9222' | |
| 839 | |
| 840 self._webkit_version = self._GetWebkitVersion(endpoint) | |
| 841 | |
| 828 # Start up a thread for long-term communication with the remote inspector. | 842 # Start up a thread for long-term communication with the remote inspector. |
| 829 self._remote_inspector_thread = _RemoteInspectorThread( | 843 self._remote_inspector_thread = _RemoteInspectorThread( |
| 830 tab_index, verbose, show_socket_messages) | 844 endpoint, tab_index, verbose, show_socket_messages) |
| 831 self._remote_inspector_thread.start() | 845 self._remote_inspector_thread.start() |
| 832 # At this point, a connection has already been made to the remote inspector. | 846 # At this point, a connection has already been made to the remote inspector. |
| 833 | 847 |
| 834 # This thread calls asyncore.loop, which activates the channel service. | 848 # This thread calls asyncore.loop, which activates the channel service. |
| 835 self._remote_inspector_driver_thread = _RemoteInspectorDriverThread() | 849 self._remote_inspector_driver_thread = _RemoteInspectorDriverThread() |
| 836 self._remote_inspector_driver_thread.start() | 850 self._remote_inspector_driver_thread.start() |
| 837 | 851 |
| 838 def __del__(self): | 852 def __del__(self): |
| 839 """Called on destruction of this object.""" | 853 """Called on destruction of this object.""" |
| 840 self.Stop() | 854 self.Stop() |
| (...skipping 16 matching lines...) Expand all Loading... | |
| 857 snapshot that was taken. | 871 snapshot that was taken. |
| 858 { | 872 { |
| 859 'url': string, # URL of the webpage that was snapshotted. | 873 'url': string, # URL of the webpage that was snapshotted. |
| 860 'raw_data': string, # The raw data as JSON string. | 874 'raw_data': string, # The raw data as JSON string. |
| 861 'total_v8_node_count': integer, # Total number of nodes in the v8 heap. | 875 'total_v8_node_count': integer, # Total number of nodes in the v8 heap. |
| 862 # Only if |include_summary| is True. | 876 # Only if |include_summary| is True. |
| 863 'total_heap_size': integer, # Total v8 heap size (number of bytes). | 877 'total_heap_size': integer, # Total v8 heap size (number of bytes). |
| 864 # Only if |include_summary| is True. | 878 # Only if |include_summary| is True. |
| 865 } | 879 } |
| 866 """ | 880 """ |
| 881 # TODO(eustas): Remove this hack after M25 is released. | |
|
yurys
2012/12/20 08:04:48
Again "... after M26..."
eustas
2012/12/20 08:27:30
Done.
| |
| 882 if self._IsWebkitVersionNotOlderThan(537, 22): | |
| 883 get_heap_snapshot_method = 'Profiler.getHeapSnapshot' | |
| 884 else: | |
| 885 get_heap_snapshot_method = 'Profiler.getProfile' | |
| 886 | |
| 867 HEAP_SNAPSHOT_MESSAGES = [ | 887 HEAP_SNAPSHOT_MESSAGES = [ |
| 868 ('Page.getResourceTree', {}), | 888 ('Page.getResourceTree', {}), |
| 869 ('Debugger.enable', {}), | 889 ('Debugger.enable', {}), |
| 870 ('Profiler.clearProfiles', {}), | 890 ('Profiler.clearProfiles', {}), |
| 871 ('Profiler.takeHeapSnapshot', {}), | 891 ('Profiler.takeHeapSnapshot', {}), |
| 872 ('Profiler.getProfile', {}), | 892 (get_heap_snapshot_method, {}), |
| 873 ] | 893 ] |
| 874 | 894 |
| 875 self._current_heap_snapshot = [] | 895 self._current_heap_snapshot = [] |
| 876 self._url = '' | 896 self._url = '' |
| 877 self._collected_heap_snapshot_data = {} | 897 self._collected_heap_snapshot_data = {} |
| 878 | 898 |
| 879 done_condition = threading.Condition() | 899 done_condition = threading.Condition() |
| 880 | 900 |
| 881 def HandleReply(reply_dict): | 901 def HandleReply(reply_dict): |
| 882 """Processes a reply message received from the remote Chrome instance. | 902 """Processes a reply message received from the remote Chrome instance. |
| (...skipping 299 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1182 | 1202 |
| 1183 Returns: | 1203 Returns: |
| 1184 A human-readable string representation of the given number of bytes. | 1204 A human-readable string representation of the given number of bytes. |
| 1185 """ | 1205 """ |
| 1186 if num_bytes < 1024: | 1206 if num_bytes < 1024: |
| 1187 return '%d B' % num_bytes | 1207 return '%d B' % num_bytes |
| 1188 elif num_bytes < 1048576: | 1208 elif num_bytes < 1048576: |
| 1189 return '%.2f KB' % (num_bytes / 1024.0) | 1209 return '%.2f KB' % (num_bytes / 1024.0) |
| 1190 else: | 1210 else: |
| 1191 return '%.2f MB' % (num_bytes / 1048576.0) | 1211 return '%.2f MB' % (num_bytes / 1048576.0) |
| 1212 | |
| 1213 @staticmethod | |
| 1214 def _GetWebkitVersion(endpoint): | |
| 1215 """Fetches Webkit version information from a remote Chrome instance. | |
| 1216 | |
| 1217 Args: | |
| 1218 endpoint: The base URL to connent to. | |
| 1219 | |
| 1220 Returns: | |
| 1221 A dictionary containing Webkit version information: | |
| 1222 { | |
| 1223 'major': integer, | |
| 1224 'minor': integer, | |
| 1225 } | |
| 1226 | |
| 1227 Raises: | |
| 1228 RuntimeError: When Webkit version info can't be fetched or parsed. | |
| 1229 """ | |
| 1230 try: | |
| 1231 f = urllib2.urlopen(endpoint + '/json/version') | |
| 1232 result = f.read(); | |
| 1233 result = simplejson.loads(result) | |
| 1234 except urllib2.URLError, e: | |
| 1235 raise RuntimeError( | |
| 1236 'Error accessing Chrome instance debugging port: ' + str(e)) | |
| 1237 | |
| 1238 if 'WebKit-Version' not in result: | |
| 1239 raise RuntimeError('WebKit-Version is not specified.') | |
| 1240 | |
| 1241 parsed = re.search('^(\d+)\.(\d+)', result['WebKit-Version']) | |
| 1242 if parsed is None: | |
| 1243 raise RuntimeError('WebKit-Version cannot be parsed.') | |
| 1244 | |
| 1245 try: | |
| 1246 info = { | |
| 1247 'major': int(parsed.group(1)), | |
| 1248 'minor': int(parsed.group(2)), | |
| 1249 } | |
| 1250 except ValueError: | |
| 1251 raise RuntimeError('WebKit-Version cannot be parsed.') | |
| 1252 | |
| 1253 return info | |
| 1254 | |
| 1255 def _IsWebkitVersionNotOlderThan(self, major, minor): | |
| 1256 """Compares remote Webkit version with specified one. | |
| 1257 | |
| 1258 Args: | |
| 1259 major: Major Webkit version. | |
| 1260 minor: Minor Webkit version. | |
| 1261 | |
| 1262 Returns: | |
| 1263 True if remote Webkit version is same or newer than specified, | |
| 1264 False otherwise. | |
| 1265 | |
| 1266 Raises: | |
| 1267 RuntimeError: If remote Webkit version hasn't been fetched yet. | |
| 1268 """ | |
| 1269 version = self._webkit_version | |
| 1270 if version is None: | |
| 1271 raise RuntimeError('WebKit version has not been fetched yet.') | |
| 1272 | |
| 1273 if version['major'] < major: | |
| 1274 return False | |
| 1275 elif version['major'] == major and version['minor'] < minor: | |
|
yurys
2012/12/20 08:04:48
version['minor'] < minor -> version['minor'] <= mi
eustas
2012/12/20 08:27:30
Not older than == same or newer.
So if minor and
yurys
2012/12/20 12:19:26
You are right.
| |
| 1276 return False | |
| 1277 else: | |
| 1278 return True | |
| OLD | NEW |