cpython/Tools/wasm/emscripten/web_example_pyrepl_jspi/src.mjs
Hood Chatham c933a6bb32
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);
}