Example #1
0
class HTTPWireProtocol(object):
    """
    Transports messages (commands and responses) over the WebDriver
    wire protocol.

    Complex objects, such as ``webdriver.Element``, ``webdriver.Frame``,
    and ``webdriver.Window`` are by default not marshaled to enable
    use of `session.transport.send` in WPT tests::

        session = webdriver.Session("127.0.0.1", 4444)
        response = transport.send("GET", "element/active", None)
        print response.body["value"]
        # => {u'element-6066-11e4-a52e-4f735466cecf': u'<uuid>'}

    Automatic marshaling is provided by ``webdriver.protocol.Encoder``
    and ``webdriver.protocol.Decoder``, which can be passed in to
    ``HTTPWireProtocol.send`` along with a reference to the current
    ``webdriver.Session``::

        session = webdriver.Session("127.0.0.1", 4444)
        response = transport.send("GET", "element/active", None,
            encoder=protocol.Encoder, decoder=protocol.Decoder,
            session=session)
        print response.body["value"]
        # => webdriver.Element
    """

    def __init__(self, host, port, url_prefix="/"):
        """
        Construct interface for communicating with the remote server.

        :param url: URL of remote WebDriver server.
        :param wait: Duration to wait for remote to appear.
        """
        self.host = host
        self.port = port
        self.url_prefix = url_prefix
        self._conn = None
        self._last_request_is_blocked = False

    def __del__(self):
        self.close()

    def close(self):
        """Closes the current HTTP connection, if there is one."""
        if self._conn:
            self._conn.close()

    @property
    def connection(self):
        """Gets the current HTTP connection, or lazily creates one."""
        if not self._conn:
            conn_kwargs = {}
            if not PY3:
                conn_kwargs["strict"] = True
            # We are not setting an HTTP timeout other than the default when the
            # connection its created. The send method has a timeout value if needed.
            self._conn = HTTPConnection(self.host, self.port, **conn_kwargs)

        return self._conn

    def url(self, suffix):
        """
        From the relative path to a command end-point,
        craft a full URL suitable to be used in a request to the HTTPD.
        """
        return urlparse.urljoin(self.url_prefix, suffix)

    def send(self,
             method,
             uri,
             body=None,
             headers=None,
             encoder=json.JSONEncoder,
             decoder=json.JSONDecoder,
             timeout=None,
             **codec_kwargs):
        """
        Send a command to the remote.

        The request `body` must be JSON serialisable unless a
        custom `encoder` has been provided.  This means complex
        objects such as ``webdriver.Element``, ``webdriver.Frame``,
        and `webdriver.Window`` are not automatically made
        into JSON.  This behaviour is, however, provided by
        ``webdriver.protocol.Encoder``, should you want it.

        Similarly, the response body is returned au natural
        as plain JSON unless a `decoder` that converts web
        element references to ``webdriver.Element`` is provided.
        Use ``webdriver.protocol.Decoder`` to achieve this behaviour.

        The client will attempt to use persistent HTTP connections.

        :param method: `GET`, `POST`, or `DELETE`.
        :param uri: Relative endpoint of the requests URL path.
        :param body: Body of the request.  Defaults to an empty
            dictionary if ``method`` is `POST`.
        :param headers: Additional dictionary of headers to include
            in the request.
        :param encoder: JSON encoder class, which defaults to
            ``json.JSONEncoder`` unless specified.
        :param decoder: JSON decoder class, which defaults to
            ``json.JSONDecoder`` unless specified.
        :param codec_kwargs: Surplus arguments passed on to `encoder`
            and `decoder` on construction.

        :return: Instance of ``webdriver.transport.Response``
            describing the HTTP response received from the remote end.

        :raises ValueError: If `body` or the response body are not
            JSON serialisable.
        """
        if body is None and method == "POST":
            body = {}

        payload = None
        if body is not None:
            try:
                payload = json.dumps(body, cls=encoder, **codec_kwargs)
            except ValueError:
                raise ValueError("Failed to encode request body as JSON:\n"
                    "%s" % json.dumps(body, indent=2))

        # When the timeout triggers, the TestRunnerManager thread will reuse
        # this connection to check if the WebDriver its alive and we may end
        # raising an httplib.CannotSendRequest exception if the WebDriver is
        # not responding and this httplib.request() call is blocked on the
        # runner thread. We use the boolean below to check for that and restart
        # the connection in that case.
        self._last_request_is_blocked = True
        response = self._request(method, uri, payload, headers, timeout=None)
        self._last_request_is_blocked = False
        return Response.from_http(response, decoder=decoder, **codec_kwargs)

    def _request(self, method, uri, payload, headers=None, timeout=None):
        if isinstance(payload, text_type):
            payload = payload.encode("utf-8")

        if headers is None:
            headers = {}
        headers.update({"Connection": "keep-alive"})

        url = self.url(uri)

        if self._last_request_is_blocked or self._has_unread_data():
            self.close()

        self.connection.request(method, url, payload, headers)

        # timeout for request has to be set just before calling httplib.getresponse()
        # and the previous value restored just after that, even on exception raised
        try:
            if timeout:
                previous_timeout = self._conn.gettimeout()
                self._conn.settimeout(timeout)
            response = self.connection.getresponse()
        finally:
            if timeout:
                self._conn.settimeout(previous_timeout)

        return response

    def _has_unread_data(self):
        return self._conn and self._conn.sock and select.select([self._conn.sock], [], [], 0)[0]