blob: 4bb881ac27773f93d824566cd07151548eeb43a2 [file]
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { test } from 'node:test';
const CHILD_SOURCE = String.raw`
import { spawn as spawnPty } from 'node-pty';
import { execFileSync } from 'node:child_process';
import {
closeSync,
constants,
fstatSync,
mkdtempSync,
open,
openSync,
rmSync,
stat,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';
const delay = promisify(setTimeout);
const root = mkdtempSync(join(tmpdir(), 'maka-node-pty-write-lifecycle-'));
const fifo = join(root, 'worker-blocker');
const sentinelPath = join(root, 'sentinel');
let readerFd;
let sentinelFd;
let terminal;
try {
execFileSync('mkfifo', [fifo]);
terminal = spawnPty(
process.execPath,
['-e', 'process.stdin.pause(); setInterval(() => {}, 1000)'],
{ cols: 80, rows: 24 },
);
const terminalFd = terminal.fd;
const reader = new Promise((resolve, reject) => {
open(fifo, constants.O_RDONLY, (error, fd) => {
if (error) reject(error);
else {
readerFd = fd;
resolve();
}
});
});
let probeCompleted = false;
const blockedPoolProbe = new Promise((resolve, reject) => {
stat(root, (error) => {
if (error) reject(error);
else {
probeCompleted = true;
resolve();
}
});
});
await delay(50);
if (probeCompleted) throw new Error('FIFO did not occupy the libuv worker');
terminal.write('R'.repeat(4 * 1024 * 1024));
setTimeout(() => terminal.kill('SIGKILL'), 5);
await new Promise((resolve) => terminal.onExit(resolve));
sentinelFd = openSync(
sentinelPath,
constants.O_RDWR | constants.O_CREAT | constants.O_TRUNC,
0o600,
);
if (sentinelFd !== terminalFd) {
throw new Error(
'Expected retired PTY fd ' + terminalFd + ' to be reused, received ' + sentinelFd,
);
}
const writerFd = openSync(fifo, constants.O_WRONLY | constants.O_NONBLOCK);
closeSync(writerFd);
const writeCompletionBarrier = new Promise((resolve, reject) => {
stat(root, (error) => (error ? reject(error) : resolve()));
});
await Promise.all([reader, blockedPoolProbe, writeCompletionBarrier]);
closeSync(readerFd);
readerFd = undefined;
if (fstatSync(sentinelFd).size !== 0) {
throw new Error('Queued PTY input reached the reused sentinel fd');
}
closeSync(sentinelFd);
sentinelFd = undefined;
console.log('sentinel-ok');
} finally {
try {
terminal?.kill('SIGKILL');
} catch {}
if (readerFd !== undefined) closeSync(readerFd);
if (sentinelFd !== undefined) closeSync(sentinelFd);
rmSync(root, { recursive: true, force: true });
}
`;
test('does not carry queued Unix PTY writes past native exit', {
skip: process.platform === 'win32' ? 'Unix PTY file-descriptor lifecycle only' : false,
}, () => {
const result = spawnSync(process.execPath, ['--input-type=module', '--eval', CHILD_SOURCE], {
cwd: process.cwd(),
encoding: 'utf8',
env: { ...process.env, UV_THREADPOOL_SIZE: '1' },
timeout: 10_000,
});
assert.ifError(result.error);
assert.equal(result.signal, null, result.stderr);
assert.equal(result.status, 0, result.stderr);
assert.equal(result.stdout.trim(), 'sentinel-ok');
assert.doesNotMatch(result.stderr, /Unhandled pty write error/);
});