Skip to content

Commit c9846b7

Browse files
committed
backport ThreadedChildWatcher
1 parent de2b3d3 commit c9846b7

File tree

9 files changed

+414
-34
lines changed

9 files changed

+414
-34
lines changed

meta.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ requirements:
2626
- greenlet ==1.1.2
2727
- pyee ==8.1.0
2828
- websockets ==10.1
29-
- psutil ==5.9.0 # [py<38]
3029
- typing_extensions # [py<39]
3130
test:
3231
requires:

playwright/__main__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919

2020

2121
def main() -> None:
22-
driver_executable = compute_driver_executable()
22+
(driver_executable, cli_entrypoint) = compute_driver_executable()
2323
completed_process = subprocess.run(
24-
[str(driver_executable), *sys.argv[1:]], env=get_driver_env()
24+
[driver_executable, cli_entrypoint, *sys.argv[1:]], env=get_driver_env()
2525
)
2626
sys.exit(completed_process.returncode)
2727

playwright/_impl/_connection.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ async def init() -> None:
187187
def stop_sync(self) -> None:
188188
self._transport.request_stop()
189189
self._dispatcher_fiber.switch()
190+
self._loop.run_until_complete(self._transport.wait_until_stopped())
190191
self.cleanup()
191192

192193
async def stop_async(self) -> None:

playwright/_impl/_driver.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,17 @@
1717
import os
1818
import sys
1919
from pathlib import Path
20+
from typing import Tuple
2021

2122
import playwright
2223
from playwright._repo_version import version
2324

2425

25-
def compute_driver_executable() -> Path:
26+
def compute_driver_executable() -> Tuple[str, str]:
27+
node_binary = "node.exe" if sys.platform == "win32" else "node"
2628
package_path = Path(inspect.getfile(playwright)).parent
27-
platform = sys.platform
28-
if platform == "win32":
29-
return package_path / "driver" / "playwright.cmd"
30-
return package_path / "driver" / "playwright.sh"
29+
base_args = package_path / "driver" / "package" / "cli.js"
30+
return (str(package_path / "driver" / node_binary), str(base_args))
3131

3232

3333
if sys.version_info.major == 3 and sys.version_info.minor == 7:

playwright/_impl/_py37ThreadedChildWatcher.py

Lines changed: 377 additions & 0 deletions
Large diffs are not rendered by default.

playwright/_impl/_transport.py

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import subprocess
2020
import sys
2121
from abc import ABC, abstractmethod
22-
from pathlib import Path
2322
from typing import Callable, Dict, Optional, Union
2423

2524
import websockets
@@ -28,7 +27,7 @@
2827
from websockets.client import connect as websocket_connect
2928

3029
from playwright._impl._api_types import Error
31-
from playwright._impl._driver import get_driver_env
30+
from playwright._impl._driver import compute_driver_executable, get_driver_env
3231
from playwright._impl._helper import ParsedMessagePayload
3332

3433

@@ -96,12 +95,9 @@ def deserialize_message(self, data: Union[str, bytes]) -> ParsedMessagePayload:
9695

9796

9897
class PipeTransport(Transport):
99-
def __init__(
100-
self, loop: asyncio.AbstractEventLoop, driver_executable: Path
101-
) -> None:
98+
def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
10299
super().__init__(loop)
103100
self._stopped = False
104-
self._driver_executable = driver_executable
105101

106102
def request_stop(self) -> None:
107103
assert self._output
@@ -110,19 +106,6 @@ def request_stop(self) -> None:
110106

111107
async def wait_until_stopped(self) -> None:
112108
await self._stopped_future
113-
# In Python 3.7, self._proc.wait() hangs because it does not use ThreadedChildWatcher
114-
# which is used in Python 3.8+. Hence waiting for child process is skipped in Python 3.7.
115-
# See https://bugs.python.org/issue35621
116-
# See https://stackoverflow.com/questions/28915607/does-asyncio-support-running-a-subprocess-from-a-non-main-thread/28917653#28917653
117-
if sys.version_info >= (3, 8):
118-
await self._proc.wait()
119-
else:
120-
import psutil
121-
122-
try:
123-
psutil.Process(self._proc.pid).wait()
124-
except psutil.NoSuchProcess:
125-
pass
126109

127110
async def connect(self) -> None:
128111
self._stopped_future: asyncio.Future = asyncio.Future()
@@ -131,14 +114,27 @@ async def connect(self) -> None:
131114
if sys.platform == "win32" and sys.stdout is None:
132115
creationflags = subprocess.CREATE_NO_WINDOW
133116

117+
original_child_watcher = asyncio.get_child_watcher()
134118
try:
135119
# For pyinstaller
136120
env = get_driver_env()
137121
if getattr(sys, "frozen", False):
138122
env.setdefault("PLAYWRIGHT_BROWSERS_PATH", "0")
139123

124+
# In Python 3.7, self._proc.wait() hangs because it does not use ThreadedChildWatcher
125+
# which is used in Python 3.8+. Since it also cleans up zombie processes, we backported it.
126+
# See https://bugs.python.org/issue35621
127+
if sys.version_info[0] == 3 and sys.version_info[1] == 7:
128+
from ._py37ThreadedChildWatcher import ( # type: ignore
129+
ThreadedChildWatcher,
130+
)
131+
132+
watcher = ThreadedChildWatcher()
133+
asyncio.set_child_watcher(watcher)
134+
(driver_executable, cli_entrypoint) = compute_driver_executable()
140135
self._proc = await asyncio.create_subprocess_exec(
141-
str(self._driver_executable),
136+
str(driver_executable),
137+
cli_entrypoint,
142138
"run-driver",
143139
stdin=asyncio.subprocess.PIPE,
144140
stdout=asyncio.subprocess.PIPE,
@@ -150,6 +146,9 @@ async def connect(self) -> None:
150146
except Exception as exc:
151147
self.on_error_future.set_exception(exc)
152148
raise exc
149+
finally:
150+
if sys.version_info[0] == 3 and sys.version_info[1] == 7:
151+
asyncio.set_child_watcher(original_child_watcher)
153152

154153
self._output = self._proc.stdin
155154

@@ -159,22 +158,30 @@ async def run(self) -> None:
159158
while not self._stopped:
160159
try:
161160
buffer = await self._proc.stdout.readexactly(4)
161+
if self._stopped:
162+
break
162163
length = int.from_bytes(buffer, byteorder="little", signed=False)
163164
buffer = bytes(0)
164165
while length:
165166
to_read = min(length, 32768)
166167
data = await self._proc.stdout.readexactly(to_read)
168+
if self._stopped:
169+
break
167170
length -= to_read
168171
if len(buffer):
169172
buffer = buffer + data
170173
else:
171174
buffer = data
175+
if self._stopped:
176+
break
172177

173178
obj = self.deserialize_message(buffer)
174179
self.on_message(obj)
175180
except asyncio.IncompleteReadError:
176181
break
177182
await asyncio.sleep(0)
183+
184+
await self._proc.wait()
178185
self._stopped_future.set_result(None)
179186

180187
def send(self, message: Dict) -> None:

playwright/async_api/_context_manager.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from typing import Any
1717

1818
from playwright._impl._connection import Connection
19-
from playwright._impl._driver import compute_driver_executable
2019
from playwright._impl._object_factory import create_remote_object
2120
from playwright._impl._transport import PipeTransport
2221
from playwright.async_api._generated import Playwright as AsyncPlaywright
@@ -31,7 +30,7 @@ async def __aenter__(self) -> AsyncPlaywright:
3130
self._connection = Connection(
3231
None,
3332
create_remote_object,
34-
PipeTransport(loop, compute_driver_executable()),
33+
PipeTransport(loop),
3534
loop,
3635
)
3736
loop.create_task(self._connection.run())

playwright/sync_api/_context_manager.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
from playwright._impl._api_types import Error
2121
from playwright._impl._connection import Connection
22-
from playwright._impl._driver import compute_driver_executable
2322
from playwright._impl._object_factory import create_remote_object
2423
from playwright._impl._playwright import Playwright
2524
from playwright._impl._transport import PipeTransport
@@ -51,7 +50,7 @@ def greenlet_main() -> None:
5150
self._connection = Connection(
5251
dispatcher_fiber,
5352
create_remote_object,
54-
PipeTransport(self._loop, compute_driver_executable()),
53+
PipeTransport(self._loop),
5554
self._loop,
5655
)
5756

@@ -73,7 +72,6 @@ def start(self) -> SyncPlaywright:
7372

7473
def __exit__(self, *args: Any) -> None:
7574
self._connection.stop_sync()
76-
self._loop.run_until_complete(self._connection._transport.wait_until_stopped())
7775
if self._own_loop:
7876
self._loop.run_until_complete(self._loop.shutdown_asyncgens())
7977
self._loop.close()

setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,6 @@ def _download_and_extract_local_driver(
212212
"greenlet==1.1.2",
213213
"pyee==8.1.0",
214214
"typing-extensions;python_version<='3.8'",
215-
"psutil==5.9.0;python_version<='3.7'",
216215
],
217216
classifiers=[
218217
"Topic :: Software Development :: Testing",

0 commit comments

Comments
 (0)