Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 59 additions & 8 deletions emrun.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,26 @@ def browser_loge(msg):
last_message_time = tick()


def browser_raw_logi(msg):
"""Prints a message to the browser stdout output stream, wihtout adding a newline.
"""
global last_message_time
msg = format_eol(msg)
browser_stdout_handle.write(msg)
browser_stdout_handle.flush()
last_message_time = tick()


def browser_raw_loge(msg):
"""Prints a message to the browser stderr output stream, wihtout adding a newline.
"""
global last_message_time
msg = format_eol(msg)
browser_stderr_handle.write(msg)
browser_stderr_handle.flush()
last_message_time = tick()


def unquote_u(source):
"""Unquotes a unicode string.
(translates ascii-encoded utf string back to utf)
Expand Down Expand Up @@ -669,6 +689,26 @@ def log_message(self, format, *args): # noqa: DC04
if 'favicon.ico' not in msg:
sys.stderr.write(msg)

def do_GET(self):
if self.path == "/in" and emrun_options.interactive:
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Cache-Control", "no-cache")
self.end_headers()
self.wfile.flush()

while True:
ch = sys.stdin.read(1)
if not ch:
self.wfile.write(b"event: close\ndata: bye\n\n")
self.wfile.flush()
return
self.wfile.write(f"data: {ord(ch)}\n\n".encode())
self.wfile.flush()
return

super().do_GET()

def do_POST(self): # # noqa: DC04
global page_exit_code, have_received_messages

Expand Down Expand Up @@ -722,23 +762,30 @@ def do_POST(self): # # noqa: DC04
return
else:
# The user page sent a message with POST. Parse the message and log it to stdout/stderr.
is_stdout = False
is_stderr = False
seq_num = -1
# The html shell is expected to send messages of form ^out^(number)^(message) or ^err^(number)^(message).

trim_index = 0
log = browser_logi
if data.startswith('^err^'):
is_stderr = True
trim_index = 5
log = browser_loge
elif data.startswith('^out^'):
is_stdout = True
if is_stderr or is_stdout:
trim_index = 5
elif data.startswith('^rawerr^'):
trim_index = 8
log = browser_raw_loge
elif data.startswith('^rawout^'):
trim_index = 8
log = browser_raw_logi
if trim_index > 0:
try:
i = data.index('^', 5)
seq_num = int(data[5:i])
i = data.index('^', trim_index)
seq_num = int(data[trim_index:i])
data = data[i + 1:]
except ValueError:
pass

log = browser_loge if is_stderr else browser_logi
self.server.handle_incoming_message(seq_num, log, data)

self.send_response(200)
Expand Down Expand Up @@ -1576,6 +1623,10 @@ def parse_args(args):

parser.add_argument('cmdlineparams', nargs='*')

parser.add_argument('--interactive', dest='interactive', action='store_true',
help='If specified, emrun streams the terminal input to the client.'
'Note that blocking reading is not supported on the client.')

# Support legacy argument names with `_` in them (but don't
# advertize these in the --help message).
for i, a in enumerate(args):
Expand Down
51 changes: 51 additions & 0 deletions src/emrun_postjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,57 @@ if (globalThis.window && (typeof ENVIRONMENT_IS_PTHREAD == 'undefined' || !ENVIR
prevErr(text);
};

// Receive inputs from emrun and forward them to the TTY input.
var inputBuf = [];
var readableHandlers = [];
var inputClosed = false;
const POLLIN = 0x1;
const POLLRDNORM = 0x040;
function notifyReadableHandlers() {
while (readableHandlers.length > 0) {
const cb = readableHandlers.shift();
if (cb) cb(POLLIN | POLLRDNORM);
}
readableHandlers = [];
}
TTY.stream_ops.poll = (stream, timeout, notifyCallback) => {
if (inputClosed || (inputBuf.length > 0)) return (POLLIN | POLLRDNORM);
if (notifyCallback) {
notifyCallback.registerCleanupFunc(() => {
const i = readableHandlers.indexOf(notifyCallback);
if (i !== -1) readableHandlers.splice(i, 1);
});
readableHandlers.push(notifyCallback);
}
};
const es = new EventSource("/in");
es.addEventListener("close", e => {
inputClosed = true;
notifyReadableHandlers();
});
es.onopen = () => {
es.onmessage = (e) => {
inputBuf.push(Number(e.data));
notifyReadableHandlers();
}
TTY.default_tty_ops.get_char = (tty) => {
const res = inputBuf.shift();
// convert "undefined" to "null" because the TTY implementation
// interprets "undefined" as EAGAIN when there is no other read data.
return res ? res : null;
};

// Forward the output without buffering and cooking
TTY.default_tty_ops.fsync(TTY); // flush buffered contents
TTY.default_tty_ops.put_char = (tty, val) => {
post('^rawout^'+(emrun_http_sequence_number++)+'^'+encodeURIComponent(UTF8ArrayToString([val])));
}
TTY.default_tty1_ops.fsync(TTY); // flush buffered contents
TTY.default_tty1_ops.put_char = (tty, val) => {
post('^rawerr^'+(emrun_http_sequence_number++)+'^'+encodeURIComponent(UTF8ArrayToString([val])));
}
}

// Notify emrun web server that this browser has successfully launched the
// page. Note that we may need to wait for the server to be ready.
var tryToSendPageload = () => {
Expand Down
8 changes: 8 additions & 0 deletions test/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5659,6 +5659,7 @@ def test_program_arg_separator(self):

def test_emrun(self):
self.emcc('test_emrun.c', ['--emrun', '-o', 'test_emrun.html'])
self.emcc('test_interactive_emrun.c', ['--emrun', '-pthread', '-sPROXY_TO_PTHREAD', '-sEXIT_RUNTIME', '-o', 'test_interactive_emrun.html'])
if not has_browser():
self.skipTest('need a browser')

Expand Down Expand Up @@ -5697,6 +5698,7 @@ def test_emrun(self):
['--private_browsing', '--port', '6941'],
['--dump_out_directory', 'other dir/multiple', '--port', '6942'],
['--dump_out_directory=foo_bar', '--port', '6942'],
['--interactive'],
]:
args = args_base + args + [self.in_dir('test_emrun.html'), '--', '1', '2', '--3', 'escaped space', 'with_underscore']
print(shlex.join(args))
Expand All @@ -5721,6 +5723,12 @@ def test_emrun(self):
self.assertContained('Testing char sequences: %20%21 ä', stdout)
self.assertContained('hello, error stream!', stderr)

args = args_base + ['--interactive', self.in_dir('test_interactive_emrun.html')]
print(shlex.join(args))
proc = self.run_process(args, check=False, input="hello")
self.assertEqual(proc.returncode, 100)
self.assertContained('hello', stdout)


class browser64(browser):
def setUp(self):
Expand Down
34 changes: 34 additions & 0 deletions test/test_interactive_emrun.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2025 The Emscripten Authors. All rights reserved.
* Emscripten is available under two separate licenses, the MIT license and the
* University of Illinois/NCSA Open Source License. Both these licenses can be
* found in the LICENSE file.
*/

#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
#include <stdlib.h>

int main(int argc, char **argv) {
fd_set readfds;
char buf[124];
ssize_t n;
int nr = 0;

while (nr < sizeof(buf)) {
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
assert(select(STDIN_FILENO + 1, &readfds, NULL, NULL, NULL) == 1);
assert(FD_ISSET(STDIN_FILENO, &readfds));
n = read(STDIN_FILENO, &(buf[nr]), sizeof(buf) - nr);
assert(n >= 0);
if (n == 0) break;
nr += n;
}

printf("%s\n", buf);

exit(0);
}