10529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch# Copyright 2014 The Chromium Authors. All rights reserved.
20529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch# Use of this source code is governed by a BSD-style license that can be
30529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch# found in the LICENSE file.
40529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
50529e5d033099cbfc42635f6f6183833b09dff6eBen Murdochimport json
60529e5d033099cbfc42635f6f6183833b09dff6eBen Murdochimport logging
70529e5d033099cbfc42635f6f6183833b09dff6eBen Murdochimport socket
80529e5d033099cbfc42635f6f6183833b09dff6eBen Murdochimport time
90529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
100529e5d033099cbfc42635f6f6183833b09dff6eBen Murdochfrom telemetry.core.backends.chrome import websocket
110529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
120529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
13116680a4aac90f2aa7413d9095a592090648e557Ben Murdochclass DispatchNotificationsUntilDoneTimeoutException(Exception):
14116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  """Exception that can be thrown from DispatchNotificationsUntilDone to
15116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  indicate timeout exception of the function.
16116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  """
17116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch
18116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  def __init__(self, elapsed_time):
19116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch    super(DispatchNotificationsUntilDoneTimeoutException, self).__init__()
20116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch    self.elapsed_time = elapsed_time
21116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch
22116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch
230529e5d033099cbfc42635f6f6183833b09dff6eBen Murdochclass InspectorWebsocket(object):
240529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
250529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch  def __init__(self, notification_handler=None, error_handler=None):
260529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    """Create a websocket handler for communicating with Inspectors.
270529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
280529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    Args:
290529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch      notification_handler: A callback for notifications received as a result of
30116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch        calling DispatchNotifications() or DispatchNotificationsUntilDone().
310529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch        Must accept a single JSON object containing the Inspector's
32116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch        notification. May return True to indicate the dispatching is done for
33116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch        DispatchNotificationsUntilDone.
340529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch      error_handler: A callback for errors in communicating with the Inspector.
350529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch        Must accept a single numeric parameter indicated the time elapsed before
360529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch        the error.
370529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    """
380529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    self._socket = None
390529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    self._cur_socket_timeout = 0
400529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    self._next_request_id = 0
410529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    self._notification_handler = notification_handler
420529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    self._error_handler = error_handler
431320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    self._all_data_received = False
440529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
450529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch  def Connect(self, url, timeout=10):
460529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    assert not self._socket
470529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    self._socket = websocket.create_connection(url, timeout=timeout)
480529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    self._cur_socket_timeout = 0
490529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    self._next_request_id = 0
500529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
510529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch  def Disconnect(self):
520529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    if self._socket:
530529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch      self._socket.close()
540529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch      self._socket = None
550529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
560529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch  def SendAndIgnoreResponse(self, req):
570529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    req['id'] = self._next_request_id
580529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    self._next_request_id += 1
590529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    data = json.dumps(req)
600529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    self._socket.send(data)
611320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    if logging.getLogger().isEnabledFor(logging.DEBUG):
621320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      logging.debug('sent [%s]', json.dumps(req, indent=2, sort_keys=True))
630529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
640529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch  def SyncRequest(self, req, timeout=10):
650529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    self.SendAndIgnoreResponse(req)
660529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
670529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    while self._socket:
680529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch      res = self._Receive(timeout)
690529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch      if 'id' in res and res['id'] == req['id']:
700529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch        return res
710529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
720529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch  def DispatchNotifications(self, timeout=10):
730529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    self._Receive(timeout)
740529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
75116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  def DispatchNotificationsUntilDone(self, timeout):
76116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch    """Dispatch notifications until notification_handler return True.
770529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
78116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch    Args:
791320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      timeout: a number that respresents the timeout in seconds.
800529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
811320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    Raises:
821320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      DispatchNotificationsUntilDoneTimeoutException if more than |timeout| has
831320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      seconds has passed since the last time any data is received or since this
841320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      function is called, whichever happens later, to when the next attempt to
851320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      receive data fails due to some WebSocketException.
861320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    """
871320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    self._all_data_received = False
88116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch    if timeout < self._cur_socket_timeout:
89116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      self._SetTimeout(timeout)
901320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    timeout_start_time = time.time()
910529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    while self._socket:
920529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch      try:
931320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        if self._Receive(timeout):
941320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          timeout_start_time = time.time()
951320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        if self._all_data_received:
960529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch          break
970529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch      except websocket.WebSocketTimeoutException:
980529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch        pass
991320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      elapsed_time = time.time() - timeout_start_time
100116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      if elapsed_time > timeout:
101116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch        raise DispatchNotificationsUntilDoneTimeoutException(elapsed_time)
1020529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
1030529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch  def _SetTimeout(self, timeout):
1040529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    if self._cur_socket_timeout != timeout:
1050529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch      self._socket.settimeout(timeout)
1060529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch      self._cur_socket_timeout = timeout
1070529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch
1080529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch  def _Receive(self, timeout=10):
1090529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    self._SetTimeout(timeout)
1100529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    start_time = time.time()
1110529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    try:
1121320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      if self._socket:
1131320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        self._all_data_received = False
1140529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch        data = self._socket.recv()
1150529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch        res = json.loads(data)
1161320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci        if logging.getLogger().isEnabledFor(logging.DEBUG):
1171320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          logging.debug('got [%s]', json.dumps(res, indent=2, sort_keys=True))
1180529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch        if 'method' in res and self._notification_handler(res):
1191320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci          self._all_data_received = True
1200529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch          return None
1210529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch        return res
1220529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch    except (socket.error, websocket.WebSocketException):
1230529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch      elapsed_time = time.time() - start_time
1240529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch      self._error_handler(elapsed_time)
125