mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
306 lines
11 KiB
TypeScript
306 lines
11 KiB
TypeScript
/** Parent-side invocation and bootstrap state for the private native runner. */
|
|
|
|
import type { StdioOptions } from 'node:child_process'
|
|
import { accessSync, constants as fsConstants, lstatSync, statSync } from 'node:fs'
|
|
import { extname, isAbsolute } from 'node:path'
|
|
import { inspect } from 'node:util'
|
|
import { fileURLToPath } from 'node:url'
|
|
import type { SubprocessSpawnSpec } 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[]]
|
|
|
|
const SOURCE_TSCONFIG_PATH = fileURLToPath(new URL('../../../../tsconfig.base.json', import.meta.url))
|
|
const RUNNER_CONTROL_ENV_PREFIXES = ['NODE_', 'TSX_'] as const
|
|
|
|
/**
|
|
* 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',
|
|
import.meta.resolve('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.
|
|
* @param invocation - resolved runner invocation whose source form needs the workspace paths map.
|
|
* @returns environment for the runner before target state is restored.
|
|
*/
|
|
export function runnerEnvironment(
|
|
selection: string,
|
|
invocation?: RunnerInvocation,
|
|
): NodeJS.ProcessEnv {
|
|
const entry = invocation?.at(-1)
|
|
const env = childEnv()
|
|
for (const name of Object.keys(env)) {
|
|
const normalized = name.toUpperCase()
|
|
if (RUNNER_CONTROL_ENV_PREFIXES.some(prefix => normalized.startsWith(prefix))) {
|
|
Reflect.deleteProperty(env, name)
|
|
}
|
|
}
|
|
return {
|
|
...env,
|
|
[SUBPROCESS_RUNNER_ENV]: selection,
|
|
SYSTEMD_LOG_TARGET: 'null',
|
|
...entry?.endsWith('.ts') === true ? { TSX_TSCONFIG_PATH: SOURCE_TSCONFIG_PATH } : {},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 direct Linux target stdio, or isolated Windows runner stdio with IPC
|
|
* on fd 3 and target carriers on fd 4 through fd 6.
|
|
* @param spec - ordinary subprocess request whose stdio modes are preserved.
|
|
* @param ipc - whether to isolate the runner and add its private Node IPC descriptor.
|
|
* @param stdinCarrier - runner fd 4 carrier; Windows ignore passes an opened null-device fd.
|
|
* @returns child-process stdio options for the runner.
|
|
*/
|
|
export function runnerStdio(
|
|
spec: SubprocessSpawnSpec,
|
|
ipc: boolean,
|
|
stdinCarrier: 'pipe' | number = 'pipe',
|
|
): StdioOptions {
|
|
const targetStdio: StdioOptions = [
|
|
spec.stdio.stdin === 'ignore' ? 'ignore' : 'pipe',
|
|
spec.stdio.stdout === 'inherit' ? 'inherit' : 'pipe',
|
|
spec.stdio.stderr === 'inherit' ? 'inherit' : 'pipe',
|
|
]
|
|
if (!ipc) return targetStdio
|
|
return [
|
|
'ignore',
|
|
'ignore',
|
|
'ignore',
|
|
'ipc',
|
|
stdinCarrier,
|
|
spec.stdio.stdout === 'inherit' ? 1 : 'pipe',
|
|
spec.stdio.stderr === 'inherit' ? 2 : 'pipe',
|
|
]
|
|
}
|
|
|
|
function windowsEnvironmentValue(
|
|
env: Readonly<Record<string, string | undefined>>,
|
|
name: 'PATH' | 'NODEFAULTCURRENTDIRECTORYINEXEPATH',
|
|
): string | undefined {
|
|
for (const key of Object.keys(env).sort()) {
|
|
if (key.toUpperCase() === name) return env[key]
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function executableCandidateExists(candidate: string): boolean {
|
|
try {
|
|
return !statSync(candidate).isDirectory()
|
|
} catch {
|
|
try {
|
|
const entry = lstatSync(candidate)
|
|
return entry.isFile() || entry.isSymbolicLink()
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
function windowsPathDirectories(path: string): string[] {
|
|
const directories: string[] = []
|
|
let start = 0
|
|
while (start < path.length) {
|
|
if (path.charAt(start) === ';') {
|
|
start += 1
|
|
continue
|
|
}
|
|
const quote = path.charAt(start)
|
|
const quoted = quote === '"' || quote === "'"
|
|
const quoteEnd = quoted
|
|
? path.indexOf(quote, start + 1)
|
|
: -1
|
|
const separator = path.indexOf(';', quoted ? quoteEnd < 0 ? path.length : quoteEnd : start)
|
|
const end = separator < 0 ? path.length : separator
|
|
let directory = path.slice(start, end)
|
|
if (directory.startsWith('"') || directory.startsWith("'")) directory = directory.slice(1)
|
|
if (directory.endsWith('"') || directory.endsWith("'")) directory = directory.slice(0, -1)
|
|
if (directory.length > 0) directories.push(directory)
|
|
start = end + 1
|
|
}
|
|
return directories
|
|
}
|
|
|
|
function windowsFileNameStart(command: string): number {
|
|
let start = command.length
|
|
while (start > 0 && !/[\\/:]/u.test(command.charAt(start - 1))) start -= 1
|
|
return start
|
|
}
|
|
|
|
function windowsSearchPathJoin(directory: string, name: string, cwd: string): string {
|
|
let prefix = cwd
|
|
let adjustedDirectory = directory
|
|
const slash = (value: string): boolean => value === '\\' || value === '/'
|
|
if (directory.length > 2 && slash(directory.charAt(0)) && slash(directory.charAt(1))) {
|
|
prefix = ''
|
|
} else if (directory.length >= 1 && slash(directory.charAt(0))) {
|
|
prefix = cwd.slice(0, 2)
|
|
} else if (
|
|
directory.length >= 2
|
|
&& directory.charAt(1) === ':'
|
|
&& (directory.length < 3 || !slash(directory.charAt(2)))
|
|
) {
|
|
if (cwd.length < 2 || cwd.slice(0, 2).toLowerCase() !== directory.slice(0, 2).toLowerCase()) {
|
|
prefix = ''
|
|
} else {
|
|
adjustedDirectory = directory.slice(2)
|
|
}
|
|
} else if (directory.length > 2 && directory.charAt(1) === ':') {
|
|
prefix = ''
|
|
}
|
|
|
|
const append = (base: string, part: string): string => {
|
|
if (base.length === 0 || part.length === 0) return base + part
|
|
return /[\\/:]$/u.test(base) ? base + part : `${base}\\${part}`
|
|
}
|
|
return append(append(prefix, adjustedDirectory), name)
|
|
}
|
|
|
|
function windowsExecutableNames(command: string, name: string): string[] {
|
|
const dot = name.indexOf('.')
|
|
const hasExtension = dot >= 0 && dot < name.length - 1
|
|
const separator = name.endsWith('.') ? '' : '.'
|
|
return [
|
|
...hasExtension ? [command] : [],
|
|
`${command}${separator}com`,
|
|
`${command}${separator}exe`,
|
|
]
|
|
}
|
|
|
|
/**
|
|
* Resolve the executable path with libuv/Node Windows spawn search order while
|
|
* preserving the caller's original command-line argv entry separately.
|
|
* @param command - original target argv[0].
|
|
* @param cwd - final target working directory used for relative search roots.
|
|
* @param env - final target environment containing the child PATH.
|
|
* @param exists - injectable non-directory candidate probe used by tests.
|
|
* @param currentEnv - runner environment supplying PATH fallback and cwd-search policy.
|
|
* @returns a resolved application name suitable for `CreateProcessW`, or undefined when no candidate exists.
|
|
*/
|
|
export function resolveWindowsExecutable(
|
|
command: string,
|
|
cwd: string,
|
|
env: Readonly<Record<string, string>>,
|
|
exists: (candidate: string) => boolean = executableCandidateExists,
|
|
currentEnv: Readonly<Record<string, string | undefined>> = process.env,
|
|
): string | undefined {
|
|
const nameStart = windowsFileNameStart(command)
|
|
const directory = command.slice(0, nameStart)
|
|
const name = command.slice(nameStart)
|
|
const hasPath = nameStart !== 0
|
|
const roots: string[] = []
|
|
if (hasPath) {
|
|
roots.push(directory)
|
|
} else {
|
|
if (windowsEnvironmentValue(currentEnv, 'NODEFAULTCURRENTDIRECTORYINEXEPATH') === undefined) {
|
|
roots.push('')
|
|
}
|
|
const path = windowsEnvironmentValue(env, 'PATH') ?? windowsEnvironmentValue(currentEnv, 'PATH') ?? ''
|
|
roots.push(...windowsPathDirectories(path))
|
|
}
|
|
|
|
for (const root of roots) {
|
|
const base = windowsSearchPathJoin(root, name, cwd)
|
|
for (const candidate of windowsExecutableNames(base, name)) {
|
|
if (exists(candidate)) return candidate
|
|
}
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
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
|
|
}
|