blob: b529ef0da08cc3052f908595d9c32f4cf9a1f340 [file]
import { execFileSync } from 'node:child_process';
import { statSync } from 'node:fs';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
const RUNTIME_HOST_DIAGNOSTIC_MAX_BYTES = 32 * 1024;
const WINDOWS_SECURITY_TEXT_MAX_CHARS = 4 * 1024;
const DIAGNOSTIC_TEXT_MAX_CHARS = 2 * 1024;
const WINDOWS_PATH_SECURITY_TIMEOUT_MS = 30_000;
const WINDOWS_DIAGNOSTIC_PATHS_ENV = 'MAKA_RUNTIME_HOST_DIAGNOSTIC_PATHS';
const WINDOWS_PATH_SECURITY_SCRIPT = `
$ErrorActionPreference = 'Stop'
$paths = ConvertFrom-Json -InputObject $env:${WINDOWS_DIAGNOSTIC_PATHS_ENV}
$results = @()
foreach ($path in @($paths)) {
try {
$acl = Get-Acl -LiteralPath $path
$results += [ordered]@{
state = 'present'
owner = $acl.Owner
sddl = $acl.Sddl
}
} catch {
$exception = $_.Exception
$nativeErrorCode = $null
if ($null -ne $exception.PSObject.Properties['NativeErrorCode']) {
$nativeErrorCode = $exception.NativeErrorCode
}
$results += [ordered]@{
state = 'unavailable'
error = [ordered]@{
name = $exception.GetType().FullName
hresult = $exception.HResult
nativeErrorCode = $nativeErrorCode
message = $exception.Message
}
}
}
}
$response = [ordered]@{ results = @($results) }
[Console]::Out.Write((ConvertTo-Json -InputObject $response -Compress -Depth 5))
`;
const STORAGE_AUTHORITY_MODULE = 'node_modules/@maka/storage/dist/root-authority.js';
const REGISTRATION_MODULE = 'node_modules/@maka/runtime-host/dist/control/registration.js';
const STARTUP_DIAGNOSTIC_MODULE =
'node_modules/@maka/runtime-host/dist/control/startup-diagnostic.js';
export async function collectRuntimeHostFailureDiagnostic(packageRoot, rootPath, options = {}) {
const platform = options.platform ?? process.platform;
const environment = options.environment ?? process.env;
const loadInstalled = options.loadInstalled ?? importInstalled;
const readPathSecurityBatch =
options.readPathSecurityBatch ??
(platform === 'win32'
? (paths) => readWindowsPathSecurityBatch(paths, environment)
: undefined);
const diagnostic = {
schemaVersion: 1,
platform,
architecture: options.architecture ?? process.arch,
runner: compactObject({
imageOS: boundedOptionalString(environment.ImageOS),
imageVersion: boundedOptionalString(environment.ImageVersion),
name: boundedOptionalString(environment.RUNNER_NAME),
environment: boundedOptionalString(environment.RUNNER_ENVIRONMENT),
}),
paths: [],
};
addPathEvidence(diagnostic.paths, 'root', rootPath);
const finish = async () => {
if (readPathSecurityBatch) {
await addPathSecurityEvidence(diagnostic.paths, readPathSecurityBatch);
}
return diagnostic;
};
let authority;
try {
authority = await loadInstalled(packageRoot, STORAGE_AUTHORITY_MODULE);
} catch (error) {
diagnostic.storageAuthority = { state: 'unavailable', error: summarizeDiagnosticError(error) };
return finish();
}
addPathEvidence(
diagnostic.paths,
'root_marker',
join(rootPath, authority.STORAGE_ROOT_MARKER_FILE),
);
let capability;
try {
capability = await authority.discoverMarkedStorageRoot({ path: rootPath });
diagnostic.storageAuthority = { state: 'valid', rootId: capability.rootId };
} catch (error) {
diagnostic.storageAuthority = { state: 'invalid', error: summarizeDiagnosticError(error) };
}
let controlRoot;
try {
controlRoot = authority.resolveRootControlNamespace();
addPathEvidence(diagnostic.paths, 'control_root', controlRoot);
} catch (error) {
diagnostic.controlNamespace = {
state: 'unavailable',
error: summarizeDiagnosticError(error),
};
}
if (!capability || !controlRoot) return finish();
const controlDirectory = join(controlRoot, capability.rootId);
addPathEvidence(diagnostic.paths, 'control_directory', controlDirectory);
try {
const registrationAuthority = await loadInstalled(packageRoot, REGISTRATION_MODULE);
const registrationPath = join(
controlDirectory,
registrationAuthority.RUNTIME_HOST_REGISTRATION_FILE,
);
addPathEvidence(diagnostic.paths, 'registration', registrationPath);
const registration = await registrationAuthority.readHostRegistration(controlDirectory);
diagnostic.registration = registration
? summarizeRegistration(registration, capability.rootId)
: { state: 'absent' };
} catch (error) {
diagnostic.registration = { state: 'unavailable', error: summarizeDiagnosticError(error) };
}
try {
const startupAuthority = await loadInstalled(packageRoot, STARTUP_DIAGNOSTIC_MODULE);
const startupPath = join(
controlDirectory,
startupAuthority.RUNTIME_HOST_STARTUP_DIAGNOSTIC_FILE,
);
addPathEvidence(diagnostic.paths, 'startup_diagnostic', startupPath);
const startup = await startupAuthority.readCandidateStartupDiagnostic(capability.rootId);
diagnostic.startup = startup ? { state: 'present', diagnostic: startup } : { state: 'absent' };
} catch (error) {
diagnostic.startup = { state: 'unavailable', error: summarizeDiagnosticError(error) };
}
return finish();
}
export function renderRuntimeHostFailureDiagnostic(diagnostic) {
return truncateUtf8Text(
JSON.stringify(diagnostic),
RUNTIME_HOST_DIAGNOSTIC_MAX_BYTES,
'<diagnostic truncated>',
);
}
export async function retireCollectedRuntimeHostStartupDiagnostic(
packageRoot,
diagnostic,
options = {},
) {
const startupAttemptId = diagnostic.startup?.diagnostic?.startupAttemptId;
const rootId = diagnostic.storageAuthority?.rootId;
if (typeof rootId !== 'string' || typeof startupAttemptId !== 'string') return false;
const loadInstalled = options.loadInstalled ?? importInstalled;
const startupAuthority = await loadInstalled(packageRoot, STARTUP_DIAGNOSTIC_MODULE);
return startupAuthority.clearSelectedCandidateStartupDiagnostic(rootId, startupAttemptId);
}
function addPathEvidence(paths, role, path) {
paths.push(inspectDiagnosticPath(role, path));
}
async function addPathSecurityEvidence(paths, readPathSecurityBatch) {
const evidence = paths.filter(
(entry) =>
entry.state === 'present' &&
(entry.role === 'root' ||
entry.role === 'control_root' ||
entry.role === 'control_directory'),
);
if (evidence.length === 0) return;
try {
const results = await readPathSecurityBatch(evidence.map(({ path }) => path));
if (!Array.isArray(results) || results.length !== evidence.length) {
throw new Error('Windows path security probe returned an invalid result count');
}
for (let index = 0; index < evidence.length; index += 1) {
evidence[index].security = results[index];
}
} catch (error) {
const unavailable = { state: 'unavailable', error: summarizeDiagnosticError(error) };
for (const entry of evidence) entry.security = unavailable;
}
}
function inspectDiagnosticPath(role, path) {
try {
const pathStat = statSync(path);
return {
role,
path,
state: 'present',
kind: pathStat.isDirectory() ? 'directory' : pathStat.isFile() ? 'file' : 'other',
size: pathStat.size,
mode: pathStat.mode,
};
} catch (error) {
return { role, path, state: 'unavailable', error: summarizeDiagnosticError(error) };
}
}
function readWindowsPathSecurityBatch(paths, environment) {
const systemRoot = environment.SystemRoot;
if (!systemRoot) throw new Error('SystemRoot is unavailable');
const output = execFileSync(
join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', WINDOWS_PATH_SECURITY_SCRIPT],
{
encoding: 'utf8',
env: { ...environment, [WINDOWS_DIAGNOSTIC_PATHS_ENV]: JSON.stringify(paths) },
timeout: WINDOWS_PATH_SECURITY_TIMEOUT_MS,
windowsHide: true,
},
);
const parsed = JSON.parse(output);
if (
typeof parsed !== 'object' ||
parsed === null ||
!Array.isArray(parsed.results) ||
parsed.results.length !== paths.length
) {
throw new Error('Windows path security probe returned an invalid response');
}
return parsed.results.map(decodeWindowsPathSecurity);
}
function decodeWindowsPathSecurity(value) {
if (
typeof value === 'object' &&
value !== null &&
value.state === 'present' &&
typeof value.owner === 'string' &&
typeof value.sddl === 'string'
) {
return {
state: 'present',
owner: value.owner.slice(0, WINDOWS_SECURITY_TEXT_MAX_CHARS),
sddl: value.sddl.slice(0, WINDOWS_SECURITY_TEXT_MAX_CHARS),
};
}
if (
typeof value === 'object' &&
value !== null &&
value.state === 'unavailable' &&
typeof value.error === 'object' &&
value.error !== null &&
typeof value.error.message === 'string'
) {
return {
state: 'unavailable',
error: compactObject({
name:
typeof value.error.name === 'string'
? boundedDiagnosticString(value.error.name)
: undefined,
hresult: Number.isSafeInteger(value.error.hresult) ? value.error.hresult : undefined,
nativeErrorCode: Number.isSafeInteger(value.error.nativeErrorCode)
? value.error.nativeErrorCode
: undefined,
message: boundedDiagnosticString(value.error.message),
}),
};
}
throw new Error('Windows path security probe returned an invalid path result');
}
function summarizeRegistration(registration, expectedRootId) {
return compactObject({
state: 'present',
schemaVersion: registration.schemaVersion,
rootIdMatches: registration.rootId === expectedRootId,
hostEpoch: registration.hostEpoch,
lifecycleState: registration.state,
lifecycleMode: registration.lifecycleMode,
pid: registration.pid,
endpointKind:
typeof registration.endpoint === 'string' && registration.endpoint.startsWith('\\\\.\\pipe\\')
? 'windows_named_pipe'
: 'other',
});
}
function summarizeDiagnosticError(error, depth = 0) {
if (!(error instanceof Error)) return { message: boundedDiagnosticString(String(error)) };
return compactObject({
name: boundedDiagnosticString(error.name),
code: typeof error.code === 'string' || typeof error.code === 'number' ? error.code : undefined,
message: boundedDiagnosticString(error.message),
cause:
depth < 3 && error.cause !== undefined
? summarizeDiagnosticError(error.cause, depth + 1)
: undefined,
});
}
function compactObject(value) {
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
}
function boundedOptionalString(value) {
return typeof value === 'string' ? boundedDiagnosticString(value) : undefined;
}
function boundedDiagnosticString(value) {
return value.slice(0, DIAGNOSTIC_TEXT_MAX_CHARS);
}
function truncateUtf8Text(value, maximumBytes, marker) {
const encoded = Buffer.from(value, 'utf8');
if (encoded.byteLength <= maximumBytes) return value;
const markerBytes = Buffer.byteLength(marker, 'utf8');
const prefix = encoded
.subarray(0, Math.max(0, maximumBytes - markerBytes))
.toString('utf8')
.replace(/\uFFFD$/u, '');
return `${prefix}${marker}`;
}
function importInstalled(packageRoot, relativePath) {
return import(pathToFileURL(join(packageRoot, relativePath)).href);
}