gh-127111: Apply prettier formatter to Emscripten web example (#127551)

Cleaned up formatting (and a stray closing tag) of the web example HTML and JS.
This commit is contained in:
Hood Chatham 2024-12-05 01:25:06 +01:00 committed by GitHub
parent 94b8f8b409
commit 2f1cee8477
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 490 additions and 425 deletions

View file

@ -1,18 +1,23 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="author" content="Katie Bell"> <meta name="author" content="Katie Bell" />
<meta name="description" content="Simple REPL for Python WASM"> <meta name="description" content="Simple REPL for Python WASM" />
<title>wasm-python terminal</title> <title>wasm-python terminal</title>
<link rel="stylesheet" href="https://unpkg.com/xterm@4.18.0/css/xterm.css" crossorigin integrity="sha384-4eEEn/eZgVHkElpKAzzPx/Kow/dTSgFk1BNe+uHdjHa+NkZJDh5Vqkq31+y7Eycd"/> <link
rel="stylesheet"
href="https://unpkg.com/xterm@4.18.0/css/xterm.css"
crossorigin
integrity="sha384-4eEEn/eZgVHkElpKAzzPx/Kow/dTSgFk1BNe+uHdjHa+NkZJDh5Vqkq31+y7Eycd"
/>
<style> <style>
body { body {
font-family: arial; font-family: arial;
max-width: 800px; max-width: 800px;
margin: 0 auto margin: 0 auto;
} }
#code { #code {
width: 100%; width: 100%;
@ -32,94 +37,113 @@
padding: 6px 18px; padding: 6px 18px;
} }
</style> </style>
<script src="https://unpkg.com/xterm@4.18.0/lib/xterm.js" crossorigin integrity="sha384-yYdNmem1ioP5Onm7RpXutin5A8TimLheLNQ6tnMi01/ZpxXdAwIm2t4fJMx1Djs+"/></script> <script
src="https://unpkg.com/xterm@4.18.0/lib/xterm.js"
crossorigin
integrity="sha384-yYdNmem1ioP5Onm7RpXutin5A8TimLheLNQ6tnMi01/ZpxXdAwIm2t4fJMx1Djs+"
/>
<script type="module"> <script type="module">
class WorkerManager { class WorkerManager {
constructor(workerURL, standardIO, readyCallBack, finishedCallback) { constructor(
this.workerURL = workerURL workerURL,
this.worker = null standardIO,
this.standardIO = standardIO readyCallBack,
this.readyCallBack = readyCallBack finishedCallback,
this.finishedCallback = finishedCallback ) {
this.workerURL = workerURL;
this.worker = null;
this.standardIO = standardIO;
this.readyCallBack = readyCallBack;
this.finishedCallback = finishedCallback;
this.initialiseWorker() this.initialiseWorker();
} }
async initialiseWorker() { async initialiseWorker() {
if (!this.worker) { if (!this.worker) {
this.worker = new Worker(this.workerURL, {type: "module"}) this.worker = new Worker(this.workerURL, {
this.worker.addEventListener('message', this.handleMessageFromWorker) type: "module",
});
this.worker.addEventListener(
"message",
this.handleMessageFromWorker,
);
} }
} }
async run(options) { async run(options) {
this.worker.postMessage({ this.worker.postMessage({
type: 'run', type: "run",
args: options.args || [], args: options.args || [],
files: options.files || {} files: options.files || {},
}) });
} }
reset() { reset() {
if (this.worker) { if (this.worker) {
this.worker.terminate() this.worker.terminate();
this.worker = null this.worker = null;
} }
this.standardIO.message('Worker process terminated.') this.standardIO.message("Worker process terminated.");
this.initialiseWorker() this.initialiseWorker();
} }
handleStdinData(inputValue) { handleStdinData(inputValue) {
if (this.stdinbuffer && this.stdinbufferInt) { if (this.stdinbuffer && this.stdinbufferInt) {
let startingIndex = 1 let startingIndex = 1;
if (this.stdinbufferInt[0] > 0) { if (this.stdinbufferInt[0] > 0) {
startingIndex = this.stdinbufferInt[0] startingIndex = this.stdinbufferInt[0];
} }
const data = new TextEncoder().encode(inputValue) const data = new TextEncoder().encode(inputValue);
data.forEach((value, index) => { data.forEach((value, index) => {
this.stdinbufferInt[startingIndex + index] = value this.stdinbufferInt[startingIndex + index] = value;
}) });
this.stdinbufferInt[0] = startingIndex + data.length - 1 this.stdinbufferInt[0] =
Atomics.notify(this.stdinbufferInt, 0, 1) startingIndex + data.length - 1;
Atomics.notify(this.stdinbufferInt, 0, 1);
} }
} }
handleMessageFromWorker = (event) => { handleMessageFromWorker = (event) => {
const type = event.data.type const type = event.data.type;
if (type === 'ready') { if (type === "ready") {
this.readyCallBack() this.readyCallBack();
} else if (type === 'stdout') { } else if (type === "stdout") {
this.standardIO.stdout(event.data.stdout) this.standardIO.stdout(event.data.stdout);
} else if (type === 'stderr') { } else if (type === "stderr") {
this.standardIO.stderr(event.data.stderr) this.standardIO.stderr(event.data.stderr);
} else if (type === 'stdin') { } else if (type === "stdin") {
// Leave it to the terminal to decide whether to chunk it into lines // Leave it to the terminal to decide whether to chunk it into lines
// or send characters depending on the use case. // or send characters depending on the use case.
this.stdinbuffer = event.data.buffer this.stdinbuffer = event.data.buffer;
this.stdinbufferInt = new Int32Array(this.stdinbuffer) this.stdinbufferInt = new Int32Array(this.stdinbuffer);
this.standardIO.stdin().then((inputValue) => { this.standardIO.stdin().then((inputValue) => {
this.handleStdinData(inputValue) this.handleStdinData(inputValue);
}) });
} else if (type === 'finished') { } else if (type === "finished") {
this.standardIO.message(`Exited with status: ${event.data.returnCode}`) this.standardIO.message(
this.finishedCallback() `Exited with status: ${event.data.returnCode}`,
} );
this.finishedCallback();
} }
};
} }
class WasmTerminal { class WasmTerminal {
constructor() { constructor() {
this.inputBuffer = new BufferQueue(); this.inputBuffer = new BufferQueue();
this.input = '' this.input = "";
this.resolveInput = null this.resolveInput = null;
this.activeInput = false this.activeInput = false;
this.inputStartCursor = null this.inputStartCursor = null;
this.xterm = new Terminal( this.xterm = new Terminal({
{ scrollback: 10000, fontSize: 14, theme: { background: '#1a1c1f' }, cols: 100} scrollback: 10000,
); fontSize: 14,
theme: { background: "#1a1c1f" },
cols: 100,
});
this.xterm.onKey((keyEvent) => { this.xterm.onKey((keyEvent) => {
// Fix for iOS Keyboard Jumping on space // Fix for iOS Keyboard Jumping on space
@ -128,7 +152,7 @@ class WasmTerminal {
} }
}); });
this.xterm.onData(this.handleTermData) this.xterm.onData(this.handleTermData);
} }
open(container) { open(container) {
@ -137,16 +161,16 @@ class WasmTerminal {
handleTermData = (data) => { handleTermData = (data) => {
const ord = data.charCodeAt(0); const ord = data.charCodeAt(0);
data = data.replace(/\r(?!\n)/g, "\n") // Convert lone CRs to LF data = data.replace(/\r(?!\n)/g, "\n"); // Convert lone CRs to LF
// Handle pasted data // Handle pasted data
if (data.length > 1 && data.includes("\n")) { if (data.length > 1 && data.includes("\n")) {
let alreadyWrittenChars = 0; let alreadyWrittenChars = 0;
// If line already had data on it, merge pasted data with it // If line already had data on it, merge pasted data with it
if (this.input != '') { if (this.input != "") {
this.inputBuffer.addData(this.input); this.inputBuffer.addData(this.input);
alreadyWrittenChars = this.input.length; alreadyWrittenChars = this.input.length;
this.input = ''; this.input = "";
} }
this.inputBuffer.addData(data); this.inputBuffer.addData(data);
// If input is active, write the first line // If input is active, write the first line
@ -173,8 +197,10 @@ class WasmTerminal {
case "\n": // ENTER case "\n": // ENTER
case "\x0a": // CTRL+J case "\x0a": // CTRL+J
case "\x0d": // CTRL+M case "\x0d": // CTRL+M
this.resolveInput(this.input + this.writeLine('\n')); this.resolveInput(
this.input = ''; this.input + this.writeLine("\n"),
);
this.input = "";
this.activeInput = false; this.activeInput = false;
break; break;
case "\x7F": // BACKSPACE case "\x7F": // BACKSPACE
@ -183,58 +209,70 @@ class WasmTerminal {
break; break;
case "\x04": // CTRL+D case "\x04": // CTRL+D
// Send empty input // Send empty input
if (this.input === '') { if (this.input === "") {
this.resolveInput('') this.resolveInput("");
this.activeInput = false; this.activeInput = false;
} }
} }
} else { } else {
this.handleCursorInsert(data); this.handleCursorInsert(data);
} }
} };
writeLine(line) { writeLine(line) {
this.xterm.write(line.slice(0, -1)) this.xterm.write(line.slice(0, -1));
this.xterm.write('\r\n'); this.xterm.write("\r\n");
return line; return line;
} }
handleCursorInsert(data) { handleCursorInsert(data) {
this.input += data; this.input += data;
this.xterm.write(data) this.xterm.write(data);
} }
handleCursorErase() { handleCursorErase() {
// Don't delete past the start of input // Don't delete past the start of input
if (this.xterm.buffer.active.cursorX <= this.inputStartCursor) { if (
return this.xterm.buffer.active.cursorX <=
this.inputStartCursor
) {
return;
} }
this.input = this.input.slice(0, -1) this.input = this.input.slice(0, -1);
this.xterm.write('\x1B[D') this.xterm.write("\x1B[D");
this.xterm.write('\x1B[P') this.xterm.write("\x1B[P");
} }
prompt = async () => { prompt = async () => {
this.activeInput = true this.activeInput = true;
// Hack to allow stdout/stderr to finish before we figure out where input starts // Hack to allow stdout/stderr to finish before we figure out where input starts
setTimeout(() => {this.inputStartCursor = this.xterm.buffer.active.cursorX}, 1) setTimeout(() => {
this.inputStartCursor =
this.xterm.buffer.active.cursorX;
}, 1);
// If line buffer has a line ready, send it immediately // If line buffer has a line ready, send it immediately
if (this.inputBuffer.hasLineReady()) { if (this.inputBuffer.hasLineReady()) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
resolve(this.writeLine(this.inputBuffer.nextLine())); resolve(
this.writeLine(this.inputBuffer.nextLine()),
);
this.activeInput = false; this.activeInput = false;
}) });
// If line buffer has an incomplete line, use it for the active line // If line buffer has an incomplete line, use it for the active line
} else if (this.inputBuffer.lastLineIsIncomplete()) { } else if (this.inputBuffer.lastLineIsIncomplete()) {
// Hack to ensure cursor input start doesn't end up after user input // Hack to ensure cursor input start doesn't end up after user input
setTimeout(() => {this.handleCursorInsert(this.inputBuffer.nextLine())}, 1); setTimeout(() => {
this.handleCursorInsert(
this.inputBuffer.nextLine(),
);
}, 1);
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.resolveInput = (value) => { this.resolveInput = (value) => {
resolve(value) resolve(value);
} };
}) });
} };
clear() { clear() {
this.xterm.clear(); this.xterm.clear();
@ -251,111 +289,129 @@ class WasmTerminal {
class BufferQueue { class BufferQueue {
constructor(xterm) { constructor(xterm) {
this.buffer = [] this.buffer = [];
} }
isEmpty() { isEmpty() {
return this.buffer.length == 0 return this.buffer.length == 0;
} }
lastLineIsIncomplete() { lastLineIsIncomplete() {
return !this.isEmpty() && !this.buffer[this.buffer.length-1].endsWith("\n") return (
!this.isEmpty() &&
!this.buffer[this.buffer.length - 1].endsWith("\n")
);
} }
hasLineReady() { hasLineReady() {
return !this.isEmpty() && this.buffer[0].endsWith("\n") return !this.isEmpty() && this.buffer[0].endsWith("\n");
} }
addData(data) { addData(data) {
let lines = data.match(/.*(\n|$)/g) let lines = data.match(/.*(\n|$)/g);
if (this.lastLineIsIncomplete()) { if (this.lastLineIsIncomplete()) {
this.buffer[this.buffer.length-1] += lines.shift() this.buffer[this.buffer.length - 1] += lines.shift();
} }
for (let line of lines) { for (let line of lines) {
this.buffer.push(line) this.buffer.push(line);
} }
} }
nextLine() { nextLine() {
return this.buffer.shift() return this.buffer.shift();
} }
} }
const runButton = document.getElementById('run') const runButton = document.getElementById("run");
const replButton = document.getElementById('repl') const replButton = document.getElementById("repl");
const stopButton = document.getElementById('stop') const stopButton = document.getElementById("stop");
const clearButton = document.getElementById('clear') const clearButton = document.getElementById("clear");
const codeBox = document.getElementById('codebox') const codeBox = document.getElementById("codebox");
window.onload = () => { window.onload = () => {
const terminal = new WasmTerminal() const terminal = new WasmTerminal();
terminal.open(document.getElementById('terminal')) terminal.open(document.getElementById("terminal"));
const stdio = { const stdio = {
stdout: (charCode) => { terminal.print(charCode) }, stdout: (charCode) => {
stderr: (charCode) => { terminal.print(charCode) }, terminal.print(charCode);
stdin: async () => {
return await terminal.prompt()
}, },
message: (text) => { terminal.writeLine(`\r\n${text}\r\n`) }, stderr: (charCode) => {
} terminal.print(charCode);
},
stdin: async () => {
return await terminal.prompt();
},
message: (text) => {
terminal.writeLine(`\r\n${text}\r\n`);
},
};
const programRunning = (isRunning) => { const programRunning = (isRunning) => {
if (isRunning) { if (isRunning) {
replButton.setAttribute('disabled', true) replButton.setAttribute("disabled", true);
runButton.setAttribute('disabled', true) runButton.setAttribute("disabled", true);
stopButton.removeAttribute('disabled') stopButton.removeAttribute("disabled");
} else { } else {
replButton.removeAttribute('disabled') replButton.removeAttribute("disabled");
runButton.removeAttribute('disabled') runButton.removeAttribute("disabled");
stopButton.setAttribute('disabled', true) stopButton.setAttribute("disabled", true);
}
} }
};
runButton.addEventListener('click', (e) => { runButton.addEventListener("click", (e) => {
terminal.clear() terminal.clear();
programRunning(true) programRunning(true);
const code = codeBox.value const code = codeBox.value;
pythonWorkerManager.run({args: ['main.py'], files: {'main.py': code}}) pythonWorkerManager.run({
}) args: ["main.py"],
files: { "main.py": code },
});
});
replButton.addEventListener('click', (e) => { replButton.addEventListener("click", (e) => {
terminal.clear() terminal.clear();
programRunning(true) programRunning(true);
// Need to use "-i -" to force interactive mode. // Need to use "-i -" to force interactive mode.
// Looks like isatty always returns false in emscripten // Looks like isatty always returns false in emscripten
pythonWorkerManager.run({args: ['-i', '-'], files: {}}) pythonWorkerManager.run({ args: ["-i", "-"], files: {} });
}) });
stopButton.addEventListener('click', (e) => { stopButton.addEventListener("click", (e) => {
programRunning(false) programRunning(false);
pythonWorkerManager.reset() pythonWorkerManager.reset();
}) });
clearButton.addEventListener('click', (e) => { clearButton.addEventListener("click", (e) => {
terminal.clear() terminal.clear();
}) });
const readyCallback = () => { const readyCallback = () => {
replButton.removeAttribute('disabled') replButton.removeAttribute("disabled");
runButton.removeAttribute('disabled') runButton.removeAttribute("disabled");
clearButton.removeAttribute('disabled') clearButton.removeAttribute("disabled");
} };
const finishedCallback = () => { const finishedCallback = () => {
programRunning(false) programRunning(false);
} };
const pythonWorkerManager = new WorkerManager('./python.worker.mjs', stdio, readyCallback, finishedCallback) const pythonWorkerManager = new WorkerManager(
} "./python.worker.mjs",
stdio,
readyCallback,
finishedCallback,
);
};
</script> </script>
</head> </head>
<body> <body>
<h1>Simple REPL for Python WASM</h1> <h1>Simple REPL for Python WASM</h1>
<textarea id="codebox" cols="108" rows="16"> <textarea id="codebox" cols="108" rows="16">
print('Welcome to WASM!') print('Welcome to WASM!')
</textarea> </textarea
>
<div class="button-container"> <div class="button-container">
<button id="run" disabled>Run</button> <button id="run" disabled>Run</button>
<button id="repl" disabled>Start REPL</button> <button id="repl" disabled>Start REPL</button>
@ -365,9 +421,13 @@ print('Welcome to WASM!')
<div id="terminal"></div> <div id="terminal"></div>
<div id="info"> <div id="info">
The simple REPL provides a limited Python experience in the browser. The simple REPL provides a limited Python experience in the browser.
<a href="https://github.com/python/cpython/blob/main/Tools/wasm/README.md"> <a
Tools/wasm/README.md</a> contains a list of known limitations and href="https://github.com/python/cpython/blob/main/Tools/wasm/README.md"
issues. Networking, subprocesses, and threading are not available. >
Tools/wasm/README.md
</a>
contains a list of known limitations and issues. Networking,
subprocesses, and threading are not available.
</div> </div>
</body> </body>
</html> </html>

View file

@ -2,64 +2,64 @@ import createEmscriptenModule from "./python.mjs";
class StdinBuffer { class StdinBuffer {
constructor() { constructor() {
this.sab = new SharedArrayBuffer(128 * Int32Array.BYTES_PER_ELEMENT) this.sab = new SharedArrayBuffer(128 * Int32Array.BYTES_PER_ELEMENT);
this.buffer = new Int32Array(this.sab) this.buffer = new Int32Array(this.sab);
this.readIndex = 1; this.readIndex = 1;
this.numberOfCharacters = 0; this.numberOfCharacters = 0;
this.sentNull = true this.sentNull = true;
} }
prompt() { prompt() {
this.readIndex = 1 this.readIndex = 1;
Atomics.store(this.buffer, 0, -1) Atomics.store(this.buffer, 0, -1);
postMessage({ postMessage({
type: 'stdin', type: "stdin",
buffer: this.sab buffer: this.sab,
}) });
Atomics.wait(this.buffer, 0, -1) Atomics.wait(this.buffer, 0, -1);
this.numberOfCharacters = this.buffer[0] this.numberOfCharacters = this.buffer[0];
} }
stdin = () => { stdin = () => {
while (this.numberOfCharacters + 1 === this.readIndex) { while (this.numberOfCharacters + 1 === this.readIndex) {
if (!this.sentNull) { if (!this.sentNull) {
// Must return null once to indicate we're done for now. // Must return null once to indicate we're done for now.
this.sentNull = true this.sentNull = true;
return null return null;
} }
this.sentNull = false this.sentNull = false;
// Prompt will reset this.readIndex to 1 // Prompt will reset this.readIndex to 1
this.prompt() this.prompt();
}
const char = this.buffer[this.readIndex]
this.readIndex += 1
return char
} }
const char = this.buffer[this.readIndex];
this.readIndex += 1;
return char;
};
} }
const stdout = (charCode) => { const stdout = (charCode) => {
if (charCode) { if (charCode) {
postMessage({ postMessage({
type: 'stdout', type: "stdout",
stdout: charCode, stdout: charCode,
}) });
} else { } else {
console.log(typeof charCode, charCode) console.log(typeof charCode, charCode);
}
} }
};
const stderr = (charCode) => { const stderr = (charCode) => {
if (charCode) { if (charCode) {
postMessage({ postMessage({
type: 'stderr', type: "stderr",
stderr: charCode, stderr: charCode,
}) });
} else { } else {
console.log(typeof charCode, charCode) console.log(typeof charCode, charCode);
}
} }
};
const stdinBuffer = new StdinBuffer() const stdinBuffer = new StdinBuffer();
const emscriptenSettings = { const emscriptenSettings = {
noInitialRun: true, noInitialRun: true,
@ -67,38 +67,43 @@ const emscriptenSettings = {
stdout: stdout, stdout: stdout,
stderr: stderr, stderr: stderr,
onRuntimeInitialized: () => { onRuntimeInitialized: () => {
postMessage({type: 'ready', stdinBuffer: stdinBuffer.sab}) postMessage({ type: "ready", stdinBuffer: stdinBuffer.sab });
}, },
async preRun(Module) { async preRun(Module) {
const versionHex = Module.HEAPU32[Module._Py_Version / 4].toString(16); const versionHex = Module.HEAPU32[Module._Py_Version / 4].toString(16);
const versionTuple = versionHex.padStart(8, "0").match(/.{1,2}/g).map((x) => parseInt(x, 16)); const versionTuple = versionHex
.padStart(8, "0")
.match(/.{1,2}/g)
.map((x) => parseInt(x, 16));
const [major, minor, ..._] = versionTuple; const [major, minor, ..._] = versionTuple;
// Prevent complaints about not finding exec-prefix by making a lib-dynload directory // Prevent complaints about not finding exec-prefix by making a lib-dynload directory
Module.FS.mkdirTree(`/lib/python${major}.${minor}/lib-dynload/`); Module.FS.mkdirTree(`/lib/python${major}.${minor}/lib-dynload/`);
Module.addRunDependency("install-stdlib"); Module.addRunDependency("install-stdlib");
const resp = await fetch(`python${major}.${minor}.zip`); const resp = await fetch(`python${major}.${minor}.zip`);
const stdlibBuffer = await resp.arrayBuffer(); const stdlibBuffer = await resp.arrayBuffer();
Module.FS.writeFile(`/lib/python${major}${minor}.zip`, new Uint8Array(stdlibBuffer), { canOwn: true }); Module.FS.writeFile(
`/lib/python${major}${minor}.zip`,
new Uint8Array(stdlibBuffer),
{ canOwn: true },
);
Module.removeRunDependency("install-stdlib"); Module.removeRunDependency("install-stdlib");
} },
} };
const modulePromise = createEmscriptenModule(emscriptenSettings); const modulePromise = createEmscriptenModule(emscriptenSettings);
onmessage = async (event) => { onmessage = async (event) => {
if (event.data.type === 'run') { if (event.data.type === "run") {
const Module = await modulePromise; const Module = await modulePromise;
if (event.data.files) { if (event.data.files) {
for (const [filename, contents] of Object.entries(event.data.files)) { for (const [filename, contents] of Object.entries(event.data.files)) {
Module.FS.writeFile(filename, contents) Module.FS.writeFile(filename, contents);
} }
} }
const ret = Module.callMain(event.data.args); const ret = Module.callMain(event.data.args);
postMessage({ postMessage({
type: 'finished', type: "finished",
returnCode: ret returnCode: ret,
}) });
} }
} };