Skip to content

chore(protocol): implement client side hello #836

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions playwright/_impl/_browser_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import asyncio
import pathlib
from pathlib import Path
from typing import Dict, List, Optional, Union, cast
from typing import TYPE_CHECKING, Dict, List, Optional, Union, cast

from playwright._impl._api_structures import (
Geolocation,
Expand All @@ -42,6 +42,9 @@
from playwright._impl._transport import WebSocketTransport
from playwright._impl._wait_helper import throw_on_timeout

if TYPE_CHECKING:
from playwright._impl._playwright import Playwright


class BrowserType(ChannelOwner):
def __init__(
Expand Down Expand Up @@ -191,23 +194,22 @@ async def connect(
self._connection._dispatcher_fiber,
self._connection._object_factory,
transport,
self._connection._loop,
)
connection._is_sync = self._connection._is_sync
connection._loop = self._connection._loop
connection._loop.create_task(connection.run())
future = connection._loop.create_task(
connection.wait_for_object_with_known_name("Playwright")
)
playwright_future = connection.get_playwright_future()

timeout_future = throw_on_timeout(timeout, Error("Connection timed out"))
done, pending = await asyncio.wait(
{transport.on_error_future, future, timeout_future},
{transport.on_error_future, playwright_future, timeout_future},
return_when=asyncio.FIRST_COMPLETED,
)
if not future.done():
future.cancel()
if not playwright_future.done():
playwright_future.cancel()
if not timeout_future.done():
timeout_future.cancel()
playwright = next(iter(done)).result()
playwright: "Playwright" = next(iter(done)).result()
self._connection._child_ws_connections.append(connection)
pre_launched_browser = playwright._initializer.get("preLaunchedBrowser")
assert pre_launched_browser
Expand Down
40 changes: 27 additions & 13 deletions playwright/_impl/_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@
import sys
import traceback
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Union
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union

from greenlet import greenlet
from pyee import AsyncIOEventEmitter

from playwright._impl._helper import ParsedMessagePayload, parse_error
from playwright._impl._transport import Transport

if TYPE_CHECKING:
from playwright._impl._playwright import Playwright


class Channel(AsyncIOEventEmitter):
def __init__(self, connection: "Connection", guid: str) -> None:
Expand Down Expand Up @@ -119,7 +122,17 @@ def __init__(self, loop: asyncio.AbstractEventLoop) -> None:

class RootChannelOwner(ChannelOwner):
def __init__(self, connection: "Connection") -> None:
super().__init__(connection, "", "", {})
super().__init__(connection, "Root", "", {})

async def initialize(self) -> "Playwright":
return from_channel(
await self._channel.send(
"initialize",
{
"sdkLanguage": "python",
},
)
)


class Connection:
Expand All @@ -128,6 +141,7 @@ def __init__(
dispatcher_fiber: Any,
object_factory: Callable[[ChannelOwner, str, str, Dict], ChannelOwner],
transport: Transport,
loop: asyncio.AbstractEventLoop,
) -> None:
self._dispatcher_fiber = dispatcher_fiber
self._transport = transport
Expand All @@ -140,6 +154,8 @@ def __init__(
self._is_sync = False
self._api_name = ""
self._child_ws_connections: List["Connection"] = []
self._loop = loop
self._playwright_future: asyncio.Future["Playwright"] = loop.create_future()

async def run_as_sync(self) -> None:
self._is_sync = True
Expand All @@ -148,8 +164,17 @@ async def run_as_sync(self) -> None:
async def run(self) -> None:
self._loop = asyncio.get_running_loop()
self._root_object = RootChannelOwner(self)

async def init() -> None:
self._playwright_future.set_result(await self._root_object.initialize())

await self._transport.connect()
self._loop.create_task(init())
await self._transport.run()

def get_playwright_future(self) -> asyncio.Future:
return self._playwright_future

def stop_sync(self) -> None:
self._transport.request_stop()
self._dispatcher_fiber.switch()
Expand All @@ -164,17 +189,6 @@ def cleanup(self) -> None:
for ws_connection in self._child_ws_connections:
ws_connection._transport.dispose()

async def wait_for_object_with_known_name(self, guid: str) -> ChannelOwner:
if guid in self._objects:
return self._objects[guid]
callback: asyncio.Future[ChannelOwner] = self._loop.create_future()

def callback_wrapper(result: ChannelOwner) -> None:
callback.set_result(result)

self._waiting_for_object[guid] = callback_wrapper
return await callback

def call_on_object_with_known_name(
self, guid: str, callback: Callable[[ChannelOwner], None]
) -> None:
Expand Down
30 changes: 19 additions & 11 deletions playwright/_impl/_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ def dispose(self) -> None:
async def wait_until_stopped(self) -> None:
pass

@abstractmethod
async def connect(self) -> None:
pass

@abstractmethod
async def run(self) -> None:
pass
Expand Down Expand Up @@ -91,14 +95,15 @@ def __init__(
self._driver_executable = driver_executable

def request_stop(self) -> None:
assert self._output
self._stopped = True
self._output.close()

async def wait_until_stopped(self) -> None:
await self._stopped_future
await self._proc.wait()

async def run(self) -> None:
async def connect(self) -> None:
self._stopped_future: asyncio.Future = asyncio.Future()
# Hide the command-line window on Windows when using Pythonw.exe
creationflags = 0
Expand All @@ -111,7 +116,7 @@ async def run(self) -> None:
if getattr(sys, "frozen", False):
env["PLAYWRIGHT_BROWSERS_PATH"] = "0"

self._proc = proc = await asyncio.create_subprocess_exec(
self._proc = await asyncio.create_subprocess_exec(
str(self._driver_executable),
"run-driver",
stdin=asyncio.subprocess.PIPE,
Expand All @@ -123,20 +128,21 @@ async def run(self) -> None:
)
except Exception as exc:
self.on_error_future.set_exception(exc)
return
raise exc

assert proc.stdout
assert proc.stdin
self._output = proc.stdin
self._output = self._proc.stdin

async def run(self) -> None:
assert self._proc.stdout
assert self._proc.stdin
while not self._stopped:
try:
buffer = await proc.stdout.readexactly(4)
buffer = await self._proc.stdout.readexactly(4)
length = int.from_bytes(buffer, byteorder="little", signed=False)
buffer = bytes(0)
while length:
to_read = min(length, 32768)
data = await proc.stdout.readexactly(to_read)
data = await self._proc.stdout.readexactly(to_read)
length -= to_read
if len(buffer):
buffer = buffer + data
Expand All @@ -151,6 +157,7 @@ async def run(self) -> None:
self._stopped_future.set_result(None)

def send(self, message: Dict) -> None:
assert self._output
data = self.serialize_message(message)
self._output.write(
len(data).to_bytes(4, byteorder="little", signed=False) + data
Expand Down Expand Up @@ -184,15 +191,16 @@ def dispose(self) -> None:
async def wait_until_stopped(self) -> None:
await self._connection.wait_closed()

async def run(self) -> None:
async def connect(self) -> None:
try:
self._connection = await websocket_connect(
self.ws_endpoint, extra_headers=self.headers
)
except Exception as exc:
self.on_error_future.set_exception(Error(f"websocket.connect: {str(exc)}"))
return
raise exc

async def run(self) -> None:
while not self._stopped:
try:
message = await self._connection.recv()
Expand Down Expand Up @@ -220,7 +228,7 @@ async def run(self) -> None:
break

def send(self, message: Dict) -> None:
if self._stopped or self._connection.closed:
if self._stopped or (hasattr(self, "_connection") and self._connection.closed):
raise Error("Playwright connection closed")
data = self.serialize_message(message)
self._loop.create_task(self._connection.send(data))
7 changes: 3 additions & 4 deletions playwright/async_api/_context_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,11 @@ async def __aenter__(self) -> AsyncPlaywright:
None,
create_remote_object,
PipeTransport(loop, compute_driver_executable()),
loop,
)
self._connection._loop = loop
loop.create_task(self._connection.run())
playwright_future = loop.create_task(
self._connection.wait_for_object_with_known_name("Playwright")
)
playwright_future = self._connection.get_playwright_future()

done, pending = await asyncio.wait(
{self._connection._transport.on_error_future, playwright_future},
return_when=asyncio.FIRST_COMPLETED,
Expand Down
57 changes: 47 additions & 10 deletions playwright/async_api/_generated.py
Original file line number Diff line number Diff line change
Expand Up @@ -2748,18 +2748,18 @@ async def goto(
Returns the main resource response. In case of multiple redirects, the navigation will resolve with the response of the
last redirect.

`frame.goto` will throw an error if:
The method will throw an error if:
- there's an SSL error (e.g. in case of self-signed certificates).
- target URL is invalid.
- the `timeout` is exceeded during navigation.
- the remote server does not respond or is unreachable.
- the main resource failed to load.

`frame.goto` will not throw an error when any valid HTTP status code is returned by the remote server, including 404
\"Not Found\" and 500 \"Internal Server Error\". The status code for such responses can be retrieved by calling
The method will not throw an error when any valid HTTP status code is returned by the remote server, including 404 \"Not
Found\" and 500 \"Internal Server Error\". The status code for such responses can be retrieved by calling
`response.status()`.

> NOTE: `frame.goto` either throws an error or returns a main resource response. The only exceptions are navigation to
> NOTE: The method either throws an error or returns a main resource response. The only exceptions are navigation to
`about:blank` or navigation to the same URL with a different hash, which would succeed and return `null`.
> NOTE: Headless mode doesn't support navigation to a PDF document. See the
[upstream issue](https://bugs.chromium.org/p/chromium/issues/detail?id=761295).
Expand Down Expand Up @@ -4936,7 +4936,42 @@ async def register(
An example of registering selector engine that queries elements based on a tag name:

```py
# FIXME: add snippet
import asyncio
from playwright.async_api import async_playwright

async def run(playwright):
tag_selector = \"\"\"
{
// Returns the first element matching given selector in the root's subtree.
query(root, selector) {
return root.querySelector(selector);
},
// Returns all elements matching given selector in the root's subtree.
queryAll(root, selector) {
return Array.from(root.querySelectorAll(selector));
}
}\"\"\"

# Register the engine. Selectors will be prefixed with \"tag=\".
await playwright.selectors.register(\"tag\", tag_selector)
browser = await playwright.chromium.launch()
page = await browser.new_page()
await page.set_content('<div><button>Click me</button></div>')

# Use the selector prefixed with its name.
button = await page.query_selector('tag=button')
# Combine it with other selector engines.
await page.click('tag=div >> text=\"Click me\"')
# Can use it in any methods supporting selectors.
button_count = await page.eval_on_selector_all('tag=button', 'buttons => buttons.length')
print(button_count)
await browser.close()

async def main():
async with async_playwright() as playwright:
await run(playwright)

asyncio.run(main())
```

Parameters
Expand Down Expand Up @@ -6389,18 +6424,18 @@ async def goto(
Returns the main resource response. In case of multiple redirects, the navigation will resolve with the response of the
last redirect.

`page.goto` will throw an error if:
The method will throw an error if:
- there's an SSL error (e.g. in case of self-signed certificates).
- target URL is invalid.
- the `timeout` is exceeded during navigation.
- the remote server does not respond or is unreachable.
- the main resource failed to load.

`page.goto` will not throw an error when any valid HTTP status code is returned by the remote server, including 404 \"Not
The method will not throw an error when any valid HTTP status code is returned by the remote server, including 404 \"Not
Found\" and 500 \"Internal Server Error\". The status code for such responses can be retrieved by calling
`response.status()`.

> NOTE: `page.goto` either throws an error or returns a main resource response. The only exceptions are navigation to
> NOTE: The method either throws an error or returns a main resource response. The only exceptions are navigation to
`about:blank` or navigation to the same URL with a different hash, which would succeed and return `null`.
> NOTE: Headless mode doesn't support navigation to a PDF document. See the
[upstream issue](https://bugs.chromium.org/p/chromium/issues/detail?id=761295).
Expand Down Expand Up @@ -10144,7 +10179,8 @@ async def launch(
Network proxy settings.
downloads_path : Union[pathlib.Path, str, NoneType]
If specified, accepted downloads are downloaded into this directory. Otherwise, temporary directory is created and is
deleted when browser is closed.
deleted when browser is closed. In either case, the downloads are deleted when the browser context they were created in
is closed.
slow_mo : Union[float, NoneType]
Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
traces_dir : Union[pathlib.Path, str, NoneType]
Expand Down Expand Up @@ -10283,7 +10319,8 @@ async def launch_persistent_context(
Network proxy settings.
downloads_path : Union[pathlib.Path, str, NoneType]
If specified, accepted downloads are downloaded into this directory. Otherwise, temporary directory is created and is
deleted when browser is closed.
deleted when browser is closed. In either case, the downloads are deleted when the browser context they were created in
is closed.
slow_mo : Union[float, NoneType]
Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
viewport : Union[{width: int, height: int}, NoneType]
Expand Down
1 change: 1 addition & 0 deletions playwright/sync_api/_context_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def greenlet_main() -> None:
dispatcher_fiber,
create_remote_object,
PipeTransport(loop, compute_driver_executable()),
loop,
)

g_self = greenlet.getcurrent()
Expand Down
Loading