#!/usr/bin/env node const crypto = require('node:crypto'); const fs = require('node:fs'); const path = require('node:path'); const { performance } = require('node:perf_hooks'); const { canonicalDigest, writeNoReplace, } = require('./ql3-physical-edge-evidence.cjs'); const MAX_PROC_BYTES = 64 * 1024; const MANIFEST_KEYS = Object.freeze([ 'deviceId', 'durationSeconds', 'evidenceClass', 'expectedExecutable', 'processId', 'profile', 'sampleIntervalMs', 'schemaVersion', ]); class QingLong3PhysicalIdleEvidenceError extends Error { constructor(message) { super(`QingLong 3.0 physical Edge idle evidence failed: ${message}`); this.name = 'QingLong3PhysicalIdleEvidenceError'; } } function exactKeys(value, expected, label) { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new QingLong3PhysicalIdleEvidenceError(`${label} must be an object`); } const actual = Object.keys(value).sort(); if (JSON.stringify(actual) !== JSON.stringify(expected)) { throw new QingLong3PhysicalIdleEvidenceError( `${label} keys must be exactly ${expected.join(', ')}`, ); } } function normalizeIdleManifest(value) { exactKeys(value, MANIFEST_KEYS, 'manifest'); if (value.schemaVersion !== 1) { throw new QingLong3PhysicalIdleEvidenceError( 'manifest schemaVersion must be 1', ); } if (value.evidenceClass !== 'physical_edge_idle_candidate') { throw new QingLong3PhysicalIdleEvidenceError( 'manifest evidenceClass must be physical_edge_idle_candidate', ); } if (value.profile !== 'edge') { throw new QingLong3PhysicalIdleEvidenceError( 'manifest profile must be edge', ); } if ( typeof value.deviceId !== 'string' || !/^[a-z0-9][a-z0-9._-]{2,63}$/.test(value.deviceId) ) { throw new QingLong3PhysicalIdleEvidenceError( 'manifest deviceId is invalid', ); } if ( !Number.isSafeInteger(value.processId) || value.processId < 2 || value.processId > 4_194_304 ) { throw new QingLong3PhysicalIdleEvidenceError( 'manifest processId is invalid', ); } if ( typeof value.expectedExecutable !== 'string' || !path.isAbsolute(value.expectedExecutable) || value.expectedExecutable.length > 4096 ) { throw new QingLong3PhysicalIdleEvidenceError( 'manifest expectedExecutable must be a bounded absolute path', ); } if ( !Number.isSafeInteger(value.durationSeconds) || value.durationSeconds < 30 || value.durationSeconds > 3600 ) { throw new QingLong3PhysicalIdleEvidenceError( 'manifest durationSeconds must be between 30 and 3600', ); } if ( !Number.isSafeInteger(value.sampleIntervalMs) || value.sampleIntervalMs < 1000 || value.sampleIntervalMs > 60_000 || (value.durationSeconds * 1000) % value.sampleIntervalMs !== 0 ) { throw new QingLong3PhysicalIdleEvidenceError( 'manifest sampleIntervalMs must divide the duration and be between 1000 and 60000', ); } return Object.freeze({ schemaVersion: 1, evidenceClass: 'physical_edge_idle_candidate', profile: 'edge', deviceId: value.deviceId, processId: value.processId, expectedExecutable: value.expectedExecutable, durationSeconds: value.durationSeconds, sampleIntervalMs: value.sampleIntervalMs, }); } function parseArguments(argv) { const options = { json: false }; let separatorSeen = false; for (const argument of argv) { if (argument === '--' && !separatorSeen) { separatorSeen = true; continue; } if (argument === '--json') { options.json = true; continue; } const separator = argument.indexOf('='); if (separator < 1) { throw new QingLong3PhysicalIdleEvidenceError( `unsupported argument ${argument}`, ); } const name = argument.slice(0, separator); const value = argument.slice(separator + 1); if (name === '--manifest') options.manifestPath = value; else if (name === '--output') options.outputPath = value; else { throw new QingLong3PhysicalIdleEvidenceError( `unsupported argument ${argument}`, ); } } for (const name of ['manifestPath', 'outputPath']) { if (!path.isAbsolute(options[name] ?? '')) { throw new QingLong3PhysicalIdleEvidenceError(`${name} must be absolute`); } } return Object.freeze(options); } function readBoundedFile(filePath, label) { const stat = fs.lstatSync(filePath); if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_PROC_BYTES) { throw new QingLong3PhysicalIdleEvidenceError( `${label} must be a bounded regular file`, ); } const value = fs.readFileSync(filePath); if (value.length > MAX_PROC_BYTES) { throw new QingLong3PhysicalIdleEvidenceError(`${label} is oversized`); } return value; } function readManifest(filePath) { try { return normalizeIdleManifest( JSON.parse(readBoundedFile(filePath, 'manifest').toString('utf8')), ); } catch (error) { if (error instanceof QingLong3PhysicalIdleEvidenceError) throw error; throw new QingLong3PhysicalIdleEvidenceError( `manifest could not be read: ${error.message}`, ); } } function safeInteger(raw, label, { allowNegative = false } = {}) { if (!/^-?\d+$/.test(raw)) { throw new QingLong3PhysicalIdleEvidenceError(`${label} is invalid`); } const value = Number(raw); if (!Number.isSafeInteger(value) || (!allowNegative && value < 0)) { throw new QingLong3PhysicalIdleEvidenceError(`${label} is invalid`); } return value; } function parseProcStat(raw) { const close = raw.lastIndexOf(') '); if (!raw.startsWith('(') && !/^\d+ \(/.test(raw)) { throw new QingLong3PhysicalIdleEvidenceError('process stat is malformed'); } if (close < 4) { throw new QingLong3PhysicalIdleEvidenceError('process stat is malformed'); } const open = raw.indexOf('('); const processId = safeInteger(raw.slice(0, open).trim(), 'process stat pid'); const fields = raw .slice(close + 2) .trim() .split(/\s+/); if (fields.length < 22 || !/^[RSDTtXZIKWP]$/.test(fields[0])) { throw new QingLong3PhysicalIdleEvidenceError('process stat is malformed'); } return Object.freeze({ processId, state: fields[0], minorFaults: safeInteger(fields[7], 'process stat minor faults'), majorFaults: safeInteger(fields[9], 'process stat major faults'), userTicks: safeInteger(fields[11], 'process stat user ticks'), systemTicks: safeInteger(fields[12], 'process stat system ticks'), threadCount: safeInteger(fields[17], 'process stat thread count'), startTicks: safeInteger(fields[19], 'process stat start ticks'), rssPages: safeInteger(fields[21], 'process stat RSS pages'), }); } function linesByName(raw, separator) { const values = new Map(); for (const line of raw.trim().split('\n')) { const index = line.indexOf(separator); if (index < 1) continue; values.set( line.slice(0, index), line.slice(index + separator.length).trim(), ); } return values; } function parseProcStatus(raw) { const values = linesByName(raw, ':'); const rss = values.get('VmRSS')?.match(/^(\d+) kB$/); const uid = values.get('Uid')?.split(/\s+/).map(Number); if ( !rss || uid?.length !== 4 || uid.some((value) => !Number.isSafeInteger(value)) ) { throw new QingLong3PhysicalIdleEvidenceError('process status is malformed'); } return Object.freeze({ uid: uid[0], rssBytes: safeInteger(rss[1], 'process status RSS') * 1024, threadCount: safeInteger( values.get('Threads') ?? '', 'process status thread count', ), voluntaryContextSwitches: safeInteger( values.get('voluntary_ctxt_switches') ?? '', 'process status voluntary context switches', ), involuntaryContextSwitches: safeInteger( values.get('nonvoluntary_ctxt_switches') ?? '', 'process status involuntary context switches', ), }); } function parseProcIo(raw) { const values = linesByName(raw, ':'); const read = (name) => safeInteger(values.get(name) ?? '', `process I/O ${name}`); return Object.freeze({ readBytes: read('read_bytes'), writeBytes: read('write_bytes'), readSyscalls: read('syscr'), writeSyscalls: read('syscw'), }); } function readProcessSnapshot(processId) { const root = `/proc/${processId}`; const stat = parseProcStat( readBoundedFile(`${root}/stat`, 'process stat').toString('utf8'), ); const status = parseProcStatus( readBoundedFile(`${root}/status`, 'process status').toString('utf8'), ); const io = parseProcIo( readBoundedFile(`${root}/io`, 'process I/O').toString('utf8'), ); const command = readBoundedFile(`${root}/cmdline`, 'process command'); if (command.length === 0) { throw new QingLong3PhysicalIdleEvidenceError('process command is empty'); } return Object.freeze({ monotonicMs: performance.now(), wallTimeMs: Date.now(), bootId: readBoundedFile('/proc/sys/kernel/random/boot_id', 'boot identity') .toString('utf8') .trim(), executable: fs.realpathSync(`${root}/exe`), commandSha256: crypto.createHash('sha256').update(command).digest('hex'), ...stat, ...status, ...io, }); } function delta(first, last, name, violations) { const value = last[name] - first[name]; if (!Number.isSafeInteger(value) || value < 0) { violations.push(`${name} counter regressed`); return null; } return value; } function validateSnapshotSeries(manifest, snapshots) { const violations = []; const expectedSamples = (manifest.durationSeconds * 1000) / manifest.sampleIntervalMs + 1; if (snapshots.length !== expectedSamples) { violations.push( `sample count ${snapshots.length} did not equal ${expectedSamples}`, ); } const first = snapshots[0]; if (!first) return Object.freeze(violations); for (const [index, snapshot] of snapshots.entries()) { if (snapshot.processId !== manifest.processId) { violations.push(`sample ${index} process identity changed`); } if (snapshot.executable !== manifest.expectedExecutable) { violations.push(`sample ${index} executable changed`); } if (snapshot.bootId !== first.bootId) { violations.push(`sample ${index} boot identity changed`); } if (snapshot.startTicks !== first.startTicks) { violations.push(`sample ${index} process generation changed`); } if (snapshot.commandSha256 !== first.commandSha256) { violations.push(`sample ${index} command identity changed`); } if (snapshot.uid !== first.uid) { violations.push(`sample ${index} process UID changed`); } if (['Z', 'X'].includes(snapshot.state)) { violations.push(`sample ${index} process was not live`); } if (index > 0 && snapshot.monotonicMs <= snapshots[index - 1].monotonicMs) { violations.push(`sample ${index} monotonic time did not advance`); } } return Object.freeze([...new Set(violations)]); } function percentile(sorted, percentage) { return sorted[ Math.min(sorted.length - 1, Math.ceil(sorted.length * percentage) - 1) ]; } function summarizeSnapshots(snapshots, violations = []) { if (snapshots.length < 2) { throw new QingLong3PhysicalIdleEvidenceError( 'at least two process samples are required', ); } const first = snapshots[0]; const last = snapshots[snapshots.length - 1]; const rss = snapshots.map(({ rssBytes }) => rssBytes).sort((a, b) => a - b); return Object.freeze({ sampleCount: snapshots.length, actualDurationMs: Number((last.monotonicMs - first.monotonicMs).toFixed(3)), rssBytes: Object.freeze({ minimum: rss[0], p50: percentile(rss, 0.5), p95: percentile(rss, 0.95), maximum: rss[rss.length - 1], final: last.rssBytes, }), threadCountMaximum: Math.max( ...snapshots.map(({ threadCount }) => threadCount), ), cpuTicks: Object.freeze({ user: delta(first, last, 'userTicks', violations), system: delta(first, last, 'systemTicks', violations), }), faults: Object.freeze({ minor: delta(first, last, 'minorFaults', violations), major: delta(first, last, 'majorFaults', violations), }), contextSwitches: Object.freeze({ voluntary: delta(first, last, 'voluntaryContextSwitches', violations), involuntary: delta(first, last, 'involuntaryContextSwitches', violations), }), io: Object.freeze({ readBytes: delta(first, last, 'readBytes', violations), writeBytes: delta(first, last, 'writeBytes', violations), readSyscalls: delta(first, last, 'readSyscalls', violations), writeSyscalls: delta(first, last, 'writeSyscalls', violations), }), }); } function buildIdleEvidenceReport({ manifest, snapshots, generatedAt }) { const violations = [...validateSnapshotSeries(manifest, snapshots)]; const summary = summarizeSnapshots(snapshots, violations); const first = snapshots[0]; const body = { schemaVersion: 1, evidenceClass: 'physical_edge_idle_candidate', supported: false, generatedAt, manifest, identity: { platform: process.platform, architecture: process.arch, node: process.version, bootId: first.bootId, processId: first.processId, processUid: first.uid, executable: first.executable, commandSha256: first.commandSha256, startTicks: first.startTicks, }, summary, qualification: { passed: violations.length === 0, violations, measures: [ 'process_rss', 'process_cpu_ticks', 'process_context_switches', 'process_io', ], doesNotProve: [ 'whole_device_cpu_wakeups', 'whole_device_flash_write_amplification', 'cold_start_or_first_ready', ], }, }; return Object.freeze({ ...body, sha256: canonicalDigest(body) }); } function delay(milliseconds) { return new Promise((resolve) => setTimeout(resolve, milliseconds)); } async function collectSnapshots(manifest) { if (process.platform !== 'linux') { throw new QingLong3PhysicalIdleEvidenceError('sampler requires Linux'); } const canonicalExecutable = fs.realpathSync(manifest.expectedExecutable); if (canonicalExecutable !== manifest.expectedExecutable) { throw new QingLong3PhysicalIdleEvidenceError( 'expectedExecutable must be canonical', ); } const sampleCount = (manifest.durationSeconds * 1000) / manifest.sampleIntervalMs + 1; const snapshots = []; for (let index = 0; index < sampleCount; index += 1) { if (index > 0) await delay(manifest.sampleIntervalMs); snapshots.push(readProcessSnapshot(manifest.processId)); } return Object.freeze(snapshots); } async function main() { if (Number(process.versions.node.split('.')[0]) < 24) { throw new QingLong3PhysicalIdleEvidenceError( 'Node.js 24 or newer is required', ); } const options = parseArguments(process.argv.slice(2)); const manifest = readManifest(options.manifestPath); const snapshots = await collectSnapshots(manifest); const report = buildIdleEvidenceReport({ manifest, snapshots, generatedAt: new Date().toISOString(), }); const serialized = `${JSON.stringify(report, null, options.json ? 0 : 2)}\n`; writeNoReplace(options.outputPath, serialized); process.stdout.write(serialized); if (!report.qualification.passed) process.exitCode = 1; } module.exports = { QingLong3PhysicalIdleEvidenceError, buildIdleEvidenceReport, normalizeIdleManifest, parseArguments, parseProcIo, parseProcStat, parseProcStatus, summarizeSnapshots, validateSnapshotSeries, }; if (require.main === module) { main().catch((error) => { process.stderr.write( `${error instanceof Error ? error.message : String(error)}\n`, ); process.exitCode = 1; }); }