gwchq-textjam 0.3.7 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,977 +1,977 @@
1
- /* eslint-disable @typescript-eslint/no-empty-function */
2
- /* global globalThis, importScripts, loadPyodide, SharedArrayBuffer, Atomics, pygal */
3
-
4
- function toAbsoluteFromOrigin(url) {
5
- if (url.startsWith("http") || url.startsWith("/")) return url;
6
- // eslint-disable-next-line no-restricted-globals
7
- return new URL(url, self.location.origin).href;
8
- }
9
-
10
- // removing submodules (matplotlib.pyplot = matplotlib)
11
- const normalizeImportName = (name) => name.split(".")[0];
12
-
13
- const WORKING_DIR = "/home/pyodide";
14
-
15
- const toStructuredCloneable = (value) => {
16
- if (value == null) return value;
17
-
18
- if (
19
- typeof value === "string" ||
20
- typeof value === "number" ||
21
- typeof value === "boolean"
22
- ) {
23
- return value;
24
- }
25
-
26
- if (value instanceof Uint8Array) {
27
- return value.slice();
28
- }
29
-
30
- if (ArrayBuffer.isView(value)) {
31
- return new Uint8Array(
32
- value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength),
33
- );
34
- }
35
-
36
- if (value instanceof ArrayBuffer) {
37
- return value.slice(0);
38
- }
39
-
40
- if (value instanceof Map) {
41
- return Object.fromEntries(
42
- [...value.entries()].map(([k, v]) => [k, toStructuredCloneable(v)]),
43
- );
44
- }
45
-
46
- if (Array.isArray(value)) {
47
- return value.map(toStructuredCloneable);
48
- }
49
-
50
- if (typeof value?.toJs === "function") {
51
- try {
52
- const converted = value.toJs({ dict_converter: Object.fromEntries });
53
- return toStructuredCloneable(converted);
54
- } catch (_) {
55
- try {
56
- const converted = value.toJs();
57
- return toStructuredCloneable(converted);
58
- } catch (_) {
59
- return String(value);
60
- }
61
- }
62
- }
63
-
64
- if (typeof value === "object") {
65
- const result = {};
66
- for (const [key, val] of Object.entries(value)) {
67
- result[key] = toStructuredCloneable(val);
68
- }
69
- return result;
70
- }
71
-
72
- return String(value);
73
- };
74
-
75
- // Nest the PyodideWorker function inside a globalThis object so we control when its initialised.
76
- const PyodideWorker = () => {
77
- let assets;
78
- let packageApiUrl;
79
-
80
- const handleInit = (data) => {
81
- assets = data.assets;
82
- packageApiUrl = data.packageApiUrl;
83
-
84
- // Import scripts dynamically based on the environment
85
- console.log("PyodideWorker", "importing scripts");
86
- importScripts(toAbsoluteFromOrigin(assets.pygalUrl));
87
-
88
- assets.pyodideBaseUrl = `${packageApiUrl}/pyodide.js`;
89
- importScripts(toAbsoluteFromOrigin(assets.pyodideBaseUrl));
90
-
91
- initialisePyodide(data);
92
- };
93
-
94
- const supportsAllFeatures = typeof SharedArrayBuffer !== "undefined";
95
-
96
- // eslint-disable-next-line no-restricted-globals
97
- if (!supportsAllFeatures && name !== "incremental-features") {
98
- console.warn(
99
- [
100
- "The code editor will not be able to capture standard input or stop execution because these HTTP headers are not set:",
101
- " - Cross-Origin-Opener-Policy: same-origin",
102
- " - Cross-Origin-Embedder-Policy: require-corp",
103
- "",
104
- "If your app can cope with or without these features, please initialize the web worker with { name: 'incremental-features' } to silence this warning.",
105
- "You can then check for the presence of { stdinBuffer, interruptBuffer } in the handleLoaded message to check whether these features are supported.",
106
- "",
107
- "If you definitely need these features, either configure your server to respond with the HTTP headers above, or register a service worker.",
108
- "Once the HTTP headers are set, the browser will block cross-domain resources so you will need to add 'crossorigin' to <script> and other tags.",
109
- "You may wish to scope the HTTP headers to only those pages that host the code editor to make the browser restriction easier to deal with.",
110
- "",
111
- "Please refer to these code snippets for registering a service worker:",
112
- " - https://github.com/RaspberryPiFoundation/python-execution-prototypes/blob/fd2c50e032cba3bb0e92e19a88eb62e5b120fe7a/pyodide/index.html#L92-L98",
113
- " - https://github.com/RaspberryPiFoundation/python-execution-prototypes/blob/fd2c50e032cba3bb0e92e19a88eb62e5b120fe7a/pyodide/serviceworker.js",
114
- ].join("\n"),
115
- );
116
- }
117
- let pyodide, pyodidePromise, stdinBuffer, interruptBuffer, stopped;
118
- let currentRunId = null;
119
- let stdinStrategy = "sab";
120
- let supportsJspi = false;
121
- let stdinFallbackConfig = null;
122
- const STDIN_CANCELLED_RESPONSE_CODE = 499;
123
- const STDIN_TIMEOUT_RESPONSE_CODE = 408;
124
- const STDIN_ABORTED_RESPONSE_CODE = 410;
125
- const STDIN_ROUTE_NOT_FOUND_RESPONSE_CODE = 404;
126
- // HTTP 204 No Content from the stdin Service Worker signals end-of-file —
127
- // distinct from a cancellation/interrupt so input() raises EOFError and the
128
- // user's `try/except EOFError` blocks keep working under the sync-XHR fallback.
129
- const STDIN_EOF_RESPONSE_CODE = 204;
130
- /** When true, input() uses postMessage + run_sync(getInputAsync) instead of setStdin (requires JSPI). */
131
- let useMessageStdin = false;
132
- /** When true, input() uses sync XHR via the Service Worker fallback. */
133
- let useSyncXhrStdin = false;
134
- /** True when current input request was cancelled from outside the worker. */
135
- let stdinCancelled = false;
136
- /** True when the sync-XHR stdin transport failed (SW unreachable / request escaped). */
137
- let stdinTransportFailed = false;
138
- /** Used when SharedArrayBuffer is unavailable: resolve for the pending input() call. */
139
- let pendingStdinResolve = null;
140
- // Until Pyodide is fully initialised, keep stdout/stderr in the dev console only.
141
- // After initialisation, route them to the UI via postMessage.
142
- let userStdStreamsEnabled = false;
143
- // When true, suppress internal library / package loader logs from the UI console.
144
- let suppressInternalStdStreams = false;
145
-
146
- const onmessage = async ({ data }) => {
147
- if (data.method !== "init") {
148
- pyodide = await pyodidePromise;
149
- }
150
-
151
- switch (data.method) {
152
- case "stdinResponse": {
153
- if (pendingStdinResolve) {
154
- let content = data.ctrlD ? null : data.content ?? "";
155
- if (content && content.endsWith("\n")) {
156
- content = content.slice(0, -1);
157
- }
158
- pendingStdinResolve(content);
159
- pendingStdinResolve = null;
160
- }
161
- break;
162
- }
163
- case "init": {
164
- handleInit(data);
165
- break;
166
- }
167
- case "createDirectories": {
168
- if (Array.isArray(data.dirs)) {
169
- for (const dir of data.dirs) {
170
- try {
171
- pyodide.FS.mkdirTree(`${WORKING_DIR}/${dir}`);
172
- } catch (e) {
173
- console.error(`Failed to create directory ${dir}:`, e);
174
- }
175
- }
176
- }
177
- break;
178
- }
179
- case "writeFile": {
180
- const encoder = new TextEncoder();
181
- pyodide.FS.writeFile(
182
- `${WORKING_DIR}/${data.filename}`,
183
- encoder.encode(data.content),
184
- );
185
- break;
186
- }
187
- case "runPython":
188
- currentRunId = data.runId || null;
189
- stdinCancelled = false;
190
- stdinTransportFailed = false;
191
- runPython(data.python, data.userModuleNames);
192
- break;
193
- case "stopPython": {
194
- // Mark as stopped so future checks can raise KeyboardInterrupt.
195
- stopped = true;
196
- // If Python is currently blocked in input() via run_sync(getInputAsync),
197
- // resolve the pending stdin promise with EOF so execution can unwind
198
- // and clearPyodideData can run.
199
- if (pendingStdinResolve) {
200
- pendingStdinResolve(null);
201
- pendingStdinResolve = null;
202
- }
203
- stdinCancelled = true;
204
- break;
205
- }
206
- default: {
207
- throw new Error(`Unsupported method: ${data.method}`);
208
- }
209
- }
210
- };
211
-
212
- // eslint-disable-next-line no-restricted-globals
213
- addEventListener("message", async (event) => {
214
- onmessage(event);
215
- });
216
-
217
- const runPython = async (python, userModuleNames) => {
218
- stopped = false;
219
-
220
- if (stdinStrategy === "unavailable") {
221
- postMessage({
222
- method: "handleError",
223
- file: "main.py",
224
- line: "",
225
- mistake: "",
226
- type: "RuntimeError",
227
- info: "Python input fallback is unavailable. Please enable and activate the stdin Service Worker, then reload the page.",
228
- });
229
- await clearPyodideData(userModuleNames);
230
- return;
231
- }
232
-
233
- // When stdin uses postMessage (no SharedArrayBuffer), run_sync() in input() requires
234
- // runPythonAsync so that JSPI stack switching can suspend until the main thread sends stdinResponse.
235
- const runUserCode = useMessageStdin
236
- ? () => pyodide.runPythonAsync(python)
237
- : () => pyodide.runPython(python);
238
-
239
- try {
240
- await withSupportForPackages(python, async () => {
241
- await runUserCode();
242
- });
243
- } catch (error) {
244
- const isStdinControlError = error?.message === "PYODIDE_STDIN_CANCELLED";
245
-
246
- if (stdinCancelled || isStdinControlError) {
247
- postMessage({
248
- method: "handleError",
249
- file: "main.py",
250
- line: "",
251
- mistake: "",
252
- type: "KeyboardInterrupt",
253
- info: "Execution interrupted",
254
- });
255
- await clearPyodideData(userModuleNames);
256
- return;
257
- }
258
-
259
- if (stdinTransportFailed) {
260
- postMessage({
261
- method: "handleError",
262
- file: "main.py",
263
- line: "",
264
- mistake: "",
265
- type: "InputUnavailableError",
266
- info: "Input timed out. Rerun your code and try again.",
267
- });
268
- await clearPyodideData(userModuleNames);
269
- return;
270
- }
271
-
272
- if (!(error instanceof pyodide.ffi.PythonError)) {
273
- postMessage({
274
- method: "handleError",
275
- file: "main.py",
276
- line: "",
277
- mistake: "",
278
- type: "RuntimeError",
279
- info:
280
- error?.message ||
281
- "Python execution failed while reading stdin fallback input.",
282
- });
283
- await clearPyodideData(userModuleNames);
284
- return;
285
- }
286
- const parsed = parsePythonError(error);
287
- // Stop resolves stdin with EOF so input() raises EOFError; show as interrupt, not error.
288
- if (stopped && parsed.type === "EOFError") {
289
- postMessage({
290
- method: "handleError",
291
- ...parsed,
292
- type: "KeyboardInterrupt",
293
- info: "Execution interrupted",
294
- });
295
- } else {
296
- postMessage({ method: "handleError", ...parsed });
297
- }
298
- }
299
-
300
- await clearPyodideData(userModuleNames);
301
- };
302
-
303
- const checkIfStopped = () => {
304
- if (stopped) {
305
- throw new pyodide.ffi.PythonError("KeyboardInterrupt");
306
- }
307
- };
308
-
309
- const withSupportForPackages = async (
310
- python,
311
- runPythonFn = async () => {},
312
- ) => {
313
- // Suppress internal loader output (e.g. "Loading pyodide-http") from the
314
- // user console while resolving imports and loading packages.
315
- suppressInternalStdStreams = true;
316
-
317
- // `find_imports` returns a PyProxy whose .toJs() copies the contents but
318
- // leaves the underlying Python object alive. Destroy it explicitly so JS
319
- // GC can't try to finalise it later from a context where the GIL isn't
320
- // held (which would surface as a NoGilError).
321
- const importsProxy = pyodide._api.pyodide_code.find_imports(python);
322
- const imports = importsProxy.toJs();
323
- importsProxy.destroy();
324
-
325
- await pyodide.runPythonAsync(`
326
- import builtins
327
- if hasattr(builtins, "_original_open"):
328
- builtins.open = builtins._original_open
329
- `);
330
-
331
- // Load packages sequentially rather than in Promise.all. Each
332
- // `loadDependency` does multiple `await` hops (vendored .before(),
333
- // loadPackage, micropip.install, …). When several run concurrently their
334
- // await points interleave and Pyodide ends up servicing multiple in-flight
335
- // package loads — that interleaving is the trigger for the NoGilError seen
336
- // when complex projects load matplotlib's dependency tree. Sequential
337
- // loading costs a few ms on cold start but matches how `loadPackage`
338
- // already serialises its own work internally.
339
- // for (const name of imports) {
340
- // checkIfStopped();
341
- // await loadDependency(name);
342
- // }
343
- await Promise.all(imports.map((name) => loadDependency(name)));
344
-
345
- checkIfStopped();
346
- await pyodide.loadPackagesFromImports(python);
347
-
348
- checkIfStopped();
349
- await pyodide.runPythonAsync(
350
- `
351
- import basthon
352
- import builtins
353
- import os
354
-
355
- MAX_FILES = 100
356
- MAX_FILE_SIZE = 8500000
357
- PROJECT_ROOT = os.path.abspath("${WORKING_DIR}")
358
-
359
- if not hasattr(builtins, "_original_open"):
360
- builtins._original_open = builtins.open
361
-
362
- ORIGINAL_OPEN = builtins._original_open
363
-
364
- def _is_project_file(filename):
365
- abs_path = os.path.abspath(filename)
366
- return abs_path == PROJECT_ROOT or abs_path.startswith(PROJECT_ROOT + os.sep)
367
-
368
- def _to_project_relative(filename):
369
- abs_path = os.path.abspath(filename)
370
- return os.path.relpath(abs_path, PROJECT_ROOT)
371
-
372
- def _custom_open(filename, mode="r", *args, **kwargs):
373
- import os
374
- abs_path = os.path.abspath(filename)
375
-
376
- if "x" in mode and os.path.exists(abs_path):
377
- raise FileExistsError(f"File '{filename}' already exists")
378
-
379
- is_text_write = ("w" in mode or "a" in mode or "x" in mode) and "b" not in mode
380
- is_project_file = _is_project_file(abs_path)
381
-
382
- if is_text_write and is_project_file:
383
- if len(os.listdir(PROJECT_ROOT)) > MAX_FILES and not os.path.exists(abs_path):
384
- raise OSError(f"File system limit reached, no more than {MAX_FILES} files allowed")
385
- class CustomFile:
386
- def __init__(self, filename):
387
- self.filename = filename
388
- self.content = ""
389
-
390
- def write(self, content):
391
- self.content += content
392
- if len(self.content) > MAX_FILE_SIZE:
393
- raise OSError(f"File '{self.filename}' exceeds maximum file size of {MAX_FILE_SIZE} bytes")
394
- with ORIGINAL_OPEN(self.filename, mode, *args, **kwargs) as f:
395
- f.write(self.content)
396
- basthon.kernel.write_file({
397
- "filename": _to_project_relative(self.filename),
398
- "content": self.content,
399
- "mode": mode
400
- })
401
-
402
- def close(self):
403
- pass
404
-
405
- def __enter__(self):
406
- return self
407
-
408
- def __exit__(self, exc_type, exc_val, exc_tb):
409
- self.close()
410
-
411
- return CustomFile(abs_path)
412
- else:
413
- return ORIGINAL_OPEN(filename, mode, *args, **kwargs)
414
-
415
- # Override the built-in open function
416
- builtins.open = _custom_open
417
- `,
418
- { filename: "__custom_open__.py" },
419
- );
420
-
421
- // Re-enable user-visible stdout / stderr for the actual user code run.
422
- suppressInternalStdStreams = false;
423
-
424
- await runPythonFn();
425
-
426
- for (let name of imports) {
427
- checkIfStopped();
428
- const pkgName = normalizeImportName(name);
429
- await vendoredPackages[pkgName]?.after();
430
- }
431
- };
432
-
433
- const loadDependency = async (name) => {
434
- checkIfStopped();
435
-
436
- const pkgName = normalizeImportName(name);
437
- // If the import is for another user file then open it and load its dependencies.
438
- if (pyodide.FS.readdir(WORKING_DIR).includes(`${pkgName}.py`)) {
439
- const fileContent = pyodide.FS.readFile(`${WORKING_DIR}/${pkgName}.py`, {
440
- encoding: "utf8",
441
- });
442
- await withSupportForPackages(fileContent);
443
- return;
444
- }
445
-
446
- // If the import is for a vendored package then run its .before() hook.
447
- const vendoredPackage = vendoredPackages[pkgName];
448
- await vendoredPackage?.before();
449
- if (vendoredPackage) {
450
- return;
451
- }
452
-
453
- // If the import is for a module built into Python then do nothing.
454
- // The PyProxy is only used as a presence check, so destroy it immediately
455
- // — leaking it leaves a Python ref for JS GC to finalise later, which
456
- // raises NoGilError when GC runs outside a GIL-held context.
457
- try {
458
- const pythonModule = pyodide.pyimport(pkgName);
459
- if (pythonModule) {
460
- pythonModule.destroy();
461
- return;
462
- }
463
- } catch (_) {}
464
-
465
- // If the import is for a package built into Pyodide then load it.
466
- // Built-ins: https://pyodide.org/en/stable/usage/packages-in-pyodide.html
467
- try {
468
- await pyodide.loadPackage(pkgName);
469
-
470
- const pyodidePackage = pyodide.pyimport(pkgName);
471
- if (pyodidePackage) {
472
- pyodidePackage.destroy();
473
- return;
474
- }
475
- } catch (_) {}
476
-
477
- // Ensure micropip is loaded which can fetch packages from PyPi.
478
- // See: https://pyodide.org/en/stable/usage/loading-packages.html
479
- if (!pyodide.micropip) {
480
- await pyodide.loadPackage("micropip");
481
- pyodide.micropip = pyodide.pyimport("micropip");
482
- }
483
-
484
- // If the import is for a PyPi package then load it.
485
- // Otherwise, don't error now so that we get an error later from Python.
486
- await pyodide.micropip.install(pkgName).catch(() => {});
487
- };
488
-
489
- const vendoredPackages = {
490
- // Support for https://pypi.org/project/py-enigma/ due to package not having a whl file on PyPi.
491
- enigma: {
492
- before: async () => {
493
- await pyodide.loadPackage(toAbsoluteFromOrigin(assets.enigmaWhlUrl));
494
- },
495
- after: () => {},
496
- },
497
- turtle: {
498
- before: async () => {
499
- pyodide.registerJsModule("basthon", fakeBasthonPackage);
500
- await pyodide.loadPackage(toAbsoluteFromOrigin(assets.turtleWhlUrl));
501
- },
502
- after: () =>
503
- pyodide.runPython(`
504
- import turtle
505
- import basthon
506
-
507
- svg_dict = turtle.Screen().show_scene()
508
- basthon.kernel.display_event({ "display_type": "turtle", "content": svg_dict })
509
- turtle.restart()
510
- `),
511
- },
512
- p5: {
513
- before: async () => {
514
- pyodide.registerJsModule("basthon", fakeBasthonPackage);
515
- await pyodide.loadPackage([
516
- "setuptools",
517
- toAbsoluteFromOrigin(assets.p5WhlUrl),
518
- ]);
519
- },
520
- after: () => {},
521
- },
522
- pygal: {
523
- before: () => {
524
- pyodide.registerJsModule("pygal", { ...pygal });
525
- pygal.config.renderChart = (content) => {
526
- postMessage({ method: "handleVisual", origin: "pygal", content });
527
- };
528
- },
529
- after: () => {},
530
- },
531
- matplotlib: {
532
- before: async () => {
533
- pyodide.registerJsModule("basthon", fakeBasthonPackage);
534
- // Patch the document object to prevent matplotlib from trying to render. Since we are running in a web worker,
535
- // the document object is not available. We will instead capture the image and send it back to the main thread.
536
- pyodide.runPython(`
537
- import js
538
-
539
- class __DummyDocument__:
540
- def __init__(self, *args, **kwargs) -> None:
541
- return
542
- def __getattr__(self, __name: str):
543
- return __DummyDocument__
544
- js.document = __DummyDocument__()
545
- `);
546
- await pyodide.loadPackage("matplotlib")?.catch(() => {});
547
- let pyodidePackage;
548
- try {
549
- pyodidePackage = pyodide.pyimport("matplotlib");
550
- } catch (_) {}
551
- if (pyodidePackage) {
552
- // Destroy the throwaway proxy before running follow-up Python — the
553
- // proxy is only used as a "did matplotlib actually load?" gate, and
554
- // leaking it would hand a Python ref to JS GC.
555
- pyodidePackage.destroy();
556
- pyodide.runPython(`
557
- import matplotlib.pyplot as plt
558
- import io
559
- import basthon
560
-
561
- def show_chart():
562
- bytes_io = io.BytesIO()
563
- plt.savefig(bytes_io, format='jpg')
564
- bytes_io.seek(0)
565
- basthon.kernel.display_event({ "display_type": "matplotlib", "content": bytes_io.read() })
566
- plt.show = show_chart
567
- `);
568
- return;
569
- }
570
- },
571
- after: () => {
572
- pyodide.runPython(`
573
- import matplotlib.pyplot as plt
574
- plt.clf()
575
- `);
576
- },
577
- },
578
- seaborn: {
579
- before: async () => {
580
- pyodide.registerJsModule("basthon", fakeBasthonPackage);
581
- // Patch the document object to prevent matplotlib from trying to render. Since we are running in a web worker,
582
- // the document object is not available. We will instead capture the image and send it back to the main thread.
583
- pyodide.runPython(`
584
- import js
585
-
586
- class __DummyDocument__:
587
- def __init__(self, *args, **kwargs) -> None:
588
- return
589
- def __getattr__(self, __name: str):
590
- return __DummyDocument__
591
- js.document = __DummyDocument__()
592
- `);
593
-
594
- // Ensure micropip is loaded which can fetch packages from PyPi.
595
- // See: https://pyodide.org/en/stable/usage/loading-packages.html
596
- if (!pyodide.micropip) {
597
- await pyodide.loadPackage("micropip");
598
- pyodide.micropip = pyodide.pyimport("micropip");
599
- }
600
-
601
- // If the import is for a PyPi package then load it.
602
- // Otherwise, don't error now so that we get an error later from Python.
603
- await pyodide.micropip.install("seaborn").catch(() => {});
604
- },
605
- after: () => {
606
- pyodide.runPython(`
607
- import matplotlib.pyplot as plt
608
- import io
609
- import basthon
610
-
611
- def is_plot_empty():
612
- fig = plt.gcf()
613
- for ax in fig.get_axes():
614
- # Check if the axes contain any lines, patches, collections, etc.
615
- if ax.lines or ax.patches or ax.collections or ax.images or ax.texts:
616
- return False
617
- return True
618
-
619
- if not is_plot_empty():
620
- bytes_io = io.BytesIO()
621
- plt.savefig(bytes_io, format='jpg')
622
- bytes_io.seek(0)
623
- basthon.kernel.display_event({ "display_type": "matplotlib", "content": bytes_io.read() })
624
-
625
- plt.clf()
626
- `);
627
- },
628
- },
629
- plotly: {
630
- before: async () => {
631
- if (!pyodide.micropip) {
632
- await pyodide.loadPackage("micropip");
633
- pyodide.micropip = pyodide.pyimport("micropip");
634
- }
635
-
636
- // If the import is for a PyPi package then load it.
637
- // Otherwise, don't error now so that we get an error later from Python.
638
- await pyodide.micropip.install("plotly").catch(() => {});
639
- await pyodide.micropip.install("pandas").catch(() => {});
640
- pyodide.registerJsModule("basthon", fakeBasthonPackage);
641
- pyodide.runPython(`
642
- import plotly.graph_objs as go
643
-
644
- def _hacked_show(self, *args, **kwargs):
645
- basthon.kernel.display_event({
646
- "display_type": "plotly",
647
- "content": self.to_json()
648
- })
649
-
650
- go.Figure.show = _hacked_show
651
- `);
652
- },
653
- after: () => {},
654
- },
655
- };
656
-
657
- const fakeBasthonPackage = {
658
- kernel: {
659
- display_event: (event) => {
660
- let payload;
661
- try {
662
- payload = event.toJs({ dict_converter: Object.fromEntries });
663
- } catch (_) {
664
- payload = event.toJs();
665
- }
666
-
667
- const origin = String(payload.display_type);
668
- const content = toStructuredCloneable(payload.content);
669
-
670
- postMessage({ method: "handleVisual", origin, content });
671
- },
672
- write_file: (event) => {
673
- let payload;
674
- try {
675
- payload = event.toJs({ dict_converter: Object.fromEntries });
676
- } catch (_) {
677
- payload = event.toJs();
678
- }
679
-
680
- const filename = String(payload.filename);
681
- const content =
682
- typeof payload.content === "string"
683
- ? payload.content
684
- : toStructuredCloneable(payload.content);
685
- const mode = String(payload.mode);
686
-
687
- postMessage({ method: "handleFileWrite", filename, content, mode });
688
- },
689
- locals: () => pyodide.runPython("globals()"),
690
- /**
691
- * Returns a Promise that resolves with the next line of stdin when the main thread
692
- * sends stdinResponse. Resolves with null on EOF (e.g. Ctrl+D).
693
- * Used when SharedArrayBuffer is unavailable (no COOP/COEP) so we cannot block via Atomics.wait.
694
- * Requires JSPI and runPythonAsync() for run_sync() to work.
695
- */
696
- getInputAsync: () => {
697
- const promise = new Promise((resolve) => {
698
- pendingStdinResolve = resolve;
699
- });
700
- postMessage({ method: "handleInput" });
701
- return promise;
702
- },
703
- },
704
- };
705
-
706
- const readInputViaSyncXhr = (runId) => {
707
- // Mark any failure with XHR setup as stdinTransportFailed for a clean error
708
- try {
709
- const requestId = crypto.randomUUID();
710
- const requestUrl = new URL(
711
- stdinFallbackConfig.endpointPath,
712
- // eslint-disable-next-line no-restricted-globals
713
- self.location.origin,
714
- );
715
- requestUrl.searchParams.set("runId", runId || "");
716
- requestUrl.searchParams.set("requestId", requestId);
717
- requestUrl.searchParams.set("clientId", stdinFallbackConfig.clientId);
718
-
719
- const xhr = new XMLHttpRequest();
720
- xhr.open("GET", requestUrl.toString(), false);
721
- xhr.setRequestHeader("X-Pyodide-Stdin-Request", "true");
722
- xhr.setRequestHeader("Cache-Control", "no-store");
723
- xhr.send(null);
724
-
725
- if (xhr.status === STDIN_CANCELLED_RESPONSE_CODE) {
726
- stdinCancelled = true;
727
- throw new Error("PYODIDE_STDIN_CANCELLED");
728
- }
729
-
730
- if (xhr.status === STDIN_TIMEOUT_RESPONSE_CODE) {
731
- stdinTransportFailed = true;
732
- throw new Error("PYODIDE_STDIN_TIMEOUT");
733
- }
734
-
735
- if (xhr.status === STDIN_ABORTED_RESPONSE_CODE) {
736
- stdinTransportFailed = true;
737
- throw new Error("PYODIDE_STDIN_ABORTED");
738
- }
739
-
740
- // EOF: orchestrator signalled Ctrl+D / no more input. Returning null tells
741
- // Pyodide's line-based stdin handler to raise EOFError at the input() call
742
- // site, rather than escalating to a KeyboardInterrupt that tears down the run.
743
- if (xhr.status === STDIN_EOF_RESPONSE_CODE) {
744
- return null;
745
- }
746
-
747
- if (xhr.status !== 200) {
748
- throw new Error(
749
- `Python input request failed with status ${xhr.status}: ${xhr.responseText}`,
750
- );
751
- }
752
-
753
- let content = xhr.responseText ?? "";
754
- if (!content.endsWith("\n")) {
755
- content += "\n";
756
- }
757
-
758
- return content;
759
- } catch (err) {
760
- if (!stdinCancelled) {
761
- stdinTransportFailed = true;
762
- }
763
- throw err;
764
- }
765
- };
766
-
767
- const clearPyodideData = async (userModuleNames) => {
768
- postMessage({ method: "handleLoading" });
769
- try {
770
- await pyodide.runPythonAsync(`
771
- import builtins
772
- import sys
773
-
774
- # Restore the real open before cleaning up
775
- if hasattr(builtins, "_original_open"):
776
- builtins.open = builtins._original_open
777
-
778
- # Remove user modules from sys.modules
779
- user_modules = ${JSON.stringify(userModuleNames)}
780
- for name in user_modules:
781
- if name in sys.modules:
782
- del sys.modules[name]
783
-
784
- # Clear all user-defined variables and modules
785
- for name in list(globals()):
786
- if not name.startswith('_') and name not in ('basthon', 'sys', 'builtins'):
787
- del globals()[name]
788
- `);
789
- } catch (error) {
790
- console.error("Error while clearing Pyodide data:", error);
791
- }
792
- console.log("clearPyodideData done");
793
- postMessage({
794
- method: "handleLoaded",
795
- stdinBuffer,
796
- interruptBuffer,
797
- stdinStrategy,
798
- supportsJspi,
799
- stdinFallbackEnabled: Boolean(stdinFallbackConfig?.enabled),
800
- });
801
- };
802
-
803
- const initialisePyodide = async (data) => {
804
- postMessage({ method: "handleLoading" });
805
-
806
- pyodidePromise = loadPyodide({
807
- stdout: (content) => {
808
- if (stopped) return;
809
- if (userStdStreamsEnabled && !suppressInternalStdStreams) {
810
- postMessage({ method: "handleOutput", stream: "stdout", content });
811
- } else {
812
- console.log(content);
813
- }
814
- },
815
- stderr: (content) => {
816
- if (stopped) return;
817
- if (userStdStreamsEnabled && !suppressInternalStdStreams) {
818
- postMessage({ method: "handleOutput", stream: "stderr", content });
819
- } else {
820
- console.error(content);
821
- }
822
- },
823
- });
824
-
825
- pyodide = await pyodidePromise;
826
-
827
- pyodide.registerJsModule("basthon", fakeBasthonPackage);
828
-
829
- supportsJspi =
830
- typeof WebAssembly?.Suspending === "function" &&
831
- typeof WebAssembly?.promising === "function";
832
-
833
- stdinFallbackConfig = {
834
- enabled: Boolean(data?.stdinFallback?.enabled),
835
- endpointPath: data?.stdinFallback?.endpointPath || "/pyodide-stdin",
836
- clientId: data?.stdinFallback?.clientId || "",
837
- };
838
-
839
- // When SharedArrayBuffer is unavailable, always use the postMessage-based
840
- // stdin path. JSPI / run_sync will raise at runtime if the environment
841
- // cannot stack-switch, but in JSPI-capable browsers this enables
842
- // interactive input() without COOP/COEP.
843
- useMessageStdin = !supportsAllFeatures && supportsJspi;
844
- useSyncXhrStdin =
845
- !supportsAllFeatures && !supportsJspi && stdinFallbackConfig.enabled;
846
- stdinStrategy = supportsAllFeatures
847
- ? "sab"
848
- : useMessageStdin
849
- ? "jspi"
850
- : useSyncXhrStdin
851
- ? "sync-xhr"
852
- : "unavailable";
853
- pyodide.globals.set("__stdin_via_message__", useMessageStdin);
854
-
855
- await pyodide.runPythonAsync(`
856
- import builtins
857
- from pyodide.ffi import run_sync as __run_sync__
858
-
859
- __old_input__ = builtins.input
860
- def __patched_input__(prompt=None):
861
- if prompt is not None:
862
- print(prompt)
863
- if __stdin_via_message__:
864
- result = __run_sync__(basthon.kernel.getInputAsync())
865
- if result is None:
866
- raise EOFError("EOF when reading a line")
867
- return result
868
- return __old_input__()
869
- builtins.input = __patched_input__
870
- `);
871
-
872
- await pyodide.runPythonAsync(`
873
- import builtins
874
- # Save the original open function only once
875
- if not hasattr(builtins, "_original_open"):
876
- builtins._original_open = builtins.open
877
- _original_open = builtins._original_open
878
- `);
879
-
880
- await pyodide.loadPackage("pyodide-http");
881
- await pyodide.runPythonAsync(`
882
- import pyodide_http
883
- pyodide_http.patch_all()
884
- `);
885
-
886
- if (supportsAllFeatures) {
887
- stdinBuffer =
888
- stdinBuffer || new Int32Array(new SharedArrayBuffer(1024 * 1024)); // 1 MiB
889
- stdinBuffer[0] = 1; // Store the length of content in the buffer at index 0.
890
- pyodide.setStdin({ isatty: true, read: readFromStdin });
891
-
892
- interruptBuffer =
893
- interruptBuffer || new Uint8Array(new SharedArrayBuffer(1));
894
- pyodide.setInterruptBuffer(interruptBuffer);
895
- } else if (useSyncXhrStdin) {
896
- pyodide.setStdin({
897
- stdin: () => readInputViaSyncXhr(currentRunId),
898
- isatty: true,
899
- });
900
- }
901
-
902
- // From this point on, anything written to stdout / stderr is considered
903
- // user-visible and will be forwarded to the UI.
904
- userStdStreamsEnabled = true;
905
-
906
- postMessage({
907
- method: "handleLoaded",
908
- stdinBuffer,
909
- interruptBuffer,
910
- stdinStrategy,
911
- supportsJspi,
912
- stdinFallbackEnabled: stdinFallbackConfig.enabled,
913
- });
914
- };
915
-
916
- const readFromStdin = (bufferToWrite) => {
917
- const previousLength = stdinBuffer[0];
918
- postMessage({ method: "handleInput" });
919
-
920
- while (true) {
921
- pyodide.checkInterrupt();
922
- const result = Atomics.wait(stdinBuffer, 0, previousLength, 100);
923
- if (result === "not-equal") {
924
- break;
925
- }
926
- }
927
-
928
- const currentLength = stdinBuffer[0];
929
- if (currentLength === -1) {
930
- return 0;
931
- } // Signals that stdin was closed.
932
-
933
- const addedBytes = stdinBuffer.slice(previousLength, currentLength);
934
- bufferToWrite.set(addedBytes);
935
-
936
- return addedBytes.length;
937
- };
938
-
939
- const parsePythonError = (error) => {
940
- const type = error.type;
941
- const [trace, info] = error.message.split(`${type}:`).map((s) => s?.trim());
942
-
943
- const lines = trace.split("\n");
944
-
945
- // if the third from last line matches /File "__custom_open__\.py", line (\d+)/g then strip off the last three lines
946
- if (
947
- lines.length > 3 &&
948
- /File "__custom_open__\.py", line (\d+)/g.test(lines[lines.length - 3])
949
- ) {
950
- lines.splice(-3, 3);
951
- }
952
-
953
- const snippetLine = lines[lines.length - 2]; // print("hi")invalid
954
- const caretLine = lines[lines.length - 1]; // ^^^^^^^
955
-
956
- const showsMistake = caretLine.includes("^");
957
- const mistake = showsMistake
958
- ? [snippetLine.slice(4), caretLine.slice(4)].join("\n")
959
- : "";
960
-
961
- const matches = [
962
- ...trace.matchAll(/File "(?!__custom_open__\.py)(.*)", line (\d+)/g),
963
- ];
964
- const match = matches[matches.length - 1];
965
-
966
- const path = match ? match[1] : "";
967
- const base = path.split("/").reverse()[0];
968
- const file = base === "<exec>" ? "main.py" : base;
969
-
970
- const line = match ? parseInt(match[2], 10) : "";
971
-
972
- return { file, line, mistake, type, info };
973
- };
974
- };
975
-
976
- globalThis.PyodideWorker = PyodideWorker;
977
- PyodideWorker();
1
+ /* eslint-disable @typescript-eslint/no-empty-function */
2
+ /* global globalThis, importScripts, loadPyodide, SharedArrayBuffer, Atomics, pygal */
3
+
4
+ function toAbsoluteFromOrigin(url) {
5
+ if (url.startsWith("http") || url.startsWith("/")) return url;
6
+ // eslint-disable-next-line no-restricted-globals
7
+ return new URL(url, self.location.origin).href;
8
+ }
9
+
10
+ // removing submodules (matplotlib.pyplot = matplotlib)
11
+ const normalizeImportName = (name) => name.split(".")[0];
12
+
13
+ const WORKING_DIR = "/home/pyodide";
14
+
15
+ const toStructuredCloneable = (value) => {
16
+ if (value == null) return value;
17
+
18
+ if (
19
+ typeof value === "string" ||
20
+ typeof value === "number" ||
21
+ typeof value === "boolean"
22
+ ) {
23
+ return value;
24
+ }
25
+
26
+ if (value instanceof Uint8Array) {
27
+ return value.slice();
28
+ }
29
+
30
+ if (ArrayBuffer.isView(value)) {
31
+ return new Uint8Array(
32
+ value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength),
33
+ );
34
+ }
35
+
36
+ if (value instanceof ArrayBuffer) {
37
+ return value.slice(0);
38
+ }
39
+
40
+ if (value instanceof Map) {
41
+ return Object.fromEntries(
42
+ [...value.entries()].map(([k, v]) => [k, toStructuredCloneable(v)]),
43
+ );
44
+ }
45
+
46
+ if (Array.isArray(value)) {
47
+ return value.map(toStructuredCloneable);
48
+ }
49
+
50
+ if (typeof value?.toJs === "function") {
51
+ try {
52
+ const converted = value.toJs({ dict_converter: Object.fromEntries });
53
+ return toStructuredCloneable(converted);
54
+ } catch (_) {
55
+ try {
56
+ const converted = value.toJs();
57
+ return toStructuredCloneable(converted);
58
+ } catch (_) {
59
+ return String(value);
60
+ }
61
+ }
62
+ }
63
+
64
+ if (typeof value === "object") {
65
+ const result = {};
66
+ for (const [key, val] of Object.entries(value)) {
67
+ result[key] = toStructuredCloneable(val);
68
+ }
69
+ return result;
70
+ }
71
+
72
+ return String(value);
73
+ };
74
+
75
+ // Nest the PyodideWorker function inside a globalThis object so we control when its initialised.
76
+ const PyodideWorker = () => {
77
+ let assets;
78
+ let packageApiUrl;
79
+
80
+ const handleInit = (data) => {
81
+ assets = data.assets;
82
+ packageApiUrl = data.packageApiUrl;
83
+
84
+ // Import scripts dynamically based on the environment
85
+ console.log("PyodideWorker", "importing scripts");
86
+ importScripts(toAbsoluteFromOrigin(assets.pygalUrl));
87
+
88
+ assets.pyodideBaseUrl = `${packageApiUrl}/pyodide.js`;
89
+ importScripts(toAbsoluteFromOrigin(assets.pyodideBaseUrl));
90
+
91
+ initialisePyodide(data);
92
+ };
93
+
94
+ const supportsAllFeatures = typeof SharedArrayBuffer !== "undefined";
95
+
96
+ // eslint-disable-next-line no-restricted-globals
97
+ if (!supportsAllFeatures && name !== "incremental-features") {
98
+ console.warn(
99
+ [
100
+ "The code editor will not be able to capture standard input or stop execution because these HTTP headers are not set:",
101
+ " - Cross-Origin-Opener-Policy: same-origin",
102
+ " - Cross-Origin-Embedder-Policy: require-corp",
103
+ "",
104
+ "If your app can cope with or without these features, please initialize the web worker with { name: 'incremental-features' } to silence this warning.",
105
+ "You can then check for the presence of { stdinBuffer, interruptBuffer } in the handleLoaded message to check whether these features are supported.",
106
+ "",
107
+ "If you definitely need these features, either configure your server to respond with the HTTP headers above, or register a service worker.",
108
+ "Once the HTTP headers are set, the browser will block cross-domain resources so you will need to add 'crossorigin' to <script> and other tags.",
109
+ "You may wish to scope the HTTP headers to only those pages that host the code editor to make the browser restriction easier to deal with.",
110
+ "",
111
+ "Please refer to these code snippets for registering a service worker:",
112
+ " - https://github.com/RaspberryPiFoundation/python-execution-prototypes/blob/fd2c50e032cba3bb0e92e19a88eb62e5b120fe7a/pyodide/index.html#L92-L98",
113
+ " - https://github.com/RaspberryPiFoundation/python-execution-prototypes/blob/fd2c50e032cba3bb0e92e19a88eb62e5b120fe7a/pyodide/serviceworker.js",
114
+ ].join("\n"),
115
+ );
116
+ }
117
+ let pyodide, pyodidePromise, stdinBuffer, interruptBuffer, stopped;
118
+ let currentRunId = null;
119
+ let stdinStrategy = "sab";
120
+ let supportsJspi = false;
121
+ let stdinFallbackConfig = null;
122
+ const STDIN_CANCELLED_RESPONSE_CODE = 499;
123
+ const STDIN_TIMEOUT_RESPONSE_CODE = 408;
124
+ const STDIN_ABORTED_RESPONSE_CODE = 410;
125
+ const STDIN_ROUTE_NOT_FOUND_RESPONSE_CODE = 404;
126
+ // HTTP 204 No Content from the stdin Service Worker signals end-of-file —
127
+ // distinct from a cancellation/interrupt so input() raises EOFError and the
128
+ // user's `try/except EOFError` blocks keep working under the sync-XHR fallback.
129
+ const STDIN_EOF_RESPONSE_CODE = 204;
130
+ /** When true, input() uses postMessage + run_sync(getInputAsync) instead of setStdin (requires JSPI). */
131
+ let useMessageStdin = false;
132
+ /** When true, input() uses sync XHR via the Service Worker fallback. */
133
+ let useSyncXhrStdin = false;
134
+ /** True when current input request was cancelled from outside the worker. */
135
+ let stdinCancelled = false;
136
+ /** True when the sync-XHR stdin transport failed (SW unreachable / request escaped). */
137
+ let stdinTransportFailed = false;
138
+ /** Used when SharedArrayBuffer is unavailable: resolve for the pending input() call. */
139
+ let pendingStdinResolve = null;
140
+ // Until Pyodide is fully initialised, keep stdout/stderr in the dev console only.
141
+ // After initialisation, route them to the UI via postMessage.
142
+ let userStdStreamsEnabled = false;
143
+ // When true, suppress internal library / package loader logs from the UI console.
144
+ let suppressInternalStdStreams = false;
145
+
146
+ const onmessage = async ({ data }) => {
147
+ if (data.method !== "init") {
148
+ pyodide = await pyodidePromise;
149
+ }
150
+
151
+ switch (data.method) {
152
+ case "stdinResponse": {
153
+ if (pendingStdinResolve) {
154
+ let content = data.ctrlD ? null : data.content ?? "";
155
+ if (content && content.endsWith("\n")) {
156
+ content = content.slice(0, -1);
157
+ }
158
+ pendingStdinResolve(content);
159
+ pendingStdinResolve = null;
160
+ }
161
+ break;
162
+ }
163
+ case "init": {
164
+ handleInit(data);
165
+ break;
166
+ }
167
+ case "createDirectories": {
168
+ if (Array.isArray(data.dirs)) {
169
+ for (const dir of data.dirs) {
170
+ try {
171
+ pyodide.FS.mkdirTree(`${WORKING_DIR}/${dir}`);
172
+ } catch (e) {
173
+ console.error(`Failed to create directory ${dir}:`, e);
174
+ }
175
+ }
176
+ }
177
+ break;
178
+ }
179
+ case "writeFile": {
180
+ const encoder = new TextEncoder();
181
+ pyodide.FS.writeFile(
182
+ `${WORKING_DIR}/${data.filename}`,
183
+ encoder.encode(data.content),
184
+ );
185
+ break;
186
+ }
187
+ case "runPython":
188
+ currentRunId = data.runId || null;
189
+ stdinCancelled = false;
190
+ stdinTransportFailed = false;
191
+ runPython(data.python, data.userModuleNames);
192
+ break;
193
+ case "stopPython": {
194
+ // Mark as stopped so future checks can raise KeyboardInterrupt.
195
+ stopped = true;
196
+ // If Python is currently blocked in input() via run_sync(getInputAsync),
197
+ // resolve the pending stdin promise with EOF so execution can unwind
198
+ // and clearPyodideData can run.
199
+ if (pendingStdinResolve) {
200
+ pendingStdinResolve(null);
201
+ pendingStdinResolve = null;
202
+ }
203
+ stdinCancelled = true;
204
+ break;
205
+ }
206
+ default: {
207
+ throw new Error(`Unsupported method: ${data.method}`);
208
+ }
209
+ }
210
+ };
211
+
212
+ // eslint-disable-next-line no-restricted-globals
213
+ addEventListener("message", async (event) => {
214
+ onmessage(event);
215
+ });
216
+
217
+ const runPython = async (python, userModuleNames) => {
218
+ stopped = false;
219
+
220
+ if (stdinStrategy === "unavailable") {
221
+ postMessage({
222
+ method: "handleError",
223
+ file: "main.py",
224
+ line: "",
225
+ mistake: "",
226
+ type: "RuntimeError",
227
+ info: "Python input fallback is unavailable. Please enable and activate the stdin Service Worker, then reload the page.",
228
+ });
229
+ await clearPyodideData(userModuleNames);
230
+ return;
231
+ }
232
+
233
+ // When stdin uses postMessage (no SharedArrayBuffer), run_sync() in input() requires
234
+ // runPythonAsync so that JSPI stack switching can suspend until the main thread sends stdinResponse.
235
+ const runUserCode = useMessageStdin
236
+ ? () => pyodide.runPythonAsync(python)
237
+ : () => pyodide.runPython(python);
238
+
239
+ try {
240
+ await withSupportForPackages(python, async () => {
241
+ await runUserCode();
242
+ });
243
+ } catch (error) {
244
+ const isStdinControlError = error?.message === "PYODIDE_STDIN_CANCELLED";
245
+
246
+ if (stdinCancelled || isStdinControlError) {
247
+ postMessage({
248
+ method: "handleError",
249
+ file: "main.py",
250
+ line: "",
251
+ mistake: "",
252
+ type: "KeyboardInterrupt",
253
+ info: "Execution interrupted",
254
+ });
255
+ await clearPyodideData(userModuleNames);
256
+ return;
257
+ }
258
+
259
+ if (stdinTransportFailed) {
260
+ postMessage({
261
+ method: "handleError",
262
+ file: "main.py",
263
+ line: "",
264
+ mistake: "",
265
+ type: "InputUnavailableError",
266
+ info: "Input timed out. Rerun your code and try again.",
267
+ });
268
+ await clearPyodideData(userModuleNames);
269
+ return;
270
+ }
271
+
272
+ if (!(error instanceof pyodide.ffi.PythonError)) {
273
+ postMessage({
274
+ method: "handleError",
275
+ file: "main.py",
276
+ line: "",
277
+ mistake: "",
278
+ type: "RuntimeError",
279
+ info:
280
+ error?.message ||
281
+ "Python execution failed while reading stdin fallback input.",
282
+ });
283
+ await clearPyodideData(userModuleNames);
284
+ return;
285
+ }
286
+ const parsed = parsePythonError(error);
287
+ // Stop resolves stdin with EOF so input() raises EOFError; show as interrupt, not error.
288
+ if (stopped && parsed.type === "EOFError") {
289
+ postMessage({
290
+ method: "handleError",
291
+ ...parsed,
292
+ type: "KeyboardInterrupt",
293
+ info: "Execution interrupted",
294
+ });
295
+ } else {
296
+ postMessage({ method: "handleError", ...parsed });
297
+ }
298
+ }
299
+
300
+ await clearPyodideData(userModuleNames);
301
+ };
302
+
303
+ const checkIfStopped = () => {
304
+ if (stopped) {
305
+ throw new pyodide.ffi.PythonError("KeyboardInterrupt");
306
+ }
307
+ };
308
+
309
+ const withSupportForPackages = async (
310
+ python,
311
+ runPythonFn = async () => {},
312
+ ) => {
313
+ // Suppress internal loader output (e.g. "Loading pyodide-http") from the
314
+ // user console while resolving imports and loading packages.
315
+ suppressInternalStdStreams = true;
316
+
317
+ // `find_imports` returns a PyProxy whose .toJs() copies the contents but
318
+ // leaves the underlying Python object alive. Destroy it explicitly so JS
319
+ // GC can't try to finalise it later from a context where the GIL isn't
320
+ // held (which would surface as a NoGilError).
321
+ const importsProxy = pyodide._api.pyodide_code.find_imports(python);
322
+ const imports = importsProxy.toJs();
323
+ importsProxy.destroy();
324
+
325
+ await pyodide.runPythonAsync(`
326
+ import builtins
327
+ if hasattr(builtins, "_original_open"):
328
+ builtins.open = builtins._original_open
329
+ `);
330
+
331
+ // Load packages sequentially rather than in Promise.all. Each
332
+ // `loadDependency` does multiple `await` hops (vendored .before(),
333
+ // loadPackage, micropip.install, …). When several run concurrently their
334
+ // await points interleave and Pyodide ends up servicing multiple in-flight
335
+ // package loads — that interleaving is the trigger for the NoGilError seen
336
+ // when complex projects load matplotlib's dependency tree. Sequential
337
+ // loading costs a few ms on cold start but matches how `loadPackage`
338
+ // already serialises its own work internally.
339
+ // for (const name of imports) {
340
+ // checkIfStopped();
341
+ // await loadDependency(name);
342
+ // }
343
+ await Promise.all(imports.map((name) => loadDependency(name)));
344
+
345
+ checkIfStopped();
346
+ await pyodide.loadPackagesFromImports(python);
347
+
348
+ checkIfStopped();
349
+ await pyodide.runPythonAsync(
350
+ `
351
+ import basthon
352
+ import builtins
353
+ import os
354
+
355
+ MAX_FILES = 100
356
+ MAX_FILE_SIZE = 8500000
357
+ PROJECT_ROOT = os.path.abspath("${WORKING_DIR}")
358
+
359
+ if not hasattr(builtins, "_original_open"):
360
+ builtins._original_open = builtins.open
361
+
362
+ ORIGINAL_OPEN = builtins._original_open
363
+
364
+ def _is_project_file(filename):
365
+ abs_path = os.path.abspath(filename)
366
+ return abs_path == PROJECT_ROOT or abs_path.startswith(PROJECT_ROOT + os.sep)
367
+
368
+ def _to_project_relative(filename):
369
+ abs_path = os.path.abspath(filename)
370
+ return os.path.relpath(abs_path, PROJECT_ROOT)
371
+
372
+ def _custom_open(filename, mode="r", *args, **kwargs):
373
+ import os
374
+ abs_path = os.path.abspath(filename)
375
+
376
+ if "x" in mode and os.path.exists(abs_path):
377
+ raise FileExistsError(f"File '{filename}' already exists")
378
+
379
+ is_text_write = ("w" in mode or "a" in mode or "x" in mode) and "b" not in mode
380
+ is_project_file = _is_project_file(abs_path)
381
+
382
+ if is_text_write and is_project_file:
383
+ if len(os.listdir(PROJECT_ROOT)) > MAX_FILES and not os.path.exists(abs_path):
384
+ raise OSError(f"File system limit reached, no more than {MAX_FILES} files allowed")
385
+ class CustomFile:
386
+ def __init__(self, filename):
387
+ self.filename = filename
388
+ self.content = ""
389
+
390
+ def write(self, content):
391
+ self.content += content
392
+ if len(self.content) > MAX_FILE_SIZE:
393
+ raise OSError(f"File '{self.filename}' exceeds maximum file size of {MAX_FILE_SIZE} bytes")
394
+ with ORIGINAL_OPEN(self.filename, mode, *args, **kwargs) as f:
395
+ f.write(self.content)
396
+ basthon.kernel.write_file({
397
+ "filename": _to_project_relative(self.filename),
398
+ "content": self.content,
399
+ "mode": mode
400
+ })
401
+
402
+ def close(self):
403
+ pass
404
+
405
+ def __enter__(self):
406
+ return self
407
+
408
+ def __exit__(self, exc_type, exc_val, exc_tb):
409
+ self.close()
410
+
411
+ return CustomFile(abs_path)
412
+ else:
413
+ return ORIGINAL_OPEN(filename, mode, *args, **kwargs)
414
+
415
+ # Override the built-in open function
416
+ builtins.open = _custom_open
417
+ `,
418
+ { filename: "__custom_open__.py" },
419
+ );
420
+
421
+ // Re-enable user-visible stdout / stderr for the actual user code run.
422
+ suppressInternalStdStreams = false;
423
+
424
+ await runPythonFn();
425
+
426
+ for (let name of imports) {
427
+ checkIfStopped();
428
+ const pkgName = normalizeImportName(name);
429
+ await vendoredPackages[pkgName]?.after();
430
+ }
431
+ };
432
+
433
+ const loadDependency = async (name) => {
434
+ checkIfStopped();
435
+
436
+ const pkgName = normalizeImportName(name);
437
+ // If the import is for another user file then open it and load its dependencies.
438
+ if (pyodide.FS.readdir(WORKING_DIR).includes(`${pkgName}.py`)) {
439
+ const fileContent = pyodide.FS.readFile(`${WORKING_DIR}/${pkgName}.py`, {
440
+ encoding: "utf8",
441
+ });
442
+ await withSupportForPackages(fileContent);
443
+ return;
444
+ }
445
+
446
+ // If the import is for a vendored package then run its .before() hook.
447
+ const vendoredPackage = vendoredPackages[pkgName];
448
+ await vendoredPackage?.before();
449
+ if (vendoredPackage) {
450
+ return;
451
+ }
452
+
453
+ // If the import is for a module built into Python then do nothing.
454
+ // The PyProxy is only used as a presence check, so destroy it immediately
455
+ // — leaking it leaves a Python ref for JS GC to finalise later, which
456
+ // raises NoGilError when GC runs outside a GIL-held context.
457
+ try {
458
+ const pythonModule = pyodide.pyimport(pkgName);
459
+ if (pythonModule) {
460
+ pythonModule.destroy();
461
+ return;
462
+ }
463
+ } catch (_) {}
464
+
465
+ // If the import is for a package built into Pyodide then load it.
466
+ // Built-ins: https://pyodide.org/en/stable/usage/packages-in-pyodide.html
467
+ try {
468
+ await pyodide.loadPackage(pkgName);
469
+
470
+ const pyodidePackage = pyodide.pyimport(pkgName);
471
+ if (pyodidePackage) {
472
+ pyodidePackage.destroy();
473
+ return;
474
+ }
475
+ } catch (_) {}
476
+
477
+ // Ensure micropip is loaded which can fetch packages from PyPi.
478
+ // See: https://pyodide.org/en/stable/usage/loading-packages.html
479
+ if (!pyodide.micropip) {
480
+ await pyodide.loadPackage("micropip");
481
+ pyodide.micropip = pyodide.pyimport("micropip");
482
+ }
483
+
484
+ // If the import is for a PyPi package then load it.
485
+ // Otherwise, don't error now so that we get an error later from Python.
486
+ await pyodide.micropip.install(pkgName).catch(() => {});
487
+ };
488
+
489
+ const vendoredPackages = {
490
+ // Support for https://pypi.org/project/py-enigma/ due to package not having a whl file on PyPi.
491
+ enigma: {
492
+ before: async () => {
493
+ await pyodide.loadPackage(toAbsoluteFromOrigin(assets.enigmaWhlUrl));
494
+ },
495
+ after: () => {},
496
+ },
497
+ turtle: {
498
+ before: async () => {
499
+ pyodide.registerJsModule("basthon", fakeBasthonPackage);
500
+ await pyodide.loadPackage(toAbsoluteFromOrigin(assets.turtleWhlUrl));
501
+ },
502
+ after: () =>
503
+ pyodide.runPython(`
504
+ import turtle
505
+ import basthon
506
+
507
+ svg_dict = turtle.Screen().show_scene()
508
+ basthon.kernel.display_event({ "display_type": "turtle", "content": svg_dict })
509
+ turtle.restart()
510
+ `),
511
+ },
512
+ p5: {
513
+ before: async () => {
514
+ pyodide.registerJsModule("basthon", fakeBasthonPackage);
515
+ await pyodide.loadPackage([
516
+ "setuptools",
517
+ toAbsoluteFromOrigin(assets.p5WhlUrl),
518
+ ]);
519
+ },
520
+ after: () => {},
521
+ },
522
+ pygal: {
523
+ before: () => {
524
+ pyodide.registerJsModule("pygal", { ...pygal });
525
+ pygal.config.renderChart = (content) => {
526
+ postMessage({ method: "handleVisual", origin: "pygal", content });
527
+ };
528
+ },
529
+ after: () => {},
530
+ },
531
+ matplotlib: {
532
+ before: async () => {
533
+ pyodide.registerJsModule("basthon", fakeBasthonPackage);
534
+ // Patch the document object to prevent matplotlib from trying to render. Since we are running in a web worker,
535
+ // the document object is not available. We will instead capture the image and send it back to the main thread.
536
+ pyodide.runPython(`
537
+ import js
538
+
539
+ class __DummyDocument__:
540
+ def __init__(self, *args, **kwargs) -> None:
541
+ return
542
+ def __getattr__(self, __name: str):
543
+ return __DummyDocument__
544
+ js.document = __DummyDocument__()
545
+ `);
546
+ await pyodide.loadPackage("matplotlib")?.catch(() => {});
547
+ let pyodidePackage;
548
+ try {
549
+ pyodidePackage = pyodide.pyimport("matplotlib");
550
+ } catch (_) {}
551
+ if (pyodidePackage) {
552
+ // Destroy the throwaway proxy before running follow-up Python — the
553
+ // proxy is only used as a "did matplotlib actually load?" gate, and
554
+ // leaking it would hand a Python ref to JS GC.
555
+ pyodidePackage.destroy();
556
+ pyodide.runPython(`
557
+ import matplotlib.pyplot as plt
558
+ import io
559
+ import basthon
560
+
561
+ def show_chart():
562
+ bytes_io = io.BytesIO()
563
+ plt.savefig(bytes_io, format='jpg')
564
+ bytes_io.seek(0)
565
+ basthon.kernel.display_event({ "display_type": "matplotlib", "content": bytes_io.read() })
566
+ plt.show = show_chart
567
+ `);
568
+ return;
569
+ }
570
+ },
571
+ after: () => {
572
+ pyodide.runPython(`
573
+ import matplotlib.pyplot as plt
574
+ plt.clf()
575
+ `);
576
+ },
577
+ },
578
+ seaborn: {
579
+ before: async () => {
580
+ pyodide.registerJsModule("basthon", fakeBasthonPackage);
581
+ // Patch the document object to prevent matplotlib from trying to render. Since we are running in a web worker,
582
+ // the document object is not available. We will instead capture the image and send it back to the main thread.
583
+ pyodide.runPython(`
584
+ import js
585
+
586
+ class __DummyDocument__:
587
+ def __init__(self, *args, **kwargs) -> None:
588
+ return
589
+ def __getattr__(self, __name: str):
590
+ return __DummyDocument__
591
+ js.document = __DummyDocument__()
592
+ `);
593
+
594
+ // Ensure micropip is loaded which can fetch packages from PyPi.
595
+ // See: https://pyodide.org/en/stable/usage/loading-packages.html
596
+ if (!pyodide.micropip) {
597
+ await pyodide.loadPackage("micropip");
598
+ pyodide.micropip = pyodide.pyimport("micropip");
599
+ }
600
+
601
+ // If the import is for a PyPi package then load it.
602
+ // Otherwise, don't error now so that we get an error later from Python.
603
+ await pyodide.micropip.install("seaborn").catch(() => {});
604
+ },
605
+ after: () => {
606
+ pyodide.runPython(`
607
+ import matplotlib.pyplot as plt
608
+ import io
609
+ import basthon
610
+
611
+ def is_plot_empty():
612
+ fig = plt.gcf()
613
+ for ax in fig.get_axes():
614
+ # Check if the axes contain any lines, patches, collections, etc.
615
+ if ax.lines or ax.patches or ax.collections or ax.images or ax.texts:
616
+ return False
617
+ return True
618
+
619
+ if not is_plot_empty():
620
+ bytes_io = io.BytesIO()
621
+ plt.savefig(bytes_io, format='jpg')
622
+ bytes_io.seek(0)
623
+ basthon.kernel.display_event({ "display_type": "matplotlib", "content": bytes_io.read() })
624
+
625
+ plt.clf()
626
+ `);
627
+ },
628
+ },
629
+ plotly: {
630
+ before: async () => {
631
+ if (!pyodide.micropip) {
632
+ await pyodide.loadPackage("micropip");
633
+ pyodide.micropip = pyodide.pyimport("micropip");
634
+ }
635
+
636
+ // If the import is for a PyPi package then load it.
637
+ // Otherwise, don't error now so that we get an error later from Python.
638
+ await pyodide.micropip.install("plotly").catch(() => {});
639
+ await pyodide.micropip.install("pandas").catch(() => {});
640
+ pyodide.registerJsModule("basthon", fakeBasthonPackage);
641
+ pyodide.runPython(`
642
+ import plotly.graph_objs as go
643
+
644
+ def _hacked_show(self, *args, **kwargs):
645
+ basthon.kernel.display_event({
646
+ "display_type": "plotly",
647
+ "content": self.to_json()
648
+ })
649
+
650
+ go.Figure.show = _hacked_show
651
+ `);
652
+ },
653
+ after: () => {},
654
+ },
655
+ };
656
+
657
+ const fakeBasthonPackage = {
658
+ kernel: {
659
+ display_event: (event) => {
660
+ let payload;
661
+ try {
662
+ payload = event.toJs({ dict_converter: Object.fromEntries });
663
+ } catch (_) {
664
+ payload = event.toJs();
665
+ }
666
+
667
+ const origin = String(payload.display_type);
668
+ const content = toStructuredCloneable(payload.content);
669
+
670
+ postMessage({ method: "handleVisual", origin, content });
671
+ },
672
+ write_file: (event) => {
673
+ let payload;
674
+ try {
675
+ payload = event.toJs({ dict_converter: Object.fromEntries });
676
+ } catch (_) {
677
+ payload = event.toJs();
678
+ }
679
+
680
+ const filename = String(payload.filename);
681
+ const content =
682
+ typeof payload.content === "string"
683
+ ? payload.content
684
+ : toStructuredCloneable(payload.content);
685
+ const mode = String(payload.mode);
686
+
687
+ postMessage({ method: "handleFileWrite", filename, content, mode });
688
+ },
689
+ locals: () => pyodide.runPython("globals()"),
690
+ /**
691
+ * Returns a Promise that resolves with the next line of stdin when the main thread
692
+ * sends stdinResponse. Resolves with null on EOF (e.g. Ctrl+D).
693
+ * Used when SharedArrayBuffer is unavailable (no COOP/COEP) so we cannot block via Atomics.wait.
694
+ * Requires JSPI and runPythonAsync() for run_sync() to work.
695
+ */
696
+ getInputAsync: () => {
697
+ const promise = new Promise((resolve) => {
698
+ pendingStdinResolve = resolve;
699
+ });
700
+ postMessage({ method: "handleInput" });
701
+ return promise;
702
+ },
703
+ },
704
+ };
705
+
706
+ const readInputViaSyncXhr = (runId) => {
707
+ // Mark any failure with XHR setup as stdinTransportFailed for a clean error
708
+ try {
709
+ const requestId = crypto.randomUUID();
710
+ const requestUrl = new URL(
711
+ stdinFallbackConfig.endpointPath,
712
+ // eslint-disable-next-line no-restricted-globals
713
+ self.location.origin,
714
+ );
715
+ requestUrl.searchParams.set("runId", runId || "");
716
+ requestUrl.searchParams.set("requestId", requestId);
717
+ requestUrl.searchParams.set("clientId", stdinFallbackConfig.clientId);
718
+
719
+ const xhr = new XMLHttpRequest();
720
+ xhr.open("GET", requestUrl.toString(), false);
721
+ xhr.setRequestHeader("X-Pyodide-Stdin-Request", "true");
722
+ xhr.setRequestHeader("Cache-Control", "no-store");
723
+ xhr.send(null);
724
+
725
+ if (xhr.status === STDIN_CANCELLED_RESPONSE_CODE) {
726
+ stdinCancelled = true;
727
+ throw new Error("PYODIDE_STDIN_CANCELLED");
728
+ }
729
+
730
+ if (xhr.status === STDIN_TIMEOUT_RESPONSE_CODE) {
731
+ stdinTransportFailed = true;
732
+ throw new Error("PYODIDE_STDIN_TIMEOUT");
733
+ }
734
+
735
+ if (xhr.status === STDIN_ABORTED_RESPONSE_CODE) {
736
+ stdinTransportFailed = true;
737
+ throw new Error("PYODIDE_STDIN_ABORTED");
738
+ }
739
+
740
+ // EOF: orchestrator signalled Ctrl+D / no more input. Returning null tells
741
+ // Pyodide's line-based stdin handler to raise EOFError at the input() call
742
+ // site, rather than escalating to a KeyboardInterrupt that tears down the run.
743
+ if (xhr.status === STDIN_EOF_RESPONSE_CODE) {
744
+ return null;
745
+ }
746
+
747
+ if (xhr.status !== 200) {
748
+ throw new Error(
749
+ `Python input request failed with status ${xhr.status}: ${xhr.responseText}`,
750
+ );
751
+ }
752
+
753
+ let content = xhr.responseText ?? "";
754
+ if (!content.endsWith("\n")) {
755
+ content += "\n";
756
+ }
757
+
758
+ return content;
759
+ } catch (err) {
760
+ if (!stdinCancelled) {
761
+ stdinTransportFailed = true;
762
+ }
763
+ throw err;
764
+ }
765
+ };
766
+
767
+ const clearPyodideData = async (userModuleNames) => {
768
+ postMessage({ method: "handleLoading" });
769
+ try {
770
+ await pyodide.runPythonAsync(`
771
+ import builtins
772
+ import sys
773
+
774
+ # Restore the real open before cleaning up
775
+ if hasattr(builtins, "_original_open"):
776
+ builtins.open = builtins._original_open
777
+
778
+ # Remove user modules from sys.modules
779
+ user_modules = ${JSON.stringify(userModuleNames)}
780
+ for name in user_modules:
781
+ if name in sys.modules:
782
+ del sys.modules[name]
783
+
784
+ # Clear all user-defined variables and modules
785
+ for name in list(globals()):
786
+ if not name.startswith('_') and name not in ('basthon', 'sys', 'builtins'):
787
+ del globals()[name]
788
+ `);
789
+ } catch (error) {
790
+ console.error("Error while clearing Pyodide data:", error);
791
+ }
792
+ console.log("clearPyodideData done");
793
+ postMessage({
794
+ method: "handleLoaded",
795
+ stdinBuffer,
796
+ interruptBuffer,
797
+ stdinStrategy,
798
+ supportsJspi,
799
+ stdinFallbackEnabled: Boolean(stdinFallbackConfig?.enabled),
800
+ });
801
+ };
802
+
803
+ const initialisePyodide = async (data) => {
804
+ postMessage({ method: "handleLoading" });
805
+
806
+ pyodidePromise = loadPyodide({
807
+ stdout: (content) => {
808
+ if (stopped) return;
809
+ if (userStdStreamsEnabled && !suppressInternalStdStreams) {
810
+ postMessage({ method: "handleOutput", stream: "stdout", content });
811
+ } else {
812
+ console.log(content);
813
+ }
814
+ },
815
+ stderr: (content) => {
816
+ if (stopped) return;
817
+ if (userStdStreamsEnabled && !suppressInternalStdStreams) {
818
+ postMessage({ method: "handleOutput", stream: "stderr", content });
819
+ } else {
820
+ console.error(content);
821
+ }
822
+ },
823
+ });
824
+
825
+ pyodide = await pyodidePromise;
826
+
827
+ pyodide.registerJsModule("basthon", fakeBasthonPackage);
828
+
829
+ supportsJspi =
830
+ typeof WebAssembly?.Suspending === "function" &&
831
+ typeof WebAssembly?.promising === "function";
832
+
833
+ stdinFallbackConfig = {
834
+ enabled: Boolean(data?.stdinFallback?.enabled),
835
+ endpointPath: data?.stdinFallback?.endpointPath || "/pyodide-stdin",
836
+ clientId: data?.stdinFallback?.clientId || "",
837
+ };
838
+
839
+ // When SharedArrayBuffer is unavailable, always use the postMessage-based
840
+ // stdin path. JSPI / run_sync will raise at runtime if the environment
841
+ // cannot stack-switch, but in JSPI-capable browsers this enables
842
+ // interactive input() without COOP/COEP.
843
+ useMessageStdin = !supportsAllFeatures && supportsJspi;
844
+ useSyncXhrStdin =
845
+ !supportsAllFeatures && !supportsJspi && stdinFallbackConfig.enabled;
846
+ stdinStrategy = supportsAllFeatures
847
+ ? "sab"
848
+ : useMessageStdin
849
+ ? "jspi"
850
+ : useSyncXhrStdin
851
+ ? "sync-xhr"
852
+ : "unavailable";
853
+ pyodide.globals.set("__stdin_via_message__", useMessageStdin);
854
+
855
+ await pyodide.runPythonAsync(`
856
+ import builtins
857
+ from pyodide.ffi import run_sync as __run_sync__
858
+
859
+ __old_input__ = builtins.input
860
+ def __patched_input__(prompt=None):
861
+ if prompt is not None:
862
+ print(prompt)
863
+ if __stdin_via_message__:
864
+ result = __run_sync__(basthon.kernel.getInputAsync())
865
+ if result is None:
866
+ raise EOFError("EOF when reading a line")
867
+ return result
868
+ return __old_input__()
869
+ builtins.input = __patched_input__
870
+ `);
871
+
872
+ await pyodide.runPythonAsync(`
873
+ import builtins
874
+ # Save the original open function only once
875
+ if not hasattr(builtins, "_original_open"):
876
+ builtins._original_open = builtins.open
877
+ _original_open = builtins._original_open
878
+ `);
879
+
880
+ await pyodide.loadPackage("pyodide-http");
881
+ await pyodide.runPythonAsync(`
882
+ import pyodide_http
883
+ pyodide_http.patch_all()
884
+ `);
885
+
886
+ if (supportsAllFeatures) {
887
+ stdinBuffer =
888
+ stdinBuffer || new Int32Array(new SharedArrayBuffer(1024 * 1024)); // 1 MiB
889
+ stdinBuffer[0] = 1; // Store the length of content in the buffer at index 0.
890
+ pyodide.setStdin({ isatty: true, read: readFromStdin });
891
+
892
+ interruptBuffer =
893
+ interruptBuffer || new Uint8Array(new SharedArrayBuffer(1));
894
+ pyodide.setInterruptBuffer(interruptBuffer);
895
+ } else if (useSyncXhrStdin) {
896
+ pyodide.setStdin({
897
+ stdin: () => readInputViaSyncXhr(currentRunId),
898
+ isatty: true,
899
+ });
900
+ }
901
+
902
+ // From this point on, anything written to stdout / stderr is considered
903
+ // user-visible and will be forwarded to the UI.
904
+ userStdStreamsEnabled = true;
905
+
906
+ postMessage({
907
+ method: "handleLoaded",
908
+ stdinBuffer,
909
+ interruptBuffer,
910
+ stdinStrategy,
911
+ supportsJspi,
912
+ stdinFallbackEnabled: stdinFallbackConfig.enabled,
913
+ });
914
+ };
915
+
916
+ const readFromStdin = (bufferToWrite) => {
917
+ const previousLength = stdinBuffer[0];
918
+ postMessage({ method: "handleInput" });
919
+
920
+ while (true) {
921
+ pyodide.checkInterrupt();
922
+ const result = Atomics.wait(stdinBuffer, 0, previousLength, 100);
923
+ if (result === "not-equal") {
924
+ break;
925
+ }
926
+ }
927
+
928
+ const currentLength = stdinBuffer[0];
929
+ if (currentLength === -1) {
930
+ return 0;
931
+ } // Signals that stdin was closed.
932
+
933
+ const addedBytes = stdinBuffer.slice(previousLength, currentLength);
934
+ bufferToWrite.set(addedBytes);
935
+
936
+ return addedBytes.length;
937
+ };
938
+
939
+ const parsePythonError = (error) => {
940
+ const type = error.type;
941
+ const [trace, info] = error.message.split(`${type}:`).map((s) => s?.trim());
942
+
943
+ const lines = trace.split("\n");
944
+
945
+ // if the third from last line matches /File "__custom_open__\.py", line (\d+)/g then strip off the last three lines
946
+ if (
947
+ lines.length > 3 &&
948
+ /File "__custom_open__\.py", line (\d+)/g.test(lines[lines.length - 3])
949
+ ) {
950
+ lines.splice(-3, 3);
951
+ }
952
+
953
+ const snippetLine = lines[lines.length - 2]; // print("hi")invalid
954
+ const caretLine = lines[lines.length - 1]; // ^^^^^^^
955
+
956
+ const showsMistake = caretLine.includes("^");
957
+ const mistake = showsMistake
958
+ ? [snippetLine.slice(4), caretLine.slice(4)].join("\n")
959
+ : "";
960
+
961
+ const matches = [
962
+ ...trace.matchAll(/File "(?!__custom_open__\.py)(.*)", line (\d+)/g),
963
+ ];
964
+ const match = matches[matches.length - 1];
965
+
966
+ const path = match ? match[1] : "";
967
+ const base = path.split("/").reverse()[0];
968
+ const file = base === "<exec>" ? "main.py" : base;
969
+
970
+ const line = match ? parseInt(match[2], 10) : "";
971
+
972
+ return { file, line, mistake, type, info };
973
+ };
974
+ };
975
+
976
+ globalThis.PyodideWorker = PyodideWorker;
977
+ PyodideWorker();