Skip to content
Open
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
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, {
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 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(['-echo']);
const changedFlags = stty(['-a']);
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.match(finalFlags, /(?:^|[\s;])icanon(?:[\s;]|$)/, 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(['echo']);
}
}
}

async function main() {
for (const exit of ['natural', 'process.exit', 'SIGINT']) {
await checkExit(exit, ['--no-restore-terminal-state'], '', false);
await checkExit(exit, [], '', true);
await checkExit(exit, ['--no-restore-terminal-state'], '', true, rawChildScript);
}
if (!process.config.variables.node_without_node_options) {
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