Skip to content

Commit 33eee2f

Browse files
sspickleclaude
andcommitted
colab: transport probe notebook (comms shim vs proxyPort websocket)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
1 parent f0993e3 commit 33eee2f

1 file changed

Lines changed: 179 additions & 0 deletions

File tree

‎colab/colab-probe.ipynb‎

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# VPython Colab transport probe\n",
8+
"\n",
9+
"Run the cells **in order, one at a time**. Each JS probe prints its verdict\n",
10+
"directly into its output. What we're testing:\n",
11+
"\n",
12+
"1. Output-frame environment: does our JS run, can it load scripts from\n",
13+
" jsDelivr (how glow would ship — no extension, no nbextensions)?\n",
14+
"2. **Comms path**: `google.colab.kernel.comms` round-trip kernel ↔ output\n",
15+
" (the ipywidgets mechanism — if this works, Colab is a comm-only\n",
16+
" frontend, the mirror image of VS Code's ws-only one).\n",
17+
"3. **proxyPort path**: can an output frame open a WebSocket to a tornado\n",
18+
" server in the kernel via Colab's port proxy? (Folklore says no —\n",
19+
" let's get data.)"
20+
]
21+
},
22+
{
23+
"cell_type": "code",
24+
"execution_count": null,
25+
"metadata": {},
26+
"outputs": [],
27+
"source": [
28+
"# Cell 1: environment + jsDelivr + register the comm target (JS side)\n",
29+
"from IPython.display import display, HTML\n",
30+
"display(HTML('''\n",
31+
"<div id=\"probe1\" style=\"font-family:monospace;white-space:pre-wrap;border:1px solid #888;padding:6px\"></div>\n",
32+
"<script>\n",
33+
"(function(){\n",
34+
" var out = document.getElementById('probe1');\n",
35+
" function log(m){ out.textContent += m + '\\\\n'; }\n",
36+
" log('probe v1 — JS is running in the output frame');\n",
37+
" var g = (typeof google !== 'undefined') ? google : null;\n",
38+
" log('google.colab: ' + !!(g && g.colab));\n",
39+
" log('kernel.comms: ' + !!(g && g.colab && g.colab.kernel && g.colab.kernel.comms));\n",
40+
" log('kernel.proxyPort: ' + !!(g && g.colab && g.colab.kernel && typeof g.colab.kernel.proxyPort === 'function'));\n",
41+
" var s = document.createElement('script');\n",
42+
" s.src = 'https://cdn.jsdelivr.net/gh/vpython/vscode-vpython@main/media/jquery.min.js';\n",
43+
" s.onload = function(){ log('jsDelivr script load: OK (jQuery ' + (window.jQuery ? jQuery.fn.jquery : '?') + ')'); };\n",
44+
" s.onerror = function(){ log('jsDelivr script load: FAILED (CSP blocks external scripts?)'); };\n",
45+
" document.head.appendChild(s);\n",
46+
" if (g && g.colab && g.colab.kernel && g.colab.kernel.comms) {\n",
47+
" try {\n",
48+
" g.colab.kernel.comms.registerTarget('vpython-probe', function(comm, openMsg){\n",
49+
" log('comm OPEN from kernel; open data: ' + JSON.stringify((openMsg && openMsg.data) || openMsg || {}));\n",
50+
" log('comm object keys: ' + Object.keys(comm).join(', '));\n",
51+
" try { comm.send({echo: 'js-received-open', t: Date.now()}); log('comm.send back: called OK'); }\n",
52+
" catch(e){ log('comm.send threw: ' + e); }\n",
53+
" // API shape unknown in advance — try both message-receipt styles and\n",
54+
" // report which one this Colab build actually has.\n",
55+
" if (comm.on_message) {\n",
56+
" log('receive style: comm.on_message (callback)');\n",
57+
" comm.on_message(function(msg){ log('JS got message: ' + JSON.stringify(msg && msg.data || msg)); comm.send({echo2: 'roundtrip', got: (msg && msg.data) || null}); });\n",
58+
" } else if (comm.messages) {\n",
59+
" log('receive style: comm.messages (async iterator)');\n",
60+
" (async function(){\n",
61+
" try { for await (var msg of comm.messages) { log('JS got message: ' + JSON.stringify(msg && msg.data || msg)); comm.send({echo2: 'roundtrip', got: (msg && msg.data) || null}); } }\n",
62+
" catch(e){ log('messages iterator ended/threw: ' + e); }\n",
63+
" })();\n",
64+
" } else { log('NO known receive mechanism on comm object'); }\n",
65+
" });\n",
66+
" log('registerTarget(vpython-probe): OK — now run cell 2');\n",
67+
" } catch(e){ log('registerTarget threw: ' + e); }\n",
68+
" }\n",
69+
"})();\n",
70+
"</script>'''))"
71+
]
72+
},
73+
{
74+
"cell_type": "code",
75+
"execution_count": null,
76+
"metadata": {},
77+
"outputs": [],
78+
"source": [
79+
"# Cell 2: kernel side — open the comm (watch cell 1's box react)\n",
80+
"from ipykernel.comm import Comm\n",
81+
"probe_got = []\n",
82+
"probe_comm = Comm(target_name='vpython-probe', data={'hello': 'from-kernel'})\n",
83+
"probe_comm.on_msg(lambda m: probe_got.append(m['content']['data']))\n",
84+
"print('comm opened; JS should have logged the OPEN. Now run cell 3.')"
85+
]
86+
},
87+
{
88+
"cell_type": "code",
89+
"execution_count": null,
90+
"metadata": {},
91+
"outputs": [],
92+
"source": [
93+
"# Cell 3: did the kernel receive the JS echo? Then send one downlink message.\n",
94+
"print('kernel received so far:', probe_got)\n",
95+
"probe_comm.send({'ping': 'kernel-to-js'})\n",
96+
"print('sent ping — cell 1 box should log it; run this cell AGAIN to see echo2 arrive')"
97+
]
98+
},
99+
{
100+
"cell_type": "code",
101+
"execution_count": null,
102+
"metadata": {},
103+
"outputs": [],
104+
"source": [
105+
"# Cell 4: proxyPort probe, kernel side — tornado ws echo server\n",
106+
"import socket, json, threading, asyncio\n",
107+
"import tornado.ioloop, tornado.web, tornado.websocket, tornado.httpserver\n",
108+
"\n",
109+
"def _free_port():\n",
110+
" s = socket.socket(); s.bind(('', 0))\n",
111+
" return s.getsockname()[1]\n",
112+
"\n",
113+
"WS_PORT = _free_port()\n",
114+
"\n",
115+
"class _Echo(tornado.websocket.WebSocketHandler):\n",
116+
" def on_message(self, message):\n",
117+
" self.write_message(json.dumps({'echo': json.loads(message), 'from': 'kernel-tornado'}))\n",
118+
" def check_origin(self, origin):\n",
119+
" return True\n",
120+
"\n",
121+
"def _serve():\n",
122+
" asyncio.set_event_loop(asyncio.new_event_loop())\n",
123+
" app = tornado.web.Application([(r'/ws', _Echo)])\n",
124+
" tornado.httpserver.HTTPServer(app).listen(WS_PORT)\n",
125+
" tornado.ioloop.IOLoop.instance().start()\n",
126+
"\n",
127+
"threading.Thread(target=_serve, daemon=True).start()\n",
128+
"print('tornado ws echo on port', WS_PORT, '— now run cell 5')"
129+
]
130+
},
131+
{
132+
"cell_type": "code",
133+
"execution_count": null,
134+
"metadata": {},
135+
"outputs": [],
136+
"source": [
137+
"# Cell 5: proxyPort probe, JS side\n",
138+
"from IPython.display import display, HTML\n",
139+
"display(HTML('''\n",
140+
"<div id=\"probe5\" style=\"font-family:monospace;white-space:pre-wrap;border:1px solid #888;padding:6px\"></div>\n",
141+
"<script>\n",
142+
"(async function(){\n",
143+
" var out = document.getElementById('probe5');\n",
144+
" function log(m){ out.textContent += m + '\\\\n'; }\n",
145+
" var PORT = ''' + str(WS_PORT) + ''';\n",
146+
" try {\n",
147+
" var base = await google.colab.kernel.proxyPort(PORT, {cache: false});\n",
148+
" log('proxyPort URL: ' + base);\n",
149+
" var wsurl = base.replace(/^http/, 'ws') + 'ws';\n",
150+
" log('trying WebSocket: ' + wsurl);\n",
151+
" var opened = false;\n",
152+
" var ws = new WebSocket(wsurl);\n",
153+
" ws.onopen = function(){ opened = true; log('WS OPEN ✅ — sending ping'); ws.send(JSON.stringify({hello:'via-proxy'})); };\n",
154+
" ws.onmessage = function(ev){ log('WS ECHO ✅: ' + ev.data); ws.close(); };\n",
155+
" ws.onerror = function(){ log('WS error event' + (opened ? ' (after open)' : ' — likely proxy rejects websockets')); };\n",
156+
" ws.onclose = function(ev){ log('WS closed, code ' + ev.code); };\n",
157+
" setTimeout(function(){ if (!opened) log('no WS connection after 8s ❌'); }, 8000);\n",
158+
" // control: does plain HTTP work through the same proxy? (isolates ws vs proxy)\n",
159+
" fetch(base + 'ws').then(function(r){ log('HTTP GET /ws through proxy: status ' + r.status + ' (any status = proxy itself works)'); })\n",
160+
" .catch(function(e){ log('HTTP GET through proxy failed: ' + e); });\n",
161+
" } catch(e){ log('proxyPort threw: ' + e); }\n",
162+
"})();\n",
163+
"</script>'''))"
164+
]
165+
}
166+
],
167+
"metadata": {
168+
"kernelspec": {
169+
"display_name": "Python 3",
170+
"language": "python",
171+
"name": "python3"
172+
},
173+
"language_info": {
174+
"name": "python"
175+
}
176+
},
177+
"nbformat": 4,
178+
"nbformat_minor": 5
179+
}

0 commit comments

Comments
 (0)