File size: 10,113 Bytes
d04f74a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | /** Closed private transports shared by the native subprocess runner. */
import {
chmodSync,
existsSync,
lstatSync,
mkdtempSync,
readFileSync,
rmdirSync,
unlinkSync,
writeFileSync,
} from 'node:fs'
import { tmpdir } from 'node:os'
import { basename, dirname, isAbsolute, join } from 'node:path'
/** Target state restored by the Linux bootstrap after systemd establishes the scope. */
export interface LinuxLaunchRequest {
cwd: string
env: Record<string, string>
control?: 'pipe'
}
/** Bounded Node-shaped error fields allowed across a private runner boundary. */
export interface SerializedRunnerError {
name: string
message: string
code?: string
syscall?: string
path?: string
}
/** A Linux pre-exec failure published beside its consumed request. */
export type LinuxStartupError =
{ type: 'error'; error: SerializedRunnerError }
/** The only parent-to-runner start message on Windows. */
export interface WindowsStartRequest {
type: 'start'
cwd: string
env: Record<string, string>
control?: 'pipe'
}
/** The only parent-to-runner control message on Windows. */
export interface WindowsTerminateRequest {
type: 'terminate'
}
/** Exactly one direct-result branch is sent by a connected Windows runner. */
export type WindowsRunnerResult =
| { type: 'target-exit'; exitCode: number }
| { type: 'error'; error: SerializedRunnerError }
/** Private paths owned by one Linux ordinary or PTY spawn. */
export interface LinuxLaunchFiles {
directory: string
requestPath: string
startupErrorPath: string
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value)
}
function hasExactKeys(value: Record<string, unknown>, required: readonly string[], optional: readonly string[] = []): boolean {
const allowed = new Set([...required, ...optional])
return required.every(key => Object.hasOwn(value, key))
&& Object.keys(value).every(key => allowed.has(key))
}
function isStringRecord(value: unknown): value is Record<string, string> {
return isRecord(value) && Object.values(value).every(entry => typeof entry === 'string')
}
function isSerializedRunnerError(value: unknown): value is SerializedRunnerError {
if (!isRecord(value) || !hasExactKeys(
value,
['name', 'message'],
['code', 'syscall', 'path'],
)) return false
return typeof value.name === 'string'
&& typeof value.message === 'string'
&& (value.code === undefined || typeof value.code === 'string')
&& (value.syscall === undefined || typeof value.syscall === 'string')
&& (value.path === undefined || typeof value.path === 'string')
}
function parseErrorResult(value: Record<string, unknown>): LinuxStartupError {
if (!hasExactKeys(value, ['type', 'error']) || !isSerializedRunnerError(value.error)) {
throw new Error('subprocess runner emitted an invalid error result')
}
if (value.type !== 'error') {
throw new Error('subprocess runner emitted an unknown error result')
}
return { type: 'error', error: value.error }
}
/**
* Create a private 0700 directory and one complete 0600 launch request.
* @param request - target cwd and complete environment for the bootstrap.
* @returns private paths owned by this launch.
*/
export function createLinuxLaunchFiles(request: LinuxLaunchRequest): LinuxLaunchFiles {
const directory = mkdtempSync(join(tmpdir(), 'dsh-subprocess-launch-'))
const files = {
directory,
requestPath: join(directory, 'launch-request.json'),
startupErrorPath: join(directory, 'startup-error.json'),
}
try {
chmodSync(directory, 0o700)
writeFileSync(files.requestPath, JSON.stringify(request), { flag: 'wx', mode: 0o600 })
return files
} catch (error) {
cleanupLinuxLaunchFiles(files)
throw error
}
}
/**
* Derive the only permitted startup-error path from an absolute request locator.
* @param requestPath - absolute path to the private launch-request file.
* @returns validated sibling paths for this launch.
*/
export function linuxLaunchFilesFromLocator(requestPath: string): LinuxLaunchFiles {
if (!isAbsolute(requestPath) || basename(requestPath) !== 'launch-request.json') {
throw new Error('subprocess runner received an invalid Linux launch-request locator')
}
const directory = dirname(requestPath)
return { directory, requestPath, startupErrorPath: join(directory, 'startup-error.json') }
}
/**
* Strictly read and remove a one-shot Linux launch request.
* @param requestPath - private launch-request path to consume.
* @returns validated target cwd and environment.
*/
export function consumeLinuxLaunchRequest(requestPath: string): LinuxLaunchRequest {
const text = readFileSync(requestPath, 'utf8')
unlinkSync(requestPath)
const value: unknown = JSON.parse(text)
if (!isRecord(value) || !hasExactKeys(value, ['cwd', 'env'], ['control'])
|| typeof value.cwd !== 'string' || !isStringRecord(value.env)
|| (value.control !== undefined && value.control !== 'pipe')) {
throw new Error('subprocess runner received an invalid Linux launch request')
}
return { cwd: value.cwd, env: value.env, ...value.control === 'pipe' ? { control: 'pipe' as const } : {} }
}
/**
* Publish one strict 0600 Linux pre-exec error.
* @param files - private paths for this launch.
* @param error - bounded spawn or runner failure to publish.
*/
export function writeLinuxStartupError(files: LinuxLaunchFiles, error: LinuxStartupError): void {
writeFileSync(files.startupErrorPath, JSON.stringify(error), { flag: 'wx', mode: 0o600 })
}
/**
* Read the Linux pre-exec error, if the bootstrap published one.
* @param path - expected startup-error path.
* @returns the validated failure, or undefined when none was published.
*/
export function readLinuxStartupError(path: string): LinuxStartupError | undefined {
if (!existsSync(path)) return undefined
const value: unknown = JSON.parse(readFileSync(path, 'utf8'))
if (!isRecord(value)) throw new Error('subprocess runner emitted an invalid startup error')
return parseErrorResult(value)
}
/**
* Strictly parse the single Windows start message.
* @param value - untrusted IPC payload.
* @returns validated target start request.
*/
export function parseWindowsStartRequest(value: unknown): WindowsStartRequest {
if (!isRecord(value) || !hasExactKeys(value, ['type', 'cwd', 'env'], ['control'])
|| value.type !== 'start' || typeof value.cwd !== 'string' || !isStringRecord(value.env)
|| (value.control !== undefined && value.control !== 'pipe')) {
throw new Error('subprocess runner received an invalid Windows start request')
}
return { type: 'start', cwd: value.cwd, env: value.env, ...value.control === 'pipe' ? { control: 'pipe' as const } : {} }
}
/**
* Return true only for the exact, payload-free Windows terminate control.
* @param value - untrusted IPC payload.
* @returns whether the payload is the exact terminate request.
*/
export function isWindowsTerminateRequest(value: unknown): value is WindowsTerminateRequest {
return isRecord(value) && hasExactKeys(value, ['type']) && value.type === 'terminate'
}
/**
* Strictly parse one of the two Windows direct-result branches.
* @param value - untrusted IPC payload.
* @returns validated direct-result message.
*/
export function parseWindowsRunnerResult(value: unknown): WindowsRunnerResult {
if (!isRecord(value) || typeof value.type !== 'string') {
throw new Error('subprocess runner emitted an invalid Windows result')
}
if (value.type === 'error') return parseErrorResult(value)
if (value.type === 'target-exit') {
const validExitCode = typeof value.exitCode === 'number'
&& Number.isSafeInteger(value.exitCode)
&& value.exitCode >= 0
if (!hasExactKeys(value, ['type', 'exitCode']) || !validExitCode) {
throw new Error('subprocess runner emitted an invalid target-exit result')
}
return {
type: 'target-exit',
exitCode: value.exitCode as number,
}
}
throw new Error(`subprocess runner emitted an unknown Windows result: ${value.type}`)
}
/**
* Convert an unknown failure into the bounded cross-process error record.
* @param error - failure caught at the process boundary.
* @returns bounded serializable error fields.
*/
export function serializeRunnerError(error: unknown): SerializedRunnerError {
const source = error instanceof Error ? error : new Error(String(error))
const node = source as NodeJS.ErrnoException & { path?: string }
return {
name: source.name,
message: source.message,
...typeof node.code === 'string' ? { code: node.code } : {},
...typeof node.syscall === 'string' ? { syscall: node.syscall } : {},
...typeof node.path === 'string' ? { path: node.path } : {},
}
}
/**
* Rebuild a Node-shaped Error from a strict runner record.
* @param serialized - validated bounded error fields.
* @returns reconstructed Error with supported Node fields.
*/
export function deserializeRunnerError(serialized: SerializedRunnerError): Error {
const error = new Error(serialized.message)
error.name = serialized.name
return Object.assign(error, {
...serialized.code === undefined ? {} : { code: serialized.code },
...serialized.syscall === undefined ? {} : { syscall: serialized.syscall },
...serialized.path === undefined ? {} : { path: serialized.path },
})
}
/**
* Best-effort removal of only the private paths created for this Linux spawn.
* @param files - exact private paths owned by this launch.
*/
export function cleanupLinuxLaunchFiles(files: LinuxLaunchFiles): void {
try {
if (lstatSync(files.directory).isSymbolicLink()) {
unlinkSync(files.directory)
return
}
for (const path of [
files.requestPath,
files.startupErrorPath,
]) {
try { unlinkSync(path) } catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
}
}
rmdirSync(files.directory)
} catch {
// Crash residue remains private and no later spawn reuses this directory.
}
}
|