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 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
65 id: A unique integer id associated with this request. | 65 id: A unique integer id associated with this request. |
66 params: A dictionary of input parameters associated with this request. | 66 params: A dictionary of input parameters associated with this request. |
67 results: A dictionary of relevant results obtained from the remote Chrome | 67 results: A dictionary of relevant results obtained from the remote Chrome |
68 instance that are associated with this request. | 68 instance that are associated with this request. |
69 is_fulfilled: A boolean indicating whether or not this request has been sent | 69 is_fulfilled: A boolean indicating whether or not this request has been sent |
70 and all relevant results for it have been obtained (i.e., this value is | 70 and all relevant results for it have been obtained (i.e., this value is |
71 True only if all results for this request are known). | 71 True only if all results for this request are known). |
72 is_fulfilled_condition: A threading.Condition for waiting for the request to | 72 is_fulfilled_condition: A threading.Condition for waiting for the request to |
73 be fulfilled. | 73 be fulfilled. |
74 """ | 74 """ |
| 75 |
75 def __init__(self, method, params, message_id): | 76 def __init__(self, method, params, message_id): |
76 """Initialize. | 77 """Initialize. |
77 | 78 |
78 Args: | 79 Args: |
79 method: The string method name for this request. | 80 method: The string method name for this request. |
80 message_id: An integer id for this request, which is assumed to be unique | 81 message_id: An integer id for this request, which is assumed to be unique |
81 from among all requests. | 82 from among all requests. |
82 """ | 83 """ |
83 self.method = method | 84 self.method = method |
84 self.id = message_id | 85 self.id = message_id |
(...skipping 19 matching lines...) Expand all Loading... |
104 communication protocol in WebKit. This class performs the lower-level work | 105 communication protocol in WebKit. This class performs the lower-level work |
105 of socket communication. | 106 of socket communication. |
106 | 107 |
107 Public Attributes: | 108 Public Attributes: |
108 handshake_done: A boolean indicating whether or not the client has completed | 109 handshake_done: A boolean indicating whether or not the client has completed |
109 the required protocol handshake with the remote Chrome instance. | 110 the required protocol handshake with the remote Chrome instance. |
110 inspector_thread: An instance of the _RemoteInspectorThread class that is | 111 inspector_thread: An instance of the _RemoteInspectorThread class that is |
111 working together with this class to communicate with a remote Chrome | 112 working together with this class to communicate with a remote Chrome |
112 instance. | 113 instance. |
113 """ | 114 """ |
| 115 |
114 def __init__(self, verbose, show_socket_messages, hostname, port, path): | 116 def __init__(self, verbose, show_socket_messages, hostname, port, path): |
115 """Initialize. | 117 """Initialize. |
116 | 118 |
117 Args: | 119 Args: |
118 verbose: A boolean indicating whether or not to use verbose logging. | 120 verbose: A boolean indicating whether or not to use verbose logging. |
119 show_socket_messages: A boolean indicating whether or not to show the | 121 show_socket_messages: A boolean indicating whether or not to show the |
120 socket messages sent/received when communicating with the remote | 122 socket messages sent/received when communicating with the remote |
121 Chrome instance. | 123 Chrome instance. |
122 hostname: The string hostname of the DevToolsSocket to which to connect. | 124 hostname: The string hostname of the DevToolsSocket to which to connect. |
123 port: The integer port number of the DevToolsSocket to which to connect. | 125 port: The integer port number of the DevToolsSocket to which to connect. |
(...skipping 146 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
270 | 272 |
271 class _RemoteInspectorThread(threading.Thread): | 273 class _RemoteInspectorThread(threading.Thread): |
272 """Manages communication using Chrome's remote inspector protocol. | 274 """Manages communication using Chrome's remote inspector protocol. |
273 | 275 |
274 This class works in conjunction with the _DevToolsSocketClient class to | 276 This class works in conjunction with the _DevToolsSocketClient class to |
275 communicate with a remote Chrome instance following the remote inspector | 277 communicate with a remote Chrome instance following the remote inspector |
276 communication protocol in WebKit. This class performs the higher-level work | 278 communication protocol in WebKit. This class performs the higher-level work |
277 of managing request and reply messages, whereas _DevToolsSocketClient handles | 279 of managing request and reply messages, whereas _DevToolsSocketClient handles |
278 the lower-level work of socket communication. | 280 the lower-level work of socket communication. |
279 """ | 281 """ |
280 def __init__(self, tab_index, verbose, show_socket_messages): | 282 |
| 283 def __init__(self, tab_index, tab_filter, verbose, show_socket_messages): |
281 """Initialize. | 284 """Initialize. |
282 | 285 |
283 Args: | 286 Args: |
284 tab_index: The integer index of the tab in the remote Chrome instance to | 287 tab_index: The integer index of the tab in the remote Chrome instance to |
285 use for snapshotting. | 288 use for snapshotting. |
| 289 tab_filter: When specified, is run over tabs of the remote Chrome |
| 290 instances to choose which one to connect to. |
286 verbose: A boolean indicating whether or not to use verbose logging. | 291 verbose: A boolean indicating whether or not to use verbose logging. |
287 show_socket_messages: A boolean indicating whether or not to show the | 292 show_socket_messages: A boolean indicating whether or not to show the |
288 socket messages sent/received when communicating with the remote | 293 socket messages sent/received when communicating with the remote |
289 Chrome instance. | 294 Chrome instance. |
290 """ | 295 """ |
291 threading.Thread.__init__(self) | 296 threading.Thread.__init__(self) |
292 self._logger = logging.getLogger('_RemoteInspectorThread') | 297 self._logger = logging.getLogger('_RemoteInspectorThread') |
293 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) | 298 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) |
294 | 299 |
295 self._killed = False | 300 self._killed = False |
296 self._requests = [] | 301 self._requests = [] |
297 self._action_queue = [] | 302 self._action_queue = [] |
298 self._action_queue_condition = threading.Condition() | 303 self._action_queue_condition = threading.Condition() |
299 self._action_specific_callback = None # Callback only for current action. | 304 self._action_specific_callback = None # Callback only for current action. |
300 self._action_specific_callback_lock = threading.Lock() | 305 self._action_specific_callback_lock = threading.Lock() |
301 self._general_callbacks = [] # General callbacks that can be long-lived. | 306 self._general_callbacks = [] # General callbacks that can be long-lived. |
302 self._general_callbacks_lock = threading.Lock() | 307 self._general_callbacks_lock = threading.Lock() |
303 self._condition_to_wait = None | 308 self._condition_to_wait = None |
304 | 309 |
305 # Create a DevToolsSocket client and wait for it to complete the remote | 310 # Create a DevToolsSocket client and wait for it to complete the remote |
306 # debugging protocol handshake with the remote Chrome instance. | 311 # debugging protocol handshake with the remote Chrome instance. |
307 result = self._IdentifyDevToolsSocketConnectionInfo(tab_index) | 312 result = self._IdentifyDevToolsSocketConnectionInfo(tab_index, tab_filter) |
308 self._client = _DevToolsSocketClient( | 313 self._client = _DevToolsSocketClient( |
309 verbose, show_socket_messages, result['host'], result['port'], | 314 verbose, show_socket_messages, result['host'], result['port'], |
310 result['path']) | 315 result['path']) |
311 self._client.inspector_thread = self | 316 self._client.inspector_thread = self |
312 while asyncore.socket_map: | 317 while asyncore.socket_map: |
313 if self._client.handshake_done or self._killed: | 318 if self._client.handshake_done or self._killed: |
314 break | 319 break |
315 asyncore.loop(timeout=1, count=1, use_poll=True) | 320 asyncore.loop(timeout=1, count=1, use_poll=True) |
316 | 321 |
317 def ClientSocketExceptionOccurred(self): | 322 def ClientSocketExceptionOccurred(self): |
(...skipping 192 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
510 # To actually request the snapshot data from a previously-taken snapshot, | 515 # To actually request the snapshot data from a previously-taken snapshot, |
511 # we need to specify the unique uid of the snapshot we want. | 516 # we need to specify the unique uid of the snapshot we want. |
512 # The relevant uid should be contained in the last | 517 # The relevant uid should be contained in the last |
513 # 'Profiler.takeHeapSnapshot' request object. | 518 # 'Profiler.takeHeapSnapshot' request object. |
514 last_req = self._GetLatestRequestOfType(request, | 519 last_req = self._GetLatestRequestOfType(request, |
515 'Profiler.takeHeapSnapshot') | 520 'Profiler.takeHeapSnapshot') |
516 if last_req and 'uid' in last_req.results: | 521 if last_req and 'uid' in last_req.results: |
517 request.params = {'type': 'HEAP', 'uid': last_req.results['uid']} | 522 request.params = {'type': 'HEAP', 'uid': last_req.results['uid']} |
518 | 523 |
519 @staticmethod | 524 @staticmethod |
520 def _IdentifyDevToolsSocketConnectionInfo(tab_index): | 525 def _IdentifyDevToolsSocketConnectionInfo(tab_index, tab_filter): |
521 """Identifies DevToolsSocket connection info from a remote Chrome instance. | 526 """Identifies DevToolsSocket connection info from a remote Chrome instance. |
522 | 527 |
523 Args: | 528 Args: |
524 tab_index: The integer index of the tab in the remote Chrome instance to | 529 tab_index: The integer index of the tab in the remote Chrome instance to |
525 which to connect. | 530 which to connect. |
| 531 tab_filter: When specified, is run over tabs of the remote Chrome instance |
| 532 to choose which one to connect to. |
526 | 533 |
527 Returns: | 534 Returns: |
528 A dictionary containing the DevToolsSocket connection info: | 535 A dictionary containing the DevToolsSocket connection info: |
529 { | 536 { |
530 'host': string, | 537 'host': string, |
531 'port': integer, | 538 'port': integer, |
532 'path': string, | 539 'path': string, |
533 } | 540 } |
534 | 541 |
535 Raises: | 542 Raises: |
536 RuntimeError: When DevToolsSocket connection info cannot be identified. | 543 RuntimeError: When DevToolsSocket connection info cannot be identified. |
537 """ | 544 """ |
538 try: | 545 try: |
539 # TODO(dennisjeffrey): Do not assume port 9222. The port should be passed | 546 # TODO(dennisjeffrey): Do not assume port 9222. The port should be passed |
540 # as input to this function. | 547 # as input to this function. |
541 f = urllib2.urlopen('http://localhost:9222/json') | 548 f = urllib2.urlopen('http://localhost:9222/json') |
542 result = f.read(); | 549 result = f.read() |
| 550 logging.debug(result) |
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_filter: |
549 raise RuntimeError( | 557 connect_to = filter(tab_filter, result)[0] |
550 'Specified tab index %d doesn\'t exist (%d tabs found)' % | 558 else: |
551 (tab_index, len(result))) | 559 if tab_index >= len(result): |
| 560 raise RuntimeError( |
| 561 'Specified tab index %d doesn\'t exist (%d tabs found)' % |
| 562 (tab_index, len(result))) |
| 563 connect_to = result[tab_index] |
552 | 564 |
553 if 'webSocketDebuggerUrl' not in result[tab_index]: | 565 logging.debug(simplejson.dumps(connect_to)) |
| 566 |
| 567 if 'webSocketDebuggerUrl' not in connect_to: |
554 raise RuntimeError('No socket URL exists for the specified tab.') | 568 raise RuntimeError('No socket URL exists for the specified tab.') |
555 | 569 |
556 socket_url = result[tab_index]['webSocketDebuggerUrl'] | 570 socket_url = connect_to['webSocketDebuggerUrl'] |
557 parsed = urlparse.urlparse(socket_url) | 571 parsed = urlparse.urlparse(socket_url) |
558 # On ChromeOS, the "ws://" scheme may not be recognized, leading to an | 572 # On ChromeOS, the "ws://" scheme may not be recognized, leading to an |
559 # incorrect netloc (and empty hostname and port attributes) in |parsed|. | 573 # incorrect netloc (and empty hostname and port attributes) in |parsed|. |
560 # Change the scheme to "http://" to fix this. | 574 # Change the scheme to "http://" to fix this. |
561 if not parsed.hostname or not parsed.port: | 575 if not parsed.hostname or not parsed.port: |
562 socket_url = 'http' + socket_url[socket_url.find(':'):] | 576 socket_url = 'http' + socket_url[socket_url.find(':'):] |
563 parsed = urlparse.urlparse(socket_url) | 577 parsed = urlparse.urlparse(socket_url) |
564 # Warning: |parsed.scheme| is incorrect after this point. | 578 # Warning: |parsed.scheme| is incorrect after this point. |
565 return ({'host': parsed.hostname, | 579 return ({'host': parsed.hostname, |
566 'port': parsed.port, | 580 'port': parsed.port, |
(...skipping 223 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
790 | 804 |
791 Public Methods: | 805 Public Methods: |
792 Stop: Close the connection to the remote inspector. Should be called when | 806 Stop: Close the connection to the remote inspector. Should be called when |
793 a user is done using this module. | 807 a user is done using this module. |
794 HeapSnapshot: Takes a v8 heap snapshot and returns the summarized data. | 808 HeapSnapshot: Takes a v8 heap snapshot and returns the summarized data. |
795 GetMemoryObjectCounts: Retrieves memory object count information. | 809 GetMemoryObjectCounts: Retrieves memory object count information. |
796 CollectGarbage: Forces a garbage collection. | 810 CollectGarbage: Forces a garbage collection. |
797 StartTimelineEventMonitoring: Starts monitoring for timeline events. | 811 StartTimelineEventMonitoring: Starts monitoring for timeline events. |
798 StopTimelineEventMonitoring: Stops monitoring for timeline events. | 812 StopTimelineEventMonitoring: Stops monitoring for timeline events. |
799 """ | 813 """ |
| 814 |
800 # TODO(dennisjeffrey): Allow a user to specify a window index too (not just a | 815 # TODO(dennisjeffrey): Allow a user to specify a window index too (not just a |
801 # tab index), when running through PyAuto. | 816 # tab index), when running through PyAuto. |
802 def __init__(self, tab_index=0, verbose=False, show_socket_messages=False): | 817 def __init__(self, tab_index=0, tab_filter=None, |
| 818 verbose=False, show_socket_messages=False): |
803 """Initialize. | 819 """Initialize. |
804 | 820 |
805 Args: | 821 Args: |
806 tab_index: The integer index of the tab in the remote Chrome instance to | 822 tab_index: The integer index of the tab in the remote Chrome instance to |
807 which to connect. Defaults to 0 (the first tab). | 823 which to connect. Defaults to 0 (the first tab). |
| 824 tab_filter: When specified, is run over tabs of the remote Chrome |
| 825 instance to choose which one to connect to. |
808 verbose: A boolean indicating whether or not to use verbose logging. | 826 verbose: A boolean indicating whether or not to use verbose logging. |
809 show_socket_messages: A boolean indicating whether or not to show the | 827 show_socket_messages: A boolean indicating whether or not to show the |
810 socket messages sent/received when communicating | 828 socket messages sent/received when communicating with the remote |
811 with the remote Chrome instance. | 829 Chrome instance. |
812 """ | 830 """ |
813 self._tab_index = tab_index | 831 self._tab_index = tab_index |
| 832 self._tab_filter = tab_filter |
814 self._verbose = verbose | 833 self._verbose = verbose |
815 self._show_socket_messages = show_socket_messages | 834 self._show_socket_messages = show_socket_messages |
816 | 835 |
817 self._timeline_started = False | 836 self._timeline_started = False |
818 | 837 |
819 logging.basicConfig() | 838 logging.basicConfig() |
820 self._logger = logging.getLogger('RemoteInspectorClient') | 839 self._logger = logging.getLogger('RemoteInspectorClient') |
821 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) | 840 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) |
822 | 841 |
823 # Creating _RemoteInspectorThread might raise an exception. This prevents an | 842 # Creating _RemoteInspectorThread might raise an exception. This prevents an |
824 # AttributeError in the destructor. | 843 # AttributeError in the destructor. |
825 self._remote_inspector_thread = None | 844 self._remote_inspector_thread = None |
826 self._remote_inspector_driver_thread = None | 845 self._remote_inspector_driver_thread = None |
827 | 846 |
828 # Start up a thread for long-term communication with the remote inspector. | 847 # Start up a thread for long-term communication with the remote inspector. |
829 self._remote_inspector_thread = _RemoteInspectorThread( | 848 self._remote_inspector_thread = _RemoteInspectorThread( |
830 tab_index, verbose, show_socket_messages) | 849 tab_index, tab_filter, verbose, show_socket_messages) |
831 self._remote_inspector_thread.start() | 850 self._remote_inspector_thread.start() |
832 # At this point, a connection has already been made to the remote inspector. | 851 # At this point, a connection has already been made to the remote inspector. |
833 | 852 |
834 # This thread calls asyncore.loop, which activates the channel service. | 853 # This thread calls asyncore.loop, which activates the channel service. |
835 self._remote_inspector_driver_thread = _RemoteInspectorDriverThread() | 854 self._remote_inspector_driver_thread = _RemoteInspectorDriverThread() |
836 self._remote_inspector_driver_thread.start() | 855 self._remote_inspector_driver_thread.start() |
837 | 856 |
838 def __del__(self): | 857 def __del__(self): |
839 """Called on destruction of this object.""" | 858 """Called on destruction of this object.""" |
840 self.Stop() | 859 self.Stop() |
(...skipping 212 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1053 ('Memory.getProcessMemoryDistribution', {}) | 1072 ('Memory.getProcessMemoryDistribution', {}) |
1054 ] | 1073 ] |
1055 | 1074 |
1056 reply_holder = [None] | 1075 reply_holder = [None] |
1057 done_condition = threading.Condition() | 1076 done_condition = threading.Condition() |
1058 def HandleReply(reply_dict): | 1077 def HandleReply(reply_dict): |
1059 """Processes a reply message received from the remote Chrome instance. | 1078 """Processes a reply message received from the remote Chrome instance. |
1060 | 1079 |
1061 Args: | 1080 Args: |
1062 reply_dict: A dictionary object representing the reply message received | 1081 reply_dict: A dictionary object representing the reply message received |
1063 from the remote Chrome instance. | 1082 from the remote Chrome instance. |
1064 """ | 1083 """ |
1065 request_id = reply_dict['id'] | 1084 request_id = reply_dict['id'] |
1066 # GC command will have id = 0, the second command id = 1 | 1085 # GC command will have id = 0, the second command id = 1 |
1067 if request_id == 0: | 1086 if request_id == 0: |
1068 logging.info('Did garbage collection') | 1087 logging.info('Did garbage collection') |
1069 return | 1088 return |
1070 if request_id != 1: | 1089 if request_id != 1: |
1071 raise RuntimeError('Unexpected request_id: %d' % request_id) | 1090 raise RuntimeError('Unexpected request_id: %d' % request_id) |
1072 reply_holder[0] = reply_dict | 1091 reply_holder[0] = reply_dict |
1073 done_condition.acquire() | 1092 done_condition.acquire() |
(...skipping 108 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) |
OLD | NEW |