'use strict';
// Tests that --process-timeout prints what kept the process running and exits
// with code 124.
const common = require('../common');
const assert = require('assert');
const { spawnSync } = require('child_process');
const fixtures = require('../common/fixtures');
const {
spawnSyncAndAssert,
spawnSyncAndExit,
} = require('../common/child_process');
// The deadline is measured from the start of the process, so on a slow machine
// it can expire before the script has set up the state under test. The scripts
// write 'ready' to stdout once they have, and run again with a longer timeout
// if they did not get that far.
function runUntilReady(script) {
for (let timeout = common.platformTimeout(1000); ; timeout *= 2) {
const start = process.hrtime.bigint();
const child = spawnSync(process.execPath, [
`--process-timeout=${timeout}ms`,
'-e',
`const { writeSync } = require('fs');\n${script}`,
], { encoding: 'utf8' });
const elapsed = Number(process.hrtime.bigint() - start) / 1e6;
if (!child.stdout.startsWith('ready\n') &&
timeout < common.platformTimeout(16000)) {
continue;
}
assert.strictEqual(child.signal, null);
assert.strictEqual(child.status, 124, child.stderr);
// Neither 'beforeExit' nor 'exit' is emitted.
assert.strictEqual(child.stdout, 'ready\n');
assert.ok(elapsed >= timeout, `Exited after ${elapsed}ms, before ${timeout}ms`);
assert.match(child.stderr, new RegExp(
`^\\(node:\\d+\\) Process timed out after ${timeout}ms \\(--process-timeout\\)\\. ` +
'Exiting with code 124\\.$', 'm'));
return child.stderr;
}
}
{
// The event loop is kept alive by a server and a timer.
const stderr = runUntilReady(`
require('net').createServer().listen(0);
setInterval(() => {}, 60_000);
process.on('beforeExit', () => writeSync(1, 'beforeExit\\n'));
process.on('exit', () => writeSync(1, 'exit\\n'));
setImmediate(() => writeSync(1, 'ready\\n'));
`);
assert.match(stderr, /^Main thread was not executing JavaScript\.$/m);
assert.match(stderr, /^Resources keeping the event loop alive:$/m);
assert.match(stderr, /^ {4}TCPServerWrap \(listening on \S+:\d+[,)]/m);
assert.match(stderr, /^ {4}Timeout \(next due in \d+ms\)$/m);
}
{
// The event loop is kept alive by various kinds of handles.
const file = JSON.stringify(fixtures.path('process-timeout', 'wait-for-parent.js'));
const stderr = runUntilReady(`
const { spawn } = require('child_process');
const dgram = require('dgram');
const { Resolver } = require('dns');
const { once } = require('events');
const fs = require('fs');
const net = require('net');
(async () => {
const server = net.createServer().listen(0, '127.0.0.1');
await once(server, 'listening');
await once(net.connect(server.address().port, '127.0.0.1'), 'connect');
// The DNS server never responds, so the query stays pending.
const udp = dgram.createSocket('udp4').bind(0, '127.0.0.1');
await once(udp, 'listening');
const resolver = new Resolver({ timeout: 60_000, tries: 1 });
resolver.setServers(['127.0.0.1:' + udp.address().port]);
resolver.resolve4('example.org', () => {});
const child = spawn(process.execPath, [${file}, String(process.pid)], {
stdio: 'ignore',
});
await once(child, 'spawn');
if (${!common.isIBMi}) fs.watch(${file});
fs.watchFile(${file}, () => {});
new MessageChannel().port1.on('message', () => {});
writeSync(1, 'ready\\n');
})();
`);
assert.match(stderr, /^Main thread was not executing JavaScript\.$/m);
assert.match(stderr,
/^ {4}TCPSocketWrap \(127\.0\.0\.1:\d+ -> 127\.0\.0\.1:\d+[,)]/m);
assert.match(stderr, /^ {4}UDPWrap \(bound to 127\.0\.0\.1:\d+[,)]/m);
// The DNS query keeps the event loop alive with handles of its own.
assert.match(stderr, /^ {4}libuv handle(?: x\d+)? \(poll\)$/m);
assert.match(stderr, /^ {4}ProcessWrap \(pid \d+\)$/m);
if (!common.isIBMi) {
assert.match(stderr, /^ {4}FSEventWrap \(watching .*wait-for-parent\.js\)$/m);
}
assert.match(stderr, /^ {4}StatWatcher \(watching .*wait-for-parent\.js\)$/m);
assert.match(stderr, /^ {4}MessagePort$/m);
}
{
// The main thread is executing JavaScript.
const stderr = runUntilReady(`
function spin() { for (;;); }
writeSync(1, 'ready\\n');
spin();
`);
// Native code writes to stderr in text mode, which ends lines with '\r\n' on
// Windows.
assert.match(stderr,
/^Main thread was executing JavaScript:\r?\n {4}at spin \(\[eval\]:\d+:\d+\)$/m);
assert.match(stderr,
/^No resources keeping the event loop alive were found\.$/m);
}
{
// Requests cannot complete while the main thread is executing JavaScript.
// Resources that are indistinguishable from each other are counted.
const stderr = runUntilReady(`
const { stat } = require('fs');
function spin() { for (;;); }
stat(process.execPath, () => {});
stat(process.execPath, () => {});
writeSync(1, 'ready\\n');
spin();
`);
assert.match(stderr,
/^Main thread was executing JavaScript:\r?\n {4}at spin \(\[eval\]:\d+:\d+\)$/m);
assert.match(stderr,
/^Resources keeping the event loop alive:\r?\n {4}FSReqCallback x2$/m);
}
{
// The main thread is executing a timer callback. The timer handle is inactive
// while timers are being processed, so no due time is printed.
const stderr = runUntilReady(`
setTimeout(function spin() {
writeSync(1, 'ready\\n');
for (;;);
}, 1);
`);
assert.match(stderr,
/^Main thread was executing JavaScript:\r?\n {4}at spin \(\[eval\]:\d+:\d+\)$/m);
assert.match(stderr, /^ {4}Timeout$/m);
}
{
// The main thread is blocked in Atomics.wait(), which can be interrupted.
const stderr = runUntilReady(`
writeSync(1, 'ready\\n');
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0);
`);
assert.match(stderr, /^Main thread was executing JavaScript:\r?\n {4}at \[eval\]:\d+:\d+$/m);
}
{
// A Worker thread keeps the event loop alive.
const stderr = runUntilReady(`
const { Worker } = require('worker_threads');
new Worker('setInterval(() => {}, 60_000)', { eval: true, name: 'poller' });
new Worker('setInterval(() => {}, 60_000)', { eval: true }).unref();
writeSync(1, 'ready\\n');
`);
assert.match(stderr, /^ {4}Worker \(thread 1, name 'poller'\)$/m);
// Workers that do not keep the event loop alive are not listed.
assert.doesNotMatch(stderr, /^ {4}Worker \(thread 2\b/m);
}
{
// Processes that exit on their own are not affected.
spawnSyncAndAssert(process.execPath, [
'--process-timeout=10m',
'-e',
'setTimeout(() => console.log("done"), 1)',
], {
stdout: 'done\n',
stderr: '',
});
spawnSyncAndExit(process.execPath, [
'--process-timeout=10m',
'-e',
'setTimeout(() => process.exit(3), 1)',
], {
status: 3,
signal: null,
stderr: '',
});
}