/* * 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 { 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, '', ); } 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); }