Skip to content

Commit 82885a1

Browse files
authored
fix(transport): handle connection error correctly (#650) (#651)
1 parent 5d976dc commit 82885a1

File tree

5 files changed

+86
-16
lines changed

5 files changed

+86
-16
lines changed

playwright/_impl/_browser_type.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import asyncio
1516
from pathlib import Path
1617
from typing import Dict, List, Optional, Union, cast
1718

@@ -179,11 +180,24 @@ async def connect(
179180
self._connection._object_factory,
180181
transport,
181182
)
183+
await connection._transport.start()
182184
connection._is_sync = self._connection._is_sync
183185
connection._loop = self._connection._loop
184186
connection._loop.create_task(connection.run())
187+
obj = asyncio.create_task(
188+
connection.wait_for_object_with_known_name("Playwright")
189+
)
190+
done, pending = await asyncio.wait(
191+
{
192+
obj,
193+
connection._transport.on_error_future, # type: ignore
194+
},
195+
return_when=asyncio.FIRST_COMPLETED,
196+
)
197+
if not obj.done():
198+
obj.cancel()
199+
playwright = next(iter(done)).result()
185200
self._connection._child_ws_connections.append(connection)
186-
playwright = await connection.wait_for_object_with_known_name("Playwright")
187201
pre_launched_browser = playwright._initializer.get("preLaunchedBrowser")
188202
assert pre_launched_browser
189203
browser = cast(Browser, from_channel(pre_launched_browser))

playwright/_impl/_transport.py

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def _get_stderr_fileno() -> Optional[int]:
4242

4343
class Transport(ABC):
4444
def __init__(self) -> None:
45+
self.on_error_future: asyncio.Future
4546
self.on_message = lambda _: None
4647

4748
@abstractmethod
@@ -55,9 +56,14 @@ def dispose(self) -> None:
5556
async def wait_until_stopped(self) -> None:
5657
pass
5758

58-
async def run(self) -> None:
59+
async def start(self) -> None:
60+
if not hasattr(self, "on_error_future"):
61+
self.on_error_future = asyncio.Future()
5962
self._loop = asyncio.get_running_loop()
60-
self.on_error_future: asyncio.Future = asyncio.Future()
63+
64+
@abstractmethod
65+
async def run(self) -> None:
66+
pass
6167

6268
@abstractmethod
6369
def send(self, message: Dict) -> None:
@@ -93,17 +99,28 @@ async def wait_until_stopped(self) -> None:
9399
await self._proc.wait()
94100

95101
async def run(self) -> None:
96-
await super().run()
102+
await self.start()
97103
self._stopped_future: asyncio.Future = asyncio.Future()
98104

99-
self._proc = proc = await asyncio.create_subprocess_exec(
100-
str(self._driver_executable),
101-
"run-driver",
102-
stdin=asyncio.subprocess.PIPE,
103-
stdout=asyncio.subprocess.PIPE,
104-
stderr=_get_stderr_fileno(),
105-
limit=32768,
106-
)
105+
try:
106+
self._proc = proc = await asyncio.create_subprocess_exec(
107+
str(self._driver_executable),
108+
"run-driver",
109+
stdin=asyncio.subprocess.PIPE,
110+
stdout=asyncio.subprocess.PIPE,
111+
stderr=_get_stderr_fileno(),
112+
limit=32768,
113+
)
114+
except FileNotFoundError:
115+
self.on_error_future.set_exception(
116+
Error(
117+
"playwright's driver is not found, You can read the contributing guide "
118+
"for some guidance on how to get everything setup for working on the code "
119+
"https://github.com/microsoft/playwright-python/blob/master/CONTRIBUTING.md"
120+
)
121+
)
122+
return
123+
107124
assert proc.stdout
108125
assert proc.stdin
109126
self._output = proc.stdin
@@ -160,15 +177,22 @@ async def wait_until_stopped(self) -> None:
160177
await self._connection.wait_closed()
161178

162179
async def run(self) -> None:
163-
await super().run()
180+
await self.start()
164181

165182
options: Dict[str, Any] = {}
166183
if self.timeout is not None:
167184
options["close_timeout"] = self.timeout / 1000
168185
options["ping_timeout"] = self.timeout / 1000
186+
169187
if self.headers is not None:
170188
options["extra_headers"] = self.headers
171-
self._connection = await websockets.connect(self.ws_endpoint, **options)
189+
try:
190+
self._connection = await websockets.connect(self.ws_endpoint, **options)
191+
except Exception as err:
192+
self.on_error_future.set_exception(
193+
Error(f"playwright's websocket endpoint connection error: {err}")
194+
)
195+
return
172196

173197
while not self._stopped:
174198
try:

playwright/async_api/_context_manager.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,22 @@ async def __aenter__(self) -> AsyncPlaywright:
3232
)
3333
loop = asyncio.get_running_loop()
3434
self._connection._loop = loop
35+
obj = asyncio.create_task(
36+
self._connection.wait_for_object_with_known_name("Playwright")
37+
)
38+
await self._connection._transport.start()
3539
loop.create_task(self._connection.run())
36-
playwright = AsyncPlaywright(
37-
await self._connection.wait_for_object_with_known_name("Playwright")
40+
done, pending = await asyncio.wait(
41+
{
42+
obj,
43+
self._connection._transport.on_error_future, # type: ignore
44+
},
45+
return_when=asyncio.FIRST_COMPLETED,
3846
)
47+
if not obj.done():
48+
obj.cancel()
49+
obj = next(iter(done)).result()
50+
playwright = AsyncPlaywright(obj) # type: ignore
3951
playwright.stop = self.__aexit__ # type: ignore
4052
return playwright
4153

tests/async/test_browsertype_connect.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,13 @@ async def test_prevent_getting_video_path(
182182
== "Path is not available when using browserType.connect(). Use save_as() to save a local copy."
183183
)
184184
remote_server.kill()
185+
186+
187+
async def test_connect_to_closed_server_without_hangs(
188+
browser_type: BrowserType, launch_server
189+
):
190+
remote_server = launch_server()
191+
remote_server.kill()
192+
with pytest.raises(Error) as exc:
193+
await browser_type.connect(remote_server.ws_endpoint)
194+
assert "playwright's websocket endpoint connection error" in exc.value.message

tests/sync/test_browsertype_connect.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,13 @@ def test_browser_type_connect_should_forward_close_events_to_pages(
145145
assert events == ["page::close", "context::close", "browser::disconnected"]
146146
remote.kill()
147147
assert events == ["page::close", "context::close", "browser::disconnected"]
148+
149+
150+
def test_connect_to_closed_server_without_hangs(
151+
browser_type: BrowserType, launch_server
152+
):
153+
remote_server = launch_server()
154+
remote_server.kill()
155+
with pytest.raises(Error) as exc:
156+
browser_type.connect(remote_server.ws_endpoint)
157+
assert "playwright's websocket endpoint connection error" in exc.value.message

0 commit comments

Comments
 (0)