Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
src: add --no-restore-terminal-state
Restoring startup terminal settings on exit can overwrite changes made
by another process sharing the terminal, such as an interactive pager.

Add a POSIX opt-out that preserves libuv's terminal mode cleanup and
restoration of the O_NONBLOCK flag.

Refs: #66440
Signed-off-by: Fatih Güzel <fatihguzeldev@gmail.com>
  • Loading branch information
fatihguzeldev committed Oct 5, 2026
commit 464554fec16515ba1564106bb4bf0995f4e2aa06
21 changes: 21 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -2435,6 +2435,26 @@ Disable support for loading a synchronous ES module graph in `require()`.

See [Loading ECMAScript modules using `require()`][].

### `--no-restore-terminal-state`

<!-- YAML
added: REPLACEME
-->

On POSIX systems, disable restoration of the terminal settings captured at startup
when the process exits. These settings are restored by default.
This option can be useful when piping output to an interactive pager that changes
the same terminal's settings:

```bash
node --no-restore-terminal-state --help | less
```

This option does not disable libuv's terminal mode cleanup or restoration of the
`O_NONBLOCK` flag on standard I/O file descriptors. Applications using this option
must restore terminal settings not restored by libuv, including changes made
through native addons or child processes. It has no effect on Windows.

### `--no-strip-types`

<!-- YAML
Expand Down Expand Up @@ -4302,6 +4322,7 @@ one is included in the list below.
* `--no-force-async-hooks-checks`
* `--no-global-search-paths`
* `--no-network-family-autoselection`
* `--no-restore-terminal-state`
* `--no-strip-types`
* `--no-warnings`
* `--no-webstorage`
Expand Down
15 changes: 15 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,19 @@ enables it.
Disable support for loading a synchronous ES module graph in \fBrequire()\fR.
See Loading ECMAScript modules using \fBrequire()\fR.
.
.It Fl -no-restore-terminal-state
On POSIX systems, disable restoration of the terminal settings captured at startup
when the process exits. These settings are restored by default.
This option can be useful when piping output to an interactive pager that changes
the same terminal's settings:
.Bd -literal
node --no-restore-terminal-state --help | less
.Ed
This option does not disable libuv's terminal mode cleanup or restoration of the
\fBO_NONBLOCK\fR flag on standard I/O file descriptors. Applications using this option
must restore terminal settings not restored by libuv, including changes made
through native addons or child processes. It has no effect on Windows.
.
.It Fl -no-strip-types
Disable type-stripping for TypeScript files.
For more information, see the TypeScript type-stripping documentation.
Expand Down Expand Up @@ -2341,6 +2354,8 @@ one is included in the list below.
.It
\fB--no-network-family-autoselection\fR
.It
\fB--no-restore-terminal-state\fR
.It
\fB--no-strip-types\fR
.It
\fB--no-warnings\fR
Expand Down
10 changes: 8 additions & 2 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -533,14 +533,16 @@ void ResetSignalHandlers() {
// variable on all platforms that we support, which we require in
// order for its value to be usable inside signal handlers.
static std::atomic<uint32_t> init_process_flags = 0;
static std::atomic<uint32_t> restore_terminal_state = 1;
static_assert(
std::is_same_v<std::underlying_type_t<ProcessInitializationFlags::Flags>,
uint32_t>);

static void PlatformInit(ProcessInitializationFlags::Flags flags) {
// init_process_flags is accessed in ResetStdio(),
// These atomics are accessed in ResetStdio(),
// which can be called from signal handlers.
CHECK(init_process_flags.is_lock_free());
CHECK(restore_terminal_state.is_lock_free());
init_process_flags.store(flags);

if (!(flags & ProcessInitializationFlags::kNoStdioInitialization)) {
Expand Down Expand Up @@ -707,7 +709,7 @@ void ResetStdio() {
CHECK_NE(err, -1);
}

if (s.isatty) {
if (s.isatty && restore_terminal_state.load()) {
sigset_t sa;
int err;

Expand Down Expand Up @@ -1261,6 +1263,10 @@ InitializeOncePerProcessInternal(
{
result->exit_code_ = InitializeNodeWithArgsInternal(
&result->args_, &result->exec_args_, &result->errors_, flags);
// cli_options is not safe to access from the signal handlers that call
// ResetStdio().
restore_terminal_state.store(
per_process::cli_options->restore_terminal_state);
if (result->exit_code_enum() != ExitCode::kNoFailure) {
result->early_return_ = true;
return result;
Expand Down
5 changes: 5 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1636,6 +1636,11 @@ PerProcessOptionsParser::PerProcessOptionsParser(
"automatically zero-fill all newly allocated Buffer instances",
BOOL_FIELD(zero_fill_all_buffers),
kAllowedInEnvvar);
AddOption("--restore-terminal-state",
"disable restoration of startup terminal settings",
BOOL_FIELD(restore_terminal_state),
kAllowedInEnvvar,
true);
AddOption("--debug-arraybuffer-allocations",
"", /* undocumented, only for debugging */
BOOL_FIELD(debug_arraybuffer_allocations),
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ class PerProcessOptions : public Options {

DEFINE_BOOL_FIELD(zero_fill_all_buffers) = false;
DEFINE_BOOL_FIELD(debug_arraybuffer_allocations) = false;
DEFINE_BOOL_FIELD(restore_terminal_state) = true;

// We enable the shared read-only heap which currently requires that the
// snapshot used in different isolates in the same process to be the same.
Expand Down
86 changes: 86 additions & 0 deletions test/pseudo-tty/test-tty-restore-terminal-state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use strict';

const common = require('../common');
const assert = require('assert');
const { spawn } = require('child_process');
const { once } = require('events');
const { spawnSyncAndExitWithoutError } = require('../common/child_process');

// Refs: https://github.com/nodejs/node/issues/66440

function stty(args) {
const { child: { stdout } } = spawnSyncAndExitWithoutError('stty', args, {

Check failure on line 12 in test/pseudo-tty/test-tty-restore-terminal-state.js

View workflow job for this annotation

GitHub Actions / aarch64-darwin: with shared libraries / build

--- stdout --- stty: 'standard input': unable to perform all requested operations [process 59534]: --- stderr --- [process 59534]: --- stdout --- [process 59534]: status = 1, signal = null /Users/runner/work/_temp/node-v27.0.0-nightly2026-10-05c642a22cba-slim/test/pseudo-tty/test-tty-restore-terminal-state.js:12 const { child: { stdout } } = spawnSyncAndExitWithoutError('stty', args, { ^ Error: - process terminated with status 1, expected 0 at stty (/Users/runner/work/_temp/node-v27.0.0-nightly2026-10-05c642a22cba-slim/test/pseudo-tty/test-tty-restore-terminal-state.js:12:33) at checkExit (/Users/runner/work/_temp/node-v27.0.0-nightly2026-10-05c642a22cba-slim/test/pseudo-tty/test-tty-restore-terminal-state.js:70:7) at process.processTicksAndRejections (node:internal/process/task_queues:104:5) at async main (/Users/runner/work/_temp/node-v27.0.0-nightly2026-10-05c642a22cba-slim/test/pseudo-tty/test-tty-restore-terminal-state.js:77:5) { options: { encoding: 'utf8', stdio: [ 'inherit', 'pipe', 'inherit' ] }, command: 'stty 2b02:1:4b00:58b:4:ff:ff:7f:17:15:12:ff:3:1c:1a:19:11:13:16:f:1:0:14:ff' } Node.js v27.0.0-pre Command: /nix/store/4l8r0s2r5fna7src82b5fc2hixdvfyw0-python3-3.14.7/bin/python3 /Users/runner/work/_temp/node-v27.0.0-nightly2026-10-05c642a22cba-slim/test/pseudo-tty/../../tools/pseudo-tty.py out/Release/node /Users/runner/work/_temp/node-v27.0.0-nightly2026-10-05c642a22cba-slim/test/pseudo-tty/test-tty-restore-terminal-state.js

Check failure on line 12 in test/pseudo-tty/test-tty-restore-terminal-state.js

View workflow job for this annotation

GitHub Actions / x86_64-darwin: with shared libraries / build

--- stdout --- stty: 'standard input': unable to perform all requested operations [process 61247]: --- stderr --- [process 61247]: --- stdout --- [process 61247]: status = 1, signal = null /Users/runner/work/_temp/node-v27.0.0-nightly2026-10-05c642a22cba-slim/test/pseudo-tty/test-tty-restore-terminal-state.js:12 const { child: { stdout } } = spawnSyncAndExitWithoutError('stty', args, { ^ Error: - process terminated with status 1, expected 0 at stty (/Users/runner/work/_temp/node-v27.0.0-nightly2026-10-05c642a22cba-slim/test/pseudo-tty/test-tty-restore-terminal-state.js:12:33) at checkExit (/Users/runner/work/_temp/node-v27.0.0-nightly2026-10-05c642a22cba-slim/test/pseudo-tty/test-tty-restore-terminal-state.js:70:7) at process.processTicksAndRejections (node:internal/process/task_queues:104:5) at async main (/Users/runner/work/_temp/node-v27.0.0-nightly2026-10-05c642a22cba-slim/test/pseudo-tty/test-tty-restore-terminal-state.js:77:5) { options: { encoding: 'utf8', stdio: [ 'inherit', 'pipe', 'inherit' ] }, command: 'stty 2b02:1:4b00:58b:4:ff:ff:7f:17:15:12:ff:3:1c:1a:19:11:13:16:f:1:0:14:ff' } Node.js v27.0.0-pre Command: /nix/store/ipycsiiswnfskqah22ygv0d937a5vx5k-python3-3.13.15/bin/python3 /Users/runner/work/_temp/node-v27.0.0-nightly2026-10-05c642a22cba-slim/test/pseudo-tty/../../tools/pseudo-tty.py out/Release/node /Users/runner/work/_temp/node-v27.0.0-nightly2026-10-05c642a22cba-slim/test/pseudo-tty/test-tty-restore-terminal-state.js
encoding: 'utf8',
stdio: ['inherit', 'pipe', 'inherit'],
});
return stdout.trim();
}

const childScript = [
'process.channel.ref();',
'process.once("message", (exit) => {',
' process.disconnect();',
' if (exit === "process.exit") process.exit(0);',
'});',
'process.send("ready");',
].join('\n');

const rawChildScript = `process.stdin.setRawMode(true);\n${childScript}`;

async function checkExit(exit, args, nodeOptions, restore, script = childScript) {
const originalMode = stty(['-g']);
const initialFlags = stty(['-a']);
assert.match(initialFlags, /(?:^|[\s;])icanon(?:[\s;]|$)/);
assert.match(initialFlags, /(?:^|[\s;])echo(?:[\s;]|$)/);

const child = spawn(process.execPath, [...args, '-e', script], {
env: { ...process.env, NODE_OPTIONS: nodeOptions },
stdio: ['inherit', 'pipe', 'inherit', 'ipc'],
});
const exited = once(child, 'exit');

try {
// The child must capture the startup settings before stty changes them.
const [message] = await Promise.race([once(child, 'message'), exited]);
assert.strictEqual(message, 'ready');
stty(['-icanon', '-echo']);
const changedFlags = stty(['-a']);
assert.match(changedFlags, /(?:^|[\s;])-icanon(?:[\s;]|$)/);
assert.match(changedFlags, /(?:^|[\s;])-echo(?:[\s;]|$)/);

if (exit === 'SIGINT') {
assert.strictEqual(child.kill('SIGINT'), true);
} else {
child.send(exit);
}

const [code, signal] = await exited;
assert.strictEqual(code, exit === 'SIGINT' ? null : 0);
assert.strictEqual(signal, exit === 'SIGINT' ? 'SIGINT' : null);
const finalFlags = stty(['-a']);
assert.strictEqual(/(?:^|[\s;])icanon(?:[\s;]|$)/.test(finalFlags), restore, exit);
assert.strictEqual(/(?:^|[\s;])echo(?:[\s;]|$)/.test(finalFlags), restore, exit);
} finally {
try {
if (child.exitCode === null && child.signalCode === null) {
child.kill('SIGKILL');
await exited;
}
} finally {
stty(originalMode.split(/\s+/));
}
}
}

async function main() {
for (const exit of ['natural', 'process.exit', 'SIGINT']) {
await checkExit(exit, [], '', true);
await checkExit(exit, ['--no-restore-terminal-state'], '', false);
await checkExit(exit, ['--no-restore-terminal-state'], '', true, rawChildScript);
}
await checkExit('natural', [], '--no-restore-terminal-state', false);
await checkExit('natural', ['--restore-terminal-state'],
'--no-restore-terminal-state', true);
}

main().then(common.mustCall());
Empty file.
Loading