1919import subprocess
2020import sys
2121from abc import ABC , abstractmethod
22- from pathlib import Path
2322from typing import Callable , Dict , Optional , Union
2423
2524import websockets
2827from websockets .client import connect as websocket_connect
2928
3029from 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
3231from playwright ._impl ._helper import ParsedMessagePayload
3332
3433
@@ -96,12 +95,9 @@ def deserialize_message(self, data: Union[str, bytes]) -> ParsedMessagePayload:
9695
9796
9897class 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 :
0 commit comments