mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,729 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
const crypto = require('node:crypto');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const {
|
||||
consumeLocalServiceManagerOutcome,
|
||||
prepareLocalServiceManagerIntent,
|
||||
} = require('../packages/ql3-local-owner-cli/dist/deployment/service-manager/serviceManagerIntent.js');
|
||||
const {
|
||||
consumeLocalServiceManagerCutoverOutcome,
|
||||
} = require('../packages/ql3-local-owner-cli/dist/deployment/service-manager/serviceCutoverConsumer.js');
|
||||
const {
|
||||
cutoverDigest,
|
||||
} = require('../packages/ql3-local-owner-cli/dist/deployment/cutover/targetEvidence.js');
|
||||
const {
|
||||
runLocalServiceBridgeCommandFile,
|
||||
} = require('../packages/ql3-local-owner-cli/dist/deployment/service-manager/serviceBridge.js');
|
||||
|
||||
const OWNER_DEPLOYMENT_CLI = path.resolve(
|
||||
__dirname,
|
||||
'../packages/ql3-local-owner-cli/dist/deployment/localDeploymentCli.js',
|
||||
);
|
||||
const NON_ROOT_SERVICE_UID = 10001;
|
||||
|
||||
class QingLong3ServiceManagerBridgeLiveActorError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'QingLong3ServiceManagerBridgeLiveActorError';
|
||||
}
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
throw new QingLong3ServiceManagerBridgeLiveActorError(message);
|
||||
}
|
||||
|
||||
function run(executable, args, accepted = [0]) {
|
||||
const result = spawnSync(executable, args, {
|
||||
encoding: 'utf8',
|
||||
timeout: 30_000,
|
||||
maxBuffer: 64 * 1024,
|
||||
});
|
||||
if (
|
||||
result.error ||
|
||||
result.signal !== null ||
|
||||
!accepted.includes(result.status)
|
||||
) {
|
||||
fail(
|
||||
`${path.basename(executable)} ${args.join(' ')} failed: ${String(
|
||||
result.stderr ?? '',
|
||||
).slice(0, 512)}`,
|
||||
);
|
||||
}
|
||||
return String(result.stdout ?? '');
|
||||
}
|
||||
|
||||
function executable(candidates, label) {
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) return fs.realpathSync(candidate);
|
||||
}
|
||||
fail(`${label} executable is unavailable`);
|
||||
}
|
||||
|
||||
function writePrivate(filePath, contents, mode = 0o600, uid = 0, gid = 0) {
|
||||
fs.writeFileSync(filePath, contents, { flag: 'wx', mode });
|
||||
fs.chownSync(filePath, uid, gid);
|
||||
fs.chmodSync(filePath, mode);
|
||||
}
|
||||
|
||||
function descriptor(kind, uid, gid, root, adopted = false) {
|
||||
const command = adopted ? fs.realpathSync(process.execPath) : '/bin/sleep';
|
||||
const commandArgs = adopted
|
||||
? `/workspace/scripts/ql3-service-manager-adopted-live-service.cjs ${root}/local-application.json`
|
||||
: '300';
|
||||
if (kind === 'systemd') {
|
||||
return [
|
||||
'[Unit]',
|
||||
'Description=QingLong 3 bridge live gate',
|
||||
'',
|
||||
'[Service]',
|
||||
'Type=simple',
|
||||
`User=${uid}`,
|
||||
`Group=${gid}`,
|
||||
`ExecStart=${command} ${commandArgs}`,
|
||||
'Restart=no',
|
||||
'',
|
||||
'[Install]',
|
||||
'WantedBy=multi-user.target',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
return [
|
||||
'#!/sbin/openrc-run',
|
||||
'name="qinglong3"',
|
||||
'description="QingLong 3 bridge live gate"',
|
||||
`command="${command}"`,
|
||||
`command_args="${commandArgs}"`,
|
||||
`command_user="${uid}:${gid}"`,
|
||||
'supervisor="supervise-daemon"',
|
||||
'retry="TERM/5/KILL/2"',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function manager(kind) {
|
||||
if (kind === 'systemd') {
|
||||
return {
|
||||
kind,
|
||||
executable: executable(
|
||||
['/usr/bin/systemctl', '/bin/systemctl'],
|
||||
'systemctl',
|
||||
),
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind,
|
||||
serviceExecutable: executable(
|
||||
['/sbin/rc-service', '/usr/sbin/rc-service'],
|
||||
'rc-service',
|
||||
),
|
||||
updateExecutable: executable(
|
||||
['/sbin/rc-update', '/usr/sbin/rc-update'],
|
||||
'rc-update',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function actionId(kind, suffix) {
|
||||
const prefix = kind === 'systemd' ? '51' : '61';
|
||||
return `123e4567-e89b-42d3-a456-42661417${prefix}${suffix}`;
|
||||
}
|
||||
|
||||
function ownerCli(uid, gid, operation, commandPath) {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[OWNER_DEPLOYMENT_CLI, operation, '--command-file', commandPath],
|
||||
{
|
||||
uid,
|
||||
gid,
|
||||
encoding: 'utf8',
|
||||
timeout: 30_000,
|
||||
maxBuffer: 64 * 1024,
|
||||
},
|
||||
);
|
||||
if (result.error || result.signal !== null || result.status !== 0) {
|
||||
fail(
|
||||
`Owner ${operation} failed: ${String(result.stderr ?? '').slice(0, 512)}`,
|
||||
);
|
||||
}
|
||||
return JSON.parse(String(result.stdout ?? ''));
|
||||
}
|
||||
|
||||
function prepare(
|
||||
root,
|
||||
kind,
|
||||
suffix,
|
||||
action,
|
||||
uid,
|
||||
gid,
|
||||
lineage = { mode: 'fresh' },
|
||||
) {
|
||||
const command = {
|
||||
schemaVersion: 1,
|
||||
operation: 'local.deployment.service-manager.intent.prepare',
|
||||
options: { deploymentRoot: root, allowRootService: uid === 0 },
|
||||
request: {
|
||||
actionId: actionId(kind, suffix),
|
||||
action,
|
||||
serviceKind: kind,
|
||||
lineage,
|
||||
requestedAtMs: Date.now(),
|
||||
},
|
||||
};
|
||||
if (uid === 0) return prepareLocalServiceManagerIntent(command);
|
||||
const commandPath = path.join(
|
||||
root,
|
||||
`${actionId(kind, suffix)}.owner-intent.json`,
|
||||
);
|
||||
writePrivate(
|
||||
commandPath,
|
||||
`${JSON.stringify(command, null, 2)}\n`,
|
||||
0o600,
|
||||
uid,
|
||||
gid,
|
||||
);
|
||||
return ownerCli(uid, gid, 'service-intent-prepare', commandPath);
|
||||
}
|
||||
|
||||
function replacePrivate(filePath, contents, mode, uid, gid) {
|
||||
fs.rmSync(filePath, { force: true });
|
||||
writePrivate(filePath, contents, mode, uid, gid);
|
||||
}
|
||||
|
||||
function adoptedFixture(root, kind, uid, gid) {
|
||||
const cutoverId = `${kind}-${uid === 0 ? 'root' : 'nonroot'}-cutover`;
|
||||
const journal = path.join(root, 'service', 'cutovers', cutoverId);
|
||||
fs.mkdirSync(journal, { recursive: true, mode: 0o700 });
|
||||
fs.chownSync(path.join(root, 'service', 'cutovers'), uid, gid);
|
||||
fs.chownSync(journal, uid, gid);
|
||||
const sourcePath = path.join(root, 'legacy.sqlite');
|
||||
const targetPath = path.join(root, 'target.sqlite');
|
||||
const recoveryPath = path.join(root, 'recovery.sqlite');
|
||||
const manifestPath = path.join(root, 'manifest.json');
|
||||
const activationPath = path.join(root, 'activation.json');
|
||||
writePrivate(sourcePath, 'legacy\n', 0o600, uid, gid);
|
||||
writePrivate(targetPath, 'target\n', 0o600, uid, gid);
|
||||
writePrivate(recoveryPath, 'legacy\n', 0o600, uid, gid);
|
||||
const manifestPayload = {
|
||||
schemaVersion: 1,
|
||||
kind: 'qinglong3-local-adoption-manifest-live-fixture',
|
||||
};
|
||||
const manifestDigest = cutoverDigest(manifestPayload);
|
||||
writePrivate(
|
||||
manifestPath,
|
||||
`${JSON.stringify({ ...manifestPayload, manifestDigest })}\n`,
|
||||
0o600,
|
||||
uid,
|
||||
gid,
|
||||
);
|
||||
const target = fs.statSync(targetPath, { bigint: true });
|
||||
const activationPayload = {
|
||||
schemaVersion: 1,
|
||||
kind: 'qinglong3-local-sqlite-activation',
|
||||
state: 'prepared',
|
||||
profile: 'edge',
|
||||
createdAtMs: Date.now(),
|
||||
adoptionManifestDigest: manifestDigest,
|
||||
planDigest: '2'.repeat(64),
|
||||
sourcePathDigest: crypto
|
||||
.createHash('sha256')
|
||||
.update(sourcePath)
|
||||
.digest('hex'),
|
||||
recoverySha256: crypto
|
||||
.createHash('sha256')
|
||||
.update(fs.readFileSync(recoveryPath))
|
||||
.digest('hex'),
|
||||
targetSha256: crypto
|
||||
.createHash('sha256')
|
||||
.update(fs.readFileSync(targetPath))
|
||||
.digest('hex'),
|
||||
targetPathDigest: crypto
|
||||
.createHash('sha256')
|
||||
.update(targetPath)
|
||||
.digest('hex'),
|
||||
targetDevice: target.dev.toString(),
|
||||
targetInode: target.ino.toString(),
|
||||
};
|
||||
const activationDigest = cutoverDigest(activationPayload);
|
||||
writePrivate(
|
||||
activationPath,
|
||||
`${JSON.stringify({ ...activationPayload, activationDigest })}\n`,
|
||||
0o600,
|
||||
uid,
|
||||
gid,
|
||||
);
|
||||
const commitmentPayload = {
|
||||
schemaVersion: 1,
|
||||
kind: 'qinglong3-local-legacy-silence-commitment',
|
||||
state: 'legacy_stopped',
|
||||
cutoverId,
|
||||
profile: 'edge',
|
||||
instanceId: `${kind}-${uid === 0 ? 'root' : 'nonroot'}-live-edge`,
|
||||
activationDigest,
|
||||
requestedAtMs: Date.now(),
|
||||
observedAtMs: Date.now(),
|
||||
previousRecordDigest: '3'.repeat(64),
|
||||
controller: {
|
||||
kind: 'docker',
|
||||
endpointDigest: '4'.repeat(64),
|
||||
legacyContainerId: '5'.repeat(64),
|
||||
legacyContainerIdentityDigest: '6'.repeat(64),
|
||||
legacySourceBindingDigest: '7'.repeat(64),
|
||||
},
|
||||
};
|
||||
const commitmentDigest = cutoverDigest(commitmentPayload);
|
||||
const commitmentPath = path.join(journal, '0002-legacy-stopped.json');
|
||||
writePrivate(
|
||||
commitmentPath,
|
||||
`${JSON.stringify({ ...commitmentPayload, commitmentDigest })}\n`,
|
||||
0o600,
|
||||
uid,
|
||||
gid,
|
||||
);
|
||||
replacePrivate(
|
||||
path.join(root, 'local-application.json'),
|
||||
`${JSON.stringify({
|
||||
schema: 'qinglong/local-application-process@v3',
|
||||
instanceId: commitmentPayload.instanceId,
|
||||
profile: 'edge',
|
||||
storage: {
|
||||
mode: 'adopted',
|
||||
sourcePath,
|
||||
targetPath,
|
||||
recoveryPath,
|
||||
manifestPath,
|
||||
activationPath,
|
||||
expectedActivationDigest: activationDigest,
|
||||
},
|
||||
runtime: {},
|
||||
pluginPackages: {},
|
||||
ai: { deployment: 'excluded' },
|
||||
cutover: {
|
||||
cutoverId,
|
||||
commitmentPath,
|
||||
expectedCommitmentDigest: commitmentDigest,
|
||||
},
|
||||
})}\n`,
|
||||
0o600,
|
||||
uid,
|
||||
gid,
|
||||
);
|
||||
replacePrivate(
|
||||
path.join(
|
||||
root,
|
||||
'service',
|
||||
kind === 'systemd' ? 'qinglong3.service' : 'qinglong3.openrc',
|
||||
),
|
||||
descriptor(kind, uid, gid, root, true),
|
||||
kind === 'systemd' ? 0o600 : 0o700,
|
||||
uid,
|
||||
gid,
|
||||
);
|
||||
const instanceRoot = path.join(root, 'service', 'cutover-instances');
|
||||
const instanceDirectory = path.join(
|
||||
instanceRoot,
|
||||
commitmentPayload.instanceId,
|
||||
);
|
||||
fs.mkdirSync(instanceDirectory, { recursive: true, mode: 0o700 });
|
||||
fs.chownSync(instanceRoot, uid, gid);
|
||||
fs.chownSync(instanceDirectory, uid, gid);
|
||||
const initialPayload = {
|
||||
schema: 'qinglong3-local-cutover-instance-head',
|
||||
schemaVersion: 1,
|
||||
revision: 1,
|
||||
instanceId: commitmentPayload.instanceId,
|
||||
profile: 'edge',
|
||||
cutoverId,
|
||||
activationDigest,
|
||||
state: 'legacy_stop_requested',
|
||||
generation: 0,
|
||||
previousHeadDigest: '0'.repeat(64),
|
||||
sourceRecordDigest: '8'.repeat(64),
|
||||
updatedAtMs: Date.now(),
|
||||
};
|
||||
const initialHeadDigest = cutoverDigest(initialPayload);
|
||||
const headPayload = {
|
||||
...initialPayload,
|
||||
revision: 2,
|
||||
state: 'legacy_stopped',
|
||||
previousHeadDigest: initialHeadDigest,
|
||||
sourceRecordDigest: commitmentDigest,
|
||||
};
|
||||
writePrivate(
|
||||
path.join(instanceDirectory, 'head.json'),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
...headPayload,
|
||||
headDigest: cutoverDigest(headPayload),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
0o600,
|
||||
uid,
|
||||
gid,
|
||||
);
|
||||
return { cutoverId, activationDigest, commitmentDigest };
|
||||
}
|
||||
|
||||
async function consumeCutover(root, prepared, uid, gid) {
|
||||
const command = {
|
||||
schemaVersion: 1,
|
||||
operation: 'local.deployment.service-manager.cutover.consume',
|
||||
options: {
|
||||
deploymentRoot: root,
|
||||
allowRootService: uid === 0,
|
||||
startupTimeoutMs: 30_000,
|
||||
startupPollMs: 50,
|
||||
},
|
||||
request: {
|
||||
actionId: prepared.actionId,
|
||||
expectedIntentDigest: prepared.intentDigest,
|
||||
},
|
||||
};
|
||||
if (uid === 0) return consumeLocalServiceManagerCutoverOutcome(command);
|
||||
const commandPath = path.join(
|
||||
root,
|
||||
`${prepared.actionId}.owner-cutover-consume.json`,
|
||||
);
|
||||
writePrivate(
|
||||
commandPath,
|
||||
`${JSON.stringify(command, null, 2)}\n`,
|
||||
0o600,
|
||||
uid,
|
||||
gid,
|
||||
);
|
||||
return ownerCli(uid, gid, 'service-cutover-consume', commandPath);
|
||||
}
|
||||
|
||||
function execute(root, controllerRoot, managerOptions, prepared, uid, gid) {
|
||||
const command = {
|
||||
schemaVersion: 1,
|
||||
operation: 'local.deployment.service-manager.execute',
|
||||
options: {
|
||||
controllerRoot,
|
||||
allowRootController: true,
|
||||
manager: managerOptions,
|
||||
},
|
||||
request: {
|
||||
intentPath: prepared.intentPath,
|
||||
expectedIntentDigest: prepared.intentDigest,
|
||||
},
|
||||
};
|
||||
const commandPath = path.join(root, `${prepared.actionId}.root-command.json`);
|
||||
writePrivate(commandPath, `${JSON.stringify(command, null, 2)}\n`);
|
||||
const result = runLocalServiceBridgeCommandFile(commandPath);
|
||||
const consumeCommand = {
|
||||
schemaVersion: 1,
|
||||
operation: 'local.deployment.service-manager.outcome.consume',
|
||||
options: { deploymentRoot: root, allowRootService: uid === 0 },
|
||||
request: {
|
||||
actionId: prepared.actionId,
|
||||
expectedIntentDigest: prepared.intentDigest,
|
||||
},
|
||||
};
|
||||
let consumed;
|
||||
if (uid === 0) {
|
||||
consumed = consumeLocalServiceManagerOutcome(consumeCommand);
|
||||
} else {
|
||||
const consumePath = path.join(
|
||||
root,
|
||||
`${prepared.actionId}.owner-consume.json`,
|
||||
);
|
||||
writePrivate(
|
||||
consumePath,
|
||||
`${JSON.stringify(consumeCommand, null, 2)}\n`,
|
||||
0o600,
|
||||
uid,
|
||||
gid,
|
||||
);
|
||||
consumed = ownerCli(uid, gid, 'service-outcome-consume', consumePath);
|
||||
}
|
||||
return { commandPath, result, consumed };
|
||||
}
|
||||
|
||||
function serviceProcessUid(kind, outcome) {
|
||||
let pid = kind === 'systemd' ? outcome.observation.mainPid : 0;
|
||||
if (kind === 'openrc') {
|
||||
for (const entry of fs.readdirSync('/proc')) {
|
||||
if (!/^\d+$/.test(entry)) continue;
|
||||
try {
|
||||
const command = fs
|
||||
.readFileSync(`/proc/${entry}/cmdline`, 'utf8')
|
||||
.split('\0');
|
||||
if (command[0] === '/bin/sleep' && command[1] === '300') {
|
||||
pid = Number(entry);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Processes can disappear during the bounded scan.
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Number.isSafeInteger(pid) || pid < 1) fail('service PID is unproved');
|
||||
const status = fs.readFileSync(`/proc/${pid}/status`, 'utf8');
|
||||
const match = /^Uid:\s+(\d+)\s/m.exec(status);
|
||||
if (!match) fail('service process UID is unavailable');
|
||||
return Number(match[1]);
|
||||
}
|
||||
|
||||
function cleanup(kind, managerOptions) {
|
||||
if (kind === 'systemd') {
|
||||
spawnSync(
|
||||
managerOptions.executable,
|
||||
['disable', '--now', 'qinglong3.service'],
|
||||
{ timeout: 10_000 },
|
||||
);
|
||||
fs.rmSync('/etc/systemd/system/qinglong3.service', { force: true });
|
||||
spawnSync(managerOptions.executable, ['daemon-reload'], {
|
||||
timeout: 10_000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
spawnSync(managerOptions.serviceExecutable, ['qinglong3', 'stop'], {
|
||||
timeout: 10_000,
|
||||
});
|
||||
spawnSync(managerOptions.updateExecutable, ['del', 'qinglong3', 'default'], {
|
||||
timeout: 10_000,
|
||||
});
|
||||
fs.rmSync('/etc/init.d/qinglong3', { force: true });
|
||||
}
|
||||
|
||||
async function main(argv) {
|
||||
if (process.getuid?.() !== 0 || process.geteuid?.() !== 0) {
|
||||
fail('live actor must run as root');
|
||||
}
|
||||
const kind = argv[0];
|
||||
const identityMode = argv[1];
|
||||
if (
|
||||
(kind !== 'systemd' && kind !== 'openrc') ||
|
||||
(identityMode !== 'root' && identityMode !== 'nonroot')
|
||||
) {
|
||||
fail(
|
||||
'usage: ql3-service-manager-bridge-live-actor.cjs <systemd|openrc> <root|nonroot>',
|
||||
);
|
||||
}
|
||||
const uid = identityMode === 'root' ? 0 : NON_ROOT_SERVICE_UID;
|
||||
const gid = uid;
|
||||
const root = `/var/lib/ql3-service-manager-${kind}-${identityMode}`;
|
||||
const controllerRoot = `/var/lib/ql3-service-bridge-${kind}-${identityMode}`;
|
||||
fs.rmSync(root, { force: true, recursive: true });
|
||||
fs.rmSync(controllerRoot, { force: true, recursive: true });
|
||||
fs.mkdirSync(root, { mode: 0o700 });
|
||||
fs.mkdirSync(path.join(root, 'service'), { mode: 0o700 });
|
||||
fs.chownSync(root, uid, gid);
|
||||
fs.chownSync(path.join(root, 'service'), uid, gid);
|
||||
writePrivate(
|
||||
path.join(root, 'local-application.json'),
|
||||
`${JSON.stringify({
|
||||
schema: 'qinglong/local-application-process@v2',
|
||||
instanceId: `${kind}-${identityMode}-live-edge`,
|
||||
profile: 'edge',
|
||||
storage: { mode: 'fresh' },
|
||||
})}\n`,
|
||||
0o600,
|
||||
uid,
|
||||
gid,
|
||||
);
|
||||
writePrivate(
|
||||
path.join(
|
||||
root,
|
||||
'service',
|
||||
kind === 'systemd' ? 'qinglong3.service' : 'qinglong3.openrc',
|
||||
),
|
||||
descriptor(kind, uid, gid),
|
||||
kind === 'systemd' ? 0o600 : 0o700,
|
||||
uid,
|
||||
gid,
|
||||
);
|
||||
const managerOptions = manager(kind);
|
||||
try {
|
||||
const install = prepare(root, kind, '01', 'install-enable-start', uid, gid);
|
||||
const installed = execute(
|
||||
root,
|
||||
controllerRoot,
|
||||
managerOptions,
|
||||
install,
|
||||
uid,
|
||||
gid,
|
||||
);
|
||||
if (
|
||||
installed.result.state !== 'active' ||
|
||||
installed.consumed.state !== 'active'
|
||||
) {
|
||||
fail(
|
||||
`install-enable-start did not prove active: ${JSON.stringify(
|
||||
installed,
|
||||
)} outcome=${fs.readFileSync(install.outcomePath, 'utf8').trim()}`,
|
||||
);
|
||||
}
|
||||
const installedOutcome = JSON.parse(
|
||||
fs.readFileSync(install.outcomePath, 'utf8'),
|
||||
);
|
||||
const installedUid = serviceProcessUid(kind, installedOutcome);
|
||||
if (installedUid !== uid) {
|
||||
fail(`installed service process UID drifted: ${installedUid} != ${uid}`);
|
||||
}
|
||||
const replay = runLocalServiceBridgeCommandFile(installed.commandPath);
|
||||
if (replay.status !== 'existing' || replay.state !== 'active') {
|
||||
fail('exact replay did not reuse the durable outcome');
|
||||
}
|
||||
const restartRequired = kind !== 'openrc' || identityMode === 'root';
|
||||
let restarted;
|
||||
if (restartRequired) {
|
||||
const restart = prepare(root, kind, '02', 'restart', uid, gid);
|
||||
restarted = execute(
|
||||
root,
|
||||
controllerRoot,
|
||||
managerOptions,
|
||||
restart,
|
||||
uid,
|
||||
gid,
|
||||
);
|
||||
if (
|
||||
restarted.result.state !== 'active' ||
|
||||
restarted.consumed.state !== 'active'
|
||||
) {
|
||||
fail('restart did not prove active');
|
||||
}
|
||||
const restartedOutcome = JSON.parse(
|
||||
fs.readFileSync(restart.outcomePath, 'utf8'),
|
||||
);
|
||||
const restartedUid = serviceProcessUid(kind, restartedOutcome);
|
||||
if (restartedUid !== uid) {
|
||||
fail(
|
||||
`restarted service process UID drifted: ${restartedUid} != ${uid}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const stop = prepare(root, kind, '03', 'stop', uid, gid);
|
||||
const stopped = execute(
|
||||
root,
|
||||
controllerRoot,
|
||||
managerOptions,
|
||||
stop,
|
||||
uid,
|
||||
gid,
|
||||
);
|
||||
if (
|
||||
stopped.result.state !== 'stopped' ||
|
||||
stopped.consumed.state !== 'stopped'
|
||||
) {
|
||||
fail('stop did not prove stopped');
|
||||
}
|
||||
cleanup(kind, managerOptions);
|
||||
const adopted = adoptedFixture(root, kind, uid, gid);
|
||||
const adoptedStart = prepare(
|
||||
root,
|
||||
kind,
|
||||
'11',
|
||||
'install-enable-start',
|
||||
uid,
|
||||
gid,
|
||||
{
|
||||
mode: 'adopted',
|
||||
cutoverId: adopted.cutoverId,
|
||||
generation: 1,
|
||||
expectedActivationDigest: adopted.activationDigest,
|
||||
previousRecordDigest: adopted.commitmentDigest,
|
||||
},
|
||||
);
|
||||
const adoptedStarted = execute(
|
||||
root,
|
||||
controllerRoot,
|
||||
managerOptions,
|
||||
adoptedStart,
|
||||
uid,
|
||||
gid,
|
||||
);
|
||||
const adoptedActive = await consumeCutover(root, adoptedStart, uid, gid);
|
||||
if (
|
||||
adoptedStarted.result.state !== 'active' ||
|
||||
adoptedActive.state !== 'target_active'
|
||||
) {
|
||||
fail('adopted service did not commit target_active');
|
||||
}
|
||||
const adoptedStop = prepare(root, kind, '13', 'stop', uid, gid, {
|
||||
mode: 'adopted',
|
||||
cutoverId: adopted.cutoverId,
|
||||
generation: 1,
|
||||
expectedActivationDigest: adopted.activationDigest,
|
||||
previousRecordDigest: adoptedActive.recordDigest,
|
||||
});
|
||||
const adoptedStoppedManager = execute(
|
||||
root,
|
||||
controllerRoot,
|
||||
managerOptions,
|
||||
adoptedStop,
|
||||
uid,
|
||||
gid,
|
||||
);
|
||||
const adoptedStopped = await consumeCutover(root, adoptedStop, uid, gid);
|
||||
if (
|
||||
adoptedStoppedManager.result.state !== 'stopped' ||
|
||||
adoptedStopped.state !== 'target_stopped'
|
||||
) {
|
||||
fail('adopted service did not commit target_stopped');
|
||||
}
|
||||
const payload = {
|
||||
schemaVersion: 1,
|
||||
evidenceClass: 'qinglong3_service_manager_bridge_live_actor',
|
||||
managerKind: kind,
|
||||
identityMode,
|
||||
serviceUid: uid,
|
||||
manager: managerOptions,
|
||||
actions: [
|
||||
installed.result,
|
||||
replay,
|
||||
...(restarted ? [restarted.result] : []),
|
||||
stopped.result,
|
||||
],
|
||||
adoptedCutover: {
|
||||
active: adoptedActive,
|
||||
stopped: adoptedStopped,
|
||||
},
|
||||
gates: {
|
||||
rootCommandFile: true,
|
||||
descriptorInstalled: true,
|
||||
enabledAndStarted: true,
|
||||
exactReplay: true,
|
||||
restartRequired,
|
||||
restarted: restarted !== undefined,
|
||||
stopped: true,
|
||||
ownerOutcomeVerified: true,
|
||||
serviceProcessIdentity: true,
|
||||
adoptedCutoverActive: true,
|
||||
adoptedCutoverStopped: true,
|
||||
},
|
||||
};
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
...payload,
|
||||
sha256: crypto
|
||||
.createHash('sha256')
|
||||
.update(JSON.stringify(payload))
|
||||
.digest('hex'),
|
||||
})}\n`,
|
||||
);
|
||||
} finally {
|
||||
cleanup(kind, managerOptions);
|
||||
fs.rmSync(root, { force: true, recursive: true });
|
||||
fs.rmSync(controllerRoot, { force: true, recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
main(process.argv.slice(2)).catch((error) => {
|
||||
process.stderr.write(
|
||||
`${JSON.stringify({
|
||||
code: 'QL3_SERVICE_MANAGER_BRIDGE_LIVE_ACTOR_FAILED',
|
||||
name: error?.name ?? 'Error',
|
||||
message: error?.message ?? 'live actor failed',
|
||||
})}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user