Files
qinglong/scripts/ql3-physical-edge-direct-service-stop.cjs
T

843 lines
26 KiB
JavaScript

#!/usr/bin/env node
'use strict';
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const { canonicalDigest } = require('./ql3-physical-edge-evidence.cjs');
const {
directProcessIdentity,
inspectServiceManager,
normalizeDirectServiceStartManifest,
normalizeSession: normalizeDirectSession,
parseStartupReceipt,
readLinuxClockTicksPerSecond,
validateDirectServiceStartReport,
} = require('./ql3-physical-edge-direct-service-start.cjs');
const MAX_INPUT_BYTES = 256 * 1024;
const UUID_V4_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const BOOT_ID_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
const START_TICKS_PATTERN = /^[1-9][0-9]{0,19}$/;
const CONTROLLER_ROOT = '/var/lib/qinglong3-service-bridge';
const SESSION_KEYS = Object.freeze([
'activeBootId',
'activeReportDigest',
'bridge',
'directSessionDigest',
'evidenceClass',
'manifestDigest',
'preparedAt',
'processId',
'processStartTicks',
'schemaVersion',
'sessionId',
'sha256',
'startupReceiptDigest',
'uid',
]);
const BRIDGE_KEYS = Object.freeze([
'actionId',
'controllerRoot',
'intentDigest',
'intentPath',
'outcomePath',
]);
const REPORT_KEYS = Object.freeze([
'evidenceClass',
'generatedAt',
'manifest',
'observed',
'qualification',
'schemaVersion',
'session',
'sha256',
'supported',
]);
const REPORT_SESSION_KEYS = Object.freeze([
'activeBootId',
'activeReportDigest',
'directSessionDigest',
'processId',
'processStartTicks',
'sessionDigest',
'sessionId',
'startupReceiptDigest',
]);
const OBSERVED_KEYS = Object.freeze([
'bridge',
'currentBootId',
'processIdentityGone',
'service',
'shutdownReceipt',
]);
const OBSERVED_BRIDGE_KEYS = Object.freeze([
'actionId',
'intentDigest',
'observationDigest',
'outcomeDigest',
'state',
]);
const OBSERVED_SERVICE_KEYS = Object.freeze([
'active',
'enabled',
'fragmentPath',
'mainPid',
]);
const SHUTDOWN_RECEIPT_KEYS = Object.freeze([
'bootId',
'instanceId',
'nodeExecutable',
'nodeVersion',
'processId',
'processStartTicks',
'profile',
'schema',
'schemaVersion',
'sha256',
'signal',
'stoppedBootAgeMs',
'stopResult',
'startupReceiptDigest',
]);
const QUALIFICATION_KEYS = Object.freeze([
'doesNotProve',
'measures',
'passed',
'violations',
]);
const MEASURES = Object.freeze([
'owner_stop_intent_root_service_bridge_owner_outcome',
'application_sigterm_shutdown_receipt',
'startup_to_shutdown_process_identity_binding',
'exact_receipted_process_identity_gone',
'init_managed_service_stopped_and_still_enabled',
]);
const EXCLUSIONS = Object.freeze([
'service_disable_or_descriptor_removal',
'unexpected_power_loss_recovery',
'whole_device_flash_write_amplification',
'firmware_or_bootloader_shutdown',
'standalone_or_cluster_profile',
'release_archive_signature_or_attestation',
]);
class QingLong3PhysicalDirectServiceStopEvidenceError extends Error {
constructor(message) {
super(
`QingLong 3.0 physical Edge direct service stop evidence failed: ${message}`,
);
this.name = 'QingLong3PhysicalDirectServiceStopEvidenceError';
}
}
function fail(message) {
throw new QingLong3PhysicalDirectServiceStopEvidenceError(message);
}
function exact(value, keys, label) {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
JSON.stringify(Object.keys(value).sort()) !==
JSON.stringify([...keys].sort())
) {
fail(`${label} shape is invalid`);
}
}
function parseArguments(argv) {
const options = { json: false };
let phase;
for (const argument of argv) {
if (argument === '--json') {
options.json = true;
continue;
}
if (!phase && ['prepare', 'resume'].includes(argument)) {
phase = argument;
continue;
}
const separator = argument.indexOf('=');
if (separator < 1) fail(`unsupported argument ${argument}`);
const name = argument.slice(0, separator);
const value = argument.slice(separator + 1);
if (name === '--manifest') options.manifestPath = value;
else if (name === '--session') options.directSessionPath = value;
else if (name === '--active-report') options.activeReportPath = value;
else if (name === '--stop-session') options.stopSessionPath = value;
else if (name === '--output') options.outputPath = value;
else if (name === '--root-command-output')
options.rootCommandOutputPath = value;
else fail(`unsupported argument ${argument}`);
}
if (!phase) fail('phase must be prepare or resume');
const required = [
'manifestPath',
'directSessionPath',
'activeReportPath',
'stopSessionPath',
...(phase === 'resume' ? ['outputPath'] : []),
];
for (const name of required) {
if (
typeof options[name] !== 'string' ||
!path.isAbsolute(options[name]) ||
path.normalize(options[name]) !== options[name]
) {
fail(`${name} must be absolute and normalized`);
}
}
if (
options.rootCommandOutputPath !== undefined &&
(phase !== 'prepare' ||
!path.isAbsolute(options.rootCommandOutputPath) ||
path.normalize(options.rootCommandOutputPath) !==
options.rootCommandOutputPath)
) {
fail('rootCommandOutputPath is valid only as a normalized prepare path');
}
return Object.freeze({ phase, ...options });
}
function currentUid() {
if (
typeof process.getuid !== 'function' ||
typeof process.geteuid !== 'function' ||
process.getuid() !== process.geteuid()
) {
fail('real and effective POSIX users must match');
}
return process.getuid();
}
function readPrivateFile(filePath, label) {
const before = fs.lstatSync(filePath, { bigint: true });
if (
!before.isFile() ||
before.isSymbolicLink() ||
Number(before.uid) !== currentUid() ||
(Number(before.mode) & 0o777) !== 0o600 ||
before.nlink !== 1n ||
before.size < 1n ||
before.size > BigInt(MAX_INPUT_BYTES) ||
fs.realpathSync(filePath) !== filePath
) {
fail(`${label} is not a private regular file`);
}
const descriptor = fs.openSync(
filePath,
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0),
);
try {
const opened = fs.fstatSync(descriptor, { bigint: true });
if (
opened.dev !== before.dev ||
opened.ino !== before.ino ||
opened.size !== before.size ||
opened.nlink !== 1n
) {
fail(`${label} identity changed while opening`);
}
const material = Buffer.alloc(Number(opened.size));
let offset = 0;
while (offset < material.byteLength) {
const count = fs.readSync(
descriptor,
material,
offset,
material.byteLength - offset,
null,
);
if (count < 1) fail(`${label} read made no progress`);
offset += count;
}
const after = fs.fstatSync(descriptor, { bigint: true });
if (
after.dev !== opened.dev ||
after.ino !== opened.ino ||
after.size !== opened.size ||
after.nlink !== 1n
) {
material.fill(0);
fail(`${label} identity changed while reading`);
}
try {
return new TextDecoder('utf-8', { fatal: true }).decode(material);
} finally {
material.fill(0);
}
} finally {
fs.closeSync(descriptor);
}
}
function readPrivateJson(filePath, label) {
try {
return JSON.parse(readPrivateFile(filePath, label));
} catch (error) {
if (error instanceof QingLong3PhysicalDirectServiceStopEvidenceError)
throw error;
fail(`${label} is not JSON`);
}
}
function writeNoReplace(filePath, contents) {
if (fs.existsSync(filePath)) fail(`${filePath} already exists`);
const parent = fs.realpathSync(path.dirname(filePath));
if (path.join(parent, path.basename(filePath)) !== filePath) {
fail('output path parent is not canonical');
}
const stat = fs.lstatSync(parent);
if (
!stat.isDirectory() ||
stat.isSymbolicLink() ||
stat.uid !== currentUid() ||
(stat.mode & 0o022) !== 0
) {
fail('output parent is not owner-controlled');
}
const descriptor = fs.openSync(
filePath,
fs.constants.O_WRONLY |
fs.constants.O_CREAT |
fs.constants.O_EXCL |
(fs.constants.O_NOFOLLOW ?? 0),
0o600,
);
try {
fs.writeFileSync(descriptor, contents, 'utf8');
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
return filePath;
}
function currentBootId() {
const value = fs
.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8')
.trim()
.toLowerCase();
if (!BOOT_ID_PATTERN.test(value)) fail('current boot ID is invalid');
return value;
}
function requireDeploymentProduct() {
try {
return require(path.join(
__dirname,
'..',
'packages/ql3-local-owner-cli/dist/deployment/localDeployment.js',
));
} catch (error) {
fail(`local deployment product must be built first: ${error.message}`);
}
}
function requireShutdownReceiptProduct() {
try {
return require(path.join(
__dirname,
'..',
'packages/ql3-local-application/dist/production-process/shutdownReceipt.js',
));
} catch (error) {
fail(`local application product must be built first: ${error.message}`);
}
}
function normalizeStopSession(value, directSession) {
exact(value, SESSION_KEYS, 'stop session');
exact(value.bridge, BRIDGE_KEYS, 'stop session bridge');
const { sha256, ...body } = value;
if (
value.schemaVersion !== 1 ||
value.evidenceClass !== 'physical_edge_direct_service_stop_session' ||
!UUID_V4_PATTERN.test(value.sessionId ?? '') ||
value.uid !== directSession.uid ||
!Number.isFinite(Date.parse(value.preparedAt ?? '')) ||
!SHA256_PATTERN.test(value.manifestDigest ?? '') ||
value.directSessionDigest !== directSession.sha256 ||
!SHA256_PATTERN.test(value.activeReportDigest ?? '') ||
!BOOT_ID_PATTERN.test(value.activeBootId ?? '') ||
!SHA256_PATTERN.test(value.startupReceiptDigest ?? '') ||
!Number.isSafeInteger(value.processId) ||
value.processId < 1 ||
!START_TICKS_PATTERN.test(value.processStartTicks ?? '') ||
value.bridge.actionId !== value.sessionId ||
value.bridge.controllerRoot !== CONTROLLER_ROOT ||
!SHA256_PATTERN.test(value.bridge.intentDigest ?? '') ||
value.bridge.intentPath !==
path.join(
directSession.paths.deploymentRoot,
'service',
'service-manager-intents',
`${value.sessionId}.json`,
) ||
value.bridge.outcomePath !==
path.join(
directSession.paths.deploymentRoot,
'service',
'service-manager-outcomes',
`${value.sessionId}.json`,
) ||
canonicalDigest(body) !== sha256
) {
fail('stop session is invalid or drifted');
}
return Object.freeze(value);
}
function processIdentityGone(processId, startTicks) {
const processRoot = `/proc/${processId}`;
if (!fs.existsSync(processRoot)) return true;
let contents;
try {
contents = fs.readFileSync(path.join(processRoot, 'stat'), 'utf8');
} catch {
fail('prior process identity cannot be inspected');
}
const end = contents.lastIndexOf(') ');
const fields =
end < 2
? []
: contents
.slice(end + 2)
.trim()
.split(/\s+/u);
const current = fields[19];
if (!current || !START_TICKS_PATTERN.test(current)) {
fail('prior process stat is invalid');
}
return current !== startTicks;
}
function evaluateDirectServiceStopEvidence({
manifest,
stopSession,
observed,
}) {
const violations = [];
try {
exact(observed, OBSERVED_KEYS, 'stop observation');
exact(observed.bridge, OBSERVED_BRIDGE_KEYS, 'stop bridge observation');
exact(observed.service, OBSERVED_SERVICE_KEYS, 'stop service observation');
exact(observed.shutdownReceipt, SHUTDOWN_RECEIPT_KEYS, 'shutdown receipt');
} catch {
return Object.freeze(['direct service stop observation shape is invalid']);
}
const receipt = observed.shutdownReceipt;
const { sha256: shutdownDigest, ...shutdownPayload } = receipt;
const recomputedShutdownDigest = crypto
.createHash('sha256')
.update('qinglong.local-application-shutdown-receipt.v1\0', 'utf8')
.update(JSON.stringify(shutdownPayload), 'utf8')
.digest('hex');
if (
canonicalDigest(manifest) !== stopSession.manifestDigest ||
observed.currentBootId !== stopSession.activeBootId ||
observed.bridge.actionId !== stopSession.bridge.actionId ||
observed.bridge.intentDigest !== stopSession.bridge.intentDigest ||
observed.bridge.state !== 'stopped' ||
!SHA256_PATTERN.test(observed.bridge.outcomeDigest ?? '') ||
!SHA256_PATTERN.test(observed.bridge.observationDigest ?? '')
) {
violations.push('Owner stop bridge binding is invalid');
}
if (
receipt.schemaVersion !== 1 ||
receipt.schema !== 'qinglong/local-application-shutdown-receipt@v1' ||
receipt.profile !== 'edge' ||
receipt.signal !== 'SIGTERM' ||
receipt.stopResult !== 'stopped' ||
receipt.startupReceiptDigest !== stopSession.startupReceiptDigest ||
receipt.bootId !== stopSession.activeBootId ||
receipt.processId !== stopSession.processId ||
receipt.processStartTicks !== stopSession.processStartTicks ||
!SHA256_PATTERN.test(shutdownDigest ?? '') ||
shutdownDigest !== recomputedShutdownDigest
) {
violations.push('application shutdown receipt binding is invalid');
}
if (
observed.processIdentityGone !== true ||
observed.service.active !== false ||
observed.service.enabled !== true ||
observed.service.fragmentPath !==
(manifest.serviceManager === 'systemd'
? '/etc/systemd/system/qinglong3.service'
: '/etc/init.d/qinglong3') ||
(manifest.serviceManager === 'systemd' && observed.service.mainPid !== 0)
) {
violations.push('init or prior process stopped state is invalid');
}
return Object.freeze(violations);
}
function buildDirectServiceStopReport({
manifest,
stopSession,
observed,
generatedAt,
}) {
const violations = evaluateDirectServiceStopEvidence({
manifest,
stopSession,
observed,
});
const body = {
schemaVersion: 1,
evidenceClass: 'physical_edge_direct_service_stop_candidate',
supported: false,
generatedAt,
manifest,
session: {
sessionId: stopSession.sessionId,
sessionDigest: stopSession.sha256,
directSessionDigest: stopSession.directSessionDigest,
activeReportDigest: stopSession.activeReportDigest,
activeBootId: stopSession.activeBootId,
startupReceiptDigest: stopSession.startupReceiptDigest,
processId: stopSession.processId,
processStartTicks: stopSession.processStartTicks,
},
observed,
qualification: {
passed: violations.length === 0,
violations,
measures: MEASURES,
doesNotProve: EXCLUSIONS,
},
};
return Object.freeze({ ...body, sha256: canonicalDigest(body) });
}
function validateDirectServiceStopReport(report, manifest) {
const violations = [];
try {
exact(report, REPORT_KEYS, 'stop report');
exact(report.session, REPORT_SESSION_KEYS, 'stop report session');
exact(report.qualification, QUALIFICATION_KEYS, 'stop qualification');
} catch {
return Object.freeze(['direct service stop report shape is invalid']);
}
const { sha256, ...body } = report;
if (
report.schemaVersion !== 1 ||
report.evidenceClass !== 'physical_edge_direct_service_stop_candidate' ||
report.supported !== false ||
!Number.isFinite(Date.parse(report.generatedAt ?? '')) ||
canonicalDigest(report.manifest) !== canonicalDigest(manifest) ||
!UUID_V4_PATTERN.test(report.session.sessionId ?? '') ||
!SHA256_PATTERN.test(report.session.sessionDigest ?? '') ||
!SHA256_PATTERN.test(report.session.directSessionDigest ?? '') ||
!SHA256_PATTERN.test(report.session.activeReportDigest ?? '') ||
!BOOT_ID_PATTERN.test(report.session.activeBootId ?? '') ||
!SHA256_PATTERN.test(report.session.startupReceiptDigest ?? '') ||
!Number.isSafeInteger(report.session.processId) ||
report.session.processId < 1 ||
!START_TICKS_PATTERN.test(report.session.processStartTicks ?? '') ||
!SHA256_PATTERN.test(sha256 ?? '') ||
canonicalDigest(body) !== sha256
) {
violations.push('direct service stop report identity is invalid');
}
const syntheticSession = {
manifestDigest: canonicalDigest(manifest),
activeReportDigest: report.session.activeReportDigest,
directSessionDigest: report.session.directSessionDigest,
activeBootId: report.session.activeBootId,
startupReceiptDigest: report.session?.startupReceiptDigest,
processId: report.session?.processId,
processStartTicks: report.session?.processStartTicks,
bridge: {
actionId: report.observed?.bridge?.actionId,
intentDigest: report.observed?.bridge?.intentDigest,
},
};
const recomputed = evaluateDirectServiceStopEvidence({
manifest,
stopSession: syntheticSession,
observed: report.observed,
});
if (
report.qualification.passed !== (recomputed.length === 0) ||
JSON.stringify(report.qualification.violations) !==
JSON.stringify(recomputed) ||
JSON.stringify(report.qualification.measures) !==
JSON.stringify(MEASURES) ||
JSON.stringify(report.qualification.doesNotProve) !==
JSON.stringify(EXCLUSIONS) ||
recomputed.length > 0
) {
violations.push('direct service stop qualification was widened');
}
return Object.freeze(violations);
}
function loadContext(options) {
const manifest = normalizeDirectServiceStartManifest(
readPrivateJson(options.manifestPath, 'manifest'),
);
const directSession = normalizeDirectSession(
readPrivateJson(options.directSessionPath, 'direct session'),
);
const activeReport = readPrivateJson(
options.activeReportPath,
'active report',
);
const activeViolations = validateDirectServiceStartReport(
activeReport,
manifest,
activeReport.observed?.after,
);
if (
activeViolations.length > 0 ||
activeReport.qualification?.passed !== true
) {
fail(`active report is invalid: ${activeViolations.join('; ')}`);
}
if (
directSession.manifestDigest !== canonicalDigest(manifest) ||
activeReport.session?.sessionDigest !== directSession.sha256 ||
directSession.uid !== currentUid() ||
currentBootId() !== activeReport.observed.after.bootId
) {
fail('active report, direct session, Owner UID or current boot drifted');
}
return Object.freeze({ manifest, directSession, activeReport });
}
function rootCommand(directSession, bridge) {
return Object.freeze({
schemaVersion: 1,
operation: 'local.deployment.service-manager.execute',
options: {
controllerRoot: bridge.controllerRoot,
allowRootController: true,
manager:
directSession.service.kind === 'systemd'
? {
kind: 'systemd',
executable: directSession.service.managerExecutable,
}
: {
kind: 'openrc',
serviceExecutable: directSession.service.managerExecutable,
updateExecutable: directSession.service.enableExecutable,
},
},
request: {
intentPath: bridge.intentPath,
expectedIntentDigest: bridge.intentDigest,
},
});
}
function preparePhase(options) {
if (process.platform !== 'linux') fail('prepare requires Linux');
const { manifest, directSession, activeReport } = loadContext(options);
if (options.rootCommandOutputPath !== undefined) {
const dataPath = fs.realpathSync(directSession.paths.dataPath);
if (
options.rootCommandOutputPath === options.stopSessionPath ||
!options.rootCommandOutputPath.startsWith(`${dataPath}${path.sep}`)
) {
fail('rootCommandOutputPath must be a distinct child of dataPath');
}
}
const receipt = parseStartupReceipt(
readPrivateFile(directSession.paths.startupReceipt, 'startup receipt'),
);
if (
receipt.sha256 !== activeReport.observed.receipt.sha256 ||
receipt.bootId !== activeReport.observed.after.bootId
) {
fail('current startup receipt drifted from the active report');
}
directProcessIdentity(directSession, receipt, readLinuxClockTicksPerSecond());
const service = inspectServiceManager(directSession, receipt.processId);
if (!service.active || !service.enabled)
fail('service is not active and enabled');
const actionId = crypto.randomUUID();
const product = requireDeploymentProduct();
const intent = product.prepareLocalServiceManagerIntent({
schemaVersion: 1,
operation: 'local.deployment.service-manager.intent.prepare',
options: {
deploymentRoot: directSession.paths.deploymentRoot,
allowRootService: directSession.uid === 0,
},
request: {
actionId,
action: 'stop',
serviceKind: directSession.service.kind,
lineage: { mode: 'fresh' },
requestedAtMs: Date.now(),
},
});
const body = {
schemaVersion: 1,
evidenceClass: 'physical_edge_direct_service_stop_session',
sessionId: actionId,
uid: directSession.uid,
preparedAt: new Date().toISOString(),
manifestDigest: canonicalDigest(manifest),
directSessionDigest: directSession.sha256,
activeReportDigest: activeReport.sha256,
activeBootId: receipt.bootId,
startupReceiptDigest: receipt.sha256,
processId: receipt.processId,
processStartTicks: receipt.processStartTicks,
bridge: {
actionId: intent.actionId,
controllerRoot: CONTROLLER_ROOT,
intentDigest: intent.intentDigest,
intentPath: intent.intentPath,
outcomePath: intent.outcomePath,
},
};
const stopSession = Object.freeze({ ...body, sha256: canonicalDigest(body) });
writeNoReplace(
options.stopSessionPath,
`${JSON.stringify(stopSession, null, 2)}\n`,
);
const command = rootCommand(directSession, stopSession.bridge);
if (options.rootCommandOutputPath !== undefined) {
writeNoReplace(
options.rootCommandOutputPath,
`${JSON.stringify(command, null, 2)}\n`,
);
}
process.stdout.write(
`${JSON.stringify({
schemaVersion: 1,
status: 'awaiting_root_service_bridge_stop',
stopSessionDigest: stopSession.sha256,
rootBridgeCommand: command,
automaticServiceManagerMutationPerformed: false,
supported: false,
})}\n`,
);
}
function resumePhase(options) {
if (process.platform !== 'linux') fail('resume requires Linux');
const { manifest, directSession, activeReport } = loadContext(options);
const stopSession = normalizeStopSession(
readPrivateJson(options.stopSessionPath, 'stop session'),
directSession,
);
if (
stopSession.manifestDigest !== canonicalDigest(manifest) ||
stopSession.activeReportDigest !== activeReport.sha256 ||
stopSession.activeBootId !== currentBootId()
) {
fail('stop session lost the active report or boot binding');
}
const product = requireDeploymentProduct();
const outcome = product.consumeLocalServiceManagerOutcome({
schemaVersion: 1,
operation: 'local.deployment.service-manager.outcome.consume',
options: {
deploymentRoot: directSession.paths.deploymentRoot,
allowRootService: directSession.uid === 0,
},
request: {
actionId: stopSession.bridge.actionId,
expectedIntentDigest: stopSession.bridge.intentDigest,
},
});
if (outcome.status !== 'verified' || outcome.state !== 'stopped') {
fail('Owner could not verify a stopped root bridge outcome');
}
const shutdownPath = `${directSession.paths.applicationConfig}.stopped.json`;
const shutdownProduct = requireShutdownReceiptProduct();
const shutdownReceipt = shutdownProduct.parseLocalApplicationShutdownReceipt(
readPrivateFile(shutdownPath, 'shutdown receipt'),
);
if (
shutdownReceipt.instanceId !== activeReport.observed.receipt.instanceId ||
shutdownReceipt.profile !== 'edge' ||
shutdownReceipt.nodeExecutable !==
activeReport.observed.receipt.nodeExecutable ||
shutdownReceipt.stoppedBootAgeMs <
activeReport.observed.receipt.activeBootAgeMs
) {
fail('shutdown receipt drifted from the active process');
}
const service = inspectServiceManager(directSession, stopSession.processId);
const observed = {
currentBootId: currentBootId(),
bridge: {
actionId: outcome.actionId,
intentDigest: stopSession.bridge.intentDigest,
outcomeDigest: outcome.outcomeDigest,
observationDigest: outcome.observationDigest,
state: outcome.state,
},
shutdownReceipt,
processIdentityGone: processIdentityGone(
stopSession.processId,
stopSession.processStartTicks,
),
service: {
active: service.active,
enabled: service.enabled,
fragmentPath: service.fragmentPath,
mainPid: service.mainPid,
},
};
const report = buildDirectServiceStopReport({
manifest,
stopSession,
observed,
generatedAt: new Date().toISOString(),
});
writeNoReplace(
options.outputPath,
`${JSON.stringify(report, null, options.json ? 0 : 2)}\n`,
);
process.stdout.write(
`${JSON.stringify(report, null, options.json ? 0 : 2)}\n`,
);
if (!report.qualification.passed) process.exitCode = 1;
}
function main() {
if (Number(process.versions.node.split('.')[0]) < 24) {
fail('Node.js 24 or newer is required');
}
const options = parseArguments(process.argv.slice(2));
if (options.phase === 'prepare') preparePhase(options);
else resumePhase(options);
}
module.exports = {
QingLong3PhysicalDirectServiceStopEvidenceError,
buildDirectServiceStopReport,
evaluateDirectServiceStopEvidence,
normalizeStopSession,
parseArguments,
validateDirectServiceStopReport,
};
if (require.main === module) {
try {
main();
} catch (error) {
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n`,
);
process.exitCode = 1;
}
}