Oregami
Repositories/oxedyne/daimond

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
11import fs from 'node:fs';
12import os from 'node:os';
13import path from 'node:path';
14import { spawn } from 'node:child_process';
15import { fileURLToPath, pathToFileURL } from 'node:url';
16import { requireFreshGateway, procLog } from './gwbin.mjs';
17import { signInFresh } from './session.mjs';
18import { GW_URL } from './ports.mjs';
19
20const HERE = path.dirname(fileURLToPath(import.meta.url));
21const ROOT = path.join(HERE, '..');
22const GWDIR = path.join(ROOT, 'gateway');
23const GW_LOG = procLog('probe_console');
24const SERVE_LOG = procLog('probe_console', 'serve');
25const APP = process.env.DAIMOND_APP || `http://localhost:${process.env.DAIMOND_PORT || 8777}`;
26const PW = process.env.DAIMOND_PW
27 || path.join(os.homedir(), '.red-pw/node_modules/playwright-core/index.mjs');
28const CHROME = process.env.DAIMOND_CHROME
29 || `${process.env.HOME}/.cache/ms-playwright/chromium-1229/chrome-linux64/chrome`;
30
31const sleep = ms => new Promise(r => setTimeout(r, ms));
32const procs = [];
33function launch(cmd, args, opts) { const p = spawn(cmd, args, opts); procs.push(p); return p; }
34function cleanup() { for (const p of procs) { try { p.kill('SIGKILL'); } catch (e) {} } }
35async 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.
50if (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
57let gw = null;
58async 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); });