blob: 8241cefcf0a4e32163b4ccb7e7c4b69d40bf1d35 [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 { createHash, randomUUID } from 'node:crypto';
import { createSocket } from 'node:dgram';
import { createServer } from 'node:net';
import { realpathSync } from 'node:fs';
import {
access,
mkdir,
open,
readFile,
realpath,
rename,
rm,
stat,
unlink,
} from 'node:fs/promises';
import { homedir } from 'node:os';
import { dirname, isAbsolute, join, resolve } from 'node:path';
import { truncateUtf8 } from '@maka/core/diagnostic-log';
import {
canonicalProjectDirectoryRootSpec,
isCanonicalRuntimeHostWebSocketPath,
PROJECT_DIRECTORY_MAX_ROOTS,
projectDirectoryRootSpecValid,
RUNTIME_HOST_PROTOCOL_VERSION,
} from '@maka/runtime-host/protocol';
import {
connectExistingRuntimeHost,
prepareConnectedRuntimeHostRetirement,
} from '@maka/runtime-host/client';
import {
resolveRuntimeHostManagedServiceId,
RUNTIME_HOST_SERVICE_LOG_MAX_BYTES,
type RuntimeHostReconciliationProvider,
type RuntimeHostServiceErrorCode,
type RuntimeHostSupervisorProvider,
} from '@maka/runtime-host/operator';
import {
withLegacyFileUpdateLockLease,
withProcessLifetimeFileUpdateLock,
} from '@maka/storage/process-lifetime-file-update-lock';
import {
discoverMarkedStorageRoot,
resolveExistingStorageRoot,
tryAcquireInteractiveRootOwner,
type InteractiveRootOwner,
type StorageRootCapability,
} from '@maka/storage/root-authority';
import {
isRuntimeHostManagedDeploymentCli,
removeRuntimeHostManagedDeployment,
resolveExistingRuntimeHostManagedDeploymentRoot,
resolveRuntimeHostManagedDeploymentForCli,
resolveRuntimeHostManagedDeploymentRoot,
} from './runtime-host-managed-deployment.js';
import { writeRuntimeHostManagedUpdatePolicy } from './runtime-host-update-policy-store.js';
import { isTemporaryNpxInstallation } from './runtime-host-cli-installation.js';
const SERVICE_CONFIG_FILE = 'runtime-host-service.json';
const SERVICE_LIFECYCLE_LOCK_FILE = 'runtime-host-setup';
const SERVICE_DEPLOYMENT_LOCK_FILE = 'runtime-host-deployment';
const DEFAULT_WEBSOCKET_PATH = '/runtime-host';
const SERVICE_OPERATION_LOCK_TIMEOUT_MS = 60_000;
const SERVICE_READY_TIMEOUT_MS = 45_000;
const SERVICE_READY_POLL_MS = 50;
export interface RuntimeHostManagedServiceConfig {
readonly schemaVersion: 1 | 2;
readonly managedDeploymentRoot?: string;
readonly rootPath: string;
readonly projectDirectoryRoots: readonly {
readonly label: string;
readonly path: string;
}[];
readonly websocket: {
readonly host: '127.0.0.1';
readonly port: number;
readonly path: string;
};
readonly launch: {
readonly nodePath: string;
readonly cliPath: string;
};
}
export type RuntimeHostServiceState =
| 'not_installed'
| 'stopped'
| 'starting'
| 'running'
| 'failed';
export interface RuntimeHostServiceObservedStatus {
readonly installed: boolean;
readonly enabled: boolean;
readonly active: boolean;
readonly state: RuntimeHostServiceState;
readonly pid: number | null;
readonly lastExitCode: number | null;
}
export interface RuntimeHostServiceBackendStatus extends RuntimeHostServiceObservedStatus {
readonly manager: 'systemd_user' | 'launch_agent';
}
export interface RuntimeHostServiceBackend {
preflightDeployment(): Promise<void>;
stageDeployment(): Promise<RuntimeHostServiceDeployment>;
/** A rejected replacement must restore the previous deployment or report update_incomplete. */
replace(config: RuntimeHostManagedServiceConfig): Promise<void>;
/** Reject partial or drifted scheduler state before replacement begins. */
verifyReplacementPreconditions(config: RuntimeHostManagedServiceConfig): Promise<void>;
/** Verify the deployment definition and, when requested, its scheduler readiness. */
verifyDeployment(
config: RuntimeHostManagedServiceConfig,
options?: {
readonly requireSchedulerReady?: boolean;
readonly acceptLegacyConfigLaunch?: boolean;
},
): Promise<void>;
status(): Promise<RuntimeHostServiceBackendStatus>;
start(): Promise<void>;
stop(): Promise<void>;
restart(): Promise<void>;
/** Stop only the Runtime Host process while preserving deployment scheduling. */
retire(): Promise<void>;
logs(): Promise<string>;
uninstall(): Promise<void>;
}
export interface RuntimeHostServiceDeployment {
apply(config: RuntimeHostManagedServiceConfig, activate: boolean): Promise<void>;
rollback(): Promise<void>;
}
export interface RuntimeHostManagedServiceStatus extends RuntimeHostServiceObservedStatus {
readonly manager:
| 'systemd_user'
| 'launch_agent'
| 'openrc_user'
| 'openrc_system'
| 'windows_task'
| 'on_demand'
| 'none';
readonly config: RuntimeHostManagedServiceConfig | null;
readonly installedVersion: string | null;
readonly lifecycle?: {
readonly mode: 'on_demand' | 'supervised';
readonly availability: 'activation' | 'session' | 'environment' | 'machine';
readonly provider?: RuntimeHostSupervisorProvider;
};
readonly reconciliation?: {
readonly trigger: 'manual' | 'activation' | 'scheduled';
readonly provider?: RuntimeHostReconciliationProvider;
};
}
export type RuntimeHostManagedServiceAction =
| 'install'
| 'configure'
| 'status'
| 'start'
| 'stop'
| 'restart'
| 'retire'
| 'logs'
| 'uninstall';
export type RuntimeHostRetirementResult =
| { readonly kind: 'active_tasks' }
| {
readonly kind: 'retired';
readonly hostEpoch: string;
readonly pid: number;
}
| { readonly kind: 'stopped' };
interface RuntimeHostManagedServiceResultBase {
readonly schemaVersion: 1;
readonly service: RuntimeHostManagedServiceStatus;
readonly retainedStateRoot?: string;
readonly logs?: string;
}
export type RuntimeHostManagedServiceResult =
| (RuntimeHostManagedServiceResultBase & {
readonly action: 'retire';
readonly retirement: RuntimeHostRetirementResult;
})
| (RuntimeHostManagedServiceResultBase & {
readonly action: 'configure';
readonly configuration: { readonly kind: 'unchanged' | 'configured' | 'active_tasks' };
readonly retirement?: never;
})
| (RuntimeHostManagedServiceResultBase & {
readonly action: 'uninstall';
readonly retirement: RuntimeHostRetirementResult;
readonly configuration?: never;
})
| (RuntimeHostManagedServiceResultBase & {
readonly action: Exclude<
RuntimeHostManagedServiceAction,
'retire' | 'configure' | 'uninstall'
>;
readonly retirement?: never;
readonly configuration?: never;
});
export interface RuntimeHostManagedServiceInput {
readonly action: RuntimeHostManagedServiceAction;
readonly clientDataRoot: string;
readonly defaultRootPath: string;
readonly rootPath?: string;
readonly projectDirectoryRoots?: readonly {
readonly label: string;
readonly path: string;
}[];
readonly websocketPort?: number;
readonly websocketPath?: string;
readonly retainManagedDeployment?: boolean;
readonly nodePath: string;
readonly cliPath: string;
readonly expectedTarget?: RuntimeHostManagedServiceTarget;
readonly expectedConfigFingerprint?: string;
readonly expectedHost?: { readonly hostEpoch: string; readonly pid: number };
readonly allowInterruptActiveTasks?: boolean;
}
export interface RuntimeHostManagedServiceTarget {
readonly serviceId: string;
readonly rootPath: string;
readonly rootId: string;
readonly deploymentId?: string;
}
export interface RuntimeHostManagedDeploymentCleanupInput {
readonly clientDataRoot: string;
readonly cliPath: string;
readonly expectedTarget: RuntimeHostManagedServiceTarget;
}
export interface RuntimeHostManagedServiceReplacementInput
extends Omit<RuntimeHostManagedServiceInput, 'action' | 'expectedTarget'> {
readonly expectedTarget: RuntimeHostManagedServiceTarget;
}
interface RuntimeHostServiceManagerDeps {
readonly allocateLoopbackPort: () => Promise<number>;
readonly waitForReady: (
config: RuntimeHostManagedServiceConfig,
backend: RuntimeHostServiceBackend,
) => Promise<void>;
readonly prepareRetirement: (
config: RuntimeHostManagedServiceConfig,
expectedPid: number,
allowInterruptActiveTasks: boolean,
) => Promise<
| { readonly kind: 'active_tasks' }
| {
readonly kind: 'prepared';
readonly hostEpoch: string;
readonly pid: number;
}
>;
readonly environment: NodeJS.ProcessEnv;
readonly homeDir: string;
readonly platform: NodeJS.Platform;
}
export type RuntimeHostServiceManagerOverrides = Partial<RuntimeHostServiceManagerDeps>;
export class RuntimeHostServiceManagerError extends Error {
constructor(
readonly code: Extract<
RuntimeHostServiceErrorCode,
| 'unsupported_platform'
| 'service_manager_unavailable'
| 'linger_disabled'
| 'not_installed'
| 'invalid_config'
| 'invalid_launch'
| 'target_mismatch'
| 'configuration_changed'
| 'configuration_incomplete'
| 'active_tasks'
| 'retirement_failed'
| 'update_requires_retirement'
| 'update_incomplete'
| 'service_manager_operation_failed'
| 'uninstall_incomplete'
>,
message: string,
options?: ErrorOptions,
) {
super(message, options);
this.name = 'RuntimeHostServiceManagerError';
}
}
function runtimeHostServiceManagerDeps(
overrides: Partial<RuntimeHostServiceManagerDeps>,
): RuntimeHostServiceManagerDeps {
return {
allocateLoopbackPort,
waitForReady: verifyRuntimeHostManagedServiceReady,
prepareRetirement: prepareRuntimeHostRetirement,
environment: process.env,
homeDir: homedir(),
platform: process.platform,
...overrides,
};
}
export async function manageRuntimeHostService(
input: RuntimeHostManagedServiceInput,
backend: RuntimeHostServiceBackend,
overrides: Partial<RuntimeHostServiceManagerDeps> = {},
): Promise<RuntimeHostManagedServiceResult> {
if (input.expectedHost) {
throw new RuntimeHostServiceManagerError(
'target_mismatch',
'An exact Host fence requires the canonical managed deployment operator',
);
}
const deps = runtimeHostServiceManagerDeps(overrides);
const configPath = resolveRuntimeHostManagedServiceConfigPath(input.clientDataRoot);
const configDirectory = dirname(configPath);
if (input.action === 'status' && !(await isExistingDirectory(configDirectory))) {
return manageRuntimeHostServiceLocked(input, backend, deps, configPath);
}
await mkdir(configDirectory, { recursive: true, mode: 0o700 });
return withProcessLifetimeFileUpdateLock(
configPath,
() => manageRuntimeHostServiceLocked(input, backend, deps, configPath),
SERVICE_OPERATION_LOCK_TIMEOUT_MS,
);
}
export async function withRuntimeHostManagedServiceLifecycleLock<T>(
clientDataRoot: string,
operation: () => Promise<T>,
timeoutMs = SERVICE_OPERATION_LOCK_TIMEOUT_MS,
): Promise<T> {
await mkdir(clientDataRoot, { recursive: true, mode: 0o700 });
return withProcessLifetimeFileUpdateLock(
join(clientDataRoot, SERVICE_LIFECYCLE_LOCK_FILE),
operation,
timeoutMs,
);
}
export async function withRuntimeHostManagedServiceDeploymentLock<T>(
clientDataRoot: string,
operation: (inheritableLeaseFd?: number) => Promise<T>,
timeoutMs = SERVICE_OPERATION_LOCK_TIMEOUT_MS,
): Promise<T> {
await mkdir(clientDataRoot, { recursive: true, mode: 0o700 });
return withProcessLifetimeFileUpdateLock(
join(clientDataRoot, SERVICE_DEPLOYMENT_LOCK_FILE),
operation,
timeoutMs,
);
}
export async function withRuntimeHostManagedServiceLegacyOperatorLeases<T>(
clientDataRoot: string,
operation: (inheritedFds: readonly number[]) => Promise<T>,
timeoutMs = SERVICE_OPERATION_LOCK_TIMEOUT_MS,
): Promise<T> {
const configPath = resolveRuntimeHostManagedServiceConfigPath(clientDataRoot);
await mkdir(dirname(configPath), { recursive: true, mode: 0o700 });
return withLegacyFileUpdateLockLease(
join(clientDataRoot, SERVICE_LIFECYCLE_LOCK_FILE),
(lifecycleFd) =>
withLegacyFileUpdateLockLease(
configPath,
(configFd) => operation([lifecycleFd, configFd]),
timeoutMs,
),
timeoutMs,
);
}
export async function replaceRuntimeHostManagedService(
input: RuntimeHostManagedServiceReplacementInput,
backend: RuntimeHostServiceBackend,
overrides: Partial<RuntimeHostServiceManagerDeps> = {},
): Promise<RuntimeHostManagedServiceStatus> {
const deps = runtimeHostServiceManagerDeps(overrides);
const configPath = resolveRuntimeHostManagedServiceConfigPath(input.clientDataRoot);
await mkdir(dirname(configPath), { recursive: true, mode: 0o700 });
return withProcessLifetimeFileUpdateLock(
configPath,
() => replaceRuntimeHostManagedServiceLocked(input, backend, deps, configPath),
SERVICE_OPERATION_LOCK_TIMEOUT_MS,
);
}
export async function cleanupRuntimeHostManagedDeployment(
input: RuntimeHostManagedDeploymentCleanupInput,
backend: RuntimeHostServiceBackend,
): Promise<void> {
await withRuntimeHostManagedServiceLifecycleLock(input.clientDataRoot, async () => {
const configPath = resolveRuntimeHostManagedServiceConfigPath(input.clientDataRoot);
await mkdir(dirname(configPath), { recursive: true, mode: 0o700 });
await withProcessLifetimeFileUpdateLock(
configPath,
async () => {
const serviceId = resolveRuntimeHostManagedServiceId(input.clientDataRoot);
assertExpectedServiceIdentity(serviceId, input.expectedTarget);
const service = await readServiceStatus(configPath, backend);
if (service.installed || service.active || service.enabled || service.config !== null) {
throw new RuntimeHostServiceManagerError(
'uninstall_incomplete',
'Runtime Host service was installed again; refusing to remove its managed deployment',
);
}
const deploymentRoot = resolveRuntimeHostManagedDeploymentForCli(serviceId, input.cliPath);
if (!deploymentRoot) {
throw new RuntimeHostServiceManagerError(
'invalid_launch',
'The Runtime Host operator does not belong to the expected managed deployment',
);
}
await removeRuntimeHostManagedDeployment(deploymentRoot, serviceId);
},
SERVICE_OPERATION_LOCK_TIMEOUT_MS,
);
});
}
async function manageRuntimeHostServiceLocked(
input: RuntimeHostManagedServiceInput,
backend: RuntimeHostServiceBackend,
deps: RuntimeHostServiceManagerDeps,
configPath: string,
): Promise<RuntimeHostManagedServiceResult> {
const serviceId = resolveRuntimeHostManagedServiceId(input.clientDataRoot);
if (input.expectedTarget) assertExpectedServiceIdentity(serviceId, input.expectedTarget);
if (input.action === 'install') {
const previous = await readServiceConfigForRepair(configPath);
const expectedRoot = await resolveExpectedServiceRoot(previous, input);
await backend.preflightDeployment();
const config = await prepareServiceConfig(
expectedRoot ? { ...input, rootPath: expectedRoot.canonicalPath } : input,
previous,
deps,
);
if (
previous &&
input.projectDirectoryRoots !== undefined &&
!sameProjectDirectoryRoots(previous, config)
) {
throw new RuntimeHostServiceManagerError(
'configuration_changed',
'An existing Runtime Host Project root policy must be changed through the configuration workflow',
);
}
await deployRuntimeHostServiceConfiguration({
backend,
deps,
configPath,
previous,
desired: config,
activate: true,
});
return result(input.action, await readServiceStatus(configPath, backend));
}
if (input.action === 'status') {
const service = await readServiceStatus(configPath, backend);
await resolveExpectedServiceRoot(service.config, input);
if (service.installed && service.config?.schemaVersion === 2) {
await backend.verifyDeployment(service.config);
}
return result(input.action, service);
}
if (input.action === 'uninstall') {
if (!input.expectedTarget) {
throw new RuntimeHostServiceManagerError(
'target_mismatch',
'Runtime Host uninstall requires the expected managed service identity',
);
}
const { config: before, invalid: invalidConfig } =
await readServiceConfigForUninstall(configPath);
const retirementRoot =
before === null && !invalidConfig
? undefined
: await resolveExpectedServiceRoot(before, input);
const retainedStateRoot =
before === null && !invalidConfig && input.expectedTarget
? input.expectedTarget.rootPath
: retirementRoot?.canonicalPath;
let retirement: RuntimeHostRetirementResult = { kind: 'stopped' };
const beforeStatus: RuntimeHostManagedServiceStatus = {
...(await backend.status()),
config: before,
installedVersion: before ? await readInstalledVersion(before.launch.cliPath) : null,
};
if (beforeStatus.installed && before && retirementRoot) {
const retired = await retireManagedRuntimeHostService(
{ ...beforeStatus, config: before },
retirementRoot,
backend,
deps,
input.allowInterruptActiveTasks ?? false,
);
retirement = retired.retirement;
if (retirement.kind === 'active_tasks') {
return {
schemaVersion: 1,
action: input.action,
service: beforeStatus,
retirement,
...(retainedStateRoot ? { retainedStateRoot } : {}),
};
}
}
const managedDeploymentRoot =
before?.managedDeploymentRoot ??
resolveRuntimeHostManagedDeploymentForCli(serviceId, input.cliPath);
let policyDeploymentRoot: string | undefined;
try {
policyDeploymentRoot = await resolveExistingRuntimeHostManagedDeploymentRoot(
managedDeploymentRoot ??
resolveRuntimeHostManagedDeploymentRoot(serviceId, {
env: deps.environment,
homeDir: deps.homeDir,
platform: deps.platform,
}),
serviceId,
);
} catch (error) {
throw new RuntimeHostServiceManagerError(
'uninstall_incomplete',
'Unable to safely inspect the managed Runtime Host deployment before revoking automatic update policy',
{ cause: error },
);
}
if (!policyDeploymentRoot && invalidConfig && !managedDeploymentRoot) {
throw new RuntimeHostServiceManagerError(
'uninstall_incomplete',
'Unable to confirm automatic update policy revocation because the service config is invalid and its managed deployment could not be located',
);
}
if (policyDeploymentRoot) {
await writeRuntimeHostManagedUpdatePolicy(policyDeploymentRoot, null);
}
await backend.uninstall();
await removeRuntimeHostServiceFile(configPath, 'service config');
if (managedDeploymentRoot && !input.retainManagedDeployment) {
try {
await removeRuntimeHostManagedDeployment(managedDeploymentRoot, serviceId);
} catch (error) {
throw new RuntimeHostServiceManagerError(
'uninstall_incomplete',
`Unable to remove the managed Runtime Host deployment at ${managedDeploymentRoot}`,
{ cause: error },
);
}
}
const service = await readServiceStatus(configPath, backend);
if (service.installed || service.active || service.enabled || service.config !== null) {
throw new RuntimeHostServiceManagerError(
'uninstall_incomplete',
`Runtime Host service still has managed state: ${service.state}`,
);
}
return {
schemaVersion: 1,
action: input.action,
service,
retirement,
...((before?.rootPath ?? retainedStateRoot)
? { retainedStateRoot: before?.rootPath ?? retainedStateRoot }
: {}),
};
}
if (input.action === 'logs') {
const service = await readServiceStatus(configPath, backend);
await resolveExpectedServiceRoot(service.config, input);
const logs = truncateUtf8(await backend.logs(), RUNTIME_HOST_SERVICE_LOG_MAX_BYTES);
return result(input.action, service, undefined, logs);
}
if (input.action === 'configure') {
if (!input.expectedTarget || input.projectDirectoryRoots === undefined) {
throw new RuntimeHostServiceManagerError(
'target_mismatch',
'Runtime Host configuration requires the expected service identity and a complete Project root policy',
);
}
if (!input.expectedConfigFingerprint) {
throw new RuntimeHostServiceManagerError(
'configuration_changed',
'Runtime Host configuration requires the observed service configuration fingerprint',
);
}
const service = await readServiceStatus(configPath, backend);
const currentConfig = service.config;
const root = await resolveExpectedServiceRoot(currentConfig, input);
if (!service.installed || !currentConfig || !root) {
throw new RuntimeHostServiceManagerError(
'not_installed',
'Runtime Host service is not installed',
);
}
if (
runtimeHostManagedServiceConfigFingerprint(currentConfig) !== input.expectedConfigFingerprint
) {
throw new RuntimeHostServiceManagerError(
'configuration_changed',
'The managed Runtime Host configuration changed; refresh it before applying this edit',
);
}
const desired = await prepareServiceConfig(
{
...input,
rootPath: currentConfig.rootPath,
nodePath: currentConfig.launch.nodePath,
cliPath: currentConfig.launch.cliPath,
},
currentConfig,
deps,
);
if (sameProjectDirectoryRoots(currentConfig, desired)) {
return configurationResult('unchanged', service);
}
await backend.preflightDeployment();
await backend.verifyDeployment(currentConfig, { acceptLegacyConfigLaunch: true });
const deployment = await backend.stageDeployment();
const retired = await retireManagedRuntimeHostService(
{ ...service, config: currentConfig },
root,
backend,
deps,
input.allowInterruptActiveTasks ?? false,
);
if (retired.retirement.kind === 'active_tasks') {
return configurationResult('active_tasks', service);
}
try {
await writeRuntimeHostServiceFile(configPath, `${JSON.stringify(desired, null, 2)}\n`, 0o600);
await deployment.apply(desired, true);
await deps.waitForReady(desired, backend);
} catch (error) {
await rollbackRuntimeHostConfiguration(
{ configPath, currentConfig, desiredConfig: desired, root, deployment },
backend,
deps,
input.allowInterruptActiveTasks ?? false,
error,
);
}
return configurationResult('configured', await readServiceStatus(configPath, backend));
}
if (input.action === 'retire') {
if (!input.expectedTarget) {
throw new RuntimeHostServiceManagerError(
'target_mismatch',
'Runtime Host retirement requires the expected managed service identity',
);
}
const service = await readServiceStatus(configPath, backend);
const currentConfig = service.config;
const root = await resolveExpectedServiceRoot(currentConfig, input);
if (!service.installed || !currentConfig || !root) {
throw new RuntimeHostServiceManagerError(
'not_installed',
'Runtime Host service is not installed',
);
}
const retired = await retireManagedRuntimeHostService(
{ ...service, config: currentConfig },
root,
backend,
deps,
input.allowInterruptActiveTasks ?? false,
);
return { schemaVersion: 1, action: input.action, ...retired };
}
const config = await readServiceConfig(configPath);
if (!config) {
throw new RuntimeHostServiceManagerError(
'not_installed',
'Runtime Host service is not installed',
);
}
const expectedRoot = await resolveExpectedServiceRoot(config, input);
if (input.action === 'start' || input.action === 'restart') {
if (config.schemaVersion === 2) await backend.verifyDeployment(config);
if (input.action === 'restart') {
const root = expectedRoot ?? (await discoverMarkedStorageRoot({ path: config.rootPath }));
const service = await readServiceStatus(configPath, backend);
const retired = await retireManagedRuntimeHostService(
{ ...service, config },
root,
backend,
deps,
input.allowInterruptActiveTasks ?? false,
);
if (retired.retirement.kind === 'active_tasks') {
throw new RuntimeHostServiceManagerError(
'active_tasks',
'Runtime Host still owns active work; it was not restarted',
);
}
}
try {
await backend.start();
await deps.waitForReady(config, backend);
} catch (error) {
try {
await backend.stop();
} catch (stopError) {
throw new RuntimeHostServiceManagerError(
'service_manager_operation_failed',
'Starting the Runtime Host managed deployment failed and its partial state could not be stopped',
{ cause: new AggregateError([error, stopError]) },
);
}
throw error;
}
} else {
await backend[input.action]();
}
return result(input.action, await readServiceStatus(configPath, backend));
}
async function replaceRuntimeHostManagedServiceLocked(
input: RuntimeHostManagedServiceReplacementInput,
backend: RuntimeHostServiceBackend,
deps: RuntimeHostServiceManagerDeps,
configPath: string,
): Promise<RuntimeHostManagedServiceStatus> {
const serviceId = resolveRuntimeHostManagedServiceId(input.clientDataRoot);
assertExpectedServiceIdentity(serviceId, input.expectedTarget);
const service = await readServiceStatus(configPath, backend);
const root = await resolveExpectedServiceRoot(service.config, input);
if (!service.installed || !service.config || !root) {
throw new RuntimeHostServiceManagerError(
'not_installed',
'Runtime Host service is not installed',
);
}
if (
service.active ||
service.pid !== null ||
(service.state !== 'stopped' && service.state !== 'failed')
) {
throw new RuntimeHostServiceManagerError(
'update_requires_retirement',
'Retire the managed Runtime Host service before replacing its package',
);
}
await backend.preflightDeployment();
const config = await prepareServiceConfig(input, service.config, deps);
let rootFence: InteractiveRootOwner | undefined =
await acquireRuntimeHostRootRetirementFence(root);
try {
await writeRuntimeHostServiceFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 0o600);
await releaseRuntimeHostRootRetirementFence(rootFence);
rootFence = undefined;
} finally {
await rootFence?.close().catch(() => undefined);
}
try {
await backend.replace(config);
} catch (error) {
if (error instanceof RuntimeHostServiceManagerError && error.code === 'update_incomplete') {
await backend.retire().catch(() => undefined);
throw error;
}
try {
await writeRuntimeHostServiceFile(
configPath,
`${JSON.stringify(service.config, null, 2)}\n`,
0o600,
);
} catch (restoreError) {
await backend.retire().catch(() => undefined);
throw new RuntimeHostServiceManagerError(
'update_incomplete',
'Replacing the Runtime Host service failed and its previous configuration could not be restored',
{ cause: new AggregateError([error, restoreError]) },
);
}
throw new RuntimeHostServiceManagerError(
'update_incomplete',
'Replacing the Runtime Host service failed; the previous deployment was restored and remains stopped',
{ cause: error },
);
}
try {
await deps.waitForReady(config, backend);
} catch (error) {
try {
await backend.retire();
} catch (stopError) {
throw new RuntimeHostServiceManagerError(
'update_incomplete',
'The replacement Runtime Host did not become ready and could not be stopped; inspect the service state before retrying',
{ cause: new AggregateError([error, stopError]) },
);
}
throw new RuntimeHostServiceManagerError(
'update_incomplete',
'The replacement Runtime Host did not become ready; the selected deployment was retained but stopped because rolling back across an unknown storage boundary is unsafe',
{ cause: error },
);
}
return readServiceStatus(configPath, backend);
}
function assertExpectedServiceIdentity(
serviceId: string,
expectedTarget: RuntimeHostManagedServiceTarget,
): void {
if (!/^[a-f0-9]{64}$/u.test(expectedTarget.serviceId) || expectedTarget.serviceId !== serviceId) {
throw new RuntimeHostServiceManagerError(
'target_mismatch',
'The managed Runtime Host service does not match the expected service identity',
);
}
}
async function resolveExpectedServiceRoot(
config: RuntimeHostManagedServiceConfig | null,
input: Pick<RuntimeHostManagedServiceInput, 'expectedTarget'>,
): Promise<StorageRootCapability<'interactive'> | undefined> {
if (!input.expectedTarget) return undefined;
try {
const root = await resolveExistingStorageRoot({
path: input.expectedTarget.rootPath,
kind: 'interactive',
expectedRootId: input.expectedTarget.rootId,
});
if (config && resolve(config.rootPath) !== root.canonicalPath) {
throw new Error('The service config points to a different State Root path');
}
return root;
} catch (error) {
throw new RuntimeHostServiceManagerError(
'target_mismatch',
'The managed Runtime Host service does not match the expected State Root',
{ cause: error },
);
}
}
export function resolveRuntimeHostManagedServiceConfigPath(clientDataRoot: string): string {
return join(clientDataRoot, SERVICE_CONFIG_FILE);
}
export { resolveRuntimeHostManagedServiceId };
export async function writeRuntimeHostServiceFile(
path: string,
contents: string,
mode: number,
): Promise<void> {
const directory = dirname(path);
await mkdir(directory, { recursive: true, mode: 0o700 });
const temporaryPath = `${path}.${randomUUID()}.tmp`;
try {
const file = await open(temporaryPath, 'wx', mode);
try {
await file.writeFile(contents, 'utf8');
await file.sync();
} finally {
await file.close();
}
await rename(temporaryPath, path);
await syncRuntimeHostServiceDirectory(directory);
} finally {
await rm(temporaryPath, { force: true });
}
}
export async function removeRuntimeHostServiceFile(path: string, label: string): Promise<void> {
let absent = false;
try {
await unlink(path);
} catch (error) {
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') absent = true;
else {
throw new RuntimeHostServiceManagerError(
'uninstall_incomplete',
`Unable to remove Runtime Host ${label} at ${path}`,
{ cause: error },
);
}
}
try {
await syncRuntimeHostServiceDirectory(dirname(path));
} catch (error) {
if (absent && error instanceof Error && 'code' in error && error.code === 'ENOENT') return;
throw new RuntimeHostServiceManagerError(
'uninstall_incomplete',
`Unable to persist removal of Runtime Host ${label} at ${path}`,
{ cause: error },
);
}
}
async function syncRuntimeHostServiceDirectory(directory: string): Promise<void> {
const parent = await open(directory, 'r');
try {
await parent.sync();
} finally {
await parent.close();
}
}
export function formatRuntimeHostServiceLogs(
sources: readonly { readonly label: string; readonly logs: string }[],
): string {
const present = sources.filter(({ logs }) => logs.length > 0);
if (present.length === 0) return '';
const separatorBytes = present.length - 1;
const sourceBudget = Math.floor(
(RUNTIME_HOST_SERVICE_LOG_MAX_BYTES - separatorBytes) / present.length,
);
return present
.map(({ label, logs }) => {
const heading = `${label}:\n`;
return `${heading}${takeUtf8Tail(logs, sourceBudget - Buffer.byteLength(heading))}`;
})
.join('\n');
}
function takeUtf8Tail(value: string, maximumBytes: number): string {
const encoded = Buffer.from(value);
if (encoded.byteLength <= maximumBytes) return value;
let start = encoded.byteLength - maximumBytes;
while (start < encoded.byteLength && (encoded[start]! & 0xc0) === 0x80) start += 1;
return encoded.subarray(start).toString('utf8');
}
async function prepareServiceConfig(
input: Omit<RuntimeHostManagedServiceInput, 'action'>,
previous: RuntimeHostManagedServiceConfig | null,
deps: RuntimeHostServiceManagerDeps,
): Promise<RuntimeHostManagedServiceConfig> {
if (!input.cliPath) {
throw new RuntimeHostServiceManagerError(
'invalid_launch',
'The current Maka CLI entry point could not be resolved',
);
}
const serviceId = resolveRuntimeHostManagedServiceId(input.clientDataRoot);
const requestedRoot = resolve(input.rootPath ?? previous?.rootPath ?? input.defaultRootPath);
const projectDirectoryRoots = await normalizeProjectDirectoryRoots(
input.projectDirectoryRoots ??
(previous
? effectiveRuntimeHostProjectDirectoryRoots(previous, deps.homeDir)
: [{ label: '~', path: deps.homeDir }]),
);
const [nodePath, cliPath] = await Promise.all([
realpath(input.nodePath),
realpath(input.cliPath),
]).catch((error) => {
throw new RuntimeHostServiceManagerError(
'invalid_launch',
'The current Node.js or Maka CLI installation is unavailable',
{ cause: error },
);
});
await assertPersistentCliInstallation(cliPath, deps.environment, deps.homeDir);
const rootPath = await normalizeStateRoot(requestedRoot);
const port =
input.websocketPort ?? previous?.websocket.port ?? (await deps.allocateLoopbackPort());
const websocketPath = input.websocketPath ?? previous?.websocket.path ?? DEFAULT_WEBSOCKET_PATH;
const requestedManagedDeploymentRoot =
previous?.managedDeploymentRoot ??
resolveRuntimeHostManagedDeploymentForCli(serviceId, cliPath);
const managedDeploymentRoot = requestedManagedDeploymentRoot
? await realpath(requestedManagedDeploymentRoot).catch((error) => {
throw new RuntimeHostServiceManagerError(
'invalid_launch',
'The managed Runtime Host deployment is unavailable',
{ cause: error },
);
})
: undefined;
if (
previous?.managedDeploymentRoot &&
(managedDeploymentRoot !== previous.managedDeploymentRoot ||
!isRuntimeHostManagedDeploymentCli(previous.managedDeploymentRoot, serviceId, cliPath))
) {
throw new RuntimeHostServiceManagerError(
'invalid_launch',
'Uninstall the managed Runtime Host service before replacing its managed package or launch path',
);
}
if (
managedDeploymentRoot &&
!isRuntimeHostManagedDeploymentCli(managedDeploymentRoot, serviceId, cliPath)
) {
throw new RuntimeHostServiceManagerError(
'invalid_launch',
'The managed Runtime Host CLI must belong to its deployment root',
);
}
const config: RuntimeHostManagedServiceConfig = {
schemaVersion: 2,
...(managedDeploymentRoot ? { managedDeploymentRoot } : {}),
rootPath,
projectDirectoryRoots,
websocket: { host: '127.0.0.1', port, path: websocketPath },
launch: { nodePath, cliPath },
};
validateServiceConfig(config, serviceId, input.clientDataRoot);
return config;
}
async function readServiceStatus(
configPath: string,
backend: RuntimeHostServiceBackend,
): Promise<RuntimeHostManagedServiceStatus> {
const [config, backendStatus] = await Promise.all([
readServiceConfig(configPath),
backend.status(),
]);
return {
...backendStatus,
config,
installedVersion: config ? await readInstalledVersion(config.launch.cliPath) : null,
...(config
? {
lifecycle: {
mode: 'supervised' as const,
availability:
backendStatus.manager === 'systemd_user'
? ('machine' as const)
: ('session' as const),
provider: backendStatus.manager === 'systemd_user' ? 'systemd_user' : 'launch_agent',
},
reconciliation: config.managedDeploymentRoot
? {
trigger: 'scheduled' as const,
provider:
backendStatus.manager === 'systemd_user'
? ('systemd_timer' as const)
: ('launch_agent_timer' as const),
}
: { trigger: 'manual' as const },
}
: {}),
};
}
async function readInstalledVersion(cliPath: string): Promise<string | null> {
try {
const packageRoot = dirname(dirname(await realpath(cliPath)));
const manifest: unknown = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8'));
if (
!isRecord(manifest) ||
manifest.name !== 'maka-agent' ||
typeof manifest.version !== 'string' ||
manifest.version.length === 0 ||
Buffer.byteLength(manifest.version, 'utf8') > 512
) {
return null;
}
return manifest.version;
} catch {
return null;
}
}
async function readServiceConfig(path: string): Promise<RuntimeHostManagedServiceConfig | null> {
let raw: string;
try {
raw = await readFile(path, 'utf8');
} catch (error) {
if (isNodeError(error, 'ENOENT')) return null;
throw error;
}
try {
const parsed: unknown = JSON.parse(raw);
const clientDataRoot = dirname(path);
validateServiceConfig(
parsed,
resolveRuntimeHostManagedServiceId(clientDataRoot),
clientDataRoot,
);
return parsed;
} catch (error) {
throw new RuntimeHostServiceManagerError(
'invalid_config',
`Invalid Runtime Host service config at ${path}`,
{ cause: error },
);
}
}
export async function readRuntimeHostManagedServiceConfig(
path: string,
): Promise<RuntimeHostManagedServiceConfig> {
const config = await readServiceConfig(path);
if (!config) {
throw new RuntimeHostServiceManagerError(
'not_installed',
'Runtime Host managed service configuration is unavailable',
);
}
return config;
}
async function readServiceConfigForRepair(
path: string,
): Promise<RuntimeHostManagedServiceConfig | null> {
try {
return await readServiceConfig(path);
} catch (error) {
if (error instanceof RuntimeHostServiceManagerError && error.code === 'invalid_config') {
return null;
}
throw error;
}
}
async function readServiceConfigForUninstall(path: string): Promise<{
readonly config: RuntimeHostManagedServiceConfig | null;
readonly invalid: boolean;
}> {
try {
return { config: await readServiceConfig(path), invalid: false };
} catch (error) {
if (error instanceof RuntimeHostServiceManagerError && error.code === 'invalid_config') {
return { config: null, invalid: true };
}
throw error;
}
}
function validateServiceConfig(
value: unknown,
serviceId: string,
clientDataRoot: string,
): asserts value is RuntimeHostManagedServiceConfig {
if (!isRecord(value) || (value.schemaVersion !== 1 && value.schemaVersion !== 2)) {
throw new TypeError('Invalid schemaVersion');
}
if (!isSafeAbsolutePath(value.rootPath)) throw new TypeError('Invalid rootPath');
if (
!Array.isArray(value.projectDirectoryRoots) ||
value.projectDirectoryRoots.length > PROJECT_DIRECTORY_MAX_ROOTS
) {
throw new TypeError('Invalid projectDirectoryRoots');
}
for (const root of value.projectDirectoryRoots) {
const canonical =
isRecord(root) && typeof root.label === 'string' && typeof root.path === 'string'
? canonicalProjectDirectoryRootSpec({ label: root.label, path: root.path })
: undefined;
if (
!isRecord(root) ||
!canonical ||
root.label !== canonical.label ||
!projectDirectoryRootSpecValid(canonical) ||
!isAbsolute(canonical.path)
) {
throw new TypeError('Invalid project directory root');
}
}
if (
new Set(value.projectDirectoryRoots.map(({ label }) => label)).size !==
value.projectDirectoryRoots.length
) {
throw new TypeError('Duplicate project directory root label');
}
const websocket = value.websocket;
if (
!isRecord(websocket) ||
websocket.host !== '127.0.0.1' ||
typeof websocket.port !== 'number' ||
!Number.isInteger(websocket.port) ||
websocket.port < 1 ||
websocket.port > 65_535 ||
!isCanonicalRuntimeHostWebSocketPath(websocket.path)
) {
throw new TypeError('Invalid websocket config');
}
if (value.peer !== undefined) throw new TypeError('Legacy peer configuration is unsupported');
const launch = value.launch;
if (
!isRecord(launch) ||
!isSafeAbsolutePath(launch.nodePath) ||
!isSafeAbsolutePath(launch.cliPath)
) {
throw new TypeError('Invalid launch config');
}
if (
value.managedDeploymentRoot !== undefined &&
(typeof value.managedDeploymentRoot !== 'string' ||
!isRuntimeHostManagedDeploymentCli(value.managedDeploymentRoot, serviceId, launch.cliPath))
) {
throw new TypeError('Invalid managed deployment root');
}
}
async function normalizeStateRoot(requestedRoot: string): Promise<string> {
try {
await mkdir(requestedRoot, { recursive: true, mode: 0o700 });
const canonical = await realpath(requestedRoot);
if (!(await stat(canonical)).isDirectory()) throw new Error('State Root is not a directory');
return canonical;
} catch (error) {
throw new RuntimeHostServiceManagerError(
'invalid_config',
`Invalid Runtime Host State Root: ${requestedRoot}`,
{ cause: error },
);
}
}
async function normalizeProjectDirectoryRoots(
roots: readonly { readonly label: string; readonly path: string }[],
): Promise<readonly { readonly label: string; readonly path: string }[]> {
let canonicalRoots: readonly {
readonly label: string;
readonly path: string;
}[];
try {
canonicalRoots = await Promise.all(
roots.map(async ({ label, path }) => {
const canonical = await realpath(path);
if (!(await stat(canonical)).isDirectory()) {
throw new Error(`Project root is not a directory: ${path}`);
}
return { label, path: canonical };
}),
);
} catch (error) {
throw new RuntimeHostServiceManagerError(
'invalid_config',
'A configured Project root is unavailable or is not a directory',
{ cause: error },
);
}
if (new Set(canonicalRoots.map(({ path }) => path)).size !== canonicalRoots.length) {
throw new RuntimeHostServiceManagerError(
'invalid_config',
'Configured Project roots must resolve to distinct directories',
);
}
return canonicalRoots;
}
export async function resolveRuntimeHostManagedProjectDirectoryRoots(
roots: readonly { readonly label: string; readonly path: string }[],
): Promise<readonly { readonly label: string; readonly path: string }[]> {
return normalizeProjectDirectoryRoots(roots);
}
async function assertPersistentCliInstallation(
cliPath: string,
environment: NodeJS.ProcessEnv,
homeDir: string,
): Promise<void> {
if (await isTemporaryNpxInstallation(cliPath, { environment, homeDir })) {
throw new RuntimeHostServiceManagerError(
'invalid_launch',
'A persistent Runtime Host service cannot use a temporary npx installation; install Maka globally and retry',
);
}
}
export async function verifyRuntimeHostManagedServiceReady(
config: RuntimeHostManagedServiceConfig,
backend: RuntimeHostServiceBackend,
): Promise<void> {
await backend.verifyDeployment(config, { requireSchedulerReady: true });
const deadline = Date.now() + SERVICE_READY_TIMEOUT_MS;
let lastFailure = 'not available';
while (Date.now() < deadline) {
const remaining = deadline - Date.now();
const connected = await connectExistingRuntimeHost({
rootPath: config.rootPath,
protocol: {
min: RUNTIME_HOST_PROTOCOL_VERSION,
max: RUNTIME_HOST_PROTOCOL_VERSION,
},
connectTimeoutMs: Math.max(1, Math.min(500, remaining)),
handshakeTimeoutMs: Math.max(1, Math.min(500, remaining)),
}).catch((error: unknown) => {
lastFailure = error instanceof Error ? error.message : String(error);
return undefined;
});
if (connected?.kind === 'connected') {
try {
const status = await connected.connection.status(Math.max(1, remaining));
if (status.state === 'ready') {
const [diagnostics, service] = await Promise.all([
connected.connection.request('host.diagnostics.query', {}),
backend.status(),
]);
if (service.active && service.pid !== null && diagnostics.pid === service.pid) return;
lastFailure = 'ready Host does not belong to the managed service process';
} else {
lastFailure = `Host state is ${status.state}`;
}
} catch (error) {
lastFailure = error instanceof Error ? error.message : String(error);
} finally {
await connected.connection.close().catch(() => undefined);
}
} else if (connected) {
lastFailure = connected.kind;
}
await new Promise<void>((resolveWait) => setTimeout(resolveWait, SERVICE_READY_POLL_MS));
}
throw new RuntimeHostServiceManagerError(
'service_manager_operation_failed',
`Runtime Host service did not become ready: ${lastFailure}`,
);
}
async function prepareRuntimeHostRetirement(
config: RuntimeHostManagedServiceConfig,
expectedPid: number,
allowInterruptActiveTasks: boolean,
): Promise<
| { readonly kind: 'active_tasks' }
| {
readonly kind: 'prepared';
readonly hostEpoch: string;
readonly pid: number;
}
> {
const connected = await connectExistingRuntimeHost({
rootPath: config.rootPath,
protocol: {
min: RUNTIME_HOST_PROTOCOL_VERSION,
max: RUNTIME_HOST_PROTOCOL_VERSION,
},
}).catch((error: unknown) => {
throw new RuntimeHostServiceManagerError(
'retirement_failed',
'Unable to connect to the managed Runtime Host before retirement',
{ cause: error },
);
});
if (connected.kind !== 'connected') {
throw new RuntimeHostServiceManagerError(
'retirement_failed',
`Managed Runtime Host cannot prepare for retirement: ${connected.kind}`,
);
}
const hostEpoch = connected.connection.hostEpoch;
try {
const diagnostics = await connected.connection.request('host.diagnostics.query', {});
if (diagnostics.pid !== expectedPid) {
throw new RuntimeHostServiceManagerError(
'retirement_failed',
'The State Root is owned by a different Runtime Host process',
);
}
const prepared = await prepareConnectedRuntimeHostRetirement(
connected.connection,
allowInterruptActiveTasks ? 'interrupt_active_work' : 'refuse_active_work',
);
if (prepared.kind === 'active_tasks') return prepared;
if (prepared.pid !== expectedPid) {
throw new RuntimeHostServiceManagerError(
'retirement_failed',
'Runtime Host process identity changed while preparing retirement',
);
}
return { ...prepared, hostEpoch };
} catch (error) {
throw new RuntimeHostServiceManagerError(
'retirement_failed',
'Managed Runtime Host could not prepare for retirement',
{ cause: error },
);
} finally {
await connected.connection.close().catch(() => undefined);
}
}
async function verifyRuntimeHostRootReleased(
root: StorageRootCapability<'interactive'>,
): Promise<void> {
try {
const owner = await tryAcquireInteractiveRootOwner(root);
if (!owner) {
throw new Error('The State Root writer is still held');
}
await owner.close();
} catch (error) {
throw new RuntimeHostServiceManagerError(
'retirement_failed',
'Runtime Host retirement did not release the State Root writer',
{ cause: error },
);
}
}
async function acquireRuntimeHostRootRetirementFence(
root: StorageRootCapability<'interactive'>,
): Promise<InteractiveRootOwner> {
try {
const owner = await tryAcquireInteractiveRootOwner(root);
if (!owner) throw new Error('The State Root writer changed while retirement was starting');
return owner;
} catch (error) {
throw new RuntimeHostServiceManagerError(
'retirement_failed',
'The State Root acquired a writer before retirement could stop the Runtime Host service',
{ cause: error },
);
}
}
async function acquirePreparedRuntimeHostRootRetirementFence(
root: StorageRootCapability<'interactive'>,
expectedPid: number,
backend: RuntimeHostServiceBackend,
): Promise<InteractiveRootOwner> {
const deadline = Date.now() + SERVICE_READY_TIMEOUT_MS;
while (Date.now() < deadline) {
const owner = await tryAcquireInteractiveRootOwner(root);
const status = await backend.status();
if (
status.pid !== expectedPid &&
!(status.pid === null && !status.active && status.state === 'stopped')
) {
await owner?.close().catch(() => undefined);
throw new RuntimeHostServiceManagerError(
'retirement_failed',
'Runtime Host service identity changed before retirement could stop the prepared Host',
);
}
if (owner) return owner;
await new Promise<void>((resolveWait) => setTimeout(resolveWait, SERVICE_READY_POLL_MS));
}
throw new RuntimeHostServiceManagerError(
'retirement_failed',
'The prepared Runtime Host did not release the State Root writer before retirement timed out',
);
}
async function retireManagedRuntimeHostService(
service: RuntimeHostManagedServiceStatus & { readonly config: RuntimeHostManagedServiceConfig },
root: StorageRootCapability<'interactive'>,
backend: RuntimeHostServiceBackend,
deps: RuntimeHostServiceManagerDeps,
allowInterruptActiveTasks: boolean,
): Promise<{
readonly service: RuntimeHostManagedServiceStatus;
readonly retirement: RuntimeHostRetirementResult;
}> {
let prepared: { readonly hostEpoch: string; readonly pid: number } | undefined;
let rootFence: InteractiveRootOwner | undefined;
if (service.pid !== null) {
const retirement = await deps.prepareRetirement(
service.config,
service.pid,
allowInterruptActiveTasks,
);
if (retirement.kind === 'active_tasks') return { service, retirement };
prepared = retirement;
rootFence = await acquirePreparedRuntimeHostRootRetirementFence(root, prepared.pid, backend);
} else if (service.active) {
throw new RuntimeHostServiceManagerError(
'retirement_failed',
'Managed Runtime Host service did not report its process identity',
);
} else if (service.state === 'starting') {
rootFence = await acquireRuntimeHostRootRetirementFence(root);
}
try {
await backend.retire();
const stopped: RuntimeHostManagedServiceStatus = {
...(await backend.status()),
config: service.config,
installedVersion: service.installedVersion,
};
if (stopped.active || stopped.state !== 'stopped' || stopped.pid !== null) {
throw new RuntimeHostServiceManagerError(
'retirement_failed',
'Runtime Host service did not reach a stable stopped state after retirement',
);
}
if (rootFence) {
await releaseRuntimeHostRootRetirementFence(rootFence);
rootFence = undefined;
} else {
await verifyRuntimeHostRootReleased(root);
}
return {
service: stopped,
retirement: prepared
? { kind: 'retired', hostEpoch: prepared.hostEpoch, pid: prepared.pid }
: { kind: 'stopped' },
};
} finally {
await rootFence?.close().catch(() => undefined);
}
}
async function releaseRuntimeHostRootRetirementFence(owner: InteractiveRootOwner): Promise<void> {
try {
await owner.close();
} catch (error) {
throw new RuntimeHostServiceManagerError(
'retirement_failed',
'Runtime Host retirement did not release the State Root writer fence',
{ cause: error },
);
}
}
async function deployRuntimeHostServiceConfiguration(input: {
readonly backend: RuntimeHostServiceBackend;
readonly deps: RuntimeHostServiceManagerDeps;
readonly configPath: string;
readonly previous: RuntimeHostManagedServiceConfig | null;
readonly desired: RuntimeHostManagedServiceConfig;
readonly activate: boolean;
}): Promise<void> {
const deployment = await input.backend.stageDeployment();
try {
await writeRuntimeHostServiceFile(
input.configPath,
`${JSON.stringify(input.desired, null, 2)}\n`,
0o600,
);
await deployment.apply(input.desired, input.activate);
if (input.activate) await input.deps.waitForReady(input.desired, input.backend);
} catch (error) {
const revision = await identifyRuntimeHostServiceConfigRevision(
input.configPath,
input.previous,
input.desired,
);
if (revision === 'current') throw error;
if (revision === 'unknown') {
try {
await quiesceRuntimeHostServiceForConfigRollback(input.backend);
} catch (rollbackError) {
throw new RuntimeHostServiceManagerError(
'service_manager_operation_failed',
'Runtime Host service deployment failed with an uncertain configuration and its process could not be stopped',
{ cause: new AggregateError([error, rollbackError]) },
);
}
throw new RuntimeHostServiceManagerError(
'service_manager_operation_failed',
'Runtime Host service deployment failed with an uncertain configuration; its process was stopped for inspection',
{ cause: error },
);
}
await rollbackDeployment(
deployment,
input.backend,
{ configPath: input.configPath, previous: input.previous },
error,
);
}
}
async function rollbackDeployment(
deployment: RuntimeHostServiceDeployment,
backend: RuntimeHostServiceBackend,
configRollback: {
readonly configPath: string;
readonly previous: RuntimeHostManagedServiceConfig | null;
},
originalError: unknown,
): Promise<void> {
const rollbackErrors: unknown[] = [];
try {
await quiesceRuntimeHostServiceForConfigRollback(backend);
} catch (rollbackError) {
throw new RuntimeHostServiceManagerError(
'service_manager_operation_failed',
'Runtime Host service installation failed and its candidate process could not be stopped; the candidate configuration was retained',
{ cause: new AggregateError([originalError, rollbackError]) },
);
}
try {
if (configRollback.previous) {
await writeRuntimeHostServiceFile(
configRollback.configPath,
`${JSON.stringify(configRollback.previous, null, 2)}\n`,
0o600,
);
} else {
await removeRuntimeHostServiceFile(configRollback.configPath, 'service config');
}
} catch (rollbackError) {
rollbackErrors.push(rollbackError);
}
if (rollbackErrors.length === 0) {
try {
await deployment.rollback();
} catch (rollbackError) {
rollbackErrors.push(rollbackError);
}
}
if (rollbackErrors.length > 0) {
throw new RuntimeHostServiceManagerError(
'service_manager_operation_failed',
'Runtime Host service deployment failed and the previous deployment could not be restored',
{ cause: new AggregateError([originalError, ...rollbackErrors]) },
);
}
throw originalError;
}
async function quiesceRuntimeHostServiceForConfigRollback(
backend: RuntimeHostServiceBackend,
): Promise<void> {
let status = await backend.status();
if (status.active || status.pid !== null || status.state === 'starting') {
await backend.retire();
status = await backend.status();
}
if (
status.active ||
status.pid !== null ||
status.state === 'running' ||
status.state === 'starting'
) {
throw new Error('The candidate Runtime Host service is still running');
}
}
async function rollbackRuntimeHostConfiguration(
input: {
readonly configPath: string;
readonly currentConfig: RuntimeHostManagedServiceConfig;
readonly desiredConfig: RuntimeHostManagedServiceConfig;
readonly root: StorageRootCapability<'interactive'>;
readonly deployment: RuntimeHostServiceDeployment;
},
backend: RuntimeHostServiceBackend,
deps: RuntimeHostServiceManagerDeps,
allowInterruptActiveTasks: boolean,
originalError: unknown,
): Promise<never> {
const revision = await identifyRuntimeHostServiceConfigRevision(
input.configPath,
input.currentConfig,
input.desiredConfig,
);
if (revision === 'current') {
try {
await input.deployment.rollback();
await backend.start();
await deps.waitForReady(input.currentConfig, backend);
} catch (rollbackError) {
throw new RuntimeHostServiceManagerError(
'configuration_incomplete',
'The Runtime Host configuration was not published and the previous Host could not be restarted',
{ cause: new AggregateError([originalError, rollbackError]) },
);
}
throw new RuntimeHostServiceManagerError(
'configuration_incomplete',
'The Runtime Host configuration could not be published; the previous configuration was restored',
{ cause: originalError },
);
}
if (revision === 'unknown') {
try {
await quiesceRuntimeHostServiceForConfigRollback(backend);
} catch (rollbackError) {
throw new RuntimeHostServiceManagerError(
'configuration_incomplete',
'The Runtime Host configuration could not be applied, its persisted revision is uncertain, and its process could not be stopped',
{ cause: new AggregateError([originalError, rollbackError]) },
);
}
throw new RuntimeHostServiceManagerError(
'configuration_incomplete',
'The Runtime Host configuration could not be applied and its persisted revision is uncertain; the Host remains stopped for inspection',
{ cause: originalError },
);
}
try {
const candidate = await readServiceStatus(input.configPath, backend);
const retired = await retireManagedRuntimeHostService(
{ ...candidate, config: input.desiredConfig },
input.root,
backend,
deps,
allowInterruptActiveTasks,
);
if (retired.retirement.kind === 'active_tasks') {
throw new Error('The candidate Runtime Host accepted active work before rollback');
}
} catch (rollbackError) {
throw new RuntimeHostServiceManagerError(
'configuration_incomplete',
'The Runtime Host configuration could not be verified and the candidate Host could not be safely retired; its configuration was retained',
{ cause: new AggregateError([originalError, rollbackError]) },
);
}
const rollbackErrors: unknown[] = [];
try {
await writeRuntimeHostServiceFile(
input.configPath,
`${JSON.stringify(input.currentConfig, null, 2)}\n`,
0o600,
);
} catch (rollbackError) {
rollbackErrors.push(rollbackError);
}
if (rollbackErrors.length === 0) {
try {
await input.deployment.rollback();
await backend.start();
await deps.waitForReady(input.currentConfig, backend);
} catch (rollbackError) {
rollbackErrors.push(rollbackError);
}
}
throw new RuntimeHostServiceManagerError(
'configuration_incomplete',
rollbackErrors.length === 0
? 'The Runtime Host configuration could not be applied; the previous configuration was restored'
: 'The Runtime Host configuration could not be applied or fully restored; inspect the service before retrying',
{
cause:
rollbackErrors.length === 0
? originalError
: new AggregateError([originalError, ...rollbackErrors]),
},
);
}
async function identifyRuntimeHostServiceConfigRevision(
configPath: string,
current: RuntimeHostManagedServiceConfig | null,
desired: RuntimeHostManagedServiceConfig,
): Promise<'current' | 'desired' | 'unknown'> {
let observed: RuntimeHostManagedServiceConfig | null;
try {
observed = await readServiceConfig(configPath);
} catch {
return 'unknown';
}
if (sameRuntimeHostManagedServiceConfig(observed, current)) return 'current';
if (sameRuntimeHostManagedServiceConfig(observed, desired)) return 'desired';
return 'unknown';
}
function sameRuntimeHostManagedServiceConfig(
left: RuntimeHostManagedServiceConfig | null,
right: RuntimeHostManagedServiceConfig | null,
): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
async function allocateLoopbackPort(): Promise<number> {
return new Promise((resolvePort, reject) => {
const server = createServer();
server.unref();
server.once('error', reject);
server.listen({ host: '127.0.0.1', port: 0, exclusive: true }, () => {
const address = server.address();
if (!address || typeof address === 'string') {
server.close();
reject(new Error('Unable to allocate a loopback port'));
return;
}
server.close((error) => (error ? reject(error) : resolvePort(address.port)));
});
});
}
export function allocateRuntimeHostLoopbackPort(): Promise<number> {
return allocateLoopbackPort();
}
async function allocatePeerPort(): Promise<number> {
return new Promise((resolvePort, reject) => {
const socket = createSocket('udp4');
socket.unref();
socket.once('error', reject);
socket.bind({ address: '0.0.0.0', port: 0, exclusive: true }, () => {
const address = socket.address();
socket.close();
resolvePort(address.port);
});
});
}
export function allocateRuntimeHostPeerPort(): Promise<number> {
return allocatePeerPort();
}
function result(
action: Exclude<RuntimeHostManagedServiceAction, 'retire' | 'configure' | 'uninstall'>,
service: RuntimeHostManagedServiceStatus,
retainedStateRoot?: string,
logs?: string,
): RuntimeHostManagedServiceResult {
return {
schemaVersion: 1,
action,
service,
...(retainedStateRoot ? { retainedStateRoot } : {}),
...(logs !== undefined ? { logs } : {}),
};
}
function configurationResult(
kind: 'unchanged' | 'configured' | 'active_tasks',
service: RuntimeHostManagedServiceStatus,
): RuntimeHostManagedServiceResult {
return { schemaVersion: 1, action: 'configure', service, configuration: { kind } };
}
export function effectiveRuntimeHostProjectDirectoryRoots(
config: RuntimeHostManagedServiceConfig,
homeDir = homedir(),
): readonly { readonly label: string; readonly path: string }[] {
if (config.schemaVersion !== 1 || config.projectDirectoryRoots.length > 0) {
return config.projectDirectoryRoots;
}
try {
return [{ label: '~', path: realpathSync(homeDir) }];
} catch {
return [];
}
}
export function runtimeHostManagedServiceConfigFingerprint(
config: RuntimeHostManagedServiceConfig,
): string {
const canonical = JSON.stringify({
managedDeploymentRoot: config.managedDeploymentRoot ?? null,
rootPath: config.rootPath,
projectDirectoryRoots: effectiveRuntimeHostProjectDirectoryRoots(config),
websocket: config.websocket,
launch: config.launch,
});
return `sha256:${createHash('sha256').update(canonical).digest('hex')}`;
}
function sameProjectDirectoryRoots(
current: RuntimeHostManagedServiceConfig,
desired: RuntimeHostManagedServiceConfig,
): boolean {
return (
JSON.stringify(effectiveRuntimeHostProjectDirectoryRoots(current)) ===
JSON.stringify(desired.projectDirectoryRoots)
);
}
function isSafeAbsolutePath(value: unknown): value is string {
return typeof value === 'string' && isAbsolute(value) && !hasControlCharacters(value);
}
function hasControlCharacters(value: string): boolean {
return /[\u0000-\u001f\u007f]/u.test(value);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isNodeError(error: unknown, code: string): error is NodeJS.ErrnoException {
return error instanceof Error && 'code' in error && error.code === code;
}
async function isExistingDirectory(path: string): Promise<boolean> {
try {
return (await stat(path)).isDirectory();
} catch (error) {
if (isNodeError(error, 'ENOENT')) return false;
throw error;
}
}