mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 18:25:13 +08:00
Putting a time limit on a Node.js process is common in many
use cases, but each of the common approaches are generally
flawed.
OS and CI timeouts are inconsistent and blind. Every OS flavor
handles process timeouts in different ways. Stock macOS and
Windows do not ship coreutils `timeout`, etc. None of the options
can explain why the process was still running or what exactly
was interrupted. Using setTimeout only fires when the event loop
is free so it misses busy loops and thread blocks and cannot
see the stack. Diagnostics triggered by signal have the same
problem.
This introduces a `--process-timeout` that should work
consistently across all runtimes. A native watchdog thread
interrupts the main thread, even in the middle of running
code. It prints the JavaScript stack (if any) and resources
keeping the event loop alive (if any), and can optionally
print a full diagnostic report. The process exits with a
distinct code (124). If the main thread is stuck in native
code, the process still exits.
$ node --process-timeout=10s -e "while(true) {}"
(node:483970) Process timed out after 10s (--process-timeout). Exiting with code 124.
Main thread was executing JavaScript:
at [eval]:1:1
at runScriptInThisContext (node:internal/vm:219:10)
at node:internal/process/execution:485:12
at [eval]-wrapper:6:24
at runScriptInContext (node:internal/process/execution:483:60)
at evalFunction (node:internal/process/execution:317:30)
at evalTypeScript (node:internal/process/execution:329:3)
at node:internal/main/eval_string:71:3
No resources keeping the event loop alive were found.
Signed-off-by: James M Snell <jasnell@gmail.com>
Assisted-by: Opencode
PR-URL: https://github.com/nodejs/node/pull/66138
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
83 lines
2.7 KiB
JavaScript
83 lines
2.7 KiB
JavaScript
'use strict';
|
|
|
|
// Tests that the inspector cannot be activated, and that sessions that could
|
|
// pause the main thread cannot be connected, while --process-timeout is used.
|
|
|
|
const common = require('../common');
|
|
common.skipIfInspectorDisabled();
|
|
|
|
const assert = require('assert');
|
|
const { spawn } = require('child_process');
|
|
const { once } = require('events');
|
|
const inspector = require('inspector');
|
|
const readline = require('readline');
|
|
const { Worker } = require('worker_threads');
|
|
const { spawnSyncAndAssert } = require('../common/child_process');
|
|
|
|
async function testActivationRequestIsIgnored() {
|
|
// Activation via SIGUSR1 (or its Windows equivalent) is ignored. The warning
|
|
// is printed from another thread, so wait for it before checking whether the
|
|
// inspector was activated.
|
|
const child = spawn(process.execPath, [
|
|
'--process-timeout=10m',
|
|
'-e',
|
|
`process._debugProcess(process.pid);
|
|
process.stdin.once('data', () => {
|
|
process.stdout.write(String(require('inspector').url()));
|
|
process.stdin.destroy();
|
|
});`,
|
|
]);
|
|
let stdout = '';
|
|
child.stdout.setEncoding('utf8');
|
|
child.stdout.on('data', (data) => { stdout += data; });
|
|
const lines = readline.createInterface({ input: child.stderr });
|
|
|
|
const [warning] = await once(lines, 'line');
|
|
assert.match(warning,
|
|
/^\(node:\d+\) Warning: Ignoring the request to activate the inspector because --process-timeout is used\.$/);
|
|
child.stdin.end('check');
|
|
|
|
const [code, signal] = await once(child, 'close');
|
|
assert.strictEqual(signal, null);
|
|
assert.strictEqual(code, 0);
|
|
assert.strictEqual(stdout, 'undefined');
|
|
}
|
|
|
|
if (process.argv[2] !== 'child') {
|
|
spawnSyncAndAssert(process.execPath, [
|
|
'--process-timeout=10m',
|
|
__filename,
|
|
'child',
|
|
], {
|
|
stderr: '',
|
|
});
|
|
|
|
testActivationRequestIsIgnored().then(common.mustCall());
|
|
return;
|
|
}
|
|
|
|
assert.throws(() => inspector.open(0), {
|
|
code: 'ERR_INSPECTOR_NOT_AVAILABLE',
|
|
message: 'The inspector cannot be activated when --process-timeout is used',
|
|
});
|
|
assert.strictEqual(inspector.url(), undefined);
|
|
|
|
// Sessions on the same thread cannot keep the thread paused, so they work.
|
|
const session = new inspector.Session();
|
|
session.connect();
|
|
session.post('Runtime.evaluate', { expression: '1 + 2' }, common.mustSucceed(({ result }) => {
|
|
assert.strictEqual(result.value, 3);
|
|
session.disconnect();
|
|
}));
|
|
|
|
const worker = new Worker(`
|
|
const assert = require('assert');
|
|
const { Session } = require('inspector');
|
|
assert.throws(() => new Session().connectToMainThread(), {
|
|
code: 'ERR_INSPECTOR_NOT_AVAILABLE',
|
|
message: 'Sessions cannot be connected to the main thread when ' +
|
|
'--process-timeout is used',
|
|
});
|
|
`, { eval: true });
|
|
worker.on('exit', common.mustCall((code) => assert.strictEqual(code, 0)));
|