blob: 121e37eef1748335532ef3b3bb68b5e93e436b41 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { test } from 'node:test';
const CHILD_SOURCE = String.raw`
import { createRequire } from 'node:module';
import {
closeSync,
constants,
fstatSync,
mkdtempSync,
openSync,
rmSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const require = createRequire(import.meta.url);
const fs = require('node:fs');
const { spawn: spawnPty } = await import('node-pty');
const root = mkdtempSync(join(tmpdir(), 'maka-node-pty-write-lifecycle-'));
const sentinelPath = join(root, 'sentinel');
const sentinelFds = [];
let terminal;
try {
terminal = spawnPty(
process.execPath,
['-e', 'process.stdin.pause(); setInterval(() => {}, 1000)'],
{ cols: 80, rows: 24 },
);
const terminalFd = terminal.fd;
// Hold the unpatched writer at its raw-fd submission boundary. Replaying that
// request only after fd reuse models the dangerous libuv schedule without
// coupling PTY teardown to starvation of the process-wide worker pool.
const originalWrite = fs.write;
let deferredRawWrite;
fs.write = (...args) => {
if (deferredRawWrite !== undefined) {
throw new Error('Expected at most one raw PTY write before the lifecycle fence');
}
deferredRawWrite = args;
};
try {
terminal.write('R'.repeat(4 * 1024 * 1024));
} finally {
fs.write = originalWrite;
}
setTimeout(() => terminal.kill('SIGKILL'), 5);
await new Promise((resolve) => terminal.onExit(resolve));
// Retain any lower-numbered descriptors until the retired PTY fd is reused.
let sentinelFd;
for (let attempt = 0; attempt < 64; attempt += 1) {
const fd = openSync(
sentinelPath,
constants.O_RDWR |
constants.O_CREAT |
(sentinelFds.length === 0 ? constants.O_TRUNC : 0),
0o600,
);
sentinelFds.push(fd);
if (fd === terminalFd) {
sentinelFd = fd;
break;
}
if (fd > terminalFd) break;
}
if (sentinelFd === undefined) {
throw new Error('Could not reuse retired PTY fd ' + terminalFd);
}
if (deferredRawWrite !== undefined) {
const [fd, buffer, offset] = deferredRawWrite;
await new Promise((resolve, reject) => {
originalWrite(fd, buffer, offset, (error) => (error ? reject(error) : resolve()));
});
}
if (fstatSync(sentinelFd).size !== 0) {
throw new Error('Queued PTY input reached the reused sentinel fd');
}
console.log('sentinel-ok');
} finally {
try {
terminal?.kill('SIGKILL');
} catch {}
for (const fd of sentinelFds) closeSync(fd);
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',
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/);
});