cpython/Tools/wasm/emscripten/web_example_pyrepl_jspi/src.mjs
Hood Chatham c933a6bb32
Some checks are pending
Tests / (push) Blocked by required conditions
Tests / Cross build Linux (push) Blocked by required conditions
Tests / Change detection (push) Waiting to run
Tests / Docs (push) Blocked by required conditions
Tests / Windows MSI (push) Blocked by required conditions
Tests / Check if Autoconf files are up to date (push) Blocked by required conditions
Tests / Check if generated files are up to date (push) Blocked by required conditions
Tests / Ubuntu SSL tests with OpenSSL (push) Blocked by required conditions
Tests / Ubuntu SSL tests with AWS-LC (push) Blocked by required conditions
Tests / WASI (push) Blocked by required conditions
Tests / Hypothesis tests on Ubuntu (push) Blocked by required conditions
Tests / Address sanitizer (push) Blocked by required conditions
Tests / Sanitizers (push) Blocked by required conditions
Tests / CIFuzz (push) Blocked by required conditions
Tests / All required checks pass (push) Blocked by required conditions
Lint / lint (push) Waiting to run
mypy / Run mypy on Lib/_pyrepl (push) Waiting to run
mypy / Run mypy on Lib/test/libregrtest (push) Waiting to run
mypy / Run mypy on Lib/tomllib (push) Waiting to run
mypy / Run mypy on Tools/build (push) Waiting to run
mypy / Run mypy on Tools/cases_generator (push) Waiting to run
mypy / Run mypy on Tools/clinic (push) Waiting to run
mypy / Run mypy on Tools/jit (push) Waiting to run
mypy / Run mypy on Tools/peg_generator (push) Waiting to run
gh-124621: Emscripten: Support pyrepl in browser (GH-136931)
Basic support for pyrepl in Emscripten. Limitations:
* requires JSPI
* no signal handling implemented

As followup work, it would be nice to implement a webworker variant
for when JSPI is not available and proper signal handling.

Because it requires JSPI, it doesn't work in Safari. Firefox requires
setting an experimental flag. All the Chromiums have full support since
May. Until we make it work without JSPI, let's keep the original web_example
around.

Co-authored-by: Łukasz Langa <lukasz@langa.pl>
Co-authored-by: Éric <merwok@netwok.org>
2025-07-22 12:13:38 +02:00

194 lines
4.9 KiB
JavaScript

// Much of this is adapted from here:
// https://github.com/mame/xterm-pty/blob/main/emscripten-pty.js
// Thanks to xterm-pty for making this possible!
import createEmscriptenModule from "./python.mjs";
import { openpty } from "https://unpkg.com/xterm-pty/index.mjs";
import "https://unpkg.com/@xterm/xterm/lib/xterm.js";
var term = new Terminal();
term.open(document.getElementById("terminal"));
const { master, slave: PTY } = openpty();
term.loadAddon(master);
globalThis.PTY = PTY;
async function setupStdlib(Module) {
const versionInt = Module.HEAPU32[Module._Py_Version >>> 2];
const major = (versionInt >>> 24) & 0xff;
const minor = (versionInt >>> 16) & 0xff;
// Prevent complaints about not finding exec-prefix by making a lib-dynload directory
Module.FS.mkdirTree(`/lib/python${major}.${minor}/lib-dynload/`);
const resp = await fetch(`python${major}.${minor}.zip`);
const stdlibBuffer = await resp.arrayBuffer();
Module.FS.writeFile(
`/lib/python${major}${minor}.zip`,
new Uint8Array(stdlibBuffer),
{ canOwn: true },
);
}
const tty_ops = {
ioctl_tcgets: () => {
const termios = PTY.ioctl("TCGETS");
const data = {
c_iflag: termios.iflag,
c_oflag: termios.oflag,
c_cflag: termios.cflag,
c_lflag: termios.lflag,
c_cc: termios.cc,
};
return data;
},
ioctl_tcsets: (_tty, _optional_actions, data) => {
PTY.ioctl("TCSETS", {
iflag: data.c_iflag,
oflag: data.c_oflag,
cflag: data.c_cflag,
lflag: data.c_lflag,
cc: data.c_cc,
});
return 0;
},
ioctl_tiocgwinsz: () => PTY.ioctl("TIOCGWINSZ").reverse(),
get_char: () => {
throw new Error("Should not happen");
},
put_char: () => {
throw new Error("Should not happen");
},
fsync: () => {},
};
const POLLIN = 1;
const POLLOUT = 4;
const waitResult = {
READY: 0,
SIGNAL: 1,
TIMEOUT: 2,
};
function onReadable() {
var handle;
var promise = new Promise((resolve) => {
handle = PTY.onReadable(() => resolve(waitResult.READY));
});
return [promise, handle];
}
function onSignal() {
// TODO: signal handling
var handle = { dispose() {} };
var promise = new Promise((resolve) => {});
return [promise, handle];
}
function onTimeout(timeout) {
var id;
var promise = new Promise((resolve) => {
if (timeout > 0) {
id = setTimeout(resolve, timeout, waitResult.TIMEOUT);
}
});
var handle = {
dispose() {
if (id) {
clearTimeout(id);
}
},
};
return [promise, handle];
}
async function waitForReadable(timeout) {
let p1, p2, p3;
let h1, h2, h3;
try {
[p1, h1] = onReadable();
[p2, h2] = onTimeout(timeout);
[p3, h3] = onSignal();
return await Promise.race([p1, p2, p3]);
} finally {
h1.dispose();
h2.dispose();
h3.dispose();
}
}
const FIONREAD = 0x541b;
const tty_stream_ops = {
async readAsync(stream, buffer, offset, length, pos /* ignored */) {
let readBytes = PTY.read(length);
if (length && !readBytes.length) {
const status = await waitForReadable(-1);
if (status === waitResult.READY) {
readBytes = PTY.read(length);
} else {
throw new Error("Not implemented");
}
}
buffer.set(readBytes, offset);
return readBytes.length;
},
write: (stream, buffer, offset, length) => {
// Note: default `buffer` is for some reason `HEAP8` (signed), while we want unsigned `HEAPU8`.
buffer = new Uint8Array(
buffer.buffer,
buffer.byteOffset,
buffer.byteLength,
);
const toWrite = Array.from(buffer.subarray(offset, offset + length));
PTY.write(toWrite);
return length;
},
async pollAsync(stream, timeout) {
if (!PTY.readable && timeout) {
await waitForReadable(timeout);
}
return (PTY.readable ? POLLIN : 0) | (PTY.writable ? POLLOUT : 0);
},
ioctl(stream, request, varargs) {
if (request === FIONREAD) {
const res = PTY.fromLdiscToUpperBuffer.length;
Module.HEAPU32[varargs / 4] = res;
return 0;
}
throw new Error("Unimplemented ioctl request");
},
};
async function setupStdio(Module) {
Object.assign(Module.TTY.default_tty_ops, tty_ops);
Object.assign(Module.TTY.stream_ops, tty_stream_ops);
}
const emscriptenSettings = {
async preRun(Module) {
Module.addRunDependency("pre-run");
Module.ENV.TERM = "xterm-256color";
// Uncomment next line to turn on tracing (messages go to browser console).
// Module.ENV.PYREPL_TRACE = "1";
// Leak module so we can try to show traceback if we crash on startup
globalThis.Module = Module;
await Promise.all([setupStdlib(Module), setupStdio(Module)]);
Module.removeRunDependency("pre-run");
},
};
try {
await createEmscriptenModule(emscriptenSettings);
} catch (e) {
// Show JavaScript exception and traceback
console.warn(e);
// Show Python exception and traceback
Module.__Py_DumpTraceback(2, Module._PyGILState_GetThisThreadState());
process.exit(1);
}