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.
  }
}