133259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck# Copyright 2014 The Chromium Authors. All rights reserved.
233259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck# Use of this source code is governed by a BSD-style license that can be
333259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck# found in the LICENSE file.
433259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
533259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reckimport errno
633259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reckimport httplib
733259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reckimport json
833259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reckimport socket
933259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reckimport sys
1033259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
1133259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reckfrom telemetry.core import exceptions
1233259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
1333259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
1433259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reckclass DevToolsClientConnectionError(exceptions.Error):
1533259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck  pass
1633259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
1733259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
1833259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reckclass DevToolsClientUrlError(DevToolsClientConnectionError):
1933259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck  pass
2033259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
2133259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
2233259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reckclass DevToolsHttp(object):
2333259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck  """A helper class to send and parse DevTools HTTP requests.
2433259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
2533259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck  This class maintains a persistent http connection to Chrome devtools.
2633259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck  Ideally, owners of instances of this class should call Disconnect() before
2733259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck  disposing of the instance. Otherwise, the connection will not be closed until
2833259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck  the instance is garbage collected.
2933259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck  """
3033259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
3133259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck  def __init__(self, devtools_port):
3233259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    self._devtools_port = devtools_port
3333259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    self._conn = None
3433259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
3533259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck  def __del__(self):
3633259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    self.Disconnect()
3733259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
3833259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck  def _Connect(self, timeout):
3933259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    """Attempts to establish a connection to Chrome devtools."""
4033259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    assert not self._conn
4133259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    try:
4233259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      host_port = '127.0.0.1:%i' % self._devtools_port
4333259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      self._conn = httplib.HTTPConnection(host_port, timeout=timeout)
4433259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    except (socket.error, httplib.HTTPException) as e:
4533259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      raise DevToolsClientConnectionError, (e,), sys.exc_info()[2]
4633259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
4733259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck  def Disconnect(self):
4833259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    """Closes the HTTP connection."""
4933259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    if not self._conn:
5033259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      return
5133259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
5233259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    try:
5333259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      self._conn.close()
5433259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    except (socket.error, httplib.HTTPException) as e:
5533259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      raise DevToolsClientConnectionError, (e,), sys.exc_info()[2]
5633259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    finally:
5733259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      self._conn = None
5833259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
5933259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck  def Request(self, path, timeout=30):
6033259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    """Sends a request to Chrome devtools.
6133259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
6233259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    This method lazily creates an HTTP connection, if one does not already
6333259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    exist.
6433259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
6533259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    Args:
6633259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      path: The DevTools URL path, without the /json/ prefix.
6733259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      timeout: Timeout defaults to 30 seconds.
6833259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
6933259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    Raises:
7033259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      DevToolsClientConnectionError: If the connection fails.
7133259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    """
7233259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    assert timeout
7333259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
7433259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    if not self._conn:
7533259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      self._Connect(timeout)
7633259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
7733259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    endpoint = '/json'
7833259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    if path:
7933259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      endpoint += '/' + path
8033259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    if self._conn.sock:
8133259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      self._conn.sock.settimeout(timeout)
8233259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    else:
8333259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      self._conn.timeout = timeout
8433259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
8533259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    try:
8633259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      # By default, httplib avoids going through the default system proxy.
8733259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      self._conn.request('GET', endpoint)
8833259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      response = self._conn.getresponse()
8933259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      return response.read()
9033259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    except (socket.error, httplib.HTTPException) as e:
9133259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      self.Disconnect()
9233259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      if isinstance(e, socket.error) and e.errno == errno.ECONNREFUSED:
9333259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck        raise DevToolsClientUrlError, (e,), sys.exc_info()[2]
9433259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      raise DevToolsClientConnectionError, (e,), sys.exc_info()[2]
9533259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
9633259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck  def RequestJson(self, path, timeout=30):
9733259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    """Sends a request and parse the response as JSON.
9833259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
9933259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    Args:
10033259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      path: The DevTools URL path, without the /json/ prefix.
10133259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      timeout: Timeout defaults to 30 seconds.
10233259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck
10333259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    Raises:
10433259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      DevToolsClientConnectionError: If the connection fails.
10533259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck      ValueError: If the response is not a valid JSON.
10633259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    """
10733259e44c8229f70ffe0cf3bb5ca9375c4feb2f9John Reck    return json.loads(self.Request(path, timeout))
108