fix(release): harden packaged smoke cleanup

This commit is contained in:
NimBold
2026-07-17 02:35:36 +03:30
parent a8dc4fb447
commit 513143d745
+154 -35
View File
@@ -17,6 +17,7 @@ if (!executableArg) {
const executable = path.resolve(executableArg); const executable = path.resolve(executableArg);
const assertNoVisibleChildWindows = process.argv.includes('--assert-no-visible-child-windows'); const assertNoVisibleChildWindows = process.argv.includes('--assert-no-visible-child-windows');
const assertPortableData = process.argv.includes('--assert-portable-data'); const assertPortableData = process.argv.includes('--assert-portable-data');
const READY_PORT_TIMEOUT_MS = 500;
const child = spawn(executable, [], { const child = spawn(executable, [], {
cwd: process.env.RUNNER_TEMP || process.env.TMPDIR || process.cwd(), cwd: process.env.RUNNER_TEMP || process.env.TMPDIR || process.cwd(),
detached: process.platform !== 'win32', detached: process.platform !== 'win32',
@@ -60,18 +61,21 @@ async function findReadyPort() {
break; break;
} }
for (let port = 6412; port <= 6422; port += 1) { const ports = Array.from({ length: 11 }, (_, index) => 6412 + index);
const matches = await Promise.all(ports.map(async port => {
try { try {
const response = await fetch(`http://127.0.0.1:${port}/ping`); const response = await fetch(`http://127.0.0.1:${port}/ping`, {
if ( signal: AbortSignal.timeout(READY_PORT_TIMEOUT_MS),
response.headers.get('x-firelink-server') === '1' });
&& response.headers.get('x-firelink-smoke-process-id') === String(child.pid) const matchesChild = response.headers.get('x-firelink-server') === '1'
) { && response.headers.get('x-firelink-smoke-process-id') === String(child.pid);
readyPort = port; await response.body?.cancel();
break; return matchesChild ? port : null;
} } catch {
} catch {} return null;
} }
}));
readyPort = matches.find(port => port !== null) ?? null;
if (readyPort === null) { if (readyPort === null) {
await sleep(250); await sleep(250);
@@ -134,46 +138,161 @@ function waitForChildExit(timeoutMs) {
} }
return new Promise(resolve => { return new Promise(resolve => {
const timer = setTimeout(() => { let timer;
const onExit = () => {
clearTimeout(timer);
child.off('exit', onExit);
resolve(true);
};
timer = setTimeout(() => {
child.off('exit', onExit);
resolve(false); resolve(false);
}, timeoutMs); }, timeoutMs);
child.once('exit', () => { child.once('exit', onExit);
clearTimeout(timer);
resolve(true);
});
}); });
} }
async function terminateChild() { function isProcessGroupAlive(rootPid) {
if (!child.pid || childExit) {
return true;
}
if (process.platform === 'win32') { if (process.platform === 'win32') {
try { return false;
execFileSync('taskkill', ['/pid', String(child.pid), '/t', '/f'], {
stdio: 'ignore',
windowsHide: true,
});
} catch {}
return waitForChildExit(10000);
} }
try { try {
process.kill(-child.pid, 'SIGTERM'); process.kill(-rootPid, 0);
} catch {
child.kill('SIGTERM');
}
if (await waitForChildExit(5000)) {
return true; return true;
} catch (error) {
return error?.code !== 'ESRCH';
}
}
async function waitForProcessGroupExit(rootPid, timeoutMs) {
if (process.platform === 'win32') {
return true;
}
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (!isProcessGroupAlive(rootPid)) {
return true;
}
await sleep(100);
}
return !isProcessGroupAlive(rootPid);
}
function windowsBundleRoot() {
return path.dirname(executable).replaceAll("'", "''");
}
function windowsBundleProcessIds() {
const root = windowsBundleRoot();
const script = `
$root = '${root}'
$rootPrefix = $root.TrimEnd('\\') + '\\'
Get-CimInstance Win32_Process |
Where-Object {
$commandLine = $_.CommandLine
$commandLine -and $commandLine.TrimStart([char]34).StartsWith($rootPrefix, [System.StringComparison]::OrdinalIgnoreCase)
} |
Select-Object -ExpandProperty ProcessId
`;
try {
const output = execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
});
return output
.split(/\r?\n/)
.map(value => Number.parseInt(value.trim(), 10))
.filter(Number.isInteger);
} catch {
return null;
}
}
function terminateWindowsBundleProcesses() {
const root = windowsBundleRoot();
const script = `
$root = '${root}'
$rootPrefix = $root.TrimEnd('\\') + '\\'
Get-CimInstance Win32_Process |
Where-Object {
$commandLine = $_.CommandLine
$commandLine -and $commandLine.TrimStart([char]34).StartsWith($rootPrefix, [System.StringComparison]::OrdinalIgnoreCase)
} |
ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
`;
try {
execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
stdio: 'ignore',
windowsHide: true,
});
} catch {}
}
async function waitForWindowsBundleExit(timeoutMs) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const processIds = windowsBundleProcessIds();
if (processIds?.length === 0) {
return true;
}
await sleep(100);
}
return windowsBundleProcessIds()?.length === 0;
}
async function terminateChild() {
if (!child.pid) {
return true;
}
const childWasRunning = !childExit;
if (process.platform === 'win32') {
if (childWasRunning) {
try {
execFileSync('taskkill', ['/pid', String(child.pid), '/t', '/f'], {
stdio: 'ignore',
windowsHide: true,
});
} catch {}
}
const childExited = await waitForChildExit(10000);
const bundleExited = await waitForWindowsBundleExit(5000);
if (childExited && bundleExited) {
return true;
}
terminateWindowsBundleProcesses();
return await waitForChildExit(5000) && await waitForWindowsBundleExit(5000);
}
if (childWasRunning && !childExit) {
try {
process.kill(-child.pid, 'SIGTERM');
} catch {
if (!childExit) {
child.kill('SIGTERM');
}
}
}
if (await waitForChildExit(5000) && await waitForProcessGroupExit(child.pid, 5000)) {
return true;
}
if (!childWasRunning || childExit) {
return false;
} }
try { try {
process.kill(-child.pid, 'SIGKILL'); process.kill(-child.pid, 'SIGKILL');
} catch { } catch {
child.kill('SIGKILL'); if (!childExit) {
child.kill('SIGKILL');
}
} }
return waitForChildExit(5000); return await waitForChildExit(5000) && await waitForProcessGroupExit(child.pid, 5000);
} }
async function assertPortableStorage() { async function assertPortableStorage() {