| /* |
| * 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 { generalizedErrorMessage } from '@maka/core/redaction'; |
| import { fileURLToPath } from 'node:url'; |
| import { |
| candidateStartupFailureExitCode, |
| classifyCandidateStartupFailure, |
| } from './candidate-startup-failure.js'; |
| import { createRuntimeHostLaunchOwnerGuard } from './candidate-launch-owner-guard.js'; |
| import { parseInteractiveRuntimeHostCandidateArguments } from './candidate-cli.js'; |
| import { writeCandidateStartupDiagnostic } from './control/startup-diagnostic.js'; |
| import { installRuntimeHostLogCapture, runtimeHostLogBuffer } from './process-diagnostics.js'; |
| import { |
| type ExecutionRuntimeHostCandidateDependencies, |
| type ExecutionRuntimeHostCandidateOptions, |
| startExecutionRuntimeHostCandidate, |
| } from './server/execution-candidate.js'; |
| import type { RuntimeHostKernel } from './server/host-kernel.js'; |
| import { runRuntimeHostProcessLifecycle } from './server/process-lifecycle.js'; |
| |
| export interface ExecutionCandidateEntryHooks { |
| /** Applied to the parsed command line before the candidate starts. */ |
| readonly overrideOptions?: ( |
| options: ExecutionRuntimeHostCandidateOptions, |
| ) => ExecutionRuntimeHostCandidateOptions; |
| readonly dependencies?: ExecutionRuntimeHostCandidateDependencies; |
| /** Runs once the candidate has won; the returned callback stops the watch. */ |
| readonly onWon?: (host: RuntimeHostKernel) => () => void; |
| } |
| |
| /** |
| * The bootstrap both candidate entry modules share. The entry file is the only |
| * thing that differs between the production and Desktop E2E processes, so the |
| * argument parsing, startup diagnostics, and lifecycle live here instead of |
| * being copied — a copy is what silently drifts when either one gains a step. |
| */ |
| export async function runExecutionCandidateEntry( |
| argv: readonly string[], |
| entrypointUrl: string, |
| hooks: ExecutionCandidateEntryHooks = {}, |
| ): Promise<never | void> { |
| installRuntimeHostLogCapture(); |
| const launchOwnerGuard = createRuntimeHostLaunchOwnerGuard(); |
| |
| let result: Awaited<ReturnType<typeof startExecutionRuntimeHostCandidate>>; |
| let rootId: string | undefined; |
| let startupAttemptId: string | undefined; |
| try { |
| const parsed = parseInteractiveRuntimeHostCandidateArguments(argv); |
| rootId = parsed.expectedRootId; |
| const { startupAttemptId: parsedStartupAttemptId, ...options } = parsed; |
| startupAttemptId = parsedStartupAttemptId; |
| result = await startExecutionRuntimeHostCandidate( |
| { |
| ...(hooks.overrideOptions ? hooks.overrideOptions(options) : options), |
| ...(launchOwnerGuard?.admission |
| ? { initialClientAdmission: launchOwnerGuard.admission } |
| : {}), |
| }, |
| { |
| ...hooks.dependencies, |
| processLaunch: { |
| executablePath: process.execPath, |
| entrypointPath: fileURLToPath(entrypointUrl), |
| }, |
| }, |
| ); |
| } catch (error) { |
| const failure = classifyCandidateStartupFailure(error); |
| const logs = runtimeHostLogBuffer.snapshot(); |
| console.error('[runtime-host] startup failed:', error); |
| if (rootId && startupAttemptId) { |
| await writeCandidateStartupDiagnostic({ |
| rootId, |
| startupAttemptId, |
| failure, |
| error, |
| logs, |
| }).catch(() => undefined); |
| } |
| process.exit(candidateStartupFailureExitCode(failure)); |
| } |
| if (result.kind === 'loser') { |
| await launchOwnerGuard?.dispose(); |
| process.exit(2); |
| } |
| |
| // A launcher that disappears without releasing this Host (Desktop quit, |
| // launcher crash) is an intentional retirement of an owned ephemeral Host, |
| // not a crash — closing with the retirement reason keeps the exit truthful. |
| launchOwnerGuard?.bind(() => result.host.close({ reason: 'retirement' })); |
| const stopWatch = hooks.onWon?.(result.host); |
| try { |
| await runRuntimeHostProcessLifecycle(result.host); |
| } catch (error) { |
| // Log the redacted, generalized message only: a full error object can |
| // carry paths and spawn arguments in its message or stack. |
| console.error( |
| `[runtime-host] lifecycle failed: ${generalizedErrorMessage(error, 'Runtime Host lifecycle failed')}`, |
| ); |
| process.exitCode = 1; |
| } finally { |
| stopWatch?.(); |
| await launchOwnerGuard?.dispose(); |
| } |
| } |