| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151 |
- /** Parent-side invocation and bootstrap state for the private native runner. */
- import type { StdioOptions } from 'node:child_process'
- import { accessSync, constants as fsConstants } from 'node:fs'
- import { extname, isAbsolute } from 'node:path'
- import { inspect } from 'node:util'
- import { fileURLToPath } from 'node:url'
- import type { SubprocessSpawnSpec, SubprocessTerminalSpawnSpec } from '@deepseek-ai/dsh-subprocess'
- import { childEnv } from './spawn.ts'
- /** The one private environment variable consumed before target state is restored. */
- export const SUBPROCESS_RUNNER_ENV = 'DSH_SUBPROCESS_RUNNER' as const
- /** Sentinel used by the packaged bootstrap for the Windows IPC runner. */
- export const WINDOWS_RUNNER_SELECTION = 'windows' as const
- /** Non-empty command tuple used to launch the private runner entry. */
- export type RunnerInvocation = [string, ...string[]]
- /**
- * Resolve the source, built, or packaged entry that calls the same runner core.
- * @returns executable and arguments for the active runtime form.
- */
- export function spawnRunnerInvocation(): RunnerInvocation {
- if ('pkg' in process) return [process.execPath]
- /* v8 ignore next -- built-artifact smoke imports the emitted JavaScript runner entry;
- * source-unit coverage cannot change import.meta.url. */
- if (extname(fileURLToPath(import.meta.url)) !== '.ts') {
- return [process.execPath, fileURLToPath(import.meta.resolve('@deepseek-ai/dsh-subprocess-local/runner'))]
- }
- return [
- process.execPath,
- '--import',
- 'tsx/esm',
- fileURLToPath(new URL('./bin.ts', import.meta.url)),
- ]
- }
- /**
- * Check the concrete runner executable and entry paths without executing a probe mode.
- * @param invocation - resolved executable and runner-entry arguments.
- * @returns whether every concrete executable or entry path is accessible.
- */
- export function runnerInvocationAvailable(invocation: RunnerInvocation = spawnRunnerInvocation()): boolean {
- try {
- if (isAbsolute(invocation[0])) accessSync(invocation[0], fsConstants.X_OK)
- const entry = invocation.at(-1)
- if (entry !== undefined && entry !== invocation[0] && isAbsolute(entry)) {
- accessSync(entry, fsConstants.R_OK)
- }
- return true
- } catch {
- return false
- }
- }
- /**
- * Build the bootstrap-safe environment; target overrides arrive through request/IPC.
- * @param selection - private runner selector or Linux launch-request locator.
- * @returns environment for the runner before target state is restored.
- */
- export function runnerEnvironment(selection: string): NodeJS.ProcessEnv {
- return childEnv({
- [SUBPROCESS_RUNNER_ENV]: selection,
- SYSTEMD_LOG_TARGET: 'null',
- })
- }
- /**
- * Read and delete the private selector before importing or restoring target state.
- * @param env - mutable environment containing the private selector.
- * @returns the consumed selector, or undefined when no runner was requested.
- */
- export function consumeRunnerSelection(env: NodeJS.ProcessEnv = process.env): string | undefined {
- const selection = env[SUBPROCESS_RUNNER_ENV]
- Reflect.deleteProperty(env, SUBPROCESS_RUNNER_ENV)
- return selection
- }
- /**
- * Require the private argv delimiter and at least one target argv entry.
- * @param argv - private runner arguments.
- * @returns copied target argv after the private delimiter.
- */
- export function parseRunnerTargetArgv(argv: readonly string[]): string[] {
- if (argv[0] !== '--' || argv.length < 2) {
- throw new Error('subprocess runner requires target argv after a private -- delimiter')
- }
- return [...argv.slice(1)]
- }
- /**
- * Build the stdio inherited unchanged by the target, optionally with Node IPC on fd 3.
- * @param spec - ordinary subprocess request whose stdio modes are preserved.
- * @param ipc - whether to append the private Node IPC descriptor.
- * @returns child-process stdio options for the runner.
- */
- export function runnerStdio(
- spec: SubprocessSpawnSpec,
- ipc: boolean,
- ): StdioOptions {
- const stdio: StdioOptions = [
- spec.stdio.stdin === 'ignore' ? 'ignore' : 'pipe',
- spec.stdio.stdout === 'inherit' ? 'inherit' : 'pipe',
- spec.stdio.stderr === 'inherit' ? 'inherit' : 'pipe',
- ]
- if (ipc) (stdio as Array<string>).push('ipc')
- return stdio
- }
- function throwNullByteError(property: string, value: string, argument: boolean): never {
- const subject = argument ? `The argument '${property}'` : `The property '${property}'`
- const error = new TypeError(`${subject} must be a string without null bytes. Received ${inspect(value)}`)
- Object.assign(error, { code: 'ERR_INVALID_ARG_VALUE' })
- throw error
- }
- function validateNoNullByte(property: string, value: string, argument = false): void {
- if (value.includes('\0')) throwNullByteError(property, value, argument)
- }
- /**
- * Materialize and synchronously validate the final target environment.
- * @param spec - final target argv, cwd, and environment overrides.
- * @returns complete target environment after Node-equivalent validation.
- */
- export function targetEnvironment(
- spec: Pick<SubprocessSpawnSpec, 'argv' | 'cwd' | 'env'>,
- ): Record<string, string> {
- spec.argv.forEach((value, index) => {
- validateNoNullByte(index === 0 ? 'file' : `args[${String(index - 1)}]`, value, true)
- })
- validateNoNullByte('options.cwd', spec.cwd)
- const env = Object.fromEntries(
- Object.entries(childEnv(spec.env)).filter((entry): entry is [string, string] => entry[1] !== undefined),
- )
- for (const [key, value] of Object.entries(env)) {
- validateNoNullByte(`options.env['${key}']`, key)
- validateNoNullByte(`options.env['${key}']`, value)
- }
- return env
- }
- /**
- * Validate Linux PTY target strings before creating its request or terminal.
- * @param spec - terminal subprocess request to validate.
- * @returns complete validated target environment.
- */
- export function validateTerminalTarget(spec: SubprocessTerminalSpawnSpec): Record<string, string> {
- return targetEnvironment(spec)
- }
|