Skip to content

Commit 917ed9b

Browse files
authored
Adding config.disable_asyncio_subprocess to allow to use WindowsSelectorEventLoopPolicy on Windows (#117)
* feat: adding disable_asyncio_subprocess to use subprocess.Popen instead (for better support on Windows) * test and document with WindowsSelectorEventLoopPolicy * update changelogs * disable_asyncio_subprocess defaults to True on Windows * using subprocess.Popen for all platforms * apply format
1 parent 85314d2 commit 917ed9b

File tree

5 files changed

+50
-21
lines changed

5 files changed

+50
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

1212
- Fixing tests so that they can run on Windows (and still run on Linux like before) @nathanfallet
13+
- Disable asyncio subprocesses for better compatibility on Windows @nathanfallet
1314
- Adding a missing Chrome Canary path for Windows @nathanfallet
1415
- Adding a flag to re-enable `--load-extension` (disabled by default in Chrome 136+) @nathanfallet
1516

docs/quickstart.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,16 @@ config.user_data_dir="/path/to/existing/profile", # by specifying it, it won't
9393
config.browser_executable_path="/path/to/some/other/browser",
9494
config.browser_args=['--some-browser-arg=true', '--some-other-option'],
9595
config.lang="en-US" # this could set iso-language-code in navigator, not recommended to change
96-
)
96+
```
97+
98+
On Windows, we recommend using `WindowsSelectorEventLoopPolicy` for better compatibility with asyncio:
99+
100+
```python
101+
import asyncio
102+
import sys
103+
104+
if sys.platform == "win32":
105+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
97106
```
98107

99108
A more concrete example, which can be found in the ./example/ folder,

tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import asyncio
12
import logging
23
import os
34
import signal
5+
import sys
46
from contextlib import AbstractAsyncContextManager
57
from enum import Enum
68
from threading import Event
@@ -83,6 +85,9 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
8385

8486
@pytest.fixture
8587
def create_browser() -> type[CreateBrowser]:
88+
if sys.platform == "win32":
89+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) # type: ignore
90+
8691
return CreateBrowser
8792

8893

zendriver/core/browser.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import pickle
1111
import re
1212
import shutil
13+
import subprocess
1314
import urllib.parse
1415
import urllib.request
1516
import warnings
@@ -54,7 +55,7 @@ class Browser:
5455
5556
"""
5657

57-
_process: asyncio.subprocess.Process | None
58+
_process: subprocess.Popen | None
5859
_process_pid: int | None
5960
_http: HTTPApi | None = None
6061
_cookies: CookieJar | None = None
@@ -354,18 +355,7 @@ async def start(self) -> Browser:
354355
"starting\n\texecutable :%s\n\narguments:\n%s", exe, "\n\t".join(params)
355356
)
356357
if not connect_existing:
357-
self._process: asyncio.subprocess.Process = (
358-
await asyncio.create_subprocess_exec(
359-
# self.config.browser_executable_path,
360-
# *cmdparams,
361-
exe,
362-
*params,
363-
stdin=asyncio.subprocess.PIPE,
364-
stdout=asyncio.subprocess.PIPE,
365-
stderr=asyncio.subprocess.PIPE,
366-
close_fds=is_posix,
367-
)
368-
)
358+
self._process = util._start_process(exe, params, is_posix)
369359
self._process_pid = self._process.pid
370360

371361
self._http = HTTPApi((self.config.host, self.config.port))
@@ -619,7 +609,7 @@ async def stop(self):
619609
self._process.kill()
620610
logger.debug("killed browser process")
621611

622-
await self._process.wait()
612+
await asyncio.to_thread(self._process.wait)
623613

624614
except ProcessLookupError:
625615
# ignore this well known race condition because it only means that

zendriver/core/util.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import asyncio
44
import logging
5+
import subprocess
56
import types
67
import typing
8+
from pathlib import Path
79
from typing import Any, Callable, List, Optional, Set, Union
810

911
from deprecated import deprecated
@@ -328,19 +330,41 @@ def cdp_get_module(domain: Union[str, types.ModuleType]):
328330
return domain_mod
329331

330332

331-
async def _read_process_stderr(
332-
process: asyncio.subprocess.Process, n: int = 2**16
333-
) -> str:
333+
def _start_process(
334+
exe: str | Path, params: List[str], is_posix: bool
335+
) -> subprocess.Popen:
336+
"""
337+
Start a subprocess with the given executable and parameters.
338+
339+
:param exe: The executable to run.
340+
:param params: List of parameters to pass to the executable.
341+
:param is_posix: Boolean indicating if the system is POSIX compliant.
342+
343+
:return: An instance of `subprocess.Popen`.
344+
"""
345+
return subprocess.Popen(
346+
[str(exe)] + params,
347+
stdin=subprocess.PIPE,
348+
stdout=subprocess.PIPE,
349+
stderr=subprocess.PIPE,
350+
close_fds=is_posix,
351+
)
352+
353+
354+
async def _read_process_stderr(process: subprocess.Popen, n: int = 2**16) -> str:
334355
"""
335356
Read the given number of bytes from the stderr of the given process.
336357
337358
Read bytes are automatically decoded to utf-8.
338359
"""
339-
if process.stderr is None:
340-
raise ValueError("Process has no stderr")
360+
361+
async def read_stderr() -> bytes:
362+
if process.stderr is None:
363+
raise ValueError("Process has no stderr")
364+
return await asyncio.to_thread(process.stderr.read, n)
341365

342366
try:
343-
return (await asyncio.wait_for(process.stderr.read(n), 0.25)).decode("utf-8")
367+
return (await asyncio.wait_for(read_stderr(), 0.25)).decode("utf-8")
344368
except asyncio.TimeoutError:
345369
logger.debug("Timeout reading process stderr")
346370
return ""

0 commit comments

Comments
 (0)