1# Copyright 2014 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import errno
6import httplib
7import json
8import socket
9import sys
10
11from telemetry.core import exceptions
12
13
14class DevToolsClientConnectionError(exceptions.Error):
15  pass
16
17
18class DevToolsClientUrlError(DevToolsClientConnectionError):
19  pass
20
21
22class DevToolsHttp(object):
23  """A helper class to send and parse DevTools HTTP requests.
24
25  This class maintains a persistent http connection to Chrome devtools.
26  Ideally, owners of instances of this class should call Disconnect() before
27  disposing of the instance. Otherwise, the connection will not be closed until
28  the instance is garbage collected.
29  """
30
31  def __init__(self, devtools_port):
32    self._devtools_port = devtools_port
33    self._conn = None
34
35  def __del__(self):
36    self.Disconnect()
37
38  def _Connect(self, timeout):
39    """Attempts to establish a connection to Chrome devtools."""
40    assert not self._conn
41    try:
42      host_port = '127.0.0.1:%i' % self._devtools_port
43      self._conn = httplib.HTTPConnection(host_port, timeout=timeout)
44    except (socket.error, httplib.HTTPException) as e:
45      raise DevToolsClientConnectionError, (e,), sys.exc_info()[2]
46
47  def Disconnect(self):
48    """Closes the HTTP connection."""
49    if not self._conn:
50      return
51
52    try:
53      self._conn.close()
54    except (socket.error, httplib.HTTPException) as e:
55      raise DevToolsClientConnectionError, (e,), sys.exc_info()[2]
56    finally:
57      self._conn = None
58
59  def Request(self, path, timeout=30):
60    """Sends a request to Chrome devtools.
61
62    This method lazily creates an HTTP connection, if one does not already
63    exist.
64
65    Args:
66      path: The DevTools URL path, without the /json/ prefix.
67      timeout: Timeout defaults to 30 seconds.
68
69    Raises:
70      DevToolsClientConnectionError: If the connection fails.
71    """
72    assert timeout
73
74    if not self._conn:
75      self._Connect(timeout)
76
77    endpoint = '/json'
78    if path:
79      endpoint += '/' + path
80    if self._conn.sock:
81      self._conn.sock.settimeout(timeout)
82    else:
83      self._conn.timeout = timeout
84
85    try:
86      # By default, httplib avoids going through the default system proxy.
87      self._conn.request('GET', endpoint)
88      response = self._conn.getresponse()
89      return response.read()
90    except (socket.error, httplib.HTTPException) as e:
91      self.Disconnect()
92      if isinstance(e, socket.error) and e.errno == errno.ECONNREFUSED:
93        raise DevToolsClientUrlError, (e,), sys.exc_info()[2]
94      raise DevToolsClientConnectionError, (e,), sys.exc_info()[2]
95
96  def RequestJson(self, path, timeout=30):
97    """Sends a request and parse the response as JSON.
98
99    Args:
100      path: The DevTools URL path, without the /json/ prefix.
101      timeout: Timeout defaults to 30 seconds.
102
103    Raises:
104      DevToolsClientConnectionError: If the connection fails.
105      ValueError: If the response is not a valid JSON.
106    """
107    return json.loads(self.Request(path, timeout))
108