blob: d957fda36f9380893dbf4844b5ed1daae68b5ce1 [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 { createRequire } from 'node:module';
import { dirname, join } from 'node:path';
import {
assertRuntimeHostProviderDefinition,
type RuntimeHostLifecycleProvider,
type RuntimeHostProviderDefinition,
type RuntimeHostSupervisorStatus,
} from './runtime-host-lifecycle-provider.js';
import { resolveRuntimeHostNativePath } from './runtime-host-peer-artifact.js';
import { RuntimeHostServiceManagerError } from './runtime-host-service-manager.js';
import { resolveRuntimeHostWindowsTaskLauncherPath } from './runtime-host-windows-task-launcher-artifact.js';
const require = createRequire(import.meta.url);
const ROOT_ID_PATTERN = /^[a-f0-9]{64}$/u;
type WindowsTaskTarget = 'host' | 'reconciliation';
interface WindowsTaskStatus {
readonly installed: boolean;
readonly enabled: boolean;
readonly state: RuntimeHostSupervisorStatus['state'];
readonly pid: number | null;
readonly lastExitCode: number | null;
}
interface WindowsTaskNativeStatus {
readonly installed: boolean;
readonly enabled: boolean;
readonly state: RuntimeHostSupervisorStatus['state'];
readonly pid?: number;
readonly lastExitCode?: number;
}
interface WindowsLifecycleNative {
readonly windowsTaskProbe: () => void;
readonly windowsTaskConverge: (
rootId: string,
target: WindowsTaskTarget,
runnerPath: string,
command: string[],
) => void;
readonly windowsTaskVerify: (
rootId: string,
target: WindowsTaskTarget,
runnerPath: string,
command: string[],
) => void;
readonly windowsTaskConvergeLauncher?: (
rootId: string,
target: WindowsTaskTarget,
launcherPath: string,
command: string[],
) => void;
readonly windowsTaskVerifyLauncher?: (
rootId: string,
target: WindowsTaskTarget,
launcherPath: string,
command: string[],
) => void;
readonly windowsTaskStatus: (rootId: string, target: WindowsTaskTarget) => unknown;
readonly windowsTaskActivate: (rootId: string) => void;
readonly windowsTaskRetire: (rootId: string) => void;
readonly windowsTaskUninstall: (rootId: string, target: WindowsTaskTarget) => void;
readonly ownCurrentProcessTree: () => void;
}
export interface WindowsRuntimeHostLifecycleProviderOptions {
readonly cliPath: string;
}
export function createWindowsRuntimeHostLifecycleProvider(
rootId: string,
options: WindowsRuntimeHostLifecycleProviderOptions,
): RuntimeHostLifecycleProvider {
if (!ROOT_ID_PATTERN.test(rootId)) {
throw new RuntimeHostServiceManagerError(
'invalid_config',
'The Runtime Host Root ID is invalid',
);
}
const native = createWindowsLifecycleNativeLoader(options.cliPath);
const runnerPath = join(dirname(options.cliPath), 'runtime-host-windows-task-runner.js');
let launcherPath: Promise<string> | undefined;
const resolveLauncherPath = (): Promise<string> => {
launcherPath ??= resolveRuntimeHostWindowsTaskLauncherPath(options.cliPath);
return launcherPath;
};
const converge = async (
target: WindowsTaskTarget,
definition: RuntimeHostProviderDefinition,
): Promise<void> => {
assertRuntimeHostProviderDefinition(definition);
const control = await native();
if (supportsLauncherProjection(control)) {
control.windowsTaskConvergeLauncher(rootId, target, await resolveLauncherPath(), [
...definition.command,
]);
} else {
control.windowsTaskConverge(rootId, target, runnerPath, [...definition.command]);
}
};
const verify = async (
target: WindowsTaskTarget,
definition: RuntimeHostProviderDefinition,
): Promise<void> => {
assertRuntimeHostProviderDefinition(definition);
const control = await native();
if (supportsLauncherProjection(control)) {
control.windowsTaskVerifyLauncher(rootId, target, await resolveLauncherPath(), [
...definition.command,
]);
} else {
control.windowsTaskVerify(rootId, target, runnerPath, [...definition.command]);
}
};
const status = async (target: WindowsTaskTarget): Promise<WindowsTaskStatus> =>
decodeStatus((await native()).windowsTaskStatus(rootId, target));
return {
supervisor: {
provider: 'windows_task',
preflight: async () => {
const control = await native();
if (supportsLauncherProjection(control)) await resolveLauncherPath();
control.windowsTaskProbe();
},
converge: (definition) => converge('host', definition),
verify: (definition) => verify('host', definition),
status: async () => {
const observed = await status('host');
return {
provider: 'windows_task',
...observed,
active: observed.state === 'running' && observed.pid !== null,
};
},
activate: async () => (await native()).windowsTaskActivate(rootId),
retire: async () => (await native()).windowsTaskRetire(rootId),
logs: async () => formatTaskStatus('host', await status('host')),
uninstall: async () => (await native()).windowsTaskUninstall(rootId, 'host'),
},
reconciliationTrigger: {
provider: 'windows_task_timer',
converge: (definition) => converge('reconciliation', definition),
verify: (definition) => verify('reconciliation', definition),
status: async () => {
const observed = await status('reconciliation');
return { installed: observed.installed, active: observed.installed && observed.enabled };
},
activate: async () => {
const observed = await status('reconciliation');
if (!observed.installed || !observed.enabled) {
throw new RuntimeHostServiceManagerError(
'target_mismatch',
'The Windows reconciliation task is not enabled',
);
}
},
logs: async () => formatTaskStatus('reconciliation', await status('reconciliation')),
uninstall: async () => (await native()).windowsTaskUninstall(rootId, 'reconciliation'),
},
};
}
export async function ownWindowsRuntimeHostProcessTree(cliPath: string): Promise<void> {
const nativePath = await resolveRuntimeHostNativePath(cliPath);
loadWindowsLifecycleNative(nativePath).ownCurrentProcessTree();
}
function createWindowsLifecycleNativeLoader(
cliPath: string,
): () => Promise<WindowsLifecycleNative> {
let native: Promise<WindowsLifecycleNative> | undefined;
return () => {
native ??= resolveRuntimeHostNativePath(cliPath).then(loadWindowsLifecycleNative);
return native;
};
}
function loadWindowsLifecycleNative(path: string): WindowsLifecycleNative {
const loaded = require(path) as Partial<WindowsLifecycleNative>;
const methods = [
'windowsTaskProbe',
'windowsTaskConverge',
'windowsTaskVerify',
'windowsTaskStatus',
'windowsTaskActivate',
'windowsTaskRetire',
'windowsTaskUninstall',
'ownCurrentProcessTree',
] as const satisfies readonly (keyof WindowsLifecycleNative)[];
if (methods.some((method) => typeof loaded[method] !== 'function')) {
throw unavailable(
'The Runtime Host native artifact does not support Windows lifecycle control',
);
}
if (
(typeof loaded.windowsTaskConvergeLauncher === 'function') !==
(typeof loaded.windowsTaskVerifyLauncher === 'function')
) {
throw unavailable(
'The Runtime Host native artifact has an incomplete Windows launcher contract',
);
}
return loaded as WindowsLifecycleNative;
}
function supportsLauncherProjection(
native: WindowsLifecycleNative,
): native is WindowsLifecycleNative &
Required<
Pick<WindowsLifecycleNative, 'windowsTaskConvergeLauncher' | 'windowsTaskVerifyLauncher'>
> {
return typeof native.windowsTaskConvergeLauncher === 'function';
}
function decodeStatus(value: unknown): WindowsTaskStatus {
if (
!isRecord(value) ||
typeof value.installed !== 'boolean' ||
typeof value.enabled !== 'boolean' ||
!['not_installed', 'stopped', 'starting', 'running', 'failed'].includes(String(value.state)) ||
!(value.pid === undefined || (Number.isSafeInteger(value.pid) && Number(value.pid) > 0)) ||
!(
value.lastExitCode === undefined ||
(Number.isSafeInteger(value.lastExitCode) && Number(value.lastExitCode) >= 0)
)
) {
throw unavailable('The Windows lifecycle status response is invalid');
}
const status = value as unknown as WindowsTaskNativeStatus;
return {
...status,
pid: status.pid ?? null,
lastExitCode: status.lastExitCode ?? null,
};
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function formatTaskStatus(target: WindowsTaskTarget, status: WindowsTaskStatus): string {
return `Windows Task Scheduler ${target}: ${JSON.stringify(status)}\n`;
}
function unavailable(message: string, cause?: unknown): RuntimeHostServiceManagerError {
return new RuntimeHostServiceManagerError('service_manager_unavailable', message, { cause });
}