| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 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 | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 6 """Chrome remote inspector utility for pyauto tests. | |
| 7 | |
| 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 | |
| 10 the same way that the Developer Tools does. This -- in theory -- should allow | |
| 11 a pyauto test to do anything that Chrome's Developer Tools does, as long as the | |
| 12 appropriate communication with the remote inspector is implemented in this | |
| 13 script. | |
| 14 | |
| 15 This script assumes that Chrome is already running on the local machine with | |
| 16 flag '--remote-debugging-port=9222' to enable remote debugging on port 9222. | |
| 17 | |
| 18 To use this module, first create an instance of class RemoteInspectorClient; | |
| 19 doing this sets up a connection to Chrome's remote inspector. Then call the | |
| 20 appropriate functions on that object to perform the desired actions with the | |
| 21 remote inspector. When done, call Stop() on the RemoteInspectorClient object | |
| 22 to stop communication with the remote inspector. | |
| 23 | |
| 24 For example, to take v8 heap snapshots from a pyauto test: | |
| 25 | |
| 26 import remote_inspector_client | |
| 27 my_client = remote_inspector_client.RemoteInspectorClient() | |
| 28 snapshot_info = my_client.HeapSnapshot(include_summary=True) | |
| 29 // Do some stuff... | |
| 30 new_snapshot_info = my_client.HeapSnapshot(include_summary=True) | |
| 31 my_client.Stop() | |
| 32 | |
| 33 It is expected that a test will only use one instance of RemoteInspectorClient | |
| 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 | |
| 36 for it arises. | |
| 37 """ | |
| 38 | |
| 39 import asyncore | |
| 40 import datetime | |
| 41 import logging | |
| 42 import optparse | |
| 43 import pprint | |
| 44 import re | |
| 45 import simplejson | |
| 46 import socket | |
| 47 import sys | |
| 48 import threading | |
| 49 import time | |
| 50 import urllib2 | |
| 51 import urlparse | |
| 52 | |
| 53 | |
| 54 class _DevToolsSocketRequest(object): | |
| 55 """A representation of a single DevToolsSocket request. | |
| 56 | |
| 57 A DevToolsSocket request is used for communication with a remote Chrome | |
| 58 instance when interacting with the renderer process of a given webpage. | |
| 59 Requests and results are passed as specially-formatted JSON messages, | |
| 60 according to a communication protocol defined in WebKit. The string | |
| 61 representation of this request will be a JSON message that is properly | |
| 62 formatted according to the communication protocol. | |
| 63 | |
| 64 Public Attributes: | |
| 65 method: The string method name associated with this request. | |
| 66 id: A unique integer id associated with this request. | |
| 67 params: A dictionary of input parameters associated with this request. | |
| 68 results: A dictionary of relevant results obtained from the remote Chrome | |
| 69 instance that are associated with this request. | |
| 70 is_fulfilled: A boolean indicating whether or not this request has been sent | |
| 71 and all relevant results for it have been obtained (i.e., this value is | |
| 72 True only if all results for this request are known). | |
| 73 is_fulfilled_condition: A threading.Condition for waiting for the request to | |
| 74 be fulfilled. | |
| 75 """ | |
| 76 | |
| 77 def __init__(self, method, params, message_id): | |
| 78 """Initialize. | |
| 79 | |
| 80 Args: | |
| 81 method: The string method name for this request. | |
| 82 message_id: An integer id for this request, which is assumed to be unique | |
| 83 from among all requests. | |
| 84 """ | |
| 85 self.method = method | |
| 86 self.id = message_id | |
| 87 self.params = params | |
| 88 self.results = {} | |
| 89 self.is_fulfilled = False | |
| 90 self.is_fulfilled_condition = threading.Condition() | |
| 91 | |
| 92 def __repr__(self): | |
| 93 json_dict = {} | |
| 94 json_dict['method'] = self.method | |
| 95 json_dict['id'] = self.id | |
| 96 if self.params: | |
| 97 json_dict['params'] = self.params | |
| 98 return simplejson.dumps(json_dict, separators=(',', ':')) | |
| 99 | |
| 100 | |
| 101 class _DevToolsSocketClient(asyncore.dispatcher): | |
| 102 """Client that communicates with a remote Chrome instance via sockets. | |
| 103 | |
| 104 This class works in conjunction with the _RemoteInspectorThread class to | |
| 105 communicate with a remote Chrome instance following the remote debugging | |
| 106 communication protocol in WebKit. This class performs the lower-level work | |
| 107 of socket communication. | |
| 108 | |
| 109 Public Attributes: | |
| 110 handshake_done: A boolean indicating whether or not the client has completed | |
| 111 the required protocol handshake with the remote Chrome instance. | |
| 112 inspector_thread: An instance of the _RemoteInspectorThread class that is | |
| 113 working together with this class to communicate with a remote Chrome | |
| 114 instance. | |
| 115 """ | |
| 116 | |
| 117 def __init__(self, verbose, show_socket_messages, hostname, port, path): | |
| 118 """Initialize. | |
| 119 | |
| 120 Args: | |
| 121 verbose: A boolean indicating whether or not to use verbose logging. | |
| 122 show_socket_messages: A boolean indicating whether or not to show the | |
| 123 socket messages sent/received when communicating with the remote | |
| 124 Chrome instance. | |
| 125 hostname: The string hostname of the DevToolsSocket to which to connect. | |
| 126 port: The integer port number of the DevToolsSocket to which to connect. | |
| 127 path: The string path of the DevToolsSocket to which to connect. | |
| 128 """ | |
| 129 asyncore.dispatcher.__init__(self) | |
| 130 | |
| 131 self._logger = logging.getLogger('_DevToolsSocketClient') | |
| 132 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) | |
| 133 | |
| 134 self._show_socket_messages = show_socket_messages | |
| 135 | |
| 136 self._read_buffer = '' | |
| 137 self._write_buffer = '' | |
| 138 | |
| 139 self._socket_buffer_lock = threading.Lock() | |
| 140 | |
| 141 self.handshake_done = False | |
| 142 self.inspector_thread = None | |
| 143 | |
| 144 # Connect to the remote Chrome instance and initiate the protocol handshake. | |
| 145 self.create_socket(socket.AF_INET, socket.SOCK_STREAM) | |
| 146 self.connect((hostname, port)) | |
| 147 | |
| 148 fields = [ | |
| 149 'Upgrade: WebSocket', | |
| 150 'Connection: Upgrade', | |
| 151 'Host: %s:%d' % (hostname, port), | |
| 152 'Origin: http://%s:%d' % (hostname, port), | |
| 153 'Sec-WebSocket-Key1: 4k0L66E ZU 8 5 <18 <TK 7 7', | |
| 154 'Sec-WebSocket-Key2: s2 20 `# 4| 3 9 U_ 1299', | |
| 155 ] | |
| 156 handshake_msg = ('GET %s HTTP/1.1\r\n%s\r\n\r\n\x47\x30\x22\x2D\x5A\x3F' | |
| 157 '\x47\x58' % (path, '\r\n'.join(fields))) | |
| 158 self._Write(handshake_msg.encode('utf-8')) | |
| 159 | |
| 160 def SendMessage(self, msg): | |
| 161 """Causes a request message to be sent to the remote Chrome instance. | |
| 162 | |
| 163 Args: | |
| 164 msg: A string message to be sent; assumed to be a JSON message in proper | |
| 165 format according to the remote debugging protocol in WebKit. | |
| 166 """ | |
| 167 # According to the communication protocol, each request message sent over | |
| 168 # the wire must begin with '\x00' and end with '\xff'. | |
| 169 self._Write('\x00' + msg.encode('utf-8') + '\xff') | |
| 170 | |
| 171 def _Write(self, msg): | |
| 172 """Causes a raw message to be sent to the remote Chrome instance. | |
| 173 | |
| 174 Args: | |
| 175 msg: A raw string message to be sent. | |
| 176 """ | |
| 177 self._write_buffer += msg | |
| 178 self.handle_write() | |
| 179 | |
| 180 def handle_write(self): | |
| 181 """Called if a writable socket can be written; overridden from asyncore.""" | |
| 182 self._socket_buffer_lock.acquire() | |
| 183 if self._write_buffer: | |
| 184 sent = self.send(self._write_buffer) | |
| 185 if self._show_socket_messages: | |
| 186 msg_type = ['Handshake', 'Message'][self._write_buffer[0] == '\x00' and | |
| 187 self._write_buffer[-1] == '\xff'] | |
| 188 msg = ('========================\n' | |
| 189 'Sent %s:\n' | |
| 190 '========================\n' | |
| 191 '%s\n' | |
| 192 '========================') % (msg_type, | |
| 193 self._write_buffer[:sent-1]) | |
| 194 print msg | |
| 195 self._write_buffer = self._write_buffer[sent:] | |
| 196 self._socket_buffer_lock.release() | |
| 197 | |
| 198 def handle_read(self): | |
| 199 """Called when a socket can be read; overridden from asyncore.""" | |
| 200 self._socket_buffer_lock.acquire() | |
| 201 if self.handshake_done: | |
| 202 # Process a message reply from the remote Chrome instance. | |
| 203 self._read_buffer += self.recv(4096) | |
| 204 pos = self._read_buffer.find('\xff') | |
| 205 while pos >= 0: | |
| 206 pos += len('\xff') | |
| 207 data = self._read_buffer[:pos-len('\xff')] | |
| 208 pos2 = data.find('\x00') | |
| 209 if pos2 >= 0: | |
| 210 data = data[pos2 + 1:] | |
| 211 self._read_buffer = self._read_buffer[pos:] | |
| 212 if self._show_socket_messages: | |
| 213 msg = ('========================\n' | |
| 214 'Received Message:\n' | |
| 215 '========================\n' | |
| 216 '%s\n' | |
| 217 '========================') % data | |
| 218 print msg | |
| 219 if self.inspector_thread: | |
| 220 self.inspector_thread.NotifyReply(data) | |
| 221 pos = self._read_buffer.find('\xff') | |
| 222 else: | |
| 223 # Process a handshake reply from the remote Chrome instance. | |
| 224 self._read_buffer += self.recv(4096) | |
| 225 pos = self._read_buffer.find('\r\n\r\n') | |
| 226 if pos >= 0: | |
| 227 pos += len('\r\n\r\n') | |
| 228 data = self._read_buffer[:pos] | |
| 229 self._read_buffer = self._read_buffer[pos:] | |
| 230 self.handshake_done = True | |
| 231 if self._show_socket_messages: | |
| 232 msg = ('=========================\n' | |
| 233 'Received Handshake Reply:\n' | |
| 234 '=========================\n' | |
| 235 '%s\n' | |
| 236 '=========================') % data | |
| 237 print msg | |
| 238 self._socket_buffer_lock.release() | |
| 239 | |
| 240 def handle_close(self): | |
| 241 """Called when the socket is closed; overridden from asyncore.""" | |
| 242 if self._show_socket_messages: | |
| 243 msg = ('=========================\n' | |
| 244 'Socket closed.\n' | |
| 245 '=========================') | |
| 246 print msg | |
| 247 self.close() | |
| 248 | |
| 249 def writable(self): | |
| 250 """Determines if writes can occur for this socket; overridden from asyncore. | |
| 251 | |
| 252 Returns: | |
| 253 True, if there is something to write to the socket, or | |
| 254 False, otherwise. | |
| 255 """ | |
| 256 return len(self._write_buffer) > 0 | |
| 257 | |
| 258 def handle_expt(self): | |
| 259 """Called when out-of-band data exists; overridden from asyncore.""" | |
| 260 self.handle_error() | |
| 261 | |
| 262 def handle_error(self): | |
| 263 """Called when an exception is raised; overridden from asyncore.""" | |
| 264 if self._show_socket_messages: | |
| 265 msg = ('=========================\n' | |
| 266 'Socket error.\n' | |
| 267 '=========================') | |
| 268 print msg | |
| 269 self.close() | |
| 270 self.inspector_thread.ClientSocketExceptionOccurred() | |
| 271 asyncore.dispatcher.handle_error(self) | |
| 272 | |
| 273 | |
| 274 class _RemoteInspectorThread(threading.Thread): | |
| 275 """Manages communication using Chrome's remote inspector protocol. | |
| 276 | |
| 277 This class works in conjunction with the _DevToolsSocketClient class to | |
| 278 communicate with a remote Chrome instance following the remote inspector | |
| 279 communication protocol in WebKit. This class performs the higher-level work | |
| 280 of managing request and reply messages, whereas _DevToolsSocketClient handles | |
| 281 the lower-level work of socket communication. | |
| 282 """ | |
| 283 | |
| 284 def __init__(self, url, tab_index, tab_filter, verbose, show_socket_messages, | |
| 285 agent_name): | |
| 286 """Initialize. | |
| 287 | |
| 288 Args: | |
| 289 url: The base URL to connent to. | |
| 290 tab_index: The integer index of the tab in the remote Chrome instance to | |
| 291 use for snapshotting. | |
| 292 tab_filter: When specified, is run over tabs of the remote Chrome | |
| 293 instances to choose which one to connect to. | |
| 294 verbose: A boolean indicating whether or not to use verbose logging. | |
| 295 show_socket_messages: A boolean indicating whether or not to show the | |
| 296 socket messages sent/received when communicating with the remote | |
| 297 Chrome instance. | |
| 298 """ | |
| 299 threading.Thread.__init__(self) | |
| 300 self._logger = logging.getLogger('_RemoteInspectorThread') | |
| 301 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) | |
| 302 | |
| 303 self._killed = False | |
| 304 self._requests = [] | |
| 305 self._action_queue = [] | |
| 306 self._action_queue_condition = threading.Condition() | |
| 307 self._action_specific_callback = None # Callback only for current action. | |
| 308 self._action_specific_callback_lock = threading.Lock() | |
| 309 self._general_callbacks = [] # General callbacks that can be long-lived. | |
| 310 self._general_callbacks_lock = threading.Lock() | |
| 311 self._condition_to_wait = None | |
| 312 self._agent_name = agent_name | |
| 313 | |
| 314 # Create a DevToolsSocket client and wait for it to complete the remote | |
| 315 # debugging protocol handshake with the remote Chrome instance. | |
| 316 result = self._IdentifyDevToolsSocketConnectionInfo( | |
| 317 url, tab_index, tab_filter) | |
| 318 self._client = _DevToolsSocketClient( | |
| 319 verbose, show_socket_messages, result['host'], result['port'], | |
| 320 result['path']) | |
| 321 self._client.inspector_thread = self | |
| 322 while asyncore.socket_map: | |
| 323 if self._client.handshake_done or self._killed: | |
| 324 break | |
| 325 asyncore.loop(timeout=1, count=1, use_poll=True) | |
| 326 | |
| 327 def ClientSocketExceptionOccurred(self): | |
| 328 """Notifies that the _DevToolsSocketClient encountered an exception.""" | |
| 329 self.Kill() | |
| 330 | |
| 331 def NotifyReply(self, msg): | |
| 332 """Notifies of a reply message received from the remote Chrome instance. | |
| 333 | |
| 334 Args: | |
| 335 msg: A string reply message received from the remote Chrome instance; | |
| 336 assumed to be a JSON message formatted according to the remote | |
| 337 debugging communication protocol in WebKit. | |
| 338 """ | |
| 339 reply_dict = simplejson.loads(msg) | |
| 340 | |
| 341 # Notify callbacks of this message received from the remote inspector. | |
| 342 self._action_specific_callback_lock.acquire() | |
| 343 if self._action_specific_callback: | |
| 344 self._action_specific_callback(reply_dict) | |
| 345 self._action_specific_callback_lock.release() | |
| 346 | |
| 347 self._general_callbacks_lock.acquire() | |
| 348 if self._general_callbacks: | |
| 349 for callback in self._general_callbacks: | |
| 350 callback(reply_dict) | |
| 351 self._general_callbacks_lock.release() | |
| 352 | |
| 353 if 'result' in reply_dict: | |
| 354 # This is the result message associated with a previously-sent request. | |
| 355 request = self.GetRequestWithId(reply_dict['id']) | |
| 356 if request: | |
| 357 request.is_fulfilled_condition.acquire() | |
| 358 request.is_fulfilled_condition.notify() | |
| 359 request.is_fulfilled_condition.release() | |
| 360 | |
| 361 def run(self): | |
| 362 """Start this thread; overridden from threading.Thread.""" | |
| 363 while not self._killed: | |
| 364 self._action_queue_condition.acquire() | |
| 365 if self._action_queue: | |
| 366 # There's a request to the remote inspector that needs to be processed. | |
| 367 messages, callback = self._action_queue.pop(0) | |
| 368 self._action_specific_callback_lock.acquire() | |
| 369 self._action_specific_callback = callback | |
| 370 self._action_specific_callback_lock.release() | |
| 371 | |
| 372 # Prepare the request list. | |
| 373 for message_id, message in enumerate(messages): | |
| 374 self._requests.append( | |
| 375 _DevToolsSocketRequest(message[0], message[1], message_id)) | |
| 376 | |
| 377 # Send out each request. Wait until each request is complete before | |
| 378 # sending the next request. | |
| 379 for request in self._requests: | |
| 380 self._FillInParams(request) | |
| 381 self._client.SendMessage(str(request)) | |
| 382 | |
| 383 request.is_fulfilled_condition.acquire() | |
| 384 self._condition_to_wait = request.is_fulfilled_condition | |
| 385 request.is_fulfilled_condition.wait() | |
| 386 request.is_fulfilled_condition.release() | |
| 387 | |
| 388 if self._killed: | |
| 389 self._client.close() | |
| 390 return | |
| 391 | |
| 392 # Clean up so things are ready for the next request. | |
| 393 self._requests = [] | |
| 394 | |
| 395 self._action_specific_callback_lock.acquire() | |
| 396 self._action_specific_callback = None | |
| 397 self._action_specific_callback_lock.release() | |
| 398 | |
| 399 # Wait until there is something to process. | |
| 400 self._condition_to_wait = self._action_queue_condition | |
| 401 self._action_queue_condition.wait() | |
| 402 self._action_queue_condition.release() | |
| 403 self._client.close() | |
| 404 | |
| 405 def Kill(self): | |
| 406 """Notify this thread that it should stop executing.""" | |
| 407 self._killed = True | |
| 408 # The thread might be waiting on a condition. | |
| 409 if self._condition_to_wait: | |
| 410 self._condition_to_wait.acquire() | |
| 411 self._condition_to_wait.notify() | |
| 412 self._condition_to_wait.release() | |
| 413 | |
| 414 def PerformAction(self, request_messages, reply_message_callback): | |
| 415 """Notify this thread of an action to perform using the remote inspector. | |
| 416 | |
| 417 Args: | |
| 418 request_messages: A list of strings representing the requests to make | |
| 419 using the remote inspector. | |
| 420 reply_message_callback: A callable to be invoked any time a message is | |
| 421 received from the remote inspector while the current action is | |
| 422 being performed. The callable should accept a single argument, | |
| 423 which is a dictionary representing a message received. | |
| 424 """ | |
| 425 self._action_queue_condition.acquire() | |
| 426 self._action_queue.append((request_messages, reply_message_callback)) | |
| 427 self._action_queue_condition.notify() | |
| 428 self._action_queue_condition.release() | |
| 429 | |
| 430 def AddMessageCallback(self, callback): | |
| 431 """Add a callback to invoke for messages received from the remote inspector. | |
| 432 | |
| 433 Args: | |
| 434 callback: A callable to be invoked any time a message is received from the | |
| 435 remote inspector. The callable should accept a single argument, which | |
| 436 is a dictionary representing a message received. | |
| 437 """ | |
| 438 self._general_callbacks_lock.acquire() | |
| 439 self._general_callbacks.append(callback) | |
| 440 self._general_callbacks_lock.release() | |
| 441 | |
| 442 def RemoveMessageCallback(self, callback): | |
| 443 """Remove a callback from the set of those to invoke for messages received. | |
| 444 | |
| 445 Args: | |
| 446 callback: A callable to remove from consideration. | |
| 447 """ | |
| 448 self._general_callbacks_lock.acquire() | |
| 449 self._general_callbacks.remove(callback) | |
| 450 self._general_callbacks_lock.release() | |
| 451 | |
| 452 def GetRequestWithId(self, request_id): | |
| 453 """Identifies the request with the specified id. | |
| 454 | |
| 455 Args: | |
| 456 request_id: An integer request id; should be unique for each request. | |
| 457 | |
| 458 Returns: | |
| 459 A request object associated with the given id if found, or | |
| 460 None otherwise. | |
| 461 """ | |
| 462 found_request = [x for x in self._requests if x.id == request_id] | |
| 463 if found_request: | |
| 464 return found_request[0] | |
| 465 return None | |
| 466 | |
| 467 def GetFirstUnfulfilledRequest(self, method): | |
| 468 """Identifies the first unfulfilled request with the given method name. | |
| 469 | |
| 470 An unfulfilled request is one for which all relevant reply messages have | |
| 471 not yet been received from the remote inspector. | |
| 472 | |
| 473 Args: | |
| 474 method: The string method name of the request for which to search. | |
| 475 | |
| 476 Returns: | |
| 477 The first request object in the request list that is not yet fulfilled | |
| 478 and is also associated with the given method name, or | |
| 479 None if no such request object can be found. | |
| 480 """ | |
| 481 for request in self._requests: | |
| 482 if not request.is_fulfilled and request.method == method: | |
| 483 return request | |
| 484 return None | |
| 485 | |
| 486 def _GetLatestRequestOfType(self, ref_req, method): | |
| 487 """Identifies the latest specified request before a reference request. | |
| 488 | |
| 489 This function finds the latest request with the specified method that | |
| 490 occurs before the given reference request. | |
| 491 | |
| 492 Args: | |
| 493 ref_req: A reference request from which to start looking. | |
| 494 method: The string method name of the request for which to search. | |
| 495 | |
| 496 Returns: | |
| 497 The latest _DevToolsSocketRequest object with the specified method, | |
| 498 if found, or None otherwise. | |
| 499 """ | |
| 500 start_looking = False | |
| 501 for request in self._requests[::-1]: | |
| 502 if request.id == ref_req.id: | |
| 503 start_looking = True | |
| 504 elif start_looking: | |
| 505 if request.method == method: | |
| 506 return request | |
| 507 return None | |
| 508 | |
| 509 def _FillInParams(self, request): | |
| 510 """Fills in parameters for requests as necessary before the request is sent. | |
| 511 | |
| 512 Args: | |
| 513 request: The _DevToolsSocketRequest object associated with a request | |
| 514 message that is about to be sent. | |
| 515 """ | |
| 516 if request.method == self._agent_name +'.takeHeapSnapshot': | |
| 517 # We always want detailed v8 heap snapshot information. | |
| 518 request.params = {'detailed': True} | |
| 519 elif request.method == self._agent_name + '.getHeapSnapshot': | |
| 520 # To actually request the snapshot data from a previously-taken snapshot, | |
| 521 # we need to specify the unique uid of the snapshot we want. | |
| 522 # The relevant uid should be contained in the last | |
| 523 # 'Profiler.takeHeapSnapshot' request object. | |
| 524 last_req = self._GetLatestRequestOfType(request, | |
| 525 self._agent_name + '.takeHeapSnapshot') | |
| 526 if last_req and 'uid' in last_req.results: | |
| 527 request.params = {'uid': last_req.results['uid']} | |
| 528 elif request.method == self._agent_name + '.getProfile': | |
| 529 # TODO(eustas): Remove this case after M27 is released. | |
| 530 last_req = self._GetLatestRequestOfType(request, | |
| 531 self._agent_name + '.takeHeapSnapshot') | |
| 532 if last_req and 'uid' in last_req.results: | |
| 533 request.params = {'type': 'HEAP', 'uid': last_req.results['uid']} | |
| 534 | |
| 535 @staticmethod | |
| 536 def _IdentifyDevToolsSocketConnectionInfo(url, tab_index, tab_filter): | |
| 537 """Identifies DevToolsSocket connection info from a remote Chrome instance. | |
| 538 | |
| 539 Args: | |
| 540 url: The base URL to connent to. | |
| 541 tab_index: The integer index of the tab in the remote Chrome instance to | |
| 542 which to connect. | |
| 543 tab_filter: When specified, is run over tabs of the remote Chrome instance | |
| 544 to choose which one to connect to. | |
| 545 | |
| 546 Returns: | |
| 547 A dictionary containing the DevToolsSocket connection info: | |
| 548 { | |
| 549 'host': string, | |
| 550 'port': integer, | |
| 551 'path': string, | |
| 552 } | |
| 553 | |
| 554 Raises: | |
| 555 RuntimeError: When DevToolsSocket connection info cannot be identified. | |
| 556 """ | |
| 557 try: | |
| 558 f = urllib2.urlopen(url + '/json') | |
| 559 result = f.read() | |
| 560 logging.debug(result) | |
| 561 result = simplejson.loads(result) | |
| 562 except urllib2.URLError, e: | |
| 563 raise RuntimeError( | |
| 564 'Error accessing Chrome instance debugging port: ' + str(e)) | |
| 565 | |
| 566 if tab_filter: | |
| 567 connect_to = filter(tab_filter, result)[0] | |
| 568 else: | |
| 569 if tab_index >= len(result): | |
| 570 raise RuntimeError( | |
| 571 'Specified tab index %d doesn\'t exist (%d tabs found)' % | |
| 572 (tab_index, len(result))) | |
| 573 connect_to = result[tab_index] | |
| 574 | |
| 575 logging.debug(simplejson.dumps(connect_to)) | |
| 576 | |
| 577 if 'webSocketDebuggerUrl' not in connect_to: | |
| 578 raise RuntimeError('No socket URL exists for the specified tab.') | |
| 579 | |
| 580 socket_url = connect_to['webSocketDebuggerUrl'] | |
| 581 parsed = urlparse.urlparse(socket_url) | |
| 582 # On ChromeOS, the "ws://" scheme may not be recognized, leading to an | |
| 583 # incorrect netloc (and empty hostname and port attributes) in |parsed|. | |
| 584 # Change the scheme to "http://" to fix this. | |
| 585 if not parsed.hostname or not parsed.port: | |
| 586 socket_url = 'http' + socket_url[socket_url.find(':'):] | |
| 587 parsed = urlparse.urlparse(socket_url) | |
| 588 # Warning: |parsed.scheme| is incorrect after this point. | |
| 589 return ({'host': parsed.hostname, | |
| 590 'port': parsed.port, | |
| 591 'path': parsed.path}) | |
| 592 | |
| 593 | |
| 594 class _RemoteInspectorDriverThread(threading.Thread): | |
| 595 """Drives the communication service with the remote inspector.""" | |
| 596 | |
| 597 def __init__(self): | |
| 598 """Initialize.""" | |
| 599 threading.Thread.__init__(self) | |
| 600 | |
| 601 def run(self): | |
| 602 """Drives the communication service with the remote inspector.""" | |
| 603 try: | |
| 604 while asyncore.socket_map: | |
| 605 asyncore.loop(timeout=1, count=1, use_poll=True) | |
| 606 except KeyboardInterrupt: | |
| 607 pass | |
| 608 | |
| 609 | |
| 610 class _V8HeapSnapshotParser(object): | |
| 611 """Parses v8 heap snapshot data.""" | |
| 612 _CHILD_TYPES = ['context', 'element', 'property', 'internal', 'hidden', | |
| 613 'shortcut', 'weak'] | |
| 614 _NODE_TYPES = ['hidden', 'array', 'string', 'object', 'code', 'closure', | |
| 615 'regexp', 'number', 'native', 'synthetic'] | |
| 616 | |
| 617 @staticmethod | |
| 618 def ParseSnapshotData(raw_data): | |
| 619 """Parses raw v8 heap snapshot data and returns the summarized results. | |
| 620 | |
| 621 The raw heap snapshot data is represented as a JSON object with the | |
| 622 following keys: 'snapshot', 'nodes', and 'strings'. | |
| 623 | |
| 624 The 'snapshot' value provides the 'title' and 'uid' attributes for the | |
| 625 snapshot. For example: | |
| 626 { u'title': u'org.webkit.profiles.user-initiated.1', u'uid': 1} | |
| 627 | |
| 628 The 'nodes' value is a list of node information from the v8 heap, with a | |
| 629 special first element that describes the node serialization layout (see | |
| 630 HeapSnapshotJSONSerializer::SerializeNodes). All other list elements | |
| 631 contain information about nodes in the v8 heap, according to the | |
| 632 serialization layout. | |
| 633 | |
| 634 The 'strings' value is a list of strings, indexed by values in the 'nodes' | |
| 635 list to associate nodes with strings. | |
| 636 | |
| 637 Args: | |
| 638 raw_data: A string representing the raw v8 heap snapshot data. | |
| 639 | |
| 640 Returns: | |
| 641 A dictionary containing the summarized v8 heap snapshot data: | |
| 642 { | |
| 643 'total_v8_node_count': integer, # Total number of nodes in the v8 heap. | |
| 644 'total_shallow_size': integer, # Total heap size, in bytes. | |
| 645 } | |
| 646 """ | |
| 647 total_node_count = 0 | |
| 648 total_shallow_size = 0 | |
| 649 constructors = {} | |
| 650 | |
| 651 # TODO(dennisjeffrey): The following line might be slow, especially on | |
| 652 # ChromeOS. Investigate faster alternatives. | |
| 653 heap = simplejson.loads(raw_data) | |
| 654 | |
| 655 index = 1 # Bypass the special first node list item. | |
| 656 node_list = heap['nodes'] | |
| 657 while index < len(node_list): | |
| 658 node_type = node_list[index] | |
| 659 node_name = node_list[index + 1] | |
| 660 node_id = node_list[index + 2] | |
| 661 node_self_size = node_list[index + 3] | |
| 662 node_retained_size = node_list[index + 4] | |
| 663 node_dominator = node_list[index + 5] | |
| 664 node_children_count = node_list[index + 6] | |
| 665 index += 7 | |
| 666 | |
| 667 node_children = [] | |
| 668 for i in xrange(node_children_count): | |
| 669 child_type = node_list[index] | |
| 670 child_type_string = _V8HeapSnapshotParser._CHILD_TYPES[int(child_type)] | |
| 671 child_name_index = node_list[index + 1] | |
| 672 child_to_node = node_list[index + 2] | |
| 673 index += 3 | |
| 674 | |
| 675 child_info = { | |
| 676 'type': child_type_string, | |
| 677 'name_or_index': child_name_index, | |
| 678 'to_node': child_to_node, | |
| 679 } | |
| 680 node_children.append(child_info) | |
| 681 | |
| 682 # Get the constructor string for this node so nodes can be grouped by | |
| 683 # constructor. | |
| 684 # See HeapSnapshot.js: WebInspector.HeapSnapshotNode.prototype. | |
| 685 type_string = _V8HeapSnapshotParser._NODE_TYPES[int(node_type)] | |
| 686 constructor_name = None | |
| 687 if type_string == 'hidden': | |
| 688 constructor_name = '(system)' | |
| 689 elif type_string == 'object': | |
| 690 constructor_name = heap['strings'][int(node_name)] | |
| 691 elif type_string == 'native': | |
| 692 pos = heap['strings'][int(node_name)].find('/') | |
| 693 if pos >= 0: | |
| 694 constructor_name = heap['strings'][int(node_name)][:pos].rstrip() | |
| 695 else: | |
| 696 constructor_name = heap['strings'][int(node_name)] | |
| 697 elif type_string == 'code': | |
| 698 constructor_name = '(compiled code)' | |
| 699 else: | |
| 700 constructor_name = '(' + type_string + ')' | |
| 701 | |
| 702 node_obj = { | |
| 703 'type': type_string, | |
| 704 'name': heap['strings'][int(node_name)], | |
| 705 'id': node_id, | |
| 706 'self_size': node_self_size, | |
| 707 'retained_size': node_retained_size, | |
| 708 'dominator': node_dominator, | |
| 709 'children_count': node_children_count, | |
| 710 'children': node_children, | |
| 711 } | |
| 712 | |
| 713 if constructor_name not in constructors: | |
| 714 constructors[constructor_name] = [] | |
| 715 constructors[constructor_name].append(node_obj) | |
| 716 | |
| 717 total_node_count += 1 | |
| 718 total_shallow_size += node_self_size | |
| 719 | |
| 720 # TODO(dennisjeffrey): Have this function also return more detailed v8 | |
| 721 # heap snapshot data when a need for it arises (e.g., using |constructors|). | |
| 722 result = {} | |
| 723 result['total_v8_node_count'] = total_node_count | |
| 724 result['total_shallow_size'] = total_shallow_size | |
| 725 return result | |
| 726 | |
| 727 | |
| 728 # TODO(dennisjeffrey): The "verbose" option used in this file should re-use | |
| 729 # pyauto's verbose flag. | |
| 730 class RemoteInspectorClient(object): | |
| 731 """Main class for interacting with Chrome's remote inspector. | |
| 732 | |
| 733 Upon initialization, a socket connection to Chrome's remote inspector will | |
| 734 be established. Users of this class should call Stop() to close the | |
| 735 connection when it's no longer needed. | |
| 736 | |
| 737 Public Methods: | |
| 738 Stop: Close the connection to the remote inspector. Should be called when | |
| 739 a user is done using this module. | |
| 740 HeapSnapshot: Takes a v8 heap snapshot and returns the summarized data. | |
| 741 GetMemoryObjectCounts: Retrieves memory object count information. | |
| 742 CollectGarbage: Forces a garbage collection. | |
| 743 StartTimelineEventMonitoring: Starts monitoring for timeline events. | |
| 744 StopTimelineEventMonitoring: Stops monitoring for timeline events. | |
| 745 """ | |
| 746 | |
| 747 # TODO(dennisjeffrey): Allow a user to specify a window index too (not just a | |
| 748 # tab index), when running through PyAuto. | |
| 749 def __init__(self, tab_index=0, tab_filter=None, | |
| 750 verbose=False, show_socket_messages=False, | |
| 751 url='http://localhost:9222'): | |
| 752 """Initialize. | |
| 753 | |
| 754 Args: | |
| 755 tab_index: The integer index of the tab in the remote Chrome instance to | |
| 756 which to connect. Defaults to 0 (the first tab). | |
| 757 tab_filter: When specified, is run over tabs of the remote Chrome | |
| 758 instance to choose which one to connect to. | |
| 759 verbose: A boolean indicating whether or not to use verbose logging. | |
| 760 show_socket_messages: A boolean indicating whether or not to show the | |
| 761 socket messages sent/received when communicating with the remote | |
| 762 Chrome instance. | |
| 763 """ | |
| 764 self._tab_index = tab_index | |
| 765 self._tab_filter = tab_filter | |
| 766 self._verbose = verbose | |
| 767 self._show_socket_messages = show_socket_messages | |
| 768 | |
| 769 self._timeline_started = False | |
| 770 | |
| 771 logging.basicConfig() | |
| 772 self._logger = logging.getLogger('RemoteInspectorClient') | |
| 773 self._logger.setLevel([logging.WARNING, logging.DEBUG][verbose]) | |
| 774 | |
| 775 # Creating _RemoteInspectorThread might raise an exception. This prevents an | |
| 776 # AttributeError in the destructor. | |
| 777 self._remote_inspector_thread = None | |
| 778 self._remote_inspector_driver_thread = None | |
| 779 | |
| 780 self._version = self._GetVersion(url) | |
| 781 | |
| 782 # TODO(loislo): Remove this hack after M28 is released. | |
| 783 self._agent_name = 'Profiler' | |
| 784 if self._IsBrowserDayNumberGreaterThan(1470): | |
| 785 self._agent_name = 'HeapProfiler' | |
| 786 | |
| 787 # Start up a thread for long-term communication with the remote inspector. | |
| 788 self._remote_inspector_thread = _RemoteInspectorThread( | |
| 789 url, tab_index, tab_filter, verbose, show_socket_messages, | |
| 790 self._agent_name) | |
| 791 self._remote_inspector_thread.start() | |
| 792 # At this point, a connection has already been made to the remote inspector. | |
| 793 | |
| 794 # This thread calls asyncore.loop, which activates the channel service. | |
| 795 self._remote_inspector_driver_thread = _RemoteInspectorDriverThread() | |
| 796 self._remote_inspector_driver_thread.start() | |
| 797 | |
| 798 def __del__(self): | |
| 799 """Called on destruction of this object.""" | |
| 800 self.Stop() | |
| 801 | |
| 802 def Stop(self): | |
| 803 """Stop/close communication with the remote inspector.""" | |
| 804 if self._remote_inspector_thread: | |
| 805 self._remote_inspector_thread.Kill() | |
| 806 self._remote_inspector_thread.join() | |
| 807 self._remote_inspector_thread = None | |
| 808 if self._remote_inspector_driver_thread: | |
| 809 self._remote_inspector_driver_thread.join() | |
| 810 self._remote_inspector_driver_thread = None | |
| 811 | |
| 812 def HeapSnapshot(self, include_summary=False): | |
| 813 """Takes a v8 heap snapshot. | |
| 814 | |
| 815 Returns: | |
| 816 A dictionary containing information for a single v8 heap | |
| 817 snapshot that was taken. | |
| 818 { | |
| 819 'url': string, # URL of the webpage that was snapshotted. | |
| 820 'raw_data': string, # The raw data as JSON string. | |
| 821 'total_v8_node_count': integer, # Total number of nodes in the v8 heap. | |
| 822 # Only if |include_summary| is True. | |
| 823 'total_heap_size': integer, # Total v8 heap size (number of bytes). | |
| 824 # Only if |include_summary| is True. | |
| 825 } | |
| 826 """ | |
| 827 HEAP_SNAPSHOT_MESSAGES = [ | |
| 828 ('Page.getResourceTree', {}), | |
| 829 ('Debugger.enable', {}), | |
| 830 (self._agent_name + '.clearProfiles', {}), | |
| 831 (self._agent_name + '.takeHeapSnapshot', {}), | |
| 832 (self._agent_name + '.getHeapSnapshot', {}), | |
| 833 ] | |
| 834 | |
| 835 self._current_heap_snapshot = [] | |
| 836 self._url = '' | |
| 837 self._collected_heap_snapshot_data = {} | |
| 838 | |
| 839 done_condition = threading.Condition() | |
| 840 | |
| 841 def HandleReply(reply_dict): | |
| 842 """Processes a reply message received from the remote Chrome instance. | |
| 843 | |
| 844 Args: | |
| 845 reply_dict: A dictionary object representing the reply message received | |
| 846 from the remote inspector. | |
| 847 """ | |
| 848 if 'result' in reply_dict: | |
| 849 # This is the result message associated with a previously-sent request. | |
| 850 request = self._remote_inspector_thread.GetRequestWithId( | |
| 851 reply_dict['id']) | |
| 852 if 'frameTree' in reply_dict['result']: | |
| 853 self._url = reply_dict['result']['frameTree']['frame']['url'] | |
| 854 elif request.method == self._agent_name + '.getHeapSnapshot': | |
| 855 # A heap snapshot has been completed. Analyze and output the data. | |
| 856 self._logger.debug('Heap snapshot taken: %s', self._url) | |
| 857 # TODO(dennisjeffrey): Parse the heap snapshot on-the-fly as the data | |
| 858 # is coming in over the wire, so we can avoid storing the entire | |
| 859 # snapshot string in memory. | |
| 860 raw_snapshot_data = ''.join(self._current_heap_snapshot) | |
| 861 self._collected_heap_snapshot_data = { | |
| 862 'url': self._url, | |
| 863 'raw_data': raw_snapshot_data} | |
| 864 if include_summary: | |
| 865 self._logger.debug('Now analyzing heap snapshot...') | |
| 866 parser = _V8HeapSnapshotParser() | |
| 867 time_start = time.time() | |
| 868 self._logger.debug('Raw snapshot data size: %.2f MB', | |
| 869 len(raw_snapshot_data) / (1024.0 * 1024.0)) | |
| 870 result = parser.ParseSnapshotData(raw_snapshot_data) | |
| 871 self._logger.debug('Time to parse data: %.2f sec', | |
| 872 time.time() - time_start) | |
| 873 count = result['total_v8_node_count'] | |
| 874 self._collected_heap_snapshot_data['total_v8_node_count'] = count | |
| 875 total_size = result['total_shallow_size'] | |
| 876 self._collected_heap_snapshot_data['total_heap_size'] = total_size | |
| 877 | |
| 878 done_condition.acquire() | |
| 879 done_condition.notify() | |
| 880 done_condition.release() | |
| 881 elif 'method' in reply_dict: | |
| 882 # This is an auxiliary message sent from the remote Chrome instance. | |
| 883 if reply_dict['method'] == self._agent_name + '.addProfileHeader': | |
| 884 snapshot_req = ( | |
| 885 self._remote_inspector_thread.GetFirstUnfulfilledRequest( | |
| 886 self._agent_name + '.takeHeapSnapshot')) | |
| 887 if snapshot_req: | |
| 888 snapshot_req.results['uid'] = reply_dict['params']['header']['uid'] | |
| 889 elif reply_dict['method'] == self._agent_name + '.addHeapSnapshotChunk': | |
| 890 self._current_heap_snapshot.append(reply_dict['params']['chunk']) | |
| 891 | |
| 892 # Tell the remote inspector to take a v8 heap snapshot, then wait until | |
| 893 # the snapshot information is available to return. | |
| 894 self._remote_inspector_thread.PerformAction(HEAP_SNAPSHOT_MESSAGES, | |
| 895 HandleReply) | |
| 896 | |
| 897 done_condition.acquire() | |
| 898 done_condition.wait() | |
| 899 done_condition.release() | |
| 900 | |
| 901 return self._collected_heap_snapshot_data | |
| 902 | |
| 903 def EvaluateJavaScript(self, expression): | |
| 904 """Evaluates a JavaScript expression and returns the result. | |
| 905 | |
| 906 Sends a message containing the expression to the remote Chrome instance we | |
| 907 are connected to, and evaluates it in the context of the tab we are | |
| 908 connected to. Blocks until the result is available and returns it. | |
| 909 | |
| 910 Returns: | |
| 911 A dictionary representing the result. | |
| 912 """ | |
| 913 EVALUATE_MESSAGES = [ | |
| 914 ('Runtime.evaluate', { 'expression': expression, | |
| 915 'objectGroup': 'group', | |
| 916 'returnByValue': True }), | |
| 917 ('Runtime.releaseObjectGroup', { 'objectGroup': 'group' }) | |
| 918 ] | |
| 919 | |
| 920 self._result = None | |
| 921 done_condition = threading.Condition() | |
| 922 | |
| 923 def HandleReply(reply_dict): | |
| 924 """Processes a reply message received from the remote Chrome instance. | |
| 925 | |
| 926 Args: | |
| 927 reply_dict: A dictionary object representing the reply message received | |
| 928 from the remote Chrome instance. | |
| 929 """ | |
| 930 if 'result' in reply_dict and 'result' in reply_dict['result']: | |
| 931 self._result = reply_dict['result']['result']['value'] | |
| 932 | |
| 933 done_condition.acquire() | |
| 934 done_condition.notify() | |
| 935 done_condition.release() | |
| 936 | |
| 937 # Tell the remote inspector to evaluate the given expression, then wait | |
| 938 # until that information is available to return. | |
| 939 self._remote_inspector_thread.PerformAction(EVALUATE_MESSAGES, | |
| 940 HandleReply) | |
| 941 | |
| 942 done_condition.acquire() | |
| 943 done_condition.wait() | |
| 944 done_condition.release() | |
| 945 | |
| 946 return self._result | |
| 947 | |
| 948 def GetMemoryObjectCounts(self): | |
| 949 """Retrieves memory object count information. | |
| 950 | |
| 951 Returns: | |
| 952 A dictionary containing the memory object count information: | |
| 953 { | |
| 954 'DOMNodeCount': integer, # Total number of DOM nodes. | |
| 955 'EventListenerCount': integer, # Total number of event listeners. | |
| 956 } | |
| 957 """ | |
| 958 MEMORY_COUNT_MESSAGES = [ | |
| 959 ('Memory.getDOMCounters', {}) | |
| 960 ] | |
| 961 | |
| 962 self._event_listener_count = None | |
| 963 self._dom_node_count = None | |
| 964 | |
| 965 done_condition = threading.Condition() | |
| 966 def HandleReply(reply_dict): | |
| 967 """Processes a reply message received from the remote Chrome instance. | |
| 968 | |
| 969 Args: | |
| 970 reply_dict: A dictionary object representing the reply message received | |
| 971 from the remote Chrome instance. | |
| 972 """ | |
| 973 if 'result' in reply_dict: | |
| 974 self._event_listener_count = reply_dict['result']['jsEventListeners'] | |
| 975 self._dom_node_count = reply_dict['result']['nodes'] | |
| 976 | |
| 977 done_condition.acquire() | |
| 978 done_condition.notify() | |
| 979 done_condition.release() | |
| 980 | |
| 981 # Tell the remote inspector to collect memory count info, then wait until | |
| 982 # that information is available to return. | |
| 983 self._remote_inspector_thread.PerformAction(MEMORY_COUNT_MESSAGES, | |
| 984 HandleReply) | |
| 985 | |
| 986 done_condition.acquire() | |
| 987 done_condition.wait() | |
| 988 done_condition.release() | |
| 989 | |
| 990 return { | |
| 991 'DOMNodeCount': self._dom_node_count, | |
| 992 'EventListenerCount': self._event_listener_count, | |
| 993 } | |
| 994 | |
| 995 def CollectGarbage(self): | |
| 996 """Forces a garbage collection.""" | |
| 997 COLLECT_GARBAGE_MESSAGES = [ | |
| 998 ('Profiler.collectGarbage', {}) | |
| 999 ] | |
| 1000 | |
| 1001 # Tell the remote inspector to do a garbage collect. We can return | |
| 1002 # immediately, since there is no result for which to wait. | |
| 1003 self._remote_inspector_thread.PerformAction(COLLECT_GARBAGE_MESSAGES, None) | |
| 1004 | |
| 1005 def StartTimelineEventMonitoring(self, event_callback): | |
| 1006 """Starts timeline event monitoring. | |
| 1007 | |
| 1008 Args: | |
| 1009 event_callback: A callable to invoke whenever a timeline event is observed | |
| 1010 from the remote inspector. The callable should take a single input, | |
| 1011 which is a dictionary containing the detailed information of a | |
| 1012 timeline event. | |
| 1013 """ | |
| 1014 if self._timeline_started: | |
| 1015 self._logger.warning('Timeline monitoring already started.') | |
| 1016 return | |
| 1017 TIMELINE_MESSAGES = [ | |
| 1018 ('Timeline.start', {}) | |
| 1019 ] | |
| 1020 | |
| 1021 self._event_callback = event_callback | |
| 1022 | |
| 1023 done_condition = threading.Condition() | |
| 1024 def HandleReply(reply_dict): | |
| 1025 """Processes a reply message received from the remote Chrome instance. | |
| 1026 | |
| 1027 Args: | |
| 1028 reply_dict: A dictionary object representing the reply message received | |
| 1029 from the remote Chrome instance. | |
| 1030 """ | |
| 1031 if 'result' in reply_dict: | |
| 1032 done_condition.acquire() | |
| 1033 done_condition.notify() | |
| 1034 done_condition.release() | |
| 1035 if reply_dict.get('method') == 'Timeline.eventRecorded': | |
| 1036 self._event_callback(reply_dict['params']['record']) | |
| 1037 | |
| 1038 # Tell the remote inspector to start the timeline. | |
| 1039 self._timeline_callback = HandleReply | |
| 1040 self._remote_inspector_thread.AddMessageCallback(self._timeline_callback) | |
| 1041 self._remote_inspector_thread.PerformAction(TIMELINE_MESSAGES, None) | |
| 1042 | |
| 1043 done_condition.acquire() | |
| 1044 done_condition.wait() | |
| 1045 done_condition.release() | |
| 1046 | |
| 1047 self._timeline_started = True | |
| 1048 | |
| 1049 def StopTimelineEventMonitoring(self): | |
| 1050 """Stops timeline event monitoring.""" | |
| 1051 if not self._timeline_started: | |
| 1052 self._logger.warning('Timeline monitoring already stopped.') | |
| 1053 return | |
| 1054 TIMELINE_MESSAGES = [ | |
| 1055 ('Timeline.stop', {}) | |
| 1056 ] | |
| 1057 | |
| 1058 done_condition = threading.Condition() | |
| 1059 def HandleReply(reply_dict): | |
| 1060 """Processes a reply message received from the remote Chrome instance. | |
| 1061 | |
| 1062 Args: | |
| 1063 reply_dict: A dictionary object representing the reply message received | |
| 1064 from the remote Chrome instance. | |
| 1065 """ | |
| 1066 if 'result' in reply_dict: | |
| 1067 done_condition.acquire() | |
| 1068 done_condition.notify() | |
| 1069 done_condition.release() | |
| 1070 | |
| 1071 # Tell the remote inspector to stop the timeline. | |
| 1072 self._remote_inspector_thread.RemoveMessageCallback(self._timeline_callback) | |
| 1073 self._remote_inspector_thread.PerformAction(TIMELINE_MESSAGES, HandleReply) | |
| 1074 | |
| 1075 done_condition.acquire() | |
| 1076 done_condition.wait() | |
| 1077 done_condition.release() | |
| 1078 | |
| 1079 self._timeline_started = False | |
| 1080 | |
| 1081 def _ConvertByteCountToHumanReadableString(self, num_bytes): | |
| 1082 """Converts an integer number of bytes into a human-readable string. | |
| 1083 | |
| 1084 Args: | |
| 1085 num_bytes: An integer number of bytes. | |
| 1086 | |
| 1087 Returns: | |
| 1088 A human-readable string representation of the given number of bytes. | |
| 1089 """ | |
| 1090 if num_bytes < 1024: | |
| 1091 return '%d B' % num_bytes | |
| 1092 elif num_bytes < 1048576: | |
| 1093 return '%.2f KB' % (num_bytes / 1024.0) | |
| 1094 else: | |
| 1095 return '%.2f MB' % (num_bytes / 1048576.0) | |
| 1096 | |
| 1097 @staticmethod | |
| 1098 def _GetVersion(endpoint): | |
| 1099 """Fetches version information from a remote Chrome instance. | |
| 1100 | |
| 1101 Args: | |
| 1102 endpoint: The base URL to connent to. | |
| 1103 | |
| 1104 Returns: | |
| 1105 A dictionary containing Browser and Content version information: | |
| 1106 { | |
| 1107 'Browser': { | |
| 1108 'major': integer, | |
| 1109 'minor': integer, | |
| 1110 'fix': integer, | |
| 1111 'day': integer | |
| 1112 }, | |
| 1113 'Content': { | |
| 1114 'name': string, | |
| 1115 'major': integer, | |
| 1116 'minor': integer | |
| 1117 } | |
| 1118 } | |
| 1119 | |
| 1120 Raises: | |
| 1121 RuntimeError: When Browser version info can't be fetched or parsed. | |
| 1122 """ | |
| 1123 try: | |
| 1124 f = urllib2.urlopen(endpoint + '/json/version') | |
| 1125 result = f.read(); | |
| 1126 result = simplejson.loads(result) | |
| 1127 except urllib2.URLError, e: | |
| 1128 raise RuntimeError( | |
| 1129 'Error accessing Chrome instance debugging port: ' + str(e)) | |
| 1130 | |
| 1131 if 'Browser' not in result: | |
| 1132 raise RuntimeError('Browser version is not specified.') | |
| 1133 | |
| 1134 parsed = re.search('^Chrome\/(\d+).(\d+).(\d+).(\d+)', result['Browser']) | |
| 1135 if parsed is None: | |
| 1136 raise RuntimeError('Browser-Version cannot be parsed.') | |
| 1137 try: | |
| 1138 day = int(parsed.group(3)) | |
| 1139 browser_info = { | |
| 1140 'major': int(parsed.group(1)), | |
| 1141 'minor': int(parsed.group(2)), | |
| 1142 'day': day, | |
| 1143 'fix': int(parsed.group(4)), | |
| 1144 } | |
| 1145 except ValueError: | |
| 1146 raise RuntimeError('Browser-Version cannot be parsed.') | |
| 1147 | |
| 1148 if 'WebKit-Version' not in result: | |
| 1149 raise RuntimeError('Content-Version is not specified.') | |
| 1150 | |
| 1151 parsed = re.search('^(\d+)\.(\d+)', result['WebKit-Version']) | |
| 1152 if parsed is None: | |
| 1153 raise RuntimeError('Content-Version cannot be parsed.') | |
| 1154 | |
| 1155 try: | |
| 1156 platform_info = { | |
| 1157 'name': 'Blink' if day > 1464 else 'WebKit', | |
| 1158 'major': int(parsed.group(1)), | |
| 1159 'minor': int(parsed.group(2)), | |
| 1160 } | |
| 1161 except ValueError: | |
| 1162 raise RuntimeError('WebKit-Version cannot be parsed.') | |
| 1163 | |
| 1164 return { | |
| 1165 'browser': browser_info, | |
| 1166 'platform': platform_info | |
| 1167 } | |
| 1168 | |
| 1169 def _IsContentVersionNotOlderThan(self, major, minor): | |
| 1170 """Compares remote Browser Content version with specified one. | |
| 1171 | |
| 1172 Args: | |
| 1173 major: Major Webkit version. | |
| 1174 minor: Minor Webkit version. | |
| 1175 | |
| 1176 Returns: | |
| 1177 True if remote Content version is same or newer than specified, | |
| 1178 False otherwise. | |
| 1179 | |
| 1180 Raises: | |
| 1181 RuntimeError: If remote Content version hasn't been fetched yet. | |
| 1182 """ | |
| 1183 if not hasattr(self, '_version'): | |
| 1184 raise RuntimeError('Browser version has not been fetched yet.') | |
| 1185 version = self._version['platform'] | |
| 1186 | |
| 1187 if version['major'] < major: | |
| 1188 return False | |
| 1189 elif version['major'] == major and version['minor'] < minor: | |
| 1190 return False | |
| 1191 else: | |
| 1192 return True | |
| 1193 | |
| 1194 def _IsBrowserDayNumberGreaterThan(self, day_number): | |
| 1195 """Compares remote Chromium day number with specified one. | |
| 1196 | |
| 1197 Args: | |
| 1198 day_number: Forth part of the chromium version. | |
| 1199 | |
| 1200 Returns: | |
| 1201 True if remote Chromium day number is same or newer than specified, | |
| 1202 False otherwise. | |
| 1203 | |
| 1204 Raises: | |
| 1205 RuntimeError: If remote Chromium version hasn't been fetched yet. | |
| 1206 """ | |
| 1207 if not hasattr(self, '_version'): | |
| 1208 raise RuntimeError('Browser revision has not been fetched yet.') | |
| 1209 version = self._version['browser'] | |
| 1210 | |
| 1211 return version['day'] > day_number | |
| OLD | NEW |