@namzu/sandbox 7.0.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +29 -1
- package/dist/backends/aci-standby-pool/index.d.ts.map +1 -1
- package/dist/backends/aci-standby-pool/index.js +95 -82
- package/dist/backends/aci-standby-pool/index.js.map +1 -1
- package/dist/backends/docker/index.d.ts.map +1 -1
- package/dist/backends/docker/index.js +108 -114
- package/dist/backends/docker/index.js.map +1 -1
- package/dist/backends/firecracker/index.d.ts.map +1 -1
- package/dist/backends/firecracker/index.js +117 -52
- package/dist/backends/firecracker/index.js.map +1 -1
- package/dist/backends/firecracker/protocol.d.ts +19 -27
- package/dist/backends/firecracker/protocol.d.ts.map +1 -1
- package/dist/backends/firecracker/protocol.js +57 -32
- package/dist/backends/firecracker/protocol.js.map +1 -1
- package/dist/backends/firecracker/transport.d.ts +23 -4
- package/dist/backends/firecracker/transport.d.ts.map +1 -1
- package/dist/backends/firecracker/transport.js +153 -32
- package/dist/backends/firecracker/transport.js.map +1 -1
- package/dist/backends/http-worker-client.d.ts +17 -0
- package/dist/backends/http-worker-client.d.ts.map +1 -0
- package/dist/backends/http-worker-client.js +173 -0
- package/dist/backends/http-worker-client.js.map +1 -0
- package/dist/backends/remote-execution-controller.d.ts +76 -0
- package/dist/backends/remote-execution-controller.d.ts.map +1 -0
- package/dist/backends/remote-execution-controller.js +294 -0
- package/dist/backends/remote-execution-controller.js.map +1 -0
- package/package.json +2 -2
- package/src/backends/aci-standby-pool/index.ts +101 -88
- package/src/backends/docker/index.ts +101 -132
- package/src/backends/firecracker/index.ts +117 -45
- package/src/backends/firecracker/protocol.ts +67 -33
- package/src/backends/firecracker/transport.ts +214 -39
- package/src/backends/http-worker-client.ts +230 -0
- package/src/backends/remote-execution-controller.ts +462 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import type { SandboxExecOptions, SandboxExecResult } from '@namzu/sdk'
|
|
2
|
+
|
|
3
|
+
const CONTROL_REQUEST_TIMEOUT_MS = 2_000
|
|
4
|
+
const CANCEL_CONFIRM_TIMEOUT_MS = 8_000
|
|
5
|
+
const RESULT_DRAIN_TIMEOUT_MS = 1_500
|
|
6
|
+
const DEFAULT_EXECUTION_TIMEOUT_MS = 5 * 60 * 1_000
|
|
7
|
+
const EXECUTION_OBSERVATION_GRACE_MS = 500
|
|
8
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647
|
|
9
|
+
const EXECUTION_ID_PATTERN =
|
|
10
|
+
/^exec_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
11
|
+
|
|
12
|
+
export interface RemoteReservation {
|
|
13
|
+
readonly ok: true
|
|
14
|
+
readonly protocolVersion: 2
|
|
15
|
+
readonly executionId: string
|
|
16
|
+
readonly leaseExpiresAt: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RemoteTerminalMetadata {
|
|
20
|
+
readonly exitCode: number
|
|
21
|
+
readonly timedOut: boolean
|
|
22
|
+
readonly durationMs: number
|
|
23
|
+
readonly signal?: string
|
|
24
|
+
readonly stdoutTruncated?: boolean
|
|
25
|
+
readonly stderrTruncated?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RemoteCancellationAcknowledgement {
|
|
29
|
+
readonly ok: true
|
|
30
|
+
readonly state: 'cancelled' | 'completed' | 'failed'
|
|
31
|
+
readonly started: boolean
|
|
32
|
+
readonly result?: RemoteTerminalMetadata
|
|
33
|
+
readonly error?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class RemoteCommandError extends Error {}
|
|
37
|
+
|
|
38
|
+
export class RemoteProtocolError extends Error {}
|
|
39
|
+
|
|
40
|
+
export class RemoteCancellationUnsupportedError extends Error {}
|
|
41
|
+
|
|
42
|
+
export interface SandboxRetirementObservation {
|
|
43
|
+
readonly accepted: boolean
|
|
44
|
+
readonly error?: Error
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class RemoteCancellationUnknownError extends Error {
|
|
48
|
+
retirement?: SandboxRetirementObservation
|
|
49
|
+
|
|
50
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
51
|
+
super(message, options)
|
|
52
|
+
this.name = 'RemoteCancellationUnknownError'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class RemoteResultIncompleteError extends Error {
|
|
57
|
+
constructor(
|
|
58
|
+
message: string,
|
|
59
|
+
readonly acknowledgement: RemoteCancellationAcknowledgement,
|
|
60
|
+
options?: ErrorOptions,
|
|
61
|
+
) {
|
|
62
|
+
super(message, options)
|
|
63
|
+
this.name = 'RemoteResultIncompleteError'
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface RemoteExecutionAdapter<Context = undefined> {
|
|
68
|
+
readonly label: string
|
|
69
|
+
reserve(signal: AbortSignal): Promise<unknown>
|
|
70
|
+
cancel(executionId: string, signal: AbortSignal): Promise<unknown>
|
|
71
|
+
execute(
|
|
72
|
+
executionId: string | undefined,
|
|
73
|
+
command: string,
|
|
74
|
+
argv: string[] | undefined,
|
|
75
|
+
opts: SandboxExecOptions | undefined,
|
|
76
|
+
transportSignal: AbortSignal,
|
|
77
|
+
context: Context | undefined,
|
|
78
|
+
): Promise<SandboxExecResult>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface RemoteExecutionControllerOptions {
|
|
82
|
+
readonly controlRequestTimeoutMs?: number
|
|
83
|
+
readonly cancelConfirmTimeoutMs?: number
|
|
84
|
+
readonly resultDrainTimeoutMs?: number
|
|
85
|
+
readonly executionObservationGraceMs?: number
|
|
86
|
+
readonly defaultExecutionTimeoutMs?: number
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isTerminalMetadata(value: unknown): value is RemoteTerminalMetadata {
|
|
90
|
+
if (!value || typeof value !== 'object') return false
|
|
91
|
+
const metadata = value as Record<string, unknown>
|
|
92
|
+
return (
|
|
93
|
+
Number.isFinite(metadata.exitCode) &&
|
|
94
|
+
typeof metadata.timedOut === 'boolean' &&
|
|
95
|
+
Number.isFinite(metadata.durationMs) &&
|
|
96
|
+
(metadata.signal === undefined || typeof metadata.signal === 'string') &&
|
|
97
|
+
(metadata.stdoutTruncated === undefined || typeof metadata.stdoutTruncated === 'boolean') &&
|
|
98
|
+
(metadata.stderrTruncated === undefined || typeof metadata.stderrTruncated === 'boolean')
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseReservation(value: unknown): RemoteReservation {
|
|
103
|
+
if (!value || typeof value !== 'object') {
|
|
104
|
+
throw new RemoteProtocolError('remote sandbox returned an invalid execution reservation')
|
|
105
|
+
}
|
|
106
|
+
const reservation = value as Record<string, unknown>
|
|
107
|
+
if (
|
|
108
|
+
reservation.ok !== true ||
|
|
109
|
+
reservation.protocolVersion !== 2 ||
|
|
110
|
+
!EXECUTION_ID_PATTERN.test(String(reservation.executionId ?? '')) ||
|
|
111
|
+
!Number.isFinite(reservation.leaseExpiresAt)
|
|
112
|
+
) {
|
|
113
|
+
throw new RemoteProtocolError('remote sandbox returned an invalid execution reservation')
|
|
114
|
+
}
|
|
115
|
+
return reservation as unknown as RemoteReservation
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function parseCancellation(value: unknown): RemoteCancellationAcknowledgement {
|
|
119
|
+
if (!value || typeof value !== 'object') {
|
|
120
|
+
throw new RemoteProtocolError('remote sandbox returned an invalid cancellation acknowledgement')
|
|
121
|
+
}
|
|
122
|
+
const acknowledgement = value as Record<string, unknown>
|
|
123
|
+
if (
|
|
124
|
+
acknowledgement.ok !== true ||
|
|
125
|
+
!['cancelled', 'completed', 'failed'].includes(String(acknowledgement.state ?? '')) ||
|
|
126
|
+
typeof acknowledgement.started !== 'boolean' ||
|
|
127
|
+
(acknowledgement.result !== undefined && !isTerminalMetadata(acknowledgement.result)) ||
|
|
128
|
+
(acknowledgement.error !== undefined && typeof acknowledgement.error !== 'string')
|
|
129
|
+
) {
|
|
130
|
+
throw new RemoteProtocolError('remote sandbox returned an invalid cancellation acknowledgement')
|
|
131
|
+
}
|
|
132
|
+
return acknowledgement as unknown as RemoteCancellationAcknowledgement
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function cancelledBeforeStart(startedAt: number): SandboxExecResult {
|
|
136
|
+
return {
|
|
137
|
+
exitCode: 1,
|
|
138
|
+
stdout: '',
|
|
139
|
+
stderr: '',
|
|
140
|
+
timedOut: false,
|
|
141
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
142
|
+
stdoutTruncated: false,
|
|
143
|
+
stderrTruncated: false,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function resultFromAcknowledgement(
|
|
148
|
+
acknowledgement: RemoteCancellationAcknowledgement,
|
|
149
|
+
startedAt: number,
|
|
150
|
+
): SandboxExecResult {
|
|
151
|
+
const result = acknowledgement.result
|
|
152
|
+
if (!result) return cancelledBeforeStart(startedAt)
|
|
153
|
+
return { ...result, stdout: '', stderr: '' }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resultIncomplete(
|
|
157
|
+
error: unknown,
|
|
158
|
+
acknowledgement: RemoteCancellationAcknowledgement,
|
|
159
|
+
): RemoteResultIncompleteError {
|
|
160
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
161
|
+
return new RemoteResultIncompleteError(
|
|
162
|
+
`Remote sandbox termination was confirmed (${acknowledgement.state}), but the terminal result/output stream was incomplete: ${message}. Do not infer complete output.`,
|
|
163
|
+
acknowledgement,
|
|
164
|
+
{ cause: error },
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function cancellationUnknown(error: unknown): RemoteCancellationUnknownError {
|
|
169
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
170
|
+
return new RemoteCancellationUnknownError(
|
|
171
|
+
`Remote sandbox cancellation could not be confirmed: ${message}. The remote outcome is unknown; do not automatically retry the command.`,
|
|
172
|
+
{ cause: error },
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function observationDeadlineMs(
|
|
177
|
+
timeout: number | undefined,
|
|
178
|
+
defaultTimeoutMs: number,
|
|
179
|
+
graceMs: number,
|
|
180
|
+
): number {
|
|
181
|
+
const requested =
|
|
182
|
+
typeof timeout === 'number' && Number.isFinite(timeout) && timeout > 0
|
|
183
|
+
? timeout
|
|
184
|
+
: defaultTimeoutMs
|
|
185
|
+
return Math.min(MAX_TIMER_DELAY_MS, requested + graceMs)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function bounded<T>(
|
|
189
|
+
label: string,
|
|
190
|
+
timeoutMs: number,
|
|
191
|
+
operation: (signal: AbortSignal) => Promise<T>,
|
|
192
|
+
callerSignal?: AbortSignal,
|
|
193
|
+
): Promise<T> {
|
|
194
|
+
callerSignal?.throwIfAborted()
|
|
195
|
+
const controller = new AbortController()
|
|
196
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
197
|
+
let removeAbort: (() => void) | undefined
|
|
198
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
199
|
+
timer = setTimeout(() => {
|
|
200
|
+
const error = new Error(`${label} exceeded ${timeoutMs}ms`)
|
|
201
|
+
controller.abort(error)
|
|
202
|
+
reject(error)
|
|
203
|
+
}, timeoutMs)
|
|
204
|
+
})
|
|
205
|
+
const callerAbort = callerSignal
|
|
206
|
+
? new Promise<never>((_, reject) => {
|
|
207
|
+
const abort = () => {
|
|
208
|
+
controller.abort(callerSignal.reason)
|
|
209
|
+
reject(callerSignal.reason)
|
|
210
|
+
}
|
|
211
|
+
removeAbort = () => callerSignal.removeEventListener('abort', abort)
|
|
212
|
+
if (callerSignal.aborted) abort()
|
|
213
|
+
else callerSignal.addEventListener('abort', abort, { once: true })
|
|
214
|
+
})
|
|
215
|
+
: undefined
|
|
216
|
+
try {
|
|
217
|
+
const pending = operation(controller.signal)
|
|
218
|
+
return await Promise.race([pending, timeout, ...(callerAbort ? [callerAbort] : [])])
|
|
219
|
+
} finally {
|
|
220
|
+
if (timer) clearTimeout(timer)
|
|
221
|
+
removeAbort?.()
|
|
222
|
+
controller.abort()
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
type Capability = 'unknown' | 'supported' | 'unsupported'
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* One state machine for remote command ownership. A v2 peer reserves every
|
|
230
|
+
* command, even when the caller supplied no signal, so transport loss can be
|
|
231
|
+
* reconciled by identity. Legacy execution is entered only after an explicit
|
|
232
|
+
* unsupported response from the peer and is fenced as unknown on any failure.
|
|
233
|
+
*/
|
|
234
|
+
export class RemoteExecutionController<Context = undefined> {
|
|
235
|
+
private capability: Capability = 'unknown'
|
|
236
|
+
private unsupportedError: RemoteCancellationUnsupportedError | undefined
|
|
237
|
+
private readonly controlRequestTimeoutMs: number
|
|
238
|
+
private readonly cancelConfirmTimeoutMs: number
|
|
239
|
+
private readonly resultDrainTimeoutMs: number
|
|
240
|
+
private readonly executionObservationGraceMs: number
|
|
241
|
+
private readonly defaultExecutionTimeoutMs: number
|
|
242
|
+
|
|
243
|
+
constructor(
|
|
244
|
+
private readonly adapter: RemoteExecutionAdapter<Context>,
|
|
245
|
+
options: RemoteExecutionControllerOptions = {},
|
|
246
|
+
) {
|
|
247
|
+
this.controlRequestTimeoutMs = options.controlRequestTimeoutMs ?? CONTROL_REQUEST_TIMEOUT_MS
|
|
248
|
+
this.cancelConfirmTimeoutMs = options.cancelConfirmTimeoutMs ?? CANCEL_CONFIRM_TIMEOUT_MS
|
|
249
|
+
this.resultDrainTimeoutMs = options.resultDrainTimeoutMs ?? RESULT_DRAIN_TIMEOUT_MS
|
|
250
|
+
this.executionObservationGraceMs =
|
|
251
|
+
options.executionObservationGraceMs ?? EXECUTION_OBSERVATION_GRACE_MS
|
|
252
|
+
this.defaultExecutionTimeoutMs =
|
|
253
|
+
options.defaultExecutionTimeoutMs ?? DEFAULT_EXECUTION_TIMEOUT_MS
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async exec(
|
|
257
|
+
command: string,
|
|
258
|
+
argv: string[] | undefined,
|
|
259
|
+
opts: SandboxExecOptions | undefined,
|
|
260
|
+
context?: Context,
|
|
261
|
+
): Promise<SandboxExecResult> {
|
|
262
|
+
const startedAt = Date.now()
|
|
263
|
+
if (opts?.signal?.aborted) return cancelledBeforeStart(startedAt)
|
|
264
|
+
if (this.capability === 'unsupported') {
|
|
265
|
+
if (opts?.signal) {
|
|
266
|
+
throw (
|
|
267
|
+
this.unsupportedError ??
|
|
268
|
+
new RemoteCancellationUnsupportedError(
|
|
269
|
+
`${this.adapter.label} was classified as cancellation-unsupported`,
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
return await this.executeLegacy(command, argv, opts, context)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let reservation: RemoteReservation
|
|
277
|
+
try {
|
|
278
|
+
reservation = parseReservation(
|
|
279
|
+
await bounded(
|
|
280
|
+
`${this.adapter.label} execution reservation`,
|
|
281
|
+
this.controlRequestTimeoutMs,
|
|
282
|
+
(signal) => this.adapter.reserve(signal),
|
|
283
|
+
opts?.signal,
|
|
284
|
+
),
|
|
285
|
+
)
|
|
286
|
+
this.capability = 'supported'
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if (opts?.signal?.aborted) return cancelledBeforeStart(startedAt)
|
|
289
|
+
if (error instanceof RemoteCancellationUnsupportedError) {
|
|
290
|
+
this.capability = 'unsupported'
|
|
291
|
+
this.unsupportedError = error
|
|
292
|
+
if (opts?.signal) throw error
|
|
293
|
+
return await this.executeLegacy(command, argv, opts, context)
|
|
294
|
+
}
|
|
295
|
+
throw error
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return await this.executeReserved(reservation, command, argv, opts, startedAt, context)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private async executeLegacy(
|
|
302
|
+
command: string,
|
|
303
|
+
argv: string[] | undefined,
|
|
304
|
+
opts: SandboxExecOptions | undefined,
|
|
305
|
+
context: Context | undefined,
|
|
306
|
+
): Promise<SandboxExecResult> {
|
|
307
|
+
const timeoutMs = observationDeadlineMs(
|
|
308
|
+
opts?.timeout,
|
|
309
|
+
this.defaultExecutionTimeoutMs,
|
|
310
|
+
this.executionObservationGraceMs,
|
|
311
|
+
)
|
|
312
|
+
try {
|
|
313
|
+
return await bounded(
|
|
314
|
+
`${this.adapter.label} legacy execution observation`,
|
|
315
|
+
timeoutMs,
|
|
316
|
+
(signal) => this.adapter.execute(undefined, command, argv, opts, signal, context),
|
|
317
|
+
)
|
|
318
|
+
} catch (error) {
|
|
319
|
+
throw new RemoteCancellationUnknownError(
|
|
320
|
+
`The legacy ${this.adapter.label} execution failed after admission without an execution id: ${error instanceof Error ? error.message : String(error)}. The remote outcome is unknown and the sandbox must not be reused.`,
|
|
321
|
+
{ cause: error },
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private async requestCancellation(
|
|
327
|
+
executionId: string,
|
|
328
|
+
): Promise<RemoteCancellationAcknowledgement> {
|
|
329
|
+
const deadlineAt = Date.now() + this.cancelConfirmTimeoutMs
|
|
330
|
+
let lastError: unknown = new Error('no cancellation attempt completed')
|
|
331
|
+
while (Date.now() < deadlineAt) {
|
|
332
|
+
const remaining = deadlineAt - Date.now()
|
|
333
|
+
try {
|
|
334
|
+
return parseCancellation(
|
|
335
|
+
await bounded(
|
|
336
|
+
`${this.adapter.label} cancellation attempt`,
|
|
337
|
+
Math.min(this.controlRequestTimeoutMs, remaining),
|
|
338
|
+
(signal) => this.adapter.cancel(executionId, signal),
|
|
339
|
+
),
|
|
340
|
+
)
|
|
341
|
+
} catch (error) {
|
|
342
|
+
lastError = error
|
|
343
|
+
const delayMs = Math.min(50, deadlineAt - Date.now())
|
|
344
|
+
if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs))
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
throw cancellationUnknown(lastError)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private async drainConfirmedExecution(
|
|
351
|
+
pending: Promise<SandboxExecResult>,
|
|
352
|
+
cancelled: RemoteCancellationAcknowledgement,
|
|
353
|
+
startedAt: number,
|
|
354
|
+
): Promise<SandboxExecResult> {
|
|
355
|
+
if (!cancelled.started) return resultFromAcknowledgement(cancelled, startedAt)
|
|
356
|
+
try {
|
|
357
|
+
return await bounded(
|
|
358
|
+
`${this.adapter.label} terminal-result drain`,
|
|
359
|
+
this.resultDrainTimeoutMs,
|
|
360
|
+
async () => await pending,
|
|
361
|
+
)
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (error instanceof RemoteCommandError && cancelled.state === 'failed') throw error
|
|
364
|
+
throw resultIncomplete(error, cancelled)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private async executeReserved(
|
|
369
|
+
reservation: RemoteReservation,
|
|
370
|
+
command: string,
|
|
371
|
+
argv: string[] | undefined,
|
|
372
|
+
opts: SandboxExecOptions | undefined,
|
|
373
|
+
startedAt: number,
|
|
374
|
+
context: Context | undefined,
|
|
375
|
+
): Promise<SandboxExecResult> {
|
|
376
|
+
if (opts?.signal?.aborted) {
|
|
377
|
+
const cancelled = await this.requestCancellation(reservation.executionId)
|
|
378
|
+
if (cancelled.started) {
|
|
379
|
+
throw resultIncomplete(
|
|
380
|
+
new Error('the peer reported an admitted command before execute was sent'),
|
|
381
|
+
cancelled,
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
return resultFromAcknowledgement(cancelled, startedAt)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
type CancellationReason = 'caller' | 'observation-timeout' | 'reconcile'
|
|
388
|
+
let cancellationReason: CancellationReason | undefined
|
|
389
|
+
let startCancellation: ((reason: CancellationReason) => void) | undefined
|
|
390
|
+
const cancellation = new Promise<RemoteCancellationAcknowledgement>((resolve, reject) => {
|
|
391
|
+
let started = false
|
|
392
|
+
startCancellation = (reason) => {
|
|
393
|
+
if (started) return
|
|
394
|
+
started = true
|
|
395
|
+
cancellationReason = reason
|
|
396
|
+
void this.requestCancellation(reservation.executionId).then(resolve, reject)
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
const onAbort = () => startCancellation?.('caller')
|
|
400
|
+
opts?.signal?.addEventListener('abort', onAbort, { once: true })
|
|
401
|
+
if (opts?.signal?.aborted) onAbort()
|
|
402
|
+
const observationTimer = setTimeout(
|
|
403
|
+
() => startCancellation?.('observation-timeout'),
|
|
404
|
+
observationDeadlineMs(
|
|
405
|
+
opts?.timeout,
|
|
406
|
+
this.defaultExecutionTimeoutMs,
|
|
407
|
+
this.executionObservationGraceMs,
|
|
408
|
+
),
|
|
409
|
+
)
|
|
410
|
+
observationTimer.unref?.()
|
|
411
|
+
|
|
412
|
+
const executionTransport = new AbortController()
|
|
413
|
+
const execution = this.adapter.execute(
|
|
414
|
+
reservation.executionId,
|
|
415
|
+
command,
|
|
416
|
+
argv,
|
|
417
|
+
opts,
|
|
418
|
+
executionTransport.signal,
|
|
419
|
+
context,
|
|
420
|
+
)
|
|
421
|
+
const observedExecution = execution.then(
|
|
422
|
+
(result) => ({ kind: 'result' as const, result }),
|
|
423
|
+
(error: unknown) => ({ kind: 'error' as const, error }),
|
|
424
|
+
)
|
|
425
|
+
const observedCancellation = cancellation.then(
|
|
426
|
+
(result) => ({ kind: 'cancelled' as const, result }),
|
|
427
|
+
(error: unknown) => ({ kind: 'cancel-error' as const, error }),
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
const first = await Promise.race([observedExecution, observedCancellation])
|
|
432
|
+
if (first.kind === 'result') return first.result
|
|
433
|
+
if (first.kind === 'cancel-error') throw first.error
|
|
434
|
+
if (first.kind === 'cancelled') {
|
|
435
|
+
const result = await this.drainConfirmedExecution(execution, first.result, startedAt)
|
|
436
|
+
return cancellationReason === 'observation-timeout' && first.result.state === 'cancelled'
|
|
437
|
+
? { ...result, timedOut: true }
|
|
438
|
+
: result
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
startCancellation?.('reconcile')
|
|
442
|
+
let confirmed: RemoteCancellationAcknowledgement
|
|
443
|
+
try {
|
|
444
|
+
confirmed = await cancellation
|
|
445
|
+
} catch (cancelError) {
|
|
446
|
+
throw cancellationUnknown(cancelError)
|
|
447
|
+
}
|
|
448
|
+
if (first.error instanceof RemoteCommandError && confirmed.state === 'failed') {
|
|
449
|
+
throw first.error
|
|
450
|
+
}
|
|
451
|
+
if (opts?.signal?.aborted && confirmed.state === 'cancelled' && !confirmed.started) {
|
|
452
|
+
return resultFromAcknowledgement(confirmed, startedAt)
|
|
453
|
+
}
|
|
454
|
+
if (!confirmed.started) throw first.error
|
|
455
|
+
throw resultIncomplete(first.error, confirmed)
|
|
456
|
+
} finally {
|
|
457
|
+
clearTimeout(observationTimer)
|
|
458
|
+
opts?.signal?.removeEventListener('abort', onAbort)
|
|
459
|
+
executionTransport.abort(new Error('remote sandbox execution observation finished'))
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|