blob: 534a3dea082160a1d78d786cc28f116e9131c10e [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { spawn } from 'node:child_process';
import { dirname, join, resolve } from 'node:path';
import { truncateUtf8 } from '@maka/core/diagnostic-log';
import { activateRuntimeHostManagedDeployment } from '@maka/runtime-host/client';
import {
compareProductReleaseVersions,
createRuntimeHostLegacyPosixOperatorCommand,
decodeRuntimeHostServiceManagementFrame,
encodeRuntimeHostServiceManagementFrame,
RUNTIME_HOST_SERVICE_ERROR_CODE_MAX_BYTES,
RUNTIME_HOST_SERVICE_ERROR_MESSAGE_MAX_BYTES,
RUNTIME_HOST_OPERATOR_ACCESS_MANAGEMENT_CAPABILITY,
RUNTIME_HOST_OPERATOR_CAPABILITY_REQUEST_ENV,
RUNTIME_HOST_OPERATOR_PROCESS_LIFETIME_LOCK_CAPABILITY,
runtimeHostOperatorInvocation,
type RuntimeHostOperatorCapability,
type RuntimeHostOperatorCommand,
type RuntimeHostServiceManagementFrame,
type RuntimeHostServiceUpdatePhase,
RuntimeHostManagedDeploymentError as RuntimeHostDeploymentAuthorityError,
} from '@maka/runtime-host/operator';
import {
isRuntimeHostDevelopmentPackageVersion,
assertRuntimeHostManagedOperatorConfig,
assertRuntimeHostManagedOperatorDeployment,
convergeRuntimeHostManagedOperator,
verifyRuntimeHostManagedOperator,
openRuntimeHostManagedPackageDeployment,
prepareRuntimeHostManagedPackageDeployment,
pruneRuntimeHostManagedPackages,
resolveRuntimeHostManagedControlRoot,
resolveRuntimeHostManagedPackageCliPath,
RuntimeHostManagedDeploymentError,
type RuntimeHostManagedPackageDeployment,
} from './runtime-host-managed-deployment.js';
import {
runtimeHostManagedServiceConfigFingerprint,
manageRuntimeHostService,
replaceRuntimeHostManagedService,
resolveRuntimeHostManagedServiceId,
RuntimeHostServiceManagerError,
verifyRuntimeHostManagedServiceReady,
withRuntimeHostManagedServiceDeploymentLock,
withRuntimeHostManagedServiceLegacyOperatorLeases,
withRuntimeHostManagedServiceLifecycleLock,
type RuntimeHostManagedServiceResult,
type RuntimeHostManagedServiceTarget,
type RuntimeHostServiceBackend,
} from './runtime-host-service-manager.js';
import {
createPlatformRuntimeHostServiceBackend,
resolveRuntimeHostLifecycleProvider,
runtimeHostServiceSummary,
} from './runtime-host-service-management-command.js';
import {
resolveManagedRuntimeHostUpdateSelection,
RuntimeHostUpdateDiscoveryError,
type RuntimeHostUpdateSelection,
} from './runtime-host-update-discovery.js';
import {
RuntimeHostUpdatePackageError,
withRuntimeHostRegistryUpdatePackage,
} from './runtime-host-update-package.js';
import type { RuntimeHostExpectedHost, RuntimeHostUpdateSelector } from './runtime-host-cli.js';
import {
canDiscardRuntimeHostLifecycleDesiredArtifacts,
convergeRuntimeHostLifecycleControlProjection,
replaceRuntimeHostLifecycle,
resolveRecoverableRuntimeHostManagedDeployment,
RuntimeHostLifecycleTransactionError,
verifyRuntimeHostLifecycleProjection,
type RuntimeHostLifecycleTransactionDeps,
} from './runtime-host-lifecycle-transaction.js';
import { launchRuntimeHostLocalSourceRetirement } from './runtime-host-local-source-retirement.js';
import { manageRuntimeHostManagedLifecycle } from './runtime-host-managed-lifecycle-manager.js';
const OPERATOR_TIMEOUT_MS = 2 * 60_000;
const OPERATOR_OUTPUT_MAX_BYTES = 256 * 1024;
export interface RuntimeHostUpdateCliOptions {
readonly json: boolean;
readonly framed: boolean;
readonly clientDataRoot: string;
readonly defaultRootPath: string;
readonly sourcePackageRoot: string;
readonly sourcePackageIntegrity?: string;
readonly version: string;
readonly expectedTarget: RuntimeHostManagedServiceTarget;
readonly expectedHost?: RuntimeHostExpectedHost;
readonly expectedConfigFingerprint?: string;
readonly expectedSourceVersion?: string;
readonly managedRootId?: string;
readonly operatorDeploymentId?: string;
readonly registrySelection?: {
readonly integrity: string;
readonly current: {
readonly version: string;
readonly cliPath: string;
};
};
readonly allowInterruptActiveTasks?: boolean;
}
export interface RuntimeHostSelectedUpdateCliOptions
extends Omit<RuntimeHostUpdateCliOptions, 'sourcePackageRoot' | 'version' | 'registrySelection'> {
readonly selector: RuntimeHostUpdateSelector;
readonly allowManualUpdate?: boolean;
}
interface RuntimeHostUpdateCliDeps {
readonly revalidateSelection: () => Promise<RuntimeHostUpdateSelectionRejection | undefined>;
readonly manage: typeof manageRuntimeHostService;
readonly replace: typeof replaceRuntimeHostManagedService;
readonly openDeployment: typeof openRuntimeHostManagedPackageDeployment;
readonly prepareDeployment: typeof prepareRuntimeHostManagedPackageDeployment;
readonly prunePackages: typeof pruneRuntimeHostManagedPackages;
readonly activateDesired: typeof activateRuntimeHostManagedDeployment;
readonly retireSource: typeof launchRuntimeHostLocalSourceRetirement;
readonly withLifecycleLock: typeof withRuntimeHostManagedServiceLifecycleLock;
readonly withDeploymentLock: typeof withRuntimeHostManagedServiceDeploymentLock;
readonly withLegacyOperatorLeases: typeof withRuntimeHostManagedServiceLegacyOperatorLeases;
readonly createBackend: (serviceId: string, clientDataRoot: string) => RuntimeHostServiceBackend;
readonly verifyReady: typeof verifyRuntimeHostManagedServiceReady;
readonly runOperator: (
operator: RuntimeHostOperatorCommand,
args: readonly string[],
invocation?: RuntimeHostOperatorInvocation,
) => Promise<RuntimeHostServiceManagementFrame>;
readonly canonical: {
readonly createLifecycleDeps: (rootId: string) => RuntimeHostLifecycleTransactionDeps;
readonly assertOperatorDeployment: typeof assertRuntimeHostManagedOperatorDeployment;
readonly recoverDeployment: typeof resolveRecoverableRuntimeHostManagedDeployment;
readonly convergeControlProjection: typeof convergeRuntimeHostLifecycleControlProjection;
readonly verifyProjection: typeof verifyRuntimeHostLifecycleProjection;
readonly assertOperatorConfig: typeof assertRuntimeHostManagedOperatorConfig;
readonly manageLifecycle: typeof manageRuntimeHostManagedLifecycle;
readonly replaceLifecycle: typeof replaceRuntimeHostLifecycle;
};
readonly writeOutput: (value: string) => unknown;
readonly writeError: (value: string) => unknown;
}
interface RuntimeHostResolvedUpdateCliDeps {
readonly revalidateSelection: () => Promise<RuntimeHostUpdateSelectionRejection | undefined>;
readonly withPackage: typeof withRuntimeHostRegistryUpdatePackage;
readonly update: typeof runManagedRuntimeHostUpdateCli;
}
interface RuntimeHostSelectedUpdateCliDeps extends RuntimeHostResolvedUpdateCliDeps {
readonly resolveSelection: typeof resolveManagedRuntimeHostUpdateSelection;
readonly writeOutput: (value: string) => unknown;
readonly writeError: (value: string) => unknown;
}
export type RuntimeHostUpdateFrame = Extract<
RuntimeHostServiceManagementFrame,
{ action: 'update' }
>;
export type RuntimeHostUpdateFrameSink = (frame: RuntimeHostUpdateFrame) => void;
function runtimeHostPackageUpdateOperation(input: {
readonly currentVersion: string;
readonly currentIntegrity: string;
readonly targetVersion: string;
readonly targetIntegrity: string;
readonly replaceExpectedHost: boolean;
}): 'already_current' | 'replace_current' | 'update' {
if (
input.currentVersion !== input.targetVersion ||
input.currentIntegrity !== input.targetIntegrity
) {
return 'update';
}
return input.replaceExpectedHost ? 'replace_current' : 'already_current';
}
interface RuntimeHostOperatorInvocation {
readonly inheritedFds?: readonly number[];
readonly capabilityRequest?: RuntimeHostOperatorCapability;
}
interface RuntimeHostUpdateSelectionRejection {
readonly code: string;
readonly message: string;
}
class RuntimeHostUpdateSelectionError extends Error {
constructor(
readonly code: string,
message: string,
) {
super(message);
this.name = 'RuntimeHostUpdateSelectionError';
}
}
export async function runManagedRuntimeHostUpdateCli(
options: RuntimeHostUpdateCliOptions,
overrides: Partial<RuntimeHostUpdateCliDeps> = {},
frameSink?: RuntimeHostUpdateFrameSink,
): Promise<number> {
const deps: RuntimeHostUpdateCliDeps = {
revalidateSelection: async () => undefined,
manage: manageRuntimeHostService,
replace: replaceRuntimeHostManagedService,
openDeployment: openRuntimeHostManagedPackageDeployment,
prepareDeployment: prepareRuntimeHostManagedPackageDeployment,
prunePackages: pruneRuntimeHostManagedPackages,
retireSource: launchRuntimeHostLocalSourceRetirement,
activateDesired: (input) =>
activateRuntimeHostManagedDeployment(input, {
reconcileActivation: async () => undefined,
}),
withLifecycleLock: withRuntimeHostManagedServiceLifecycleLock,
withDeploymentLock: withRuntimeHostManagedServiceDeploymentLock,
withLegacyOperatorLeases: withRuntimeHostManagedServiceLegacyOperatorLeases,
createBackend: createPlatformRuntimeHostServiceBackend,
verifyReady: verifyRuntimeHostManagedServiceReady,
runOperator: runManagedRuntimeHostOperator,
canonical: {
createLifecycleDeps: (rootId) => ({
convergeOperator: (currentConfig, desiredConfig) =>
convergeRuntimeHostManagedOperator(currentConfig, desiredConfig),
verifyOperator: verifyRuntimeHostManagedOperator,
resolveProvider: resolveRuntimeHostLifecycleProvider,
}),
assertOperatorDeployment: assertRuntimeHostManagedOperatorDeployment,
recoverDeployment: resolveRecoverableRuntimeHostManagedDeployment,
convergeControlProjection: convergeRuntimeHostLifecycleControlProjection,
verifyProjection: verifyRuntimeHostLifecycleProjection,
assertOperatorConfig: assertRuntimeHostManagedOperatorConfig,
manageLifecycle: manageRuntimeHostManagedLifecycle,
replaceLifecycle: replaceRuntimeHostLifecycle,
},
writeOutput: (value) => process.stdout.write(value),
writeError: (value) => process.stderr.write(value),
...overrides,
};
let deployment: RuntimeHostManagedPackageDeployment | undefined;
let exactTargetObserved = false;
let cutoverStarted = false;
let retired = false;
const emit =
frameSink ?? ((frame: RuntimeHostUpdateFrame) => presentUpdateFrame(frame, options, deps));
if (options.expectedHost && !options.managedRootId) {
emit({
schemaVersion: 1,
kind: 'error',
action: 'update',
error: {
code: 'target_mismatch',
message: 'A Host identity fence requires canonical managed deployment authority',
},
});
return 1;
}
if (options.managedRootId) {
return runCanonicalRuntimeHostUpdate(
{ ...options, managedRootId: options.managedRootId },
deps,
emit,
);
}
try {
return await deps.withDeploymentLock(options.clientDataRoot, async () => {
try {
const rejection = await deps.revalidateSelection();
if (rejection) {
throw new RuntimeHostUpdateSelectionError(rejection.code, rejection.message);
}
const serviceId = resolveRuntimeHostManagedServiceId(options.clientDataRoot);
if (serviceId !== options.expectedTarget.serviceId) {
throw new RuntimeHostServiceManagerError(
'target_mismatch',
'The managed Runtime Host update does not match the expected service identity',
);
}
const backend = deps.createBackend(serviceId, options.clientDataRoot);
const common = {
clientDataRoot: options.clientDataRoot,
defaultRootPath: options.defaultRootPath,
nodePath: process.execPath,
cliPath: join(options.sourcePackageRoot, 'dist', 'cli.js'),
expectedTarget: options.expectedTarget,
} as const;
const status = await deps.manage({ ...common, action: 'status' }, backend);
const currentVersion = requireManagedVersion(status);
const serviceConfig = status.service.config;
if (!serviceConfig?.managedDeploymentRoot) {
throw new RuntimeHostServiceManagerError(
'invalid_launch',
'The Runtime Host service is not owned by a Maka managed deployment',
);
}
await backend.verifyReplacementPreconditions(serviceConfig);
const deploymentRoot = serviceConfig.managedDeploymentRoot;
const currentCliPath = resolve(serviceConfig.launch.cliPath);
const targetCliPath = resolveRuntimeHostManagedPackageCliPath(
deploymentRoot,
options.version,
options.sourcePackageIntegrity ?? options.registrySelection?.integrity,
);
const expectedCurrent = options.registrySelection?.current;
const selectedDeploymentStillCurrent =
!expectedCurrent ||
(currentVersion === expectedCurrent.version &&
currentCliPath === resolve(expectedCurrent.cliPath));
const targetDeploymentIsCurrent =
currentVersion === options.version && currentCliPath === targetCliPath;
exactTargetObserved = targetDeploymentIsCurrent;
if (!selectedDeploymentStillCurrent && !targetDeploymentIsCurrent) {
throw new RuntimeHostServiceManagerError(
'target_mismatch',
'The managed Runtime Host changed after its update candidate was selected',
);
}
emit(progress('checking', currentVersion, options.version));
let activeTargetNeedsRepair = false;
if (targetDeploymentIsCurrent && status.service.active) {
try {
await deps.verifyReady(serviceConfig, backend);
} catch {
activeTargetNeedsRepair = true;
}
if (!activeTargetNeedsRepair) {
const currentDeployment = await deps.openDeployment({
serviceId,
clientDataRoot: options.clientDataRoot,
deploymentRoot,
cliPath: serviceConfig.launch.cliPath,
version: options.version,
});
await currentDeployment.cleanup();
emit({
schemaVersion: 1,
kind: 'result',
action: 'update',
service: runtimeHostServiceSummary(status),
...operatorCapabilities(),
update: { kind: 'already_current', version: options.version },
});
return 0;
}
}
const currentOperator = createRuntimeHostLegacyPosixOperatorCommand(
join(serviceConfig.managedDeploymentRoot, 'operator'),
);
let currentOperatorUsesProcessLifetimeLock = false;
let currentOperatorUnavailable = false;
if (status.service.active) {
try {
currentOperatorUsesProcessLifetimeLock = operatorUsesProcessLifetimeLock(
await deps.runOperator(
currentOperator,
['status', '--framed', ...expectedTargetArgs(options.expectedTarget)],
{
capabilityRequest: RUNTIME_HOST_OPERATOR_PROCESS_LIFETIME_LOCK_CAPABILITY,
},
),
);
} catch (error) {
if (!activeTargetNeedsRepair) throw error;
currentOperatorUnavailable = true;
}
}
emit(progress('staging', currentVersion, options.version));
deployment = await deps.withLifecycleLock(options.clientDataRoot, () =>
targetDeploymentIsCurrent
? deps.openDeployment({
serviceId,
clientDataRoot: options.clientDataRoot,
deploymentRoot,
cliPath: serviceConfig.launch.cliPath,
version: options.version,
})
: deps.prepareDeployment({
serviceId,
clientDataRoot: options.clientDataRoot,
sourcePackageRoot: options.sourcePackageRoot,
version: options.version,
...((options.sourcePackageIntegrity ?? options.registrySelection?.integrity)
? {
packageIntegrity:
options.sourcePackageIntegrity ?? options.registrySelection?.integrity,
}
: {}),
}),
);
if (deployment.root !== deploymentRoot) {
throw new RuntimeHostServiceManagerError(
'target_mismatch',
'The staged Runtime Host package belongs to a different managed deployment',
);
}
if (resolve(deployment.cliPath) !== targetCliPath) {
throw new RuntimeHostServiceManagerError(
'target_mismatch',
'The staged Runtime Host package does not match the selected deployment identity',
);
}
if (status.service.active) {
emit(progress('retiring', currentVersion, options.version));
const runCurrentOperator = (args: readonly string[]) =>
currentOperatorUsesProcessLifetimeLock
? deps.runOperator(currentOperator, args)
: deps.withLegacyOperatorLeases(options.clientDataRoot, (inheritedFds) =>
deps.runOperator(currentOperator, args, {
inheritedFds,
}),
);
let retirement: RuntimeHostServiceManagementFrame = currentOperatorUnavailable
? {
schemaVersion: 1,
kind: 'error',
action: 'retire',
error: {
code: 'retirement_failed',
message: 'The active Runtime Host operator is unavailable',
},
}
: await runCurrentOperator([
'retire',
'--framed',
...expectedTargetArgs(options.expectedTarget),
...(options.allowInterruptActiveTasks ? ['--allow-interrupt-active-tasks'] : []),
]);
if (
activeTargetNeedsRepair &&
retirement.kind === 'error' &&
retirement.action === 'retire' &&
retirement.error.code === 'retirement_failed'
) {
if (!options.allowInterruptActiveTasks) {
retirement = activeTasksRetirementFrame(status);
} else {
const forced = await deps.withLifecycleLock(options.clientDataRoot, async () => {
await backend.retire();
return deps.manage({ ...common, action: 'status' }, backend);
});
if (
forced.service.active ||
forced.service.pid !== null ||
forced.service.state !== 'stopped'
) {
throw new RuntimeHostServiceManagerError(
'retirement_failed',
'The unreachable Runtime Host did not reach a stable stopped state',
);
}
retirement = {
schemaVersion: 1,
kind: 'result',
action: 'retire',
service: runtimeHostServiceSummary(forced),
...operatorCapabilities(),
retirement: { kind: 'stopped' },
};
}
}
if (retirement.kind === 'error') {
if (retirement.action !== 'retire') {
throw new Error(
'The current Runtime Host operator returned an unrelated retirement error',
);
}
const staged = deployment;
deployment = undefined;
await staged.rollback();
emit({ ...retirement, action: 'update' });
return 1;
}
if (retirement.kind !== 'result' || retirement.action !== 'retire') {
throw new Error(
'The current Runtime Host operator returned an invalid retirement result',
);
}
if (retirement.retirement.kind === 'active_tasks') {
const staged = deployment;
deployment = undefined;
await staged.rollback();
emit({
schemaVersion: 1,
kind: 'result',
action: 'update',
service: retirement.service,
...operatorCapabilities(),
update: {
kind: 'active_tasks',
currentVersion,
targetVersion: options.version,
},
});
return 1;
}
retired = true;
}
const targetDeployment = deployment;
const updated = await deps.withLifecycleLock(options.clientDataRoot, async () => {
const stopped = await deps.manage({ ...common, action: 'status' }, backend);
if (
requireManagedVersion(stopped) !== currentVersion ||
stopped.service.active ||
stopped.service.pid !== null ||
(stopped.service.state !== 'stopped' && stopped.service.state !== 'failed')
) {
throw new RuntimeHostServiceManagerError(
'target_mismatch',
'The managed Runtime Host changed while the update was preparing its replacement',
);
}
cutoverStarted = true;
await targetDeployment.activate();
emit(progress('replacing', currentVersion, options.version));
const updatedService = await deps.replace(
{
...common,
cliPath: targetDeployment.cliPath,
expectedTarget: options.expectedTarget,
},
backend,
);
if (
updatedService.installedVersion !== options.version ||
!updatedService.active ||
!updatedService.config ||
resolve(updatedService.config.launch.cliPath) !== targetCliPath
) {
throw new RuntimeHostServiceManagerError(
'update_incomplete',
'The replacement Runtime Host did not report the selected package version as ready',
);
}
await targetDeployment.cleanup();
return {
schemaVersion: 1,
action: 'status',
service: updatedService,
} as const;
});
emit({
schemaVersion: 1,
kind: 'result',
action: 'update',
service: runtimeHostServiceSummary(updated),
...operatorCapabilities(),
update: targetDeploymentIsCurrent
? { kind: 'repaired', version: options.version }
: {
kind: 'updated',
previousVersion: currentVersion,
targetVersion: options.version,
},
});
return 0;
} catch (error) {
if (deployment && !cutoverStarted) {
const staged = deployment;
deployment = undefined;
try {
await staged.rollback();
} catch (rollbackError) {
throw new RuntimeHostManagedDeploymentError(
'deployment_failed',
'The Runtime Host update failed and its staged package could not be removed',
{ cause: new AggregateError([error, rollbackError]) },
);
}
}
throw error;
}
});
} catch (error) {
const updateIncomplete = exactTargetObserved || retired || cutoverStarted;
const reportedError = updateIncomplete
? new RuntimeHostServiceManagerError(
'update_incomplete',
'The Runtime Host update did not complete; run the update again to reconcile the managed installation',
{ cause: error },
)
: error;
const code =
reportedError instanceof RuntimeHostServiceManagerError ||
reportedError instanceof RuntimeHostManagedDeploymentError ||
reportedError instanceof RuntimeHostUpdateSelectionError
? reportedError.code
: 'internal_service_error';
const message = reportedError instanceof Error ? reportedError.message : String(reportedError);
emit({
schemaVersion: 1,
kind: 'error',
action: 'update',
error: {
code:
truncateUtf8(code, RUNTIME_HOST_SERVICE_ERROR_CODE_MAX_BYTES) || 'internal_service_error',
message:
truncateUtf8(message, RUNTIME_HOST_SERVICE_ERROR_MESSAGE_MAX_BYTES) ||
'Runtime Host update failed',
},
});
return 1;
}
}
async function runCanonicalRuntimeHostUpdate(
options: RuntimeHostUpdateCliOptions & { readonly managedRootId: string },
deps: RuntimeHostUpdateCliDeps,
emit: RuntimeHostUpdateFrameSink,
): Promise<number> {
let staged: RuntimeHostManagedPackageDeployment | undefined;
try {
return await deps.withDeploymentLock(
resolveRuntimeHostManagedControlRoot(options.managedRootId),
async (inheritableAuthorityLeaseFd) => {
await deps.canonical.assertOperatorDeployment(
options.managedRootId,
options.operatorDeploymentId,
process.argv[1] ?? '',
);
const rejection = await deps.revalidateSelection();
if (rejection) {
throw new RuntimeHostUpdateSelectionError(rejection.code, rejection.message);
}
const lifecycleDeps = deps.canonical.createLifecycleDeps(options.managedRootId);
const recovered = await deps.canonical.recoverDeployment(
options.managedRootId,
lifecycleDeps,
{
expectedTarget: options.expectedTarget,
...(options.expectedHost ? { expectedOwner: options.expectedHost } : {}),
allowInterruptActiveTasks: options.allowInterruptActiveTasks ?? false,
},
);
if (recovered.kind === 'absent') {
throw new RuntimeHostServiceManagerError(
'not_installed',
'The managed Runtime Host deployment is not installed',
);
}
const current = recovered.config;
if (
options.expectedSourceVersion &&
current.launch.package.version !== options.expectedSourceVersion
) {
throw new RuntimeHostServiceManagerError(
'target_mismatch',
'The installed Runtime Host package changed; check it again before updating',
);
}
await deps.canonical.convergeControlProjection(current, lifecycleDeps);
await deps.canonical.verifyProjection(current, lifecycleDeps);
deps.canonical.assertOperatorConfig(
current,
options.operatorDeploymentId,
process.argv[1] ?? '',
);
const currentStatus = await deps.canonical.manageLifecycle(
options.managedRootId,
{
action: 'status',
clientDataRoot: options.clientDataRoot,
defaultRootPath: options.defaultRootPath,
nodePath: process.execPath,
cliPath: process.argv[1] ?? '',
expectedTarget: options.expectedTarget,
},
{ resolveProvider: resolveRuntimeHostLifecycleProvider },
);
if (
options.expectedConfigFingerprint &&
(!currentStatus.service.config ||
runtimeHostManagedServiceConfigFingerprint(currentStatus.service.config) !==
options.expectedConfigFingerprint)
) {
throw new RuntimeHostServiceManagerError(
'target_mismatch',
'The managed Runtime Host configuration changed; check it again before updating',
);
}
const developmentArtifact =
isRuntimeHostDevelopmentPackageVersion(options.version) ||
isRuntimeHostDevelopmentPackageVersion(current.launch.package.version);
if (developmentArtifact && !options.sourcePackageIntegrity) {
throw new RuntimeHostServiceManagerError(
'target_mismatch',
'Development deployments require an explicitly selected source artifact; source hashes do not establish release order',
);
}
if (
!developmentArtifact &&
compareProductReleaseVersions(options.version, current.launch.package.version) < 0
) {
throw new RuntimeHostServiceManagerError(
'target_mismatch',
'This update would downgrade the shared Runtime Host. Update the Client instead.',
);
}
const targetIntegrity =
options.sourcePackageIntegrity ??
options.registrySelection?.integrity ??
(options.version === current.launch.package.version
? current.launch.package.integrity
: undefined);
if (!targetIntegrity) {
throw new RuntimeHostServiceManagerError(
'invalid_launch',
'An exact package identity is required for a managed update',
);
}
const expectedCurrent = options.registrySelection?.current;
const currentCliPath = resolveRuntimeHostManagedPackageCliPath(
current.deploymentRoot,
current.launch.package.version,
current.launch.package.integrity,
);
const targetCliPath = resolveRuntimeHostManagedPackageCliPath(
current.deploymentRoot,
options.version,
targetIntegrity,
);
const selectedDeploymentStillCurrent =
!expectedCurrent ||
(current.launch.package.version === expectedCurrent.version &&
currentCliPath === resolve(expectedCurrent.cliPath));
const targetDeploymentIsCurrent =
current.launch.package.version === options.version && currentCliPath === targetCliPath;
if (!selectedDeploymentStillCurrent && !targetDeploymentIsCurrent) {
throw new RuntimeHostServiceManagerError(
'target_mismatch',
'The managed Runtime Host changed after its update candidate was selected',
);
}
const updateOperation = runtimeHostPackageUpdateOperation({
currentVersion: current.launch.package.version,
currentIntegrity: current.launch.package.integrity,
targetVersion: options.version,
targetIntegrity,
replaceExpectedHost: options.expectedHost !== undefined,
});
if (updateOperation === 'already_current') {
await deps.prunePackages(current);
emit({
schemaVersion: 1,
kind: 'result',
action: 'update',
service: runtimeHostServiceSummary(currentStatus),
...operatorCapabilities(),
update: { kind: 'already_current', version: options.version },
});
return 0;
}
emit(progress('checking', current.launch.package.version, options.version));
if (updateOperation === 'update') {
emit(progress('staging', current.launch.package.version, options.version));
staged = await deps.prepareDeployment({
serviceId: options.managedRootId,
clientDataRoot: options.clientDataRoot,
sourcePackageRoot: options.sourcePackageRoot,
version: options.version,
packageIntegrity: targetIntegrity,
deploymentRoot: current.deploymentRoot,
});
}
const desired = {
...current,
configRevision: current.configRevision + 1,
launch: {
...current.launch,
package: {
kind: 'npm_registry' as const,
version: options.version,
integrity: targetIntegrity,
},
},
};
emit(progress('retiring', current.launch.package.version, options.version));
const replacement = await deps.canonical.replaceLifecycle({
operation: 'update',
validateRetiredState: async () => {
emit(progress('replacing', current.launch.package.version, options.version));
},
current,
desired,
allowInterruptActiveTasks: options.allowInterruptActiveTasks ?? false,
...(options.expectedHost ? { expectedOwner: options.expectedHost } : {}),
...(desired.lifecycle.mode === 'on_demand'
? {
...(options.expectedHost
? {
prepareSourceRetirement: (signal?: AbortSignal) => {
if (inheritableAuthorityLeaseFd === undefined) {
throw new RuntimeHostServiceManagerError(
'target_mismatch',
'Source-package retirement requires the deployment authority lease',
);
}
return deps.retireSource({
sourceCliPath: currentCliPath,
sourceNodePath: current.launch.nodePath,
rootPath: current.root.path,
expectedRootId: current.root.id,
expectedHostEpoch: options.expectedHost!.hostEpoch,
activeWorkPolicy: options.allowInterruptActiveTasks
? 'interrupt_active_work'
: 'refuse_active_work',
inheritableAuthorityLeaseFd,
...(signal ? { signal } : {}),
});
},
}
: {}),
activateDesired: async () => {
await deps.activateDesired({ rootId: options.managedRootId });
},
}
: {}),
deps: lifecycleDeps,
});
if (replacement.kind === 'active_tasks') {
if (staged) {
await staged.rollback();
staged = undefined;
}
emit({
schemaVersion: 1,
kind: 'result',
action: 'update',
service: runtimeHostServiceSummary(currentStatus),
...operatorCapabilities(),
update: {
kind: 'active_tasks',
currentVersion: current.launch.package.version,
targetVersion: options.version,
},
});
return 1;
}
staged = undefined;
const updated = await deps.canonical.manageLifecycle(
options.managedRootId,
{
action: 'status',
clientDataRoot: options.clientDataRoot,
defaultRootPath: options.defaultRootPath,
nodePath: process.execPath,
cliPath: process.argv[1] ?? '',
expectedTarget: options.expectedTarget,
},
{ resolveProvider: resolveRuntimeHostLifecycleProvider },
);
await deps.prunePackages(desired);
emit({
schemaVersion: 1,
kind: 'result',
action: 'update',
service: runtimeHostServiceSummary(updated),
...operatorCapabilities(),
update:
updateOperation === 'replace_current'
? { kind: 'repaired', version: options.version }
: {
kind: 'updated',
previousVersion: current.launch.package.version,
targetVersion: options.version,
},
});
return 0;
},
);
} catch (error) {
if (staged && canDiscardRuntimeHostLifecycleDesiredArtifacts(error)) {
await staged.rollback().catch(() => undefined);
}
const code =
error instanceof RuntimeHostServiceManagerError ||
error instanceof RuntimeHostManagedDeploymentError ||
error instanceof RuntimeHostDeploymentAuthorityError
? error.code
: error instanceof RuntimeHostLifecycleTransactionError && error.code === 'owner_changed'
? 'target_mismatch'
: error instanceof RuntimeHostLifecycleTransactionError && error.code === 'active_tasks'
? 'active_tasks'
: 'update_incomplete';
emit({
schemaVersion: 1,
kind: 'error',
action: 'update',
error: {
code: truncateUtf8(code, RUNTIME_HOST_SERVICE_ERROR_CODE_MAX_BYTES),
message: truncateUtf8(
error instanceof Error ? error.message : String(error),
RUNTIME_HOST_SERVICE_ERROR_MESSAGE_MAX_BYTES,
),
},
});
return 1;
}
}
export async function runManagedRuntimeHostSelectedUpdateCli(
options: RuntimeHostSelectedUpdateCliOptions,
overrides: Partial<RuntimeHostSelectedUpdateCliDeps> = {},
frameSink?: RuntimeHostUpdateFrameSink,
): Promise<number> {
const deps: RuntimeHostSelectedUpdateCliDeps = {
revalidateSelection: async () => undefined,
resolveSelection: resolveManagedRuntimeHostUpdateSelection,
withPackage: withRuntimeHostRegistryUpdatePackage,
update: runManagedRuntimeHostUpdateCli,
writeOutput: (value) => process.stdout.write(value),
writeError: (value) => process.stderr.write(value),
...overrides,
};
const emit =
frameSink ?? ((frame: RuntimeHostUpdateFrame) => presentUpdateFrame(frame, options, deps));
try {
const selection = await deps.resolveSelection({
clientDataRoot: options.clientDataRoot,
defaultRootPath: options.defaultRootPath,
selector: options.selector,
expectedTarget: options.expectedTarget,
...(options.managedRootId ? { managedRootId: options.managedRootId } : {}),
...(options.operatorDeploymentId
? { operatorDeploymentId: options.operatorDeploymentId }
: {}),
...(options.allowInterruptActiveTasks ? { allowInterruptActiveTasks: true } : {}),
});
return await runManagedRuntimeHostResolvedUpdateCli(options, selection, deps, emit);
} catch (error) {
const code =
error instanceof RuntimeHostUpdateDiscoveryError ||
error instanceof RuntimeHostServiceManagerError ||
error instanceof RuntimeHostUpdatePackageError
? error.code
: error instanceof RuntimeHostLifecycleTransactionError && error.code === 'active_tasks'
? 'active_tasks'
: 'update_resolution_failed';
const message = error instanceof Error ? error.message : String(error);
emit({
schemaVersion: 1,
kind: 'error',
action: 'update',
error: {
code:
truncateUtf8(code, RUNTIME_HOST_SERVICE_ERROR_CODE_MAX_BYTES) ||
'update_resolution_failed',
message:
truncateUtf8(message, RUNTIME_HOST_SERVICE_ERROR_MESSAGE_MAX_BYTES) ||
'Unable to prepare the Runtime Host update',
},
});
return 1;
}
}
export async function runManagedRuntimeHostResolvedUpdateCli(
options: RuntimeHostSelectedUpdateCliOptions,
selection: RuntimeHostUpdateSelection,
overrides: Partial<RuntimeHostResolvedUpdateCliDeps>,
frameSink: RuntimeHostUpdateFrameSink,
): Promise<number> {
const deps: RuntimeHostResolvedUpdateCliDeps = {
revalidateSelection: async () => undefined,
withPackage: withRuntimeHostRegistryUpdatePackage,
update: runManagedRuntimeHostUpdateCli,
...overrides,
};
try {
if (
selection.outcome.kind === 'manual_action' &&
!(
(options.expectedHost ||
(options.allowManualUpdate &&
options.managedRootId &&
options.expectedTarget.deploymentId)) &&
selection.outcome.reason !== 'target_not_newer'
)
) {
frameSink({
schemaVersion: 1,
kind: 'error',
action: 'update',
error: {
code: 'update_not_admitted',
message: manualUpdateRequiredMessage(
selection.candidate.version,
selection.outcome.reason,
),
},
});
return 1;
}
const apply = async (packageRoot: string) => {
const {
selector: _selector,
allowManualUpdate: _allowManualUpdate,
...updateOptions
} = options;
return await deps.update(
{
...updateOptions,
sourcePackageRoot: packageRoot,
version: selection.candidate.version,
registrySelection: {
integrity: selection.candidate.integrity,
current: {
version: selection.service.installedVersion,
cliPath: selection.currentCliPath,
},
},
},
{ revalidateSelection: deps.revalidateSelection },
frameSink,
);
};
return selection.outcome.kind === 'current'
? await apply(dirname(dirname(selection.currentCliPath)))
: await deps.withPackage(selection.candidate, apply);
} catch (error) {
const code =
error instanceof RuntimeHostUpdateDiscoveryError ||
error instanceof RuntimeHostServiceManagerError ||
error instanceof RuntimeHostUpdatePackageError
? error.code
: 'update_resolution_failed';
const message = error instanceof Error ? error.message : String(error);
frameSink({
schemaVersion: 1,
kind: 'error',
action: 'update',
error: {
code:
truncateUtf8(code, RUNTIME_HOST_SERVICE_ERROR_CODE_MAX_BYTES) ||
'update_resolution_failed',
message:
truncateUtf8(message, RUNTIME_HOST_SERVICE_ERROR_MESSAGE_MAX_BYTES) ||
'Unable to prepare the Runtime Host update',
},
});
return 1;
}
}
function presentUpdateFrame(
frame: RuntimeHostUpdateFrame,
options: Pick<RuntimeHostUpdateCliOptions, 'json' | 'framed'>,
deps: Pick<RuntimeHostSelectedUpdateCliDeps, 'writeOutput' | 'writeError'>,
): void {
if (options.framed) {
deps.writeOutput(encodeRuntimeHostServiceManagementFrame(frame));
} else if (frame.kind === 'progress') {
if (!options.json) deps.writeError(`${humanPhase(frame.phase)}\n`);
} else if (options.json) {
deps.writeOutput(`${JSON.stringify(frame)}\n`);
} else if (frame.kind === 'error') {
deps.writeError(`${frame.error.message}\n`);
} else {
if (frame.action !== 'update') {
throw new TypeError('Managed Runtime Host update returned an unrelated result');
}
deps.writeOutput(`${humanResult(frame)}\n`);
}
}
function progress(
phase: RuntimeHostServiceUpdatePhase,
currentVersion: string,
targetVersion: string,
): RuntimeHostUpdateFrame {
return {
schemaVersion: 1,
kind: 'progress',
action: 'update',
phase,
currentVersion,
targetVersion,
};
}
function requireManagedVersion(result: RuntimeHostManagedServiceResult): string {
if (!result.service.installed || !result.service.config) {
throw new RuntimeHostServiceManagerError(
'not_installed',
'Runtime Host service is not installed',
);
}
if (!result.service.installedVersion) {
throw new RuntimeHostServiceManagerError(
'invalid_launch',
'The installed Runtime Host package version could not be identified',
);
}
return result.service.installedVersion;
}
function expectedTargetArgs(target: RuntimeHostManagedServiceTarget): string[] {
return [
'--expected-service-id',
target.serviceId,
'--expected-root-path',
target.rootPath,
'--expected-root-id',
target.rootId,
];
}
function activeTasksRetirementFrame(
status: RuntimeHostManagedServiceResult,
): RuntimeHostServiceManagementFrame {
return {
schemaVersion: 1,
kind: 'result',
action: 'retire',
service: runtimeHostServiceSummary(status),
...operatorCapabilities(),
retirement: { kind: 'active_tasks' },
};
}
function operatorUsesProcessLifetimeLock(frame: RuntimeHostServiceManagementFrame): boolean {
if (frame.kind === 'error') {
throw new RuntimeHostServiceManagerError(
'service_manager_operation_failed',
`The current Runtime Host operator could not report its lock protocol: ${frame.error.message}`,
);
}
if (frame.action !== 'status') {
throw new Error('The current Runtime Host operator returned an invalid capability result');
}
return (
frame.operatorCapabilities?.includes(RUNTIME_HOST_OPERATOR_PROCESS_LIFETIME_LOCK_CAPABILITY) ===
true
);
}
function operatorCapabilities(): {
readonly operatorCapabilities?: (typeof RUNTIME_HOST_OPERATOR_ACCESS_MANAGEMENT_CAPABILITY)[];
} {
return process.env[RUNTIME_HOST_OPERATOR_CAPABILITY_REQUEST_ENV] ===
RUNTIME_HOST_OPERATOR_ACCESS_MANAGEMENT_CAPABILITY
? {
operatorCapabilities: [RUNTIME_HOST_OPERATOR_ACCESS_MANAGEMENT_CAPABILITY],
}
: {};
}
async function runManagedRuntimeHostOperator(
operator: RuntimeHostOperatorCommand,
args: readonly string[],
invocation: RuntimeHostOperatorInvocation = {},
): Promise<RuntimeHostServiceManagementFrame> {
return new Promise((resolve, reject) => {
const inheritedFds = invocation.inheritedFds ?? [];
const command = runtimeHostOperatorInvocation(operator, args);
const child = spawn(command.executable, [...command.args], {
// A detached legacy operator keeps the inherited advisory leases alive if
// this updater is interrupted, so an exact retry never steals active work.
detached: process.platform !== 'win32',
env: invocation.capabilityRequest
? {
...process.env,
[RUNTIME_HOST_OPERATOR_CAPABILITY_REQUEST_ENV]: invocation.capabilityRequest,
}
: process.env,
stdio: ['ignore', 'pipe', 'pipe', ...inheritedFds],
windowsHide: true,
});
if (!child.stdout || !child.stderr) {
child.kill('SIGKILL');
reject(new Error('The current Runtime Host operator did not expose its output streams'));
return;
}
const stdoutStream = child.stdout;
const stderrStream = child.stderr;
let stdout = '';
let stderr = '';
let failure: Error | undefined;
const append = (current: string, chunk: Buffer): string => {
const next = current + chunk.toString('utf8');
if (Buffer.byteLength(next) > OPERATOR_OUTPUT_MAX_BYTES) {
failure = new Error('The current Runtime Host operator returned too much output');
child.kill('SIGKILL');
}
return next;
};
stdoutStream.on('data', (chunk: Buffer) => {
stdout = append(stdout, chunk);
});
stderrStream.on('data', (chunk: Buffer) => {
stderr = append(stderr, chunk);
});
child.once('error', (error) => {
failure = error;
});
const timeout = setTimeout(() => {
failure = new Error('The current Runtime Host operator timed out');
child.kill('SIGKILL');
}, OPERATOR_TIMEOUT_MS);
child.once('close', () => {
clearTimeout(timeout);
let frame: RuntimeHostServiceManagementFrame | undefined;
for (const line of stdout.split(/\r?\n/u)) {
frame = decodeRuntimeHostServiceManagementFrame(line) ?? frame;
}
if (frame) resolve(frame);
else
reject(
failure ??
new Error(stderr.trim() || 'The current Runtime Host operator returned no result'),
);
});
});
}
function humanPhase(phase: RuntimeHostServiceUpdatePhase): string {
if (phase === 'checking') return 'Checking the managed Runtime Host update...';
if (phase === 'staging') return 'Staging the replacement package...';
if (phase === 'retiring') return 'Retiring the current Runtime Host...';
return 'Starting and verifying the replacement Runtime Host...';
}
function humanResult(
frame: Extract<RuntimeHostServiceManagementFrame, { kind: 'result'; action: 'update' }>,
): string {
if (frame.update.kind === 'already_current') {
return `Runtime Host ${frame.update.version} is already installed.`;
}
if (frame.update.kind === 'active_tasks') {
return 'Runtime Host still owns active work. Retry with explicit interruption authority.';
}
if (frame.update.kind === 'repaired') {
return `Runtime Host ${frame.update.version} was restored to a ready state.`;
}
if (frame.update.previousVersion === frame.update.targetVersion) {
return `Runtime Host package ${frame.update.targetVersion} was updated.`;
}
return `Runtime Host was updated from ${frame.update.previousVersion} to ${frame.update.targetVersion}.`;
}
function manualUpdateRequiredMessage(
targetVersion: string,
reason:
| 'target_not_newer'
| 'current_compatibility_unknown'
| 'target_compatibility_unknown'
| 'compatibility_mismatch',
): string {
if (reason === 'target_not_newer') {
return `Maka ${targetVersion} is older than the installed Runtime Host and will not be applied automatically`;
}
if (reason === 'current_compatibility_unknown') {
return 'The installed Runtime Host does not publish enough compatibility evidence for an automatic update';
}
if (reason === 'target_compatibility_unknown') {
return `Maka ${targetVersion} does not publish enough compatibility evidence for an automatic update`;
}
return `Maka ${targetVersion} requires an explicit manual compatibility transition`;
}