'use strict'; // Runtime preflight: at first launch per app version, fans out parallel checks // (OS, resources, write permission, security block, system libs, network, GPU, // IPv4/v6, clock skew) under hard per-check timeouts, writes a verdict file // keyed by version, and emits a [preflight] beacon line backend.log can pick up // for fleet reporting. Subsequent launches read the cache and skip work. // Every check is `check(env, opts) => {status, reason}` where env is injected // so unit tests can drive every branch without touching real OS/network/spawn. const path = require('path'); function defaultEnv() { return { fs: require('fs'), child_process: require('child_process'), dns: require('dns'), http: require('http'), https: require('https'), os: require('os'), now: () => Date.now(), platform: process.platform, arch: process.arch, }; } // Wraps a check fn with a hard timeout + never-throw contract. Unknown after // timeout is 'warn' (not 'fail') so transient hangs don't false-positive. async function withTimeout(name, fn, timeoutMs) { const t0 = Date.now(); let timer; const timeoutPromise = new Promise((resolve) => { timer = setTimeout(() => resolve({ status: 'warn', reason: `timeout ${timeoutMs}ms` }), timeoutMs); }); let result; try { result = await Promise.race([ Promise.resolve().then(() => fn()).catch((e) => ({ status: 'warn', reason: `threw: ${String((e && e.message) || e)}` })), timeoutPromise, ]); } catch (e) { result = { status: 'warn', reason: `wrapper-threw: ${String((e && e.message) || e)}` }; } finally { if (timer) clearTimeout(timer); } if (!result || !result.status) result = { status: 'warn', reason: 'no result returned' }; return { name, status: result.status, reason: result.reason || '', durationMs: Date.now() - t0 }; } async function checkOs(env) { const p = env.platform, a = env.arch; if (!['win32', 'darwin', 'linux'].includes(p)) return { status: 'fail', reason: `unsupported platform ${p}` }; if (!['x64', 'arm64'].includes(a)) return { status: 'fail', reason: `unsupported arch ${a}` }; let rel = ''; try { rel = env.os.release(); } catch {} if (p === 'darwin') { const major = Number(String(rel).split('.')[0] || 0); if (major < 22) return { status: 'warn', reason: `macOS darwin ${rel} < 22 (macOS 13)` }; } if (p === 'win32') { const major = Number(String(rel).split('.')[0] || 0); if (major < 10) return { status: 'fail', reason: `windows ${rel} < 10` }; } return { status: 'ok', reason: `${p}/${a} release=${rel}` }; } async function checkResources(env) { let total = 0, free = 0, cpus = 0; try { total = env.os.totalmem(); free = env.os.freemem(); cpus = (env.os.cpus() || []).length; } catch (e) { return { status: 'warn', reason: `os api: ${String(e)}` }; } if (total < 4 * 1024 ** 3) return { status: 'warn', reason: `total memory ${(total / 1073741824).toFixed(1)}GB < 4GB` }; if (cpus < 2) return { status: 'warn', reason: `${cpus} cpu(s), recommended 2+` }; if (typeof env.fs.statfsSync === 'function') { try { const stat = env.fs.statfsSync(env.os.homedir()); const freeBytes = Number(stat.bavail) * Number(stat.bsize); if (freeBytes < 2 * 1024 ** 3) return { status: 'warn', reason: `home dir free ${(freeBytes / 1073741824).toFixed(1)}GB < 2GB` }; } catch (e) { return { status: 'warn', reason: `statfs threw: ${String(e)}` }; } } return { status: 'ok', reason: `mem=${(total / 1073741824).toFixed(1)}GB free=${(free / 1073741824).toFixed(1)}GB cpus=${cpus}` }; } async function checkAppdataWritable(env, dataDir) { if (!dataDir) return { status: 'warn', reason: 'no dataDir provided' }; try { env.fs.mkdirSync(dataDir, { recursive: true }); const probe = path.join(dataDir, '.preflight-probe'); env.fs.writeFileSync(probe, 'ok'); env.fs.unlinkSync(probe); return { status: 'ok', reason: `writable: ${dataDir}` }; } catch (e) { return { status: 'fail', reason: `write blocked at ${dataDir}: ${String((e && e.message) || e)}` }; } } async function checkSecurityBlock(env) { // We're already running, so any platform-level launch block already fired. // This is a soft probe to surface "may prompt next launch" cases. if (env.platform === 'darwin') { return await new Promise((resolve) => { try { env.child_process.execFile('xattr', ['-l', process.execPath], { timeout: 1500 }, (err, stdout) => { if (err) return resolve({ status: 'warn', reason: `xattr failed: ${String(err.message || err)}` }); if (/com\.apple\.quarantine/.test(String(stdout || ''))) return resolve({ status: 'warn', reason: 'app has com.apple.quarantine flag' }); resolve({ status: 'ok', reason: 'no quarantine flag' }); }); } catch (e) { resolve({ status: 'warn', reason: `xattr threw: ${String(e)}` }); } }); } if (env.platform === 'win32') { return await new Promise((resolve) => { try { env.child_process.execFile('powershell.exe', ['-NoProfile', '-Command', '(Get-MpComputerStatus).AntivirusEnabled'], { timeout: 1800 }, (err, stdout) => { if (err) return resolve({ status: 'warn', reason: `Get-MpComputerStatus failed: ${String(err.message || err)}` }); resolve({ status: 'ok', reason: `defender antivirus=${String(stdout || '').trim()}` }); }); } catch (e) { resolve({ status: 'warn', reason: `pwsh threw: ${String(e)}` }); } }); } return { status: 'ok', reason: 'linux: no os-level launch gate' }; } async function checkSystemLibs(env) { // We're a running Electron process; CRT/dyld/glibc are loaded by definition. // The detailed VCRedist/dyld probe lives in verify-python-health which spawns // the bundled interpreter; here we just attest we made it this far. return { status: 'ok', reason: `${env.platform} libs loaded (process is running)` }; } // Pick the module that matches the URL scheme; http rigs in tests should not // require https, and a malformed URL should warn cleanly rather than throw. function pickHttpModule(env, url) { return /^https:/i.test(url) ? env.https : /^http:/i.test(url) ? env.http : null; } async function checkNetwork(env, opts = {}) { const url = opts.url || 'https://api.openswarm.com/'; const timeoutMs = opts.timeoutMs || 4000; const mod = pickHttpModule(env, url); if (!mod) return { status: 'warn', reason: `unsupported URL scheme: ${url}` }; return await new Promise((resolve) => { let done = false; const finish = (v) => { if (done) return; done = true; resolve(v); }; try { const req = mod.get(url, (res) => { res.resume(); const sc = res.statusCode; finish({ status: sc >= 500 ? 'warn' : 'ok', reason: `${url} HTTP ${sc}` }); }); req.on('error', (e) => finish({ status: 'warn', reason: `${url} error: ${String((e && e.message) || e)}` })); req.setTimeout(timeoutMs, () => { try { req.destroy(); } catch {} finish({ status: 'warn', reason: `${url} timed out at ${timeoutMs}ms` }); }); } catch (e) { finish({ status: 'warn', reason: `${url} threw: ${String(e)}` }); } }); } async function checkGpu(env, opts = {}) { const app = opts.app; if (!app || typeof app.getGPUFeatureStatus !== 'function') return { status: 'warn', reason: 'no app handle (electron not in main proc)' }; try { const status = app.getGPUFeatureStatus() || {}; const compositing = status['compositing'] || 'unknown'; if (/disabled|software/i.test(String(compositing))) return { status: 'warn', reason: `gpu compositing=${compositing}` }; return { status: 'ok', reason: `gpu compositing=${compositing}` }; } catch (e) { return { status: 'warn', reason: `gpu probe threw: ${String(e)}` }; } } async function checkDualStack(env, opts = {}) { const host = opts.host || 'api.openswarm.com'; const timeoutMs = opts.timeoutMs || 3000; const lookup = (family) => new Promise((resolve) => { let done = false; const t = setTimeout(() => { if (!done) { done = true; resolve(null); } }, timeoutMs); try { env.dns.lookup(host, { family }, (err, addr) => { if (done) return; done = true; clearTimeout(t); resolve(err ? null : addr); }); } catch { if (!done) { done = true; clearTimeout(t); resolve(null); } } }); const [v4, v6] = await Promise.all([lookup(4), lookup(6)]); if (!v4 && !v6) return { status: 'warn', reason: `dns failed for both families on ${host}` }; return { status: 'ok', reason: `v4=${!!v4} v6=${!!v6}` }; } async function checkClock(env, opts = {}) { const url = opts.url || 'https://www.google.com'; const timeoutMs = opts.timeoutMs || 3000; const mod = pickHttpModule(env, url); if (!mod) return { status: 'warn', reason: `unsupported URL scheme: ${url}` }; return await new Promise((resolve) => { let done = false; const finish = (v) => { if (done) return; done = true; resolve(v); }; try { const req = mod.request(url, { method: 'HEAD' }, (res) => { const remote = res.headers['date']; if (!remote) return finish({ status: 'warn', reason: 'no Date header on response' }); const remoteMs = Date.parse(remote); const skewMs = Math.abs(env.now() - remoteMs); if (skewMs > 5 * 60 * 1000) return finish({ status: 'warn', reason: `clock skew ${(skewMs / 60000).toFixed(1)}min vs ${url}` }); finish({ status: 'ok', reason: `clock skew ${skewMs}ms` }); }); req.on('error', (e) => finish({ status: 'warn', reason: `${url} error: ${String((e && e.message) || e)}` })); req.setTimeout(timeoutMs, () => { try { req.destroy(); } catch {} finish({ status: 'warn', reason: `${url} timed out` }); }); req.end(); } catch (e) { finish({ status: 'warn', reason: `clock probe threw: ${String(e)}` }); } }); } // Load auto-tunings emitted by the dogfood aggregator: a check the loop has // repeatedly seen false-positive on its platform is silently downgraded from // fail -> warn here so a known-noisy probe can't single-handedly scare a user. // File is bundled at build time (scripts/ci/preflight-tunings.json -> resources). function loadTunings(env) { const candidates = [ path.join(__dirname, '..', 'scripts', 'ci', 'preflight-tunings.json'), path.join(process.resourcesPath || '', 'preflight-tunings.json'), ]; for (const p of candidates) { try { return JSON.parse(env.fs.readFileSync(p, 'utf8')); } catch {} } return null; } function applyTunings(results, tunings, platform) { if (!tunings || !Array.isArray(tunings.demote)) return results; const demoted = new Set(tunings.demote.filter((d) => d.platform === platform).map((d) => d.check)); if (!demoted.size) return results; return results.map((r) => (r.status === 'fail' && demoted.has(r.name)) ? { ...r, status: 'warn', reason: `${r.reason} [auto-demoted by dogfood tuning]` } : r); } async function run(env, opts = {}) { env = env || defaultEnv(); const tasks = [ withTimeout('os', () => checkOs(env), 500), withTimeout('resources', () => checkResources(env), 2000), withTimeout('appdata-writable', () => checkAppdataWritable(env, opts.dataDir), 2000), withTimeout('security-block', () => checkSecurityBlock(env), 2200), withTimeout('system-libs', () => checkSystemLibs(env), 500), withTimeout('network', () => checkNetwork(env, opts.network), 4500), withTimeout('gpu', () => checkGpu(env, opts.gpu), 1500), withTimeout('dual-stack', () => checkDualStack(env, opts.dualStack), 3500), withTimeout('clock', () => checkClock(env, opts.clock), 3500), ]; const rawResults = await Promise.all(tasks); const tunings = loadTunings(env); const results = applyTunings(rawResults, tunings, env.platform); const verdict = results.some((r) => r.status === 'fail') ? 'fail' : results.some((r) => r.status === 'warn') ? 'warn' : 'ok'; return { verdict, results, totalMs: Math.max(...results.map((r) => r.durationMs)), startedAt: env.now(), tuningsApplied: tunings ? tunings.demote.length : 0 }; } function cachePath(dataDir, appVersion) { return path.join(dataDir, `preflight-${appVersion}.json`); } function readCache(env, dataDir, appVersion) { try { const raw = JSON.parse(env.fs.readFileSync(cachePath(dataDir, appVersion), 'utf8')); if (raw && raw.appVersion === appVersion && raw.verdict === 'ok') return raw; } catch {} return null; } function writeCache(env, dataDir, appVersion, payload) { try { env.fs.mkdirSync(dataDir, { recursive: true }); env.fs.writeFileSync(cachePath(dataDir, appVersion), JSON.stringify({ appVersion, ...payload }, null, 2)); return true; } catch { return false; } } function pruneOldCaches(env, dataDir, currentVersion) { try { for (const f of env.fs.readdirSync(dataDir)) { const m = /^preflight-(.+)\.json$/.exec(f); if (m && m[1] !== currentVersion) { try { env.fs.unlinkSync(path.join(dataDir, f)); } catch {} } } } catch {} } module.exports = { defaultEnv, withTimeout, checkOs, checkResources, checkAppdataWritable, checkSecurityBlock, checkSystemLibs, checkNetwork, checkGpu, checkDualStack, checkClock, run, cachePath, readCache, writeCache, pruneOldCaches, loadTunings, applyTunings, };