Skip to content

Commit a267b4f

Browse files
committed
feat: WsPipeTransport
1 parent 6fa9500 commit a267b4f

File tree

8 files changed

+173
-25
lines changed

8 files changed

+173
-25
lines changed

playwright/_impl/_browser_type.py

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@
4444
async_readfile,
4545
locals_to_params,
4646
)
47-
from playwright._impl._json_pipe import JsonPipeTransport
4847
from playwright._impl._network import serialize_headers, to_client_certificates_protocol
4948
from playwright._impl._waiter import throw_on_timeout
49+
from playwright._impl._ws_pipe import WsPipeTransport
5050

5151
if TYPE_CHECKING:
5252
from playwright._impl._playwright import Playwright
@@ -224,21 +224,14 @@ async def connect(
224224
slowMo = 0
225225

226226
headers = {**(headers if headers else {}), "x-playwright-browser": self.name}
227-
local_utils = self._connection.local_utils
228-
pipe_channel = (
229-
await local_utils._channel.send_return_as_dict(
230-
"connect",
231-
None,
232-
{
233-
"wsEndpoint": wsEndpoint,
234-
"headers": headers,
235-
"slowMo": slowMo,
236-
"timeout": timeout if timeout is not None else 0,
237-
"exposeNetwork": exposeNetwork,
238-
},
239-
)
240-
)["pipe"]
241-
transport = JsonPipeTransport(self._connection._loop, pipe_channel)
227+
transport = WsPipeTransport(
228+
self._connection._loop,
229+
ws_endpoint=wsEndpoint,
230+
headers=headers,
231+
slow_mo=slowMo,
232+
timeout=timeout if timeout is not None else 0,
233+
expose_network=exposeNetwork,
234+
)
242235

243236
connection = Connection(
244237
self._connection._dispatcher_fiber,

playwright/_impl/_connection.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,6 @@ def _send_message_to_server(
367367
getattr(task, "__pw_stack_trace__", traceback.extract_stack(limit=10)),
368368
)
369369
callback.no_reply = no_reply
370-
self._callbacks[id] = callback
371370
stack_trace_information = cast(ParsedStackTrace, self._api_zone.get())
372371
frames = stack_trace_information.get("frames", [])
373372
location = (
@@ -399,8 +398,8 @@ def _send_message_to_server(
399398
if self._tracing_count > 0 and frames and object._guid != "localUtils":
400399
self.local_utils.add_stack_to_tracing_no_reply(id, frames)
401400

402-
self._transport.send(message)
403401
self._callbacks[id] = callback
402+
self._transport.send(message)
404403

405404
return callback
406405

playwright/_impl/_fake_pipe.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import asyncio
2+
from pathlib import Path
3+
from typing import Dict
4+
5+
from playwright._impl._driver import compute_driver_executable, get_driver_env
6+
from playwright._impl._transport import Transport
7+
8+
9+
class FakePipeTransport(Transport):
10+
def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
11+
super().__init__(loop)
12+
13+
def request_stop(self) -> None:
14+
self._stopped_future.set_result(None)
15+
16+
async def wait_until_stopped(self) -> None:
17+
await self._stopped_future
18+
19+
async def connect(self) -> None:
20+
self._stopped_future = asyncio.Future()
21+
22+
async def run(self) -> None:
23+
await self._stopped_future
24+
25+
def send(self, message: Dict) -> None:
26+
if message["method"] != "initialize":
27+
try:
28+
for path in compute_driver_executable():
29+
Path(path).stat()
30+
except FileNotFoundError as e:
31+
return self.on_error_future.set_exception(e)
32+
33+
return self.on_error_future.set_exception(FileNotFoundError)
34+
35+
for type_, initializer, guid in [
36+
(
37+
"BrowserType",
38+
{"executablePath": "", "name": name},
39+
f"browser-type@{name}",
40+
)
41+
for name in ["chromium", "firefox", "webkit"]
42+
] + [
43+
("LocalUtils", {"deviceDescriptors": []}, "localUtils"),
44+
(
45+
"Playwright",
46+
{
47+
name: {"guid": f"browser-type@{name}"}
48+
for name in ["chromium", "firefox", "webkit"]
49+
}
50+
| {
51+
"utils": {"guid": "localUtils"},
52+
},
53+
"Playwright",
54+
),
55+
]:
56+
self.on_message(
57+
{
58+
"guid": "",
59+
"method": "__create__",
60+
"params": {
61+
"type": type_,
62+
"initializer": initializer,
63+
"guid": guid,
64+
},
65+
}
66+
)
67+
68+
self.on_message(
69+
{"id": message["id"], "result": {"playwright": {"guid": "Playwright"}}}
70+
)
71+

playwright/_impl/_ws_pipe.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import asyncio
2+
from typing import Dict
3+
4+
from pyee.asyncio import AsyncIOEventEmitter
5+
from websockets import ConnectionClosed, connect
6+
7+
from playwright._impl._transport import Transport
8+
9+
10+
class WsPipeTransport(AsyncIOEventEmitter, Transport):
11+
def __init__(
12+
self,
13+
loop: asyncio.AbstractEventLoop,
14+
*,
15+
ws_endpoint: str,
16+
timeout: float = None,
17+
slow_mo: float = None,
18+
headers: Dict[str, str] = None,
19+
expose_network: str = None,
20+
) -> None:
21+
super().__init__(loop)
22+
Transport.__init__(self, loop)
23+
self._stop_event = asyncio.Event()
24+
25+
self._ws_endpoint = ws_endpoint
26+
self._timeout = timeout
27+
self._slow_mo = slow_mo
28+
self._headers = headers
29+
self._expose_network = expose_network
30+
31+
def request_stop(self) -> None:
32+
self._stop_event.set()
33+
34+
def dispose(self) -> None:
35+
self.on_error_future.cancel()
36+
37+
async def wait_until_stopped(self) -> None:
38+
await self._ws.wait_closed()
39+
40+
async def connect(self) -> None:
41+
try:
42+
self._ws = await connect(
43+
self._ws_endpoint, additional_headers=self._headers, max_size=None
44+
)
45+
except Exception as e:
46+
self.on_error_future.set_exception(e)
47+
raise e
48+
49+
async def run(self) -> None:
50+
wait = asyncio.create_task(self._stop_event.wait())
51+
52+
while not self._stop_event.is_set():
53+
recv = asyncio.create_task(self._ws.recv(False))
54+
await asyncio.wait([recv, wait], return_when=asyncio.FIRST_COMPLETED)
55+
56+
if self._stop_event.is_set():
57+
recv.cancel()
58+
break
59+
60+
try:
61+
self.on_message(self.deserialize_message(recv.result()))
62+
except ConnectionClosed:
63+
if not self._stop_event.is_set():
64+
self.on_error_future.set_exception(
65+
Exception("Connection closed while reading from the driver")
66+
)
67+
break
68+
69+
wait.cancel()
70+
self.emit("close", "")
71+
await self._ws.close()
72+
73+
def send(self, message: Dict) -> None:
74+
self._ws.protocol.send_binary(self.serialize_message(message))
75+
self._ws.send_data()

playwright/async_api/_context_manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
from typing import Any
1717

1818
from playwright._impl._connection import Connection
19+
from playwright._impl._fake_pipe import FakePipeTransport
1920
from playwright._impl._object_factory import create_remote_object
20-
from playwright._impl._transport import PipeTransport
2121
from playwright.async_api._generated import Playwright as AsyncPlaywright
2222

2323

@@ -31,7 +31,7 @@ async def __aenter__(self) -> AsyncPlaywright:
3131
self._connection = Connection(
3232
None,
3333
create_remote_object,
34-
PipeTransport(loop),
34+
FakePipeTransport(loop),
3535
loop,
3636
)
3737
loop.create_task(self._connection.run())

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ requires-python = ">=3.9"
1717
# - uv pip compile pyproject.toml -o requirements.txt
1818
dependencies = [
1919
"pyee>=13,<14",
20-
"greenlet>=3.1.1,<4.0.0"
20+
"greenlet>=3.1.1,<4.0.0",
21+
"websockets>=14,<16"
2122
]
2223
classifiers = [
2324
"Topic :: Software Development :: Testing",

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ pyee==13.0.0
66
# via playwright (pyproject.toml)
77
typing-extensions==4.14.1
88
# via pyee
9+
websockets==15.0.1
10+
# via playwright (pyproject.toml)

setup.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@
7272
"platform": "win32",
7373
"zip_name": "win32_arm64",
7474
},
75+
{
76+
"wheel": "any.whl",
77+
"machine": platform.machine().lower(),
78+
"platform": sys.platform,
79+
"zip_name": "",
80+
},
7581
]
7682

7783
if len(sys.argv) == 2 and sys.argv[1] == "--list-wheels":
@@ -120,9 +126,6 @@ def download_driver(zip_name: str) -> None:
120126
class PlaywrightBDistWheelCommand(BDistWheelCommand):
121127
def run(self) -> None:
122128
super().run()
123-
os.makedirs("driver", exist_ok=True)
124-
os.makedirs("playwright/driver", exist_ok=True)
125-
self._download_and_extract_local_driver()
126129

127130
wheel = None
128131
if os.getenv("PLAYWRIGHT_TARGET_WHEEL", None):
@@ -142,7 +145,11 @@ def run(self) -> None:
142145
)
143146
)[0]
144147
assert wheel
145-
self._build_wheel(wheel)
148+
if wheel["zip_name"]:
149+
os.makedirs("driver", exist_ok=True)
150+
os.makedirs("playwright/driver", exist_ok=True)
151+
self._download_and_extract_local_driver()
152+
self._build_wheel(wheel)
146153

147154
def _build_wheel(
148155
self,

0 commit comments

Comments
 (0)