diff --git a/src/modules/process.c b/src/modules/process.c index 7a6ff67..721d42b 100644 --- a/src/modules/process.c +++ b/src/modules/process.c @@ -664,16 +664,11 @@ static void stdin_start_reading(void) { if (!stdin_state.tty_initialized) { uv_loop_t *loop = uv_default_loop(); if (uv_tty_init(loop, &stdin_state.tty, STDIN_FILENO, 1) != 0) return; -#ifndef _WIN32 - uv_tty_set_mode(&stdin_state.tty, tty_is_raw_mode(STDIN_FILENO) ? UV_TTY_MODE_RAW : UV_TTY_MODE_NORMAL); -#endif stdin_state.tty.data = NULL; stdin_state.tty_initialized = true; - } else { -#ifndef _WIN32 - uv_tty_set_mode(&stdin_state.tty, tty_is_raw_mode(STDIN_FILENO) ? UV_TTY_MODE_RAW : UV_TTY_MODE_NORMAL); -#endif + uv_unref((uv_handle_t *)&stdin_state.tty); } + uv_ref((uv_handle_t *)&stdin_state.tty); stdin_state.reading = true; uv_read_start((uv_stream_t *)&stdin_state.tty, stdin_alloc_buffer, on_stdin_read); } @@ -682,6 +677,7 @@ static void stdin_stop_reading(void) { if (!stdin_state.reading) return; uv_read_stop((uv_stream_t *)&stdin_state.tty); stdin_state.reading = false; + uv_unref((uv_handle_t *)&stdin_state.tty); } #ifndef _WIN32 diff --git a/src/modules/tty.c b/src/modules/tty.c index 6bf1804..4bba294 100644 --- a/src/modules/tty.c +++ b/src/modules/tty.c @@ -197,9 +197,6 @@ static ant_value_t tty_readstream__read(ant_t *js, ant_value_t *args, int nargs) tty_read_stream_emit_error(state, "open", rc); return js_mkundef(); } - #ifndef _WIN32 - uv_tty_set_mode(&state->tty, tty_is_raw_mode(state->fd) ? UV_TTY_MODE_RAW : UV_TTY_MODE_NORMAL); - #endif state->tty.data = state; state->initialized = true; } @@ -727,7 +724,6 @@ static ant_value_t tty_write_stream_has_colors(ant_t *js, ant_value_t *args, int static ant_value_t tty_read_stream_set_raw_mode(ant_t *js, ant_value_t *args, int nargs) { ant_value_t this_obj = js_getthis(js); - tty_read_stream_state_t *state = tty_read_stream_state_from_obj(this_obj); if (!is_special_object(this_obj)) { return js_mkerr_typed(js, JS_ERR_TYPE, "setRawMode() requires a ReadStream receiver"); } @@ -737,11 +733,6 @@ static ant_value_t tty_read_stream_set_raw_mode(ant_t *js, ant_value_t *args, in if (!tty_set_raw_mode(fd, enable)) { return js_mkerr_typed(js, JS_ERR_GENERIC, "Failed to set raw mode for fd %d", fd); } -#ifndef _WIN32 - if (state && state->initialized && !state->closing) { - uv_tty_set_mode(&state->tty, enable ? UV_TTY_MODE_RAW : UV_TTY_MODE_NORMAL); - } -#endif js_set(js, this_obj, "isRaw", js_bool(enable)); return this_obj; } diff --git a/tests/amp/mini_amp_tui_repro.js b/tests/amp/mini_amp_tui_repro.js index f80e783..97b9491 100644 --- a/tests/amp/mini_amp_tui_repro.js +++ b/tests/amp/mini_amp_tui_repro.js @@ -170,7 +170,7 @@ function shutdown() { if (!running) return; running = false; clearInterval(timer); - process.stdout.write("\x1b[0m\x1b[2J\x1b[H"); + process.stdout.write("\x1b[0m\x1b[2J\x1b[H\x1b[?25h"); if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") { process.stdin.setRawMode(false); } diff --git a/tests/amp/mini_amp_widget_tui_repro.js b/tests/amp/mini_amp_widget_tui_repro.js index 95383ce..2bd3ed3 100644 --- a/tests/amp/mini_amp_widget_tui_repro.js +++ b/tests/amp/mini_amp_widget_tui_repro.js @@ -344,7 +344,7 @@ process.stdin.on("data", (chunk) => { for (const ch of chunk) { if (ch === "q" || ch === "\u0003") { clearInterval(timer); - process.stdout.write("\x1b[0m\x1b[2J\x1b[H"); + process.stdout.write("\x1b[0m\x1b[2J\x1b[H\x1b[?25h"); process.exit(0); } if (ch === "m") { diff --git a/tests/test_mini_amp_tui_exit_restores_tty.cjs b/tests/test_mini_amp_tui_exit_restores_tty.cjs new file mode 100644 index 0000000..c366a76 --- /dev/null +++ b/tests/test_mini_amp_tui_exit_restores_tty.cjs @@ -0,0 +1,92 @@ +const { spawnSync } = require('child_process'); + +function fail(message) { + throw new Error(message); +} + +if (process.platform === 'win32') { + console.log('skipping mini amp tty restore test on win32'); + process.exit(0); +} + +const script = ` +import os, pty, select, signal, sys, termios, time + +exec_path = sys.argv[1] +script_path = sys.argv[2] + +pid, master = pty.fork() +if pid == 0: + os.execv(exec_path, [exec_path, script_path]) + +sent = False +exit_code = None +deadline = time.time() + 8.0 + +while time.time() < deadline: + done, status = os.waitpid(pid, os.WNOHANG) + if done == pid: + exit_code = os.waitstatus_to_exitcode(status) + break + + r, _, _ = select.select([master], [], [], 0.1) + if master not in r: + continue + + try: + chunk = os.read(master, 4096) + except OSError: + break + + if not chunk: + break + + if not sent: + os.write(master, b'q') + sent = True + +if exit_code is None: + os.kill(pid, signal.SIGKILL) + _, status = os.waitpid(pid, 0) + exit_code = os.waitstatus_to_exitcode(status) + +attrs = termios.tcgetattr(master) +lflag = attrs[3] + +print(f"exit={exit_code}") +print(f"isig={1 if (lflag & termios.ISIG) else 0}") +print(f"icanon={1 if (lflag & termios.ICANON) else 0}") +print(f"echo={1 if (lflag & termios.ECHO) else 0}") +`; + +const result = spawnSync('python3', ['-c', script, process.execPath, 'tests/amp/mini_amp_tui_repro.js'], { + encoding: 'utf8', + timeout: 10000, +}); + +if (result.error && result.error.code === 'ENOENT') { + console.log('skipping mini amp tty restore test because `python3` is unavailable'); + process.exit(0); +} + +if (result.error) throw result.error; + +const output = `${result.stdout || ''}${result.stderr || ''}`; + +if (!output.includes('exit=0')) { + fail(`expected repro process to exit cleanly\n${output}`); +} + +if (!output.includes('isig=1')) { + fail(`expected ISIG restored after repro exit\n${output}`); +} + +if (!output.includes('icanon=1')) { + fail(`expected ICANON restored after repro exit\n${output}`); +} + +if (!output.includes('echo=1')) { + fail(`expected ECHO restored after repro exit\n${output}`); +} + +console.log('mini amp tui repro exits and restores tty flags');