import { spawn, spawnSync } from 'node:child_process'; import fs from 'node:fs'; import net from 'node:net'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const executable = path.join( repoRoot, 'apps', 'desktop', 'release', 'win-unpacked', '工业智核 APS.exe', ); function reservePort() { return new Promise((resolve, reject) => { const server = net.createServer(); server.once('error', reject); server.listen({ host: '127.0.0.1', port: 0, exclusive: true }, () => { const address = server.address(); const port = typeof address === 'object' && address ? address.port : 0; server.close((error) => error ? reject(error) : resolve(port)); }); }); } function delay(milliseconds) { return new Promise((resolve) => setTimeout(resolve, milliseconds)); } async function waitForPage(debugPort, child) { const deadline = Date.now() + 60_000; while (Date.now() < deadline) { if (child.exitCode !== null) { throw new Error(`packaged Electron exited before smoke validation: ${child.exitCode}`); } try { const response = await fetch(`http://127.0.0.1:${debugPort}/json/list`); const targets = await response.json(); const page = targets.find((target) => ( target.type === 'page' && target.url.startsWith('http://127.0.0.1:') )); if (page) return page; } catch { // The DevTools endpoint starts after the Electron main process. } await delay(250); } throw new Error('packaged Electron page did not become ready within 60 seconds'); } function evaluate(webSocketDebuggerUrl, expression) { return new Promise((resolve, reject) => { const socket = new WebSocket(webSocketDebuggerUrl); const timer = setTimeout(() => { socket.close(); reject(new Error('DevTools evaluation timed out')); }, 10_000); socket.addEventListener('open', () => { socket.send(JSON.stringify({ id: 1, method: 'Runtime.evaluate', params: { expression, returnByValue: true }, })); }); socket.addEventListener('message', (event) => { const message = JSON.parse(String(event.data)); if (message.id !== 1) return; clearTimeout(timer); socket.close(); if (message.error || message.result?.exceptionDetails) { reject(new Error(JSON.stringify(message.error ?? message.result.exceptionDetails))); return; } resolve(message.result.result.value); }); socket.addEventListener('error', () => { clearTimeout(timer); reject(new Error('DevTools WebSocket failed')); }); }); } async function waitForRenderer(page, child) { const expression = `JSON.stringify({ bridge: Boolean(window.apsDesktop), mode: window.apsDesktop?.mode, ready: document.readyState, origin: location.origin, skillsDir: window.apsDesktop?.skillsDir, title: document.title, rootChildren: document.querySelector('#root')?.childElementCount ?? 0 })`; const deadline = Date.now() + 30_000; let lastResult = null; while (Date.now() < deadline) { if (child.exitCode !== null) { throw new Error(`packaged Electron exited during renderer validation: ${child.exitCode}`); } try { lastResult = await evaluate(page.webSocketDebuggerUrl, expression); const renderer = JSON.parse(lastResult); if ( renderer.origin.startsWith('http://127.0.0.1:') && renderer.ready === 'complete' && renderer.bridge && renderer.rootChildren > 0 ) return renderer; } catch { // Navigation replaces the initial about:blank execution context. } await delay(250); } throw new Error(`sandboxed preload validation failed: ${lastResult}`); } function stopProcessTree(child) { if (!Number.isInteger(child.pid) || child.pid <= 0) return; if (process.platform === 'win32') { spawnSync('taskkill.exe', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true, }); return; } child.kill('SIGKILL'); } async function removeUserDataDir(userDataDir) { let lastError = null; for (let attempt = 0; attempt < 5; attempt += 1) { try { fs.rmSync(userDataDir, { recursive: true, force: true }); return; } catch (error) { lastError = error; await delay(500 * (attempt + 1)); } } console.warn(`could not remove smoke user data: ${lastError}`); } async function main() { if (!fs.existsSync(executable)) { throw new Error(`packaged Electron executable is missing: ${executable}`); } const debugPort = await reservePort(); const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aps-electron-smoke-')); const child = spawn(executable, [ `--remote-debugging-port=${debugPort}`, `--user-data-dir=${userDataDir}`, ], { cwd: path.dirname(executable), stdio: 'ignore', windowsHide: true, }); try { const page = await waitForPage(debugPort, child); const renderer = await waitForRenderer(page, child); if (renderer.mode !== 'desktop') throw new Error(`unexpected desktop mode: ${renderer.mode}`); const directHealth = await fetch(`${renderer.origin}/api/health`); if (directHealth.status !== 403) { throw new Error(`direct Sidecar request returned ${directHealth.status}, expected 403`); } console.log(JSON.stringify({ electronPid: child.pid, origin: renderer.origin, bridge: renderer.bridge, mode: renderer.mode, ready: renderer.ready, directHealthStatus: directHealth.status, skillsDir: renderer.skillsDir, title: renderer.title, rootChildren: renderer.rootChildren, })); } finally { stopProcessTree(child); await delay(750); await removeUserDataDir(userDataDir); } } await main();