oxedyne/daimond/dev/probe_console.mjs
7.5 KiB, 1 run
created by r2519314175:39, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | // probe_console.mjs — open the operator console as an owner and say what the |
| 2 | // page actually did: every admin response, every console message, and what |
| 3 | // each panel holds afterwards. |
| 4 | // |
| 5 | // node dev/probe_console.mjs |
| 6 | // |
| 7 | // Written to tell an empty panel that FAILED from an empty panel that was |
| 8 | // never filled. `verify_releases` can only report that a wait expired; this |
| 9 | // reports the responses behind it. |
| 10 | |
| 11 | import fs from 'node:fs'; |
| 12 | import os from 'node:os'; |
| 13 | import path from 'node:path'; |
| 14 | import { spawn } from 'node:child_process'; |
| 15 | import { fileURLToPath, pathToFileURL } from 'node:url'; |
| 16 | import { requireFreshGateway, procLog } from './gwbin.mjs'; |
| 17 | import { signInFresh } from './session.mjs'; |
| 18 | import { GW_URL } from './ports.mjs'; |
| 19 | |
| 20 | const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 21 | const ROOT = path.join(HERE, '..'); |
| 22 | const GWDIR = path.join(ROOT, 'gateway'); |
| 23 | const GW_LOG = procLog('probe_console'); |
| 24 | const SERVE_LOG = procLog('probe_console', 'serve'); |
| 25 | const APP = process.env.DAIMOND_APP || `http://localhost:${process.env.DAIMOND_PORT || 8777}`; |
| 26 | const PW = process.env.DAIMOND_PW |
| 27 | || path.join(os.homedir(), '.red-pw/node_modules/playwright-core/index.mjs'); |
| 28 | const CHROME = process.env.DAIMOND_CHROME |
| 29 | || `${process.env.HOME}/.cache/ms-playwright/chromium-1229/chrome-linux64/chrome`; |
| 30 | |
| 31 | const sleep = ms => new Promise(r => setTimeout(r, ms)); |
| 32 | const procs = []; |
| 33 | function launch(cmd, args, opts) { const p = spawn(cmd, args, opts); procs.push(p); return p; } |
| 34 | function cleanup() { for (const p of procs) { try { p.kill('SIGKILL'); } catch (e) {} } } |
| 35 | async function waitFor(fn, ms = 30000, gap = 300) { |
| 36 | const t0 = Date.now(); |
| 37 | for (;;) { |
| 38 | try { if (await fn()) return true; } catch (e) {} |
| 39 | if (Date.now() - t0 > ms) return false; |
| 40 | await sleep(gap); |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | // This is a diagnostic, not a gate, and it is the one thing here that may |
| 45 | // deliberately drive a binary older than the tree: the question it answers is |
| 46 | // what the PAGE did with a reply, and the library underneath is beside the |
| 47 | // point. `DAIMOND_PROBE_ANY_BUILD=1` says so out loud, and prints the build's |
| 48 | // age so nothing read from this run can be mistaken for a measurement of the |
| 49 | // current source. Every verifier still refuses. |
| 50 | if (process.env.DAIMOND_PROBE_ANY_BUILD === '1') { |
| 51 | const bin = path.join(GWDIR, 'target/release/daimond_gateway'); |
| 52 | console.log(' NOTE driving ' + bin + ' as it stands, whatever its age — this is a probe.'); |
| 53 | } else { |
| 54 | requireFreshGateway(); |
| 55 | } |
| 56 | |
| 57 | let gw = null; |
| 58 | async function startGateway(owner) { |
| 59 | if (gw) { try { gw.kill('SIGKILL'); } catch (e) {} await sleep(1500); } |
| 60 | gw = launch(path.join(GWDIR, 'target/release/daimond_gateway'), [], { |
| 61 | cwd: GWDIR, |
| 62 | env: { ...process.env, APP_MODE: 'sandbox', |
| 63 | ...(owner ? { DAIMOND_OWNER_ACCOUNTS: owner } : {}) }, |
| 64 | stdio: GW_LOG.stdio, |
| 65 | }); |
| 66 | return await waitFor(async () => (await fetch(`${GW_URL}/api/health`)).ok); |
| 67 | } |
| 68 | |
| 69 | (async () => { |
| 70 | if (!await startGateway(null)) { console.log('gateway did not start'); GW_LOG.report(); process.exit(1); } |
| 71 | let already = false; |
| 72 | try { already = (await fetch(`${APP}/console/`)).ok; } catch (e) {} |
| 73 | if (!already) launch('node', ['dev/serve.mjs'], { cwd: ROOT, stdio: SERVE_LOG.stdio }); |
| 74 | if (!await waitFor(async () => (await fetch(`${APP}/console/`)).ok, 15000)) { |
| 75 | console.log('dev server did not serve'); cleanup(); process.exit(1); |
| 76 | } |
| 77 | |
| 78 | const { chromium } = await import(pathToFileURL(PW).href); |
| 79 | // DISPLAY is dropped, and it is the reason a wait for content here could |
| 80 | // expire over a panel that was filled thirty seconds earlier. This session's |
| 81 | // DISPLAY is an X display forwarded over SSH; a headless Chrome still |
| 82 | // consults it, and when nothing answers the compositor never produces a |
| 83 | // frame -- so `requestAnimationFrame` never fires, and `waitForFunction` |
| 84 | // polls on rAF by default. `dev/harness.mjs` drops it for the same reason, |
| 85 | // and this file launches its own browser rather than using the harness, so |
| 86 | // it has to know the same thing. |
| 87 | const env = { ...process.env }; |
| 88 | delete env.DISPLAY; |
| 89 | const browser = await chromium.launch({ |
| 90 | executablePath: CHROME, headless: true, args: ['--no-sandbox'], env }); |
| 91 | try { |
| 92 | const ctx = await browser.newContext({ viewport: { width: 1400, height: 1100 } }); |
| 93 | const page = await ctx.newPage(); |
| 94 | const acct = await signInFresh(page, APP); |
| 95 | await startGateway(acct); |
| 96 | |
| 97 | const said = [], errs = [], responses = []; |
| 98 | page.on('console', m => said.push(m.type() + ': ' + m.text())); |
| 99 | page.on('pageerror', e => errs.push(e.message)); |
| 100 | page.on('response', async r => { |
| 101 | const u = r.url(); |
| 102 | if (u.includes('/api/admin') || u.includes('transparency')) { |
| 103 | responses.push(r.status() + ' ' + u.replace(APP, '').slice(0, 80)); |
| 104 | } |
| 105 | }); |
| 106 | |
| 107 | fs.mkdirSync(path.join(HERE, 'shots'), { recursive: true }); |
| 108 | const t0 = Date.now(); |
| 109 | await page.goto(`${APP}/console/`, { waitUntil: 'domcontentloaded' }); |
| 110 | await page.waitForSelector('#admin-app:not([hidden])', { timeout: 15000 }); |
| 111 | const filled = await page.waitForFunction(() => { |
| 112 | const el = document.getElementById('admin-rel-declared'); |
| 113 | return !!el && el.children.length > 0; |
| 114 | }, null, { timeout: 40000 }).then(() => true).catch(() => false); |
| 115 | console.log(`\n releases list filled: ${filled} after ${Date.now() - t0} ms`); |
| 116 | |
| 117 | const state = await page.evaluate(() => ({ |
| 118 | relChildren: (document.getElementById('admin-rel-declared') || {}).children?.length ?? -1, |
| 119 | relHint: (document.getElementById('admin-rel-hint') || {}).textContent || '', |
| 120 | relStat: (document.getElementById('admin-rel-status') || {}).textContent || '', |
| 121 | relErr: (document.getElementById('admin-rel-err') || {}).textContent || '', |
| 122 | status: (document.getElementById('admin-status') || {}).textContent || '', |
| 123 | kpis: document.querySelectorAll('#admin-kpis .admin-kpi').length, |
| 124 | ops: document.querySelectorAll('#admin-ops-list .admin-rel-row, #admin-ops-list .admin-op-row').length, |
| 125 | accounts: document.querySelectorAll('#admin-accounts tbody tr').length, |
| 126 | ledger: document.querySelectorAll('#admin-ledger tbody tr').length, |
| 127 | })); |
| 128 | console.log(' page state: ' + JSON.stringify(state, null, 1)); |
| 129 | |
| 130 | // What Refresh does now: ask for a fresh reading of the store, wait a |
| 131 | // bounded time for it, and say which of those two happened. |
| 132 | const before = await page.evaluate(() => |
| 133 | (document.getElementById('admin-health') || {}).textContent || ''); |
| 134 | const t1 = Date.now(); |
| 135 | await page.click('#admin-refresh'); |
| 136 | await page.waitForFunction(() => { |
| 137 | const h = (document.getElementById('admin-health') || {}).textContent || ''; |
| 138 | return /just now|fresh reading/.test(h); |
| 139 | }, null, { timeout: 30000 }).catch(() => {}); |
| 140 | const after = await page.evaluate(() => |
| 141 | (document.getElementById('admin-health') || {}).textContent || ''); |
| 142 | console.log(`\n health line before refresh: "${before}"`); |
| 143 | console.log(` health line after refresh: "${after}" (${Date.now() - t1} ms)`); |
| 144 | await page.locator('#admin-kpis').screenshot({ path: path.join(HERE, 'shots', 'console-kpis.png') }) |
| 145 | .catch(() => {}); |
| 146 | await page.locator('#admin-health').screenshot({ path: path.join(HERE, 'shots', 'console-health.png') }) |
| 147 | .catch(() => {}); |
| 148 | console.log('\n admin responses:'); |
| 149 | for (const r of responses) console.log(' ' + r); |
| 150 | if (errs.length) { console.log('\n page errors:'); for (const e of errs) console.log(' ' + e); } |
| 151 | if (said.length) { console.log('\n console said:'); for (const s of said.slice(-20)) console.log(' ' + s); } |
| 152 | } catch (e) { |
| 153 | console.error(e); |
| 154 | GW_LOG.report(); |
| 155 | } finally { |
| 156 | await browser.close(); |
| 157 | cleanup(); |
| 158 | } |
| 159 | process.exit(0); |
| 160 | })().catch(e => { console.error(e); cleanup(); process.exit(1); }); |