mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
dad5b7bc5e
Preserve legacy source data when portable sanitization cannot replace it, reject malformed settings without panicking, and make portable pairing regeneration durable before UI state changes.\n\nMake the packaged smoke assertion tolerate slow WebView startup and clarify AppImage storage behavior.\n\nRefs #15
236 lines
6.1 KiB
JavaScript
Executable File
236 lines
6.1 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
import { execFileSync, spawn } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
function argValue(name) {
|
|
const index = process.argv.indexOf(name);
|
|
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
}
|
|
|
|
const executableArg = argValue('--executable');
|
|
if (!executableArg) {
|
|
console.error('Pass --executable <path>.');
|
|
process.exit(1);
|
|
}
|
|
|
|
const executable = path.resolve(executableArg);
|
|
const assertNoVisibleChildWindows = process.argv.includes('--assert-no-visible-child-windows');
|
|
const assertPortableData = process.argv.includes('--assert-portable-data');
|
|
const child = spawn(executable, [], {
|
|
cwd: process.env.RUNNER_TEMP || process.env.TMPDIR || process.cwd(),
|
|
detached: process.platform !== 'win32',
|
|
env: {
|
|
...process.env,
|
|
FIRELINK_SMOKE_TEST: '1',
|
|
WEBKIT_DISABLE_COMPOSITING_MODE: '1',
|
|
GDK_BACKEND: 'x11',
|
|
},
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
|
|
let stderr = '';
|
|
let spawnError = null;
|
|
let readyPort = null;
|
|
let childExit = null;
|
|
|
|
child.on('error', error => {
|
|
spawnError = error;
|
|
});
|
|
|
|
child.on('exit', (code, signal) => {
|
|
childExit = { code, signal };
|
|
if (readyPort === null) {
|
|
console.error(`Child exited prematurely with code ${code} signal ${signal}`);
|
|
}
|
|
});
|
|
|
|
child.stderr.on('data', data => {
|
|
stderr += data.toString();
|
|
});
|
|
child.stdout.on('data', () => {});
|
|
|
|
function sleep(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
async function findReadyPort() {
|
|
for (let attempt = 0; attempt < 200 && readyPort === null; attempt += 1) {
|
|
if (spawnError || childExit) {
|
|
break;
|
|
}
|
|
|
|
for (let port = 6412; port <= 6422; port += 1) {
|
|
try {
|
|
const response = await fetch(`http://127.0.0.1:${port}/ping`);
|
|
if (
|
|
response.headers.get('x-firelink-server') === '1'
|
|
&& response.headers.get('x-firelink-smoke-process-id') === String(child.pid)
|
|
) {
|
|
readyPort = port;
|
|
break;
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
if (readyPort === null) {
|
|
await sleep(250);
|
|
}
|
|
}
|
|
}
|
|
|
|
function assertNoVisibleWindows(rootPid) {
|
|
if (process.platform !== 'win32') {
|
|
return;
|
|
}
|
|
|
|
const script = `
|
|
$root = ${rootPid}
|
|
$all = Get-CimInstance Win32_Process
|
|
$pending = @($root)
|
|
$descendants = @()
|
|
while ($pending.Count -gt 0) {
|
|
$parent = $pending[0]
|
|
if ($pending.Count -eq 1) {
|
|
$pending = @()
|
|
} else {
|
|
$pending = $pending[1..($pending.Count - 1)]
|
|
}
|
|
|
|
$children = @($all | Where-Object { $_.ParentProcessId -eq $parent })
|
|
foreach ($child in $children) {
|
|
$descendants += $child.ProcessId
|
|
$pending += $child.ProcessId
|
|
}
|
|
}
|
|
|
|
$visible = foreach ($childPid in $descendants) {
|
|
$process = Get-Process -Id $childPid -ErrorAction SilentlyContinue
|
|
if ($process -and $process.MainWindowHandle -ne 0) {
|
|
"$($process.ProcessName)($childPid)"
|
|
}
|
|
}
|
|
|
|
if ($visible.Count -gt 0) {
|
|
Write-Error "Visible child process windows detected: $($visible -join ', ')"
|
|
exit 1
|
|
}
|
|
`;
|
|
|
|
try {
|
|
execFileSync('powershell', ['-NoProfile', '-Command', script], {
|
|
stdio: 'pipe',
|
|
windowsHide: true,
|
|
});
|
|
} catch (error) {
|
|
const detail = [error.stdout?.toString(), error.stderr?.toString()].filter(Boolean).join('\n').trim();
|
|
throw new Error(detail || 'Visible child process window check failed.');
|
|
}
|
|
}
|
|
|
|
function waitForChildExit(timeoutMs) {
|
|
if (childExit) {
|
|
return Promise.resolve(true);
|
|
}
|
|
|
|
return new Promise(resolve => {
|
|
const timer = setTimeout(() => {
|
|
resolve(false);
|
|
}, timeoutMs);
|
|
child.once('exit', () => {
|
|
clearTimeout(timer);
|
|
resolve(true);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function terminateChild() {
|
|
if (!child.pid || childExit) {
|
|
return true;
|
|
}
|
|
|
|
if (process.platform === 'win32') {
|
|
try {
|
|
execFileSync('taskkill', ['/pid', String(child.pid), '/t', '/f'], {
|
|
stdio: 'ignore',
|
|
windowsHide: true,
|
|
});
|
|
} catch {}
|
|
return waitForChildExit(10000);
|
|
}
|
|
|
|
try {
|
|
process.kill(-child.pid, 'SIGTERM');
|
|
} catch {
|
|
child.kill('SIGTERM');
|
|
}
|
|
if (await waitForChildExit(5000)) {
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
process.kill(-child.pid, 'SIGKILL');
|
|
} catch {
|
|
child.kill('SIGKILL');
|
|
}
|
|
return waitForChildExit(5000);
|
|
}
|
|
|
|
async function assertPortableStorage() {
|
|
const portableRoot = path.dirname(executable);
|
|
const marker = path.join(portableRoot, 'portable.flag');
|
|
const database = path.join(portableRoot, 'data', 'firelink.sqlite');
|
|
const webviewData = path.join(portableRoot, 'data', 'webview');
|
|
|
|
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
const markerReady = fs.statSync(marker, { throwIfNoEntry: false })?.isFile();
|
|
const databaseReady = fs.statSync(database, { throwIfNoEntry: false })?.isFile();
|
|
const webviewReady = fs.statSync(webviewData, { throwIfNoEntry: false })?.isDirectory();
|
|
if (markerReady && databaseReady && webviewReady) {
|
|
return;
|
|
}
|
|
await sleep(250);
|
|
}
|
|
|
|
throw new Error(
|
|
`Portable storage was not ready: marker=${marker}, database=${database}, webview=${webviewData}`,
|
|
);
|
|
}
|
|
|
|
try {
|
|
await findReadyPort();
|
|
|
|
if (readyPort === null) {
|
|
if (spawnError) {
|
|
throw new Error(`Packaged Firelink failed to start: ${spawnError.message}`);
|
|
} else if (childExit) {
|
|
throw new Error(
|
|
`Packaged Firelink exited before exposing extension ping endpoint with code ${childExit.code} signal ${childExit.signal}.`,
|
|
);
|
|
} else {
|
|
throw new Error(`Packaged Firelink did not expose extension ping endpoint. Stderr:\n${stderr.slice(-1000)}`);
|
|
}
|
|
}
|
|
|
|
if (assertNoVisibleChildWindows) {
|
|
assertNoVisibleWindows(child.pid);
|
|
}
|
|
if (assertPortableData) {
|
|
await assertPortableStorage();
|
|
}
|
|
|
|
if (childExit) {
|
|
throw new Error(`Packaged Firelink exited after exposing extension ping endpoint with code ${childExit.code} signal ${childExit.signal}.`);
|
|
}
|
|
|
|
console.log(`Packaged Firelink smoke passed on 127.0.0.1:${readyPort}`);
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|
process.exitCode = 1;
|
|
} finally {
|
|
if (!await terminateChild()) {
|
|
console.error('Packaged Firelink could not be terminated cleanly; refusing to report smoke success.');
|
|
process.exitCode = 1;
|
|
}
|
|
}
|