Skip to content

Commit 3254f6e

Browse files
authored
Merge pull request #292 from vpython/fix/281-frontend-connect-timeout
Fail loudly instead of hanging forever when no frontend connects (#281)
2 parents a9a6bd4 + 5774de4 commit 3254f6e

3 files changed

Lines changed: 139 additions & 2 deletions

File tree

‎vpython/_frontend_wait.py‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Bounded wait for the browser-side (glowcomm) frontend to connect.
2+
3+
Historically `import vpython` in a notebook busy-waited forever on the
4+
websocket handshake. In any frontend that does not run vpython's injected
5+
JavaScript — VS Code notebooks and Google Colab are the common ones — that
6+
loop never exits and the kernel appears to hang (issue #281). A loud, prompt
7+
error beats a silent forever.
8+
9+
Injected clock/sleep/environ keep this unit-testable without a kernel.
10+
"""
11+
import os
12+
import time
13+
14+
DEFAULT_TIMEOUT = 30.0 # seconds; generous for slow nbextension first-runs
15+
_POLL = 0.1
16+
17+
18+
def _timeout_from(environ):
19+
raw = environ.get('VPYTHON_CONNECT_TIMEOUT')
20+
if raw:
21+
try:
22+
value = float(raw)
23+
if value > 0:
24+
return value
25+
except ValueError:
26+
pass
27+
return DEFAULT_TIMEOUT
28+
29+
30+
def _environment_note(environ):
31+
if 'VSCODE_PID' in environ or 'VSCODE_CWD' in environ:
32+
return ("It looks like this kernel was started by VS Code. VS Code's "
33+
"notebook UI does not run vpython's frontend JavaScript, so "
34+
"VPython cannot display there yet.")
35+
if 'COLAB_RELEASE_TAG' in environ or 'COLAB_GPU' in environ:
36+
return ("It looks like this is Google Colab. Colab's output sandbox "
37+
"does not run vpython's frontend JavaScript, so VPython "
38+
"cannot display there yet.")
39+
return ("The notebook frontend never ran vpython's JavaScript. This "
40+
"happens in notebook UIs that do not support Jupyter "
41+
"nbextensions/labextensions.")
42+
43+
44+
def wait_for_frontend(is_connected, environ=None, _time=time.time,
45+
_sleep=time.sleep):
46+
"""Poll `is_connected()` until true, or raise RuntimeError on timeout."""
47+
environ = os.environ if environ is None else environ
48+
timeout = _timeout_from(environ)
49+
deadline = _time() + timeout
50+
while not is_connected():
51+
if _time() >= deadline:
52+
raise RuntimeError(
53+
"no VPython frontend connected after {:.0f}s.\n{}\n"
54+
"VPython works in Jupyter Notebook and JupyterLab opened in a "
55+
"web browser. VS Code support is tracked at "
56+
"https://github.com/vpython/vpython-jupyter/issues/281 .\n"
57+
"(If your environment is just slow to start, raise the limit "
58+
"with the VPYTHON_CONNECT_TIMEOUT environment variable.)"
59+
.format(timeout, _environment_note(environ)))
60+
_sleep(_POLL)

‎vpython/test/test_frontend_wait.py‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""The frontend-connect wait must never hang forever (issue #281).
2+
3+
Pure-python tests: the helper takes its clock, sleeper, and environment as
4+
parameters, so no kernel, websocket, or notebook is involved.
5+
"""
6+
import pytest
7+
8+
from vpython._frontend_wait import wait_for_frontend, DEFAULT_TIMEOUT
9+
10+
11+
class FakeClock:
12+
def __init__(self):
13+
self.now = 0.0
14+
15+
def time(self):
16+
return self.now
17+
18+
def sleep(self, dt):
19+
self.now += dt
20+
21+
22+
def test_returns_as_soon_as_the_frontend_connects():
23+
clock = FakeClock()
24+
flips_at = 1.0
25+
wait_for_frontend(lambda: clock.now >= flips_at,
26+
environ={}, _time=clock.time, _sleep=clock.sleep)
27+
assert clock.now < flips_at + 1.0 # returned promptly, no full-timeout burn
28+
29+
30+
def test_raises_instead_of_hanging_when_nothing_ever_connects():
31+
clock = FakeClock()
32+
with pytest.raises(RuntimeError) as exc:
33+
wait_for_frontend(lambda: False,
34+
environ={}, _time=clock.time, _sleep=clock.sleep)
35+
msg = str(exc.value)
36+
assert 'no VPython frontend connected' in msg
37+
assert 'vpython-jupyter/issues/281' in msg
38+
assert 'Jupyter' in msg # points at an environment that works
39+
40+
41+
def test_message_names_vscode_when_running_under_vscode():
42+
clock = FakeClock()
43+
with pytest.raises(RuntimeError) as exc:
44+
wait_for_frontend(lambda: False,
45+
environ={'VSCODE_PID': '123'},
46+
_time=clock.time, _sleep=clock.sleep)
47+
assert 'VS Code' in str(exc.value)
48+
49+
50+
def test_message_names_colab_when_running_under_colab():
51+
clock = FakeClock()
52+
with pytest.raises(RuntimeError) as exc:
53+
wait_for_frontend(lambda: False,
54+
environ={'COLAB_RELEASE_TAG': 'x'},
55+
_time=clock.time, _sleep=clock.sleep)
56+
assert 'Colab' in str(exc.value)
57+
58+
59+
def test_timeout_is_overridable_via_environment():
60+
clock = FakeClock()
61+
with pytest.raises(RuntimeError):
62+
wait_for_frontend(lambda: False,
63+
environ={'VPYTHON_CONNECT_TIMEOUT': '2'},
64+
_time=clock.time, _sleep=clock.sleep)
65+
assert clock.now < DEFAULT_TIMEOUT # honored the shorter override
66+
67+
68+
def test_bogus_override_falls_back_to_default():
69+
clock = FakeClock()
70+
with pytest.raises(RuntimeError):
71+
wait_for_frontend(lambda: False,
72+
environ={'VPYTHON_CONNECT_TIMEOUT': 'soon'},
73+
_time=clock.time, _sleep=clock.sleep)
74+
assert clock.now >= DEFAULT_TIMEOUT

‎vpython/with_notebook.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,11 @@ def start_server():
150150
t.start()
151151
# Setup Comm Channel and websocket
152152
baseObj.glow = GlowWidget(wsport=__SOCKET_PORT, wsuri='/ws')
153-
while (not wsConnected):
154-
time.sleep(0.1) # wait for websocket to connect
153+
# Bounded wait (issue #281): in frontends that never run vpython's injected
154+
# JavaScript — VS Code notebooks, Colab — the old unbounded loop hung the
155+
# kernel forever inside `import vpython`. Fail loudly instead.
156+
from ._frontend_wait import wait_for_frontend
157+
wait_for_frontend(lambda: wsConnected)
155158

156159
baseObj.trigger() # start the trigger ping-pong process
157160

0 commit comments

Comments
 (0)