Skip to content

Commit

Permalink
Merge pull request #18 from uvapl/feature/new-stdin
Browse files Browse the repository at this point in the history
✨ stdin ✨
  • Loading branch information
stgm authored Apr 19, 2024
2 parents 8157d84 + b5503eb commit 0cb2294
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 30 deletions.
67 changes: 67 additions & 0 deletions static/js/layout-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ function getAllEditorFiles() {
}));
}

/**
* Disposes the user input when active. This is actived once user input is
* requested through the `waitForInput` function.
*/
function disposeUserInput() {
if (isObject(window._userInputDisposable) && typeof window._userInputDisposable.dispose === 'function') {
window._userInputDisposable.dispose();
window._userInputDisposable = null;
}
}

/**
* Runs the code inside the worker by sending all files to the worker along with
* the current active tab name.
Expand All @@ -40,6 +51,9 @@ function runCode(clearTerm = false) {
if ($('#run-code').prop('disabled')) {
return;
} else if (window._workerApi.isRunningCode) {
hideTermCursor();
term.write('\nProcess terminated\n');
disposeUserInput();
return window._workerApi.terminate();
}

Expand Down Expand Up @@ -239,6 +253,58 @@ function showTermCursor() {
term.write('\x1b[?25h');
}

/**
* Enable stdin in the terminal and record the user's keystrokes. Once the user
* presses ENTER, the promise is resolved with the user's input.
*
* @returns {Promise<string>} The user's input.
*/
function waitForInput() {
return new Promise(resolve => {
// Immediately focus the terminal when user input is requested.
showTermCursor();
term.focus();

// Disable some special characters.
// For all input sequences, see http://xtermjs.org/docs/api/vtfeatures/#c0
const blacklistedKeys = [
'\u007f', // Backspace
'\t', // Tab
'\r', // Enter
]

// Keep track of the value that is typed by the user.
let value = '';
window._userInputDisposable = term.onKey(e => {
// Only append allowed characters.
if (!blacklistedKeys.includes(e.key)) {
term.write(e.key);
value += e.key;
}

// Remove the last character when pressing backspace. This is done by
// triggering a backspace '\b' character and then insert a space at that
// position to clear the character.
if (e.key === '\u007f' && value.length > 0) {
term.write('\b \b');
value = value.slice(0, -1);
}

// If the user presses enter, resolve the promise.
if (e.key === '\r') {
disposeUserInput();

// Trigger a real enter in the terminal.
term.write('\n');
value += '\n';

hideTermCursor();
resolve(value);
}
});
});
}

let term;
const fitAddon = new FitAddon.FitAddon();
function TerminalComponent(container, state) {
Expand All @@ -259,6 +325,7 @@ function TerminalComponent(container, state) {
term = new Terminal({
convertEol: true,
disableStdin: true,
cursorBlink: true,
fontSize,
lineHeight: 1.2
})
Expand Down
42 changes: 41 additions & 1 deletion static/js/worker-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,30 @@
class WorkerAPI {
proglang = null;
isRunningCode = false;
sharedMem = null;

constructor(proglang) {
this.proglang = proglang;
this._createWorker();
}

/**
* Checks whether the browser enabled support for WebAssembly.Memory object
* usage by trying to create a new SharedArrayBuffer object. This object can
* only be created whenever both the Cross-Origin-Opener-Policy and
* Cross-Origin-Embedder-Policy headers are set.
*
* @returns {boolean} True if browser supports shared memory, false otherwise.
*/
hasSharedMemEnabled() {
try {
new SharedArrayBuffer(1024);
return true;
} catch (e) {
return false;
}
}

_createWorker() {
if (this.worker) {
this.isRunningCode = false;
Expand All @@ -26,10 +44,18 @@ class WorkerAPI {
this.port = channel.port1;
this.port.onmessage = this.onmessage.bind(this);

if (this.hasSharedMemEnabled()) {
this.sharedMem = new WebAssembly.Memory({
initial: 1,
maximum: 80,
shared: true,
});
}

const remotePort = channel.port2;
this.worker.postMessage({
id: 'constructor',
data: remotePort
data: { remotePort, sharedMem: this.sharedMem },
}, [remotePort]);
}

Expand Down Expand Up @@ -127,6 +153,20 @@ class WorkerAPI {
term.write(event.data.data);
break;

case 'readStdin':
waitForInput().then((value) => {
const view = new Uint8Array(this.sharedMem.buffer);
for (let i = 0; i < value.length; i++) {
// To the shared memory.
view[i] = value.charCodeAt(i);
}
// Set the last byte to the null terminator.
view[value.length] = 0;

Atomics.notify(new Int32Array(this.sharedMem.buffer), 0);
});
break;

case 'runButtonCommandCallback':
$(event.data.selector).prop('disabled', false);
break;
Expand Down
86 changes: 57 additions & 29 deletions static/js/workers/clang.worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,30 +61,30 @@ class Memory {
}
}

read8(o) { return this.u8[o]; }
read32(o) { return this.u32[o >> 2]; }
write8(o, v) { this.u8[o] = v; }
write32(o, v) { this.u32[o >> 2] = v; }
write64(o, vlo, vhi = 0) { this.write32(o, vlo); this.write32(o + 4, vhi); }
read8(offset) { return this.u8[offset]; }
read32(offset) { return this.u32[offset >> 2]; }
write8(offset, val) { this.u8[offset] = val; }
write32(offset, val) { this.u32[offset >> 2] = val; }
write64(offset, vlo, vhi = 0) { this.write32(offset, vlo); this.write32(offset + 4, vhi); }

readStr(o, len) {
return readStr(this.u8, o, len);
readStr(offset, len) {
return readStr(this.u8, offset, len);
}

// Null-terminated string.
writeStr(o, str) {
o += this.write(o, str);
this.write8(o, 0);
writeStr(offset, str) {
offset += this.write(offset, str);
this.write8(offset, 0);
return str.length + 1;
}

write(o, buf) {
write(offset, buf) {
if (buf instanceof ArrayBuffer) {
return this.write(o, new Uint8Array(buf));
return this.write(offset, new Uint8Array(buf));
} else if (typeof buf === 'string') {
return this.write(o, buf.split('').map(x => x.charCodeAt(0)));
return this.write(offset, buf.split('').map(x => x.charCodeAt(0)));
} else {
const dst = new Uint8Array(this.buffer, o, buf.length);
const dst = new Uint8Array(this.buffer, offset, buf.length);
dst.set(buf);
return buf.length;
}
Expand All @@ -95,6 +95,8 @@ class MemFS {
constructor(options) {
const compileStreaming = options.compileStreaming;
this.hostWrite = options.hostWrite;
this.hostRead = options.hostRead;
this.sharedMem = options.sharedMem;
this.stdinStr = options.stdinStr || "";
this.stdinStrPos = 0;
this.memfsFilename = options.memfsFilename;
Expand Down Expand Up @@ -171,28 +173,44 @@ class MemFS {
}

host_read(fd, iovs, iovs_len, nread) {
let str = '';

this.hostRead();
Atomics.wait(new Int32Array(this.sharedMem.buffer), 0, 0);

// Read the value stored in memory.
const sharedMem = new Uint8Array(this.sharedMem.buffer);
for (let i = 0; i < sharedMem.length; i++) {
if (sharedMem[i] === 0) {
// Null terminator found, terminate the loop.
break;
}

str += String.fromCharCode(sharedMem[i]);
}

// Clean shared memory.
sharedMem.fill(0);

this.hostMem_.check();
assert(fd === 0);
let size = 0;

const strLen = str.length;
let bytesWritten = 0;
for (let i = 0; i < iovs_len; ++i) {
const buf = this.hostMem_.read32(iovs);
iovs += 4;
const len = this.hostMem_.read32(iovs);
iovs += 4;
const lenToWrite = Math.min(len, (this.stdinStr.length - this.stdinStrPos));
if (lenToWrite === 0) {
break;
}
this.hostMem_.write(buf, this.stdinStr.substr(this.stdinStrPos, lenToWrite));
size += lenToWrite;
this.stdinStrPos += lenToWrite;
if (lenToWrite !== len) {
break;
}

const remainingBytes = strLen - bytesWritten;
const bytesToWrite = Math.min(len, remainingBytes);
const slice = str.slice(bytesWritten, bytesWritten + bytesToWrite);
this.hostMem_.write(buf, slice);
bytesWritten += bytesToWrite;
}
// For logging
// this.hostWrite("Read "+ size + "bytes, pos: "+ this.stdinStrPos + "\n");
this.hostMem_.write32(nread, size);

this.hostMem_.write32(nread, bytesWritten);
return ESUCCESS;
}

Expand Down Expand Up @@ -429,6 +447,8 @@ class API extends BaseAPI {
super(options);
this.moduleCache = {};
this.readBuffer = options.readBuffer;
this.sharedMem = options.sharedMem;
this.hostRead = options.hostRead;
this.compileStreaming = options.compileStreaming;
this.clangFilename = options.clang || 'clang';
this.lldFilename = options.lld || 'lld';
Expand All @@ -445,6 +465,8 @@ class API extends BaseAPI {
this.memfs = new MemFS({
compileStreaming: this.compileStreaming,
hostWrite: this.hostWrite,
hostRead: this.hostRead,
sharedMem: this.sharedMem,
memfsFilename: options.memfs || 'memfs',
});
this.ready = this.memfs.ready.then(() => {
Expand Down Expand Up @@ -571,9 +593,11 @@ let currentApp = null;
const onAnyMessage = async event => {
switch (event.data.id) {
case 'constructor':
port = event.data.data;
port = event.data.data.remotePort;
port.onmessage = onAnyMessage;
api = new API({
sharedMem: event.data.data.sharedMem,

async readBuffer(filename) {
const response = await fetch(filename);
return response.arrayBuffer();
Expand All @@ -588,6 +612,10 @@ const onAnyMessage = async event => {
port.postMessage({ id: 'write', data: s });
},

hostRead() {
port.postMessage({ id: 'readStdin' });
},

readyCallback() {
port.postMessage({ id: 'ready' });
},
Expand Down

0 comments on commit 0cb2294

Please sign in to comment.