Skip to content

Commit 5981155

Browse files
authored
chore(protocol): implement client side hello (microsoft#836)
1 parent 646af02 commit 5981155

File tree

8 files changed

+152
-58
lines changed

8 files changed

+152
-58
lines changed

playwright/_impl/_browser_type.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import asyncio
1616
import pathlib
1717
from pathlib import Path
18-
from typing import Dict, List, Optional, Union, cast
18+
from typing import TYPE_CHECKING, Dict, List, Optional, Union, cast
1919

2020
from playwright._impl._api_structures import (
2121
Geolocation,
@@ -42,6 +42,9 @@
4242
from playwright._impl._transport import WebSocketTransport
4343
from playwright._impl._wait_helper import throw_on_timeout
4444

45+
if TYPE_CHECKING:
46+
from playwright._impl._playwright import Playwright
47+
4548

4649
class BrowserType(ChannelOwner):
4750
def __init__(
@@ -191,23 +194,22 @@ async def connect(
191194
self._connection._dispatcher_fiber,
192195
self._connection._object_factory,
193196
transport,
197+
self._connection._loop,
194198
)
195199
connection._is_sync = self._connection._is_sync
196-
connection._loop = self._connection._loop
197200
connection._loop.create_task(connection.run())
198-
future = connection._loop.create_task(
199-
connection.wait_for_object_with_known_name("Playwright")
200-
)
201+
playwright_future = connection.get_playwright_future()
202+
201203
timeout_future = throw_on_timeout(timeout, Error("Connection timed out"))
202204
done, pending = await asyncio.wait(
203-
{transport.on_error_future, future, timeout_future},
205+
{transport.on_error_future, playwright_future, timeout_future},
204206
return_when=asyncio.FIRST_COMPLETED,
205207
)
206-
if not future.done():
207-
future.cancel()
208+
if not playwright_future.done():
209+
playwright_future.cancel()
208210
if not timeout_future.done():
209211
timeout_future.cancel()
210-
playwright = next(iter(done)).result()
212+
playwright: "Playwright" = next(iter(done)).result()
211213
self._connection._child_ws_connections.append(connection)
212214
pre_launched_browser = playwright._initializer.get("preLaunchedBrowser")
213215
assert pre_launched_browser

playwright/_impl/_connection.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@
1616
import sys
1717
import traceback
1818
from pathlib import Path
19-
from typing import Any, Callable, Dict, List, Optional, Union
19+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
2020

2121
from greenlet import greenlet
2222
from pyee import AsyncIOEventEmitter
2323

2424
from playwright._impl._helper import ParsedMessagePayload, parse_error
2525
from playwright._impl._transport import Transport
2626

27+
if TYPE_CHECKING:
28+
from playwright._impl._playwright import Playwright
29+
2730

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

120123
class RootChannelOwner(ChannelOwner):
121124
def __init__(self, connection: "Connection") -> None:
122-
super().__init__(connection, "", "", {})
125+
super().__init__(connection, "Root", "", {})
126+
127+
async def initialize(self) -> "Playwright":
128+
return from_channel(
129+
await self._channel.send(
130+
"initialize",
131+
{
132+
"sdkLanguage": "python",
133+
},
134+
)
135+
)
123136

124137

125138
class Connection:
@@ -128,6 +141,7 @@ def __init__(
128141
dispatcher_fiber: Any,
129142
object_factory: Callable[[ChannelOwner, str, str, Dict], ChannelOwner],
130143
transport: Transport,
144+
loop: asyncio.AbstractEventLoop,
131145
) -> None:
132146
self._dispatcher_fiber = dispatcher_fiber
133147
self._transport = transport
@@ -140,6 +154,8 @@ def __init__(
140154
self._is_sync = False
141155
self._api_name = ""
142156
self._child_ws_connections: List["Connection"] = []
157+
self._loop = loop
158+
self._playwright_future: asyncio.Future["Playwright"] = loop.create_future()
143159

144160
async def run_as_sync(self) -> None:
145161
self._is_sync = True
@@ -148,8 +164,17 @@ async def run_as_sync(self) -> None:
148164
async def run(self) -> None:
149165
self._loop = asyncio.get_running_loop()
150166
self._root_object = RootChannelOwner(self)
167+
168+
async def init() -> None:
169+
self._playwright_future.set_result(await self._root_object.initialize())
170+
171+
await self._transport.connect()
172+
self._loop.create_task(init())
151173
await self._transport.run()
152174

175+
def get_playwright_future(self) -> asyncio.Future:
176+
return self._playwright_future
177+
153178
def stop_sync(self) -> None:
154179
self._transport.request_stop()
155180
self._dispatcher_fiber.switch()
@@ -164,17 +189,6 @@ def cleanup(self) -> None:
164189
for ws_connection in self._child_ws_connections:
165190
ws_connection._transport.dispose()
166191

167-
async def wait_for_object_with_known_name(self, guid: str) -> ChannelOwner:
168-
if guid in self._objects:
169-
return self._objects[guid]
170-
callback: asyncio.Future[ChannelOwner] = self._loop.create_future()
171-
172-
def callback_wrapper(result: ChannelOwner) -> None:
173-
callback.set_result(result)
174-
175-
self._waiting_for_object[guid] = callback_wrapper
176-
return await callback
177-
178192
def call_on_object_with_known_name(
179193
self, guid: str, callback: Callable[[ChannelOwner], None]
180194
) -> None:

playwright/_impl/_transport.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ def dispose(self) -> None:
6060
async def wait_until_stopped(self) -> None:
6161
pass
6262

63+
@abstractmethod
64+
async def connect(self) -> None:
65+
pass
66+
6367
@abstractmethod
6468
async def run(self) -> None:
6569
pass
@@ -91,14 +95,15 @@ def __init__(
9195
self._driver_executable = driver_executable
9296

9397
def request_stop(self) -> None:
98+
assert self._output
9499
self._stopped = True
95100
self._output.close()
96101

97102
async def wait_until_stopped(self) -> None:
98103
await self._stopped_future
99104
await self._proc.wait()
100105

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

114-
self._proc = proc = await asyncio.create_subprocess_exec(
119+
self._proc = await asyncio.create_subprocess_exec(
115120
str(self._driver_executable),
116121
"run-driver",
117122
stdin=asyncio.subprocess.PIPE,
@@ -123,20 +128,21 @@ async def run(self) -> None:
123128
)
124129
except Exception as exc:
125130
self.on_error_future.set_exception(exc)
126-
return
131+
raise exc
127132

128-
assert proc.stdout
129-
assert proc.stdin
130-
self._output = proc.stdin
133+
self._output = self._proc.stdin
131134

135+
async def run(self) -> None:
136+
assert self._proc.stdout
137+
assert self._proc.stdin
132138
while not self._stopped:
133139
try:
134-
buffer = await proc.stdout.readexactly(4)
140+
buffer = await self._proc.stdout.readexactly(4)
135141
length = int.from_bytes(buffer, byteorder="little", signed=False)
136142
buffer = bytes(0)
137143
while length:
138144
to_read = min(length, 32768)
139-
data = await proc.stdout.readexactly(to_read)
145+
data = await self._proc.stdout.readexactly(to_read)
140146
length -= to_read
141147
if len(buffer):
142148
buffer = buffer + data
@@ -151,6 +157,7 @@ async def run(self) -> None:
151157
self._stopped_future.set_result(None)
152158

153159
def send(self, message: Dict) -> None:
160+
assert self._output
154161
data = self.serialize_message(message)
155162
self._output.write(
156163
len(data).to_bytes(4, byteorder="little", signed=False) + data
@@ -184,15 +191,16 @@ def dispose(self) -> None:
184191
async def wait_until_stopped(self) -> None:
185192
await self._connection.wait_closed()
186193

187-
async def run(self) -> None:
194+
async def connect(self) -> None:
188195
try:
189196
self._connection = await websocket_connect(
190197
self.ws_endpoint, extra_headers=self.headers
191198
)
192199
except Exception as exc:
193200
self.on_error_future.set_exception(Error(f"websocket.connect: {str(exc)}"))
194-
return
201+
raise exc
195202

203+
async def run(self) -> None:
196204
while not self._stopped:
197205
try:
198206
message = await self._connection.recv()
@@ -220,7 +228,7 @@ async def run(self) -> None:
220228
break
221229

222230
def send(self, message: Dict) -> None:
223-
if self._stopped or self._connection.closed:
231+
if self._stopped or (hasattr(self, "_connection") and self._connection.closed):
224232
raise Error("Playwright connection closed")
225233
data = self.serialize_message(message)
226234
self._loop.create_task(self._connection.send(data))

playwright/async_api/_context_manager.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,11 @@ async def __aenter__(self) -> AsyncPlaywright:
3232
None,
3333
create_remote_object,
3434
PipeTransport(loop, compute_driver_executable()),
35+
loop,
3536
)
36-
self._connection._loop = loop
3737
loop.create_task(self._connection.run())
38-
playwright_future = loop.create_task(
39-
self._connection.wait_for_object_with_known_name("Playwright")
40-
)
38+
playwright_future = self._connection.get_playwright_future()
39+
4140
done, pending = await asyncio.wait(
4241
{self._connection._transport.on_error_future, playwright_future},
4342
return_when=asyncio.FIRST_COMPLETED,

playwright/async_api/_generated.py

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2748,18 +2748,18 @@ async def goto(
27482748
Returns the main resource response. In case of multiple redirects, the navigation will resolve with the response of the
27492749
last redirect.
27502750
2751-
`frame.goto` will throw an error if:
2751+
The method will throw an error if:
27522752
- there's an SSL error (e.g. in case of self-signed certificates).
27532753
- target URL is invalid.
27542754
- the `timeout` is exceeded during navigation.
27552755
- the remote server does not respond or is unreachable.
27562756
- the main resource failed to load.
27572757
2758-
`frame.goto` will not throw an error when any valid HTTP status code is returned by the remote server, including 404
2759-
\"Not Found\" and 500 \"Internal Server Error\". The status code for such responses can be retrieved by calling
2758+
The method will not throw an error when any valid HTTP status code is returned by the remote server, including 404 \"Not
2759+
Found\" and 500 \"Internal Server Error\". The status code for such responses can be retrieved by calling
27602760
`response.status()`.
27612761
2762-
> NOTE: `frame.goto` either throws an error or returns a main resource response. The only exceptions are navigation to
2762+
> NOTE: The method either throws an error or returns a main resource response. The only exceptions are navigation to
27632763
`about:blank` or navigation to the same URL with a different hash, which would succeed and return `null`.
27642764
> NOTE: Headless mode doesn't support navigation to a PDF document. See the
27652765
[upstream issue](https://bugs.chromium.org/p/chromium/issues/detail?id=761295).
@@ -4936,7 +4936,42 @@ async def register(
49364936
An example of registering selector engine that queries elements based on a tag name:
49374937
49384938
```py
4939-
# FIXME: add snippet
4939+
import asyncio
4940+
from playwright.async_api import async_playwright
4941+
4942+
async def run(playwright):
4943+
tag_selector = \"\"\"
4944+
{
4945+
// Returns the first element matching given selector in the root's subtree.
4946+
query(root, selector) {
4947+
return root.querySelector(selector);
4948+
},
4949+
// Returns all elements matching given selector in the root's subtree.
4950+
queryAll(root, selector) {
4951+
return Array.from(root.querySelectorAll(selector));
4952+
}
4953+
}\"\"\"
4954+
4955+
# Register the engine. Selectors will be prefixed with \"tag=\".
4956+
await playwright.selectors.register(\"tag\", tag_selector)
4957+
browser = await playwright.chromium.launch()
4958+
page = await browser.new_page()
4959+
await page.set_content('<div><button>Click me</button></div>')
4960+
4961+
# Use the selector prefixed with its name.
4962+
button = await page.query_selector('tag=button')
4963+
# Combine it with other selector engines.
4964+
await page.click('tag=div >> text=\"Click me\"')
4965+
# Can use it in any methods supporting selectors.
4966+
button_count = await page.eval_on_selector_all('tag=button', 'buttons => buttons.length')
4967+
print(button_count)
4968+
await browser.close()
4969+
4970+
async def main():
4971+
async with async_playwright() as playwright:
4972+
await run(playwright)
4973+
4974+
asyncio.run(main())
49404975
```
49414976
49424977
Parameters
@@ -6389,18 +6424,18 @@ async def goto(
63896424
Returns the main resource response. In case of multiple redirects, the navigation will resolve with the response of the
63906425
last redirect.
63916426
6392-
`page.goto` will throw an error if:
6427+
The method will throw an error if:
63936428
- there's an SSL error (e.g. in case of self-signed certificates).
63946429
- target URL is invalid.
63956430
- the `timeout` is exceeded during navigation.
63966431
- the remote server does not respond or is unreachable.
63976432
- the main resource failed to load.
63986433
6399-
`page.goto` will not throw an error when any valid HTTP status code is returned by the remote server, including 404 \"Not
6434+
The method will not throw an error when any valid HTTP status code is returned by the remote server, including 404 \"Not
64006435
Found\" and 500 \"Internal Server Error\". The status code for such responses can be retrieved by calling
64016436
`response.status()`.
64026437
6403-
> NOTE: `page.goto` either throws an error or returns a main resource response. The only exceptions are navigation to
6438+
> NOTE: The method either throws an error or returns a main resource response. The only exceptions are navigation to
64046439
`about:blank` or navigation to the same URL with a different hash, which would succeed and return `null`.
64056440
> NOTE: Headless mode doesn't support navigation to a PDF document. See the
64066441
[upstream issue](https://bugs.chromium.org/p/chromium/issues/detail?id=761295).
@@ -10144,7 +10179,8 @@ async def launch(
1014410179
Network proxy settings.
1014510180
downloads_path : Union[pathlib.Path, str, NoneType]
1014610181
If specified, accepted downloads are downloaded into this directory. Otherwise, temporary directory is created and is
10147-
deleted when browser is closed.
10182+
deleted when browser is closed. In either case, the downloads are deleted when the browser context they were created in
10183+
is closed.
1014810184
slow_mo : Union[float, NoneType]
1014910185
Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
1015010186
traces_dir : Union[pathlib.Path, str, NoneType]
@@ -10283,7 +10319,8 @@ async def launch_persistent_context(
1028310319
Network proxy settings.
1028410320
downloads_path : Union[pathlib.Path, str, NoneType]
1028510321
If specified, accepted downloads are downloaded into this directory. Otherwise, temporary directory is created and is
10286-
deleted when browser is closed.
10322+
deleted when browser is closed. In either case, the downloads are deleted when the browser context they were created in
10323+
is closed.
1028710324
slow_mo : Union[float, NoneType]
1028810325
Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
1028910326
viewport : Union[{width: int, height: int}, NoneType]

playwright/sync_api/_context_manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def greenlet_main() -> None:
5656
dispatcher_fiber,
5757
create_remote_object,
5858
PipeTransport(loop, compute_driver_executable()),
59+
loop,
5960
)
6061

6162
g_self = greenlet.getcurrent()

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy