gh-97747: Improvements to WASM browser REPL. (#97665)

Improvements to WASM browser REPL.

Adds a text box to write and run code outside the REPL, a stop button, and handling of Ctrl-D for EOF.
This commit is contained in:
Katie Bell 2024-05-31 17:58:46 +10:00 committed by GitHub
parent 0d07182821
commit 010aaa32fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 69 additions and 8 deletions

View File

@ -35,11 +35,12 @@
<script src="https://unpkg.com/xterm@4.18.0/lib/xterm.js" crossorigin integrity="sha384-yYdNmem1ioP5Onm7RpXutin5A8TimLheLNQ6tnMi01/ZpxXdAwIm2t4fJMx1Djs+"/></script>
<script type="module">
class WorkerManager {
constructor(workerURL, standardIO, readyCallBack) {
constructor(workerURL, standardIO, readyCallBack, finishedCallback) {
this.workerURL = workerURL
this.worker = null
this.standardIO = standardIO
this.readyCallBack = readyCallBack
this.finishedCallback = finishedCallback
this.initialiseWorker()
}
@ -59,6 +60,15 @@ class WorkerManager {
})
}
reset() {
if (this.worker) {
this.worker.terminate()
this.worker = null
}
this.standardIO.message('Worker process terminated.')
this.initialiseWorker()
}
handleStdinData(inputValue) {
if (this.stdinbuffer && this.stdinbufferInt) {
let startingIndex = 1
@ -92,7 +102,8 @@ class WorkerManager {
this.handleStdinData(inputValue)
})
} else if (type === 'finished') {
this.standardIO.stderr(`Exited with status: ${event.data.returnCode}\r\n`)
this.standardIO.message(`Exited with status: ${event.data.returnCode}`)
this.finishedCallback()
}
}
}
@ -168,9 +179,14 @@ class WasmTerminal {
break;
case "\x7F": // BACKSPACE
case "\x08": // CTRL+H
case "\x04": // CTRL+D
this.handleCursorErase(true);
break;
case "\x04": // CTRL+D
// Send empty input
if (this.input === '') {
this.resolveInput('')
this.activeInput = false;
}
}
} else {
this.handleCursorInsert(data);
@ -265,9 +281,13 @@ class BufferQueue {
}
}
const runButton = document.getElementById('run')
const replButton = document.getElementById('repl')
const stopButton = document.getElementById('stop')
const clearButton = document.getElementById('clear')
const codeBox = document.getElementById('codebox')
window.onload = () => {
const terminal = new WasmTerminal()
terminal.open(document.getElementById('terminal'))
@ -277,35 +297,72 @@ window.onload = () => {
stderr: (charCode) => { terminal.print(charCode) },
stdin: async () => {
return await terminal.prompt()
},
message: (text) => { terminal.writeLine(`\r\n${text}\r\n`) },
}
const programRunning = (isRunning) => {
if (isRunning) {
replButton.setAttribute('disabled', true)
runButton.setAttribute('disabled', true)
stopButton.removeAttribute('disabled')
} else {
replButton.removeAttribute('disabled')
runButton.removeAttribute('disabled')
stopButton.setAttribute('disabled', true)
}
}
runButton.addEventListener('click', (e) => {
terminal.clear()
programRunning(true)
const code = codeBox.value
pythonWorkerManager.run({args: ['main.py'], files: {'main.py': code}})
})
replButton.addEventListener('click', (e) => {
terminal.clear()
programRunning(true)
// Need to use "-i -" to force interactive mode.
// Looks like isatty always returns false in emscripten
pythonWorkerManager.run({args: ['-i', '-'], files: {}})
})
stopButton.addEventListener('click', (e) => {
programRunning(false)
pythonWorkerManager.reset()
})
clearButton.addEventListener('click', (e) => {
terminal.clear()
})
const readyCallback = () => {
replButton.removeAttribute('disabled')
runButton.removeAttribute('disabled')
clearButton.removeAttribute('disabled')
}
const pythonWorkerManager = new WorkerManager('./python.worker.js', stdio, readyCallback)
const finishedCallback = () => {
programRunning(false)
}
const pythonWorkerManager = new WorkerManager('./python.worker.js', stdio, readyCallback, finishedCallback)
}
</script>
</head>
<body>
<h1>Simple REPL for Python WASM</h1>
<div id="terminal"></div>
<textarea id="codebox" cols="108" rows="16">
print('Welcome to WASM!')
</textarea>
<div class="button-container">
<button id="run" disabled>Run</button>
<button id="repl" disabled>Start REPL</button>
<button id="stop" disabled>Stop</button>
<button id="clear" disabled>Clear</button>
</div>
<div id="terminal"></div>
<div id="info">
The simple REPL provides a limited Python experience in the browser.
<a href="https://github.com/python/cpython/blob/main/Tools/wasm/README.md">

View File

@ -19,18 +19,18 @@ class StdinBuffer {
}
stdin = () => {
if (this.numberOfCharacters + 1 === this.readIndex) {
while (this.numberOfCharacters + 1 === this.readIndex) {
if (!this.sentNull) {
// Must return null once to indicate we're done for now.
this.sentNull = true
return null
}
this.sentNull = false
// Prompt will reset this.readIndex to 1
this.prompt()
}
const char = this.buffer[this.readIndex]
this.readIndex += 1
// How do I send an EOF??
return char
}
}
@ -71,7 +71,11 @@ var Module = {
onmessage = (event) => {
if (event.data.type === 'run') {
// TODO: Set up files from event.data.files
if (event.data.files) {
for (const [filename, contents] of Object.entries(event.data.files)) {
Module.FS.writeFile(filename, contents)
}
}
const ret = callMain(event.data.args)
postMessage({
type: 'finished',