Something went wrong. Try again.
Hardware-in-the-loop development platform for the Commodore 64.
Something went wrong. Try again.
3.4 kB · 89 lines
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990'use strict';
const { spawn } = require('node:child_process');const fs = require('node:fs/promises');const os = require('node:os');const path = require('node:path');const { registerResource } = require('./lifecycle.cjs');
function withTimeout(promise, ms, message) { let timer; return Promise.race([ promise, new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), ms); }), ]).finally(() => clearTimeout(timer));}
// Node owns the directory even if the fixture must be killed before Rust drops// its guard. No project path supplied by the operator is ever removed here.async function startFixtureServer({ command = 'cargo', args, readyTimeout = 90_000, environment = {}, onDirectoryCreated } = {}) { const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'ultimate-tool-browser-')); const child = spawn(command, args || [ 'test', '--locked', '-p', 'ultimate-tool-server', '--test', 'browser_fixture', 'browser_fixture_server', '--', '--ignored', '--exact', '--nocapture', ], { stdio: ['pipe', 'pipe', 'pipe'], detached: true, env: { ...process.env, ...environment, ULTIMATE_TOOL_BROWSER_ROOT: directory }, }); let output = ''; let closed = false; let launchError; const exit = new Promise((resolve) => { child.once('error', (error) => { launchError = error; resolve(); }); child.once('close', () => { closed = true; resolve(); }); }); let cleaning; const stop = () => { if (cleaning) return cleaning; cleaning = (async () => { if (!closed && child.pid) { child.stdin.end('shutdown\n'); try { await withTimeout(exit, 5000, 'Fixture graceful shutdown timed out'); } catch { terminate(child); await withTimeout(exit, 5000, 'Fixture process group did not exit after termination'); } } await fs.rm(directory, { recursive: true, force: true }); unregister(); })(); return cleaning; }; function emergencyCleanup() { terminate(child); require('node:fs').rmSync(directory, { recursive: true, force: true }); } const unregister = registerResource(stop, emergencyCleanup, 0); onDirectoryCreated?.(directory); try { const readiness = new Promise((resolve, reject) => { function receive(chunk) { output = (output + chunk.toString()).slice(-12_000); const match = output.match(/ULTIMATE_TOOL_BROWSER_READY (http:\/\/127\.0\.0\.1:\d+)/); if (match) resolve(match[1]); } child.stdout.on('data', receive); child.stderr.on('data', receive); exit.then(() => reject(launchError || new Error(`Fixture exited before readiness: ${output.slice(-3000)}`))); }); const url = await withTimeout(readiness, readyTimeout, 'Browser fixture readiness timed out'); const response = await fetch(`${url}/api/v1/health`, { signal: AbortSignal.timeout(5000) }); if (!response.ok) throw new Error(`Fixture health returned ${response.status}`); return { url, directory, pid: child.pid, stop }; } catch (error) { await stop(); error.fixtureDirectory = directory; error.fixturePid = child.pid; throw error; }}
function terminate(child) { if (!child.pid) return; try { process.kill(-child.pid, 'SIGKILL'); } catch (error) { if (error.code !== 'ESRCH') throw error; }}
module.exports = { startFixtureServer, withTimeout };