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