@namzu/sandbox 6.1.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 +62 -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 +163 -111
- 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 +212 -149
- 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 +154 -71
- 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 +26 -7
- package/dist/backends/firecracker/transport.d.ts.map +1 -1
- package/dist/backends/firecracker/transport.js +235 -72
- 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/readiness.d.ts +43 -0
- package/dist/backends/readiness.d.ts.map +1 -0
- package/dist/backends/readiness.js +150 -0
- package/dist/backends/readiness.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/dist/egress/proxy.d.ts.map +1 -1
- package/dist/egress/proxy.js +20 -3
- package/dist/egress/proxy.js.map +1 -1
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/backends/aci-standby-pool/index.ts +199 -117
- package/src/backends/docker/index.ts +235 -175
- package/src/backends/firecracker/index.ts +163 -63
- package/src/backends/firecracker/protocol.ts +67 -33
- package/src/backends/firecracker/transport.ts +301 -77
- package/src/backends/http-worker-client.ts +230 -0
- package/src/backends/readiness.ts +173 -0
- package/src/backends/remote-execution-controller.ts +462 -0
- package/src/egress/proxy.ts +18 -3
- package/src/index.ts +27 -0
|
@@ -44,6 +44,7 @@ import type {
|
|
|
44
44
|
ContainerSandboxMountSource,
|
|
45
45
|
ResolvedContainerSandboxLayout,
|
|
46
46
|
Sandbox,
|
|
47
|
+
SandboxDestroyOptions,
|
|
47
48
|
SandboxEnvironment,
|
|
48
49
|
SandboxExecOptions,
|
|
49
50
|
SandboxExecResult,
|
|
@@ -54,6 +55,15 @@ import type {
|
|
|
54
55
|
import { asSandboxId } from '@namzu/sdk'
|
|
55
56
|
|
|
56
57
|
import type { SandboxBackend, SandboxBackendOptions } from '../../index.js'
|
|
58
|
+
import { HttpWorkerClient } from '../http-worker-client.js'
|
|
59
|
+
import {
|
|
60
|
+
OperationDeadline,
|
|
61
|
+
OperationDeadlineExpired,
|
|
62
|
+
probeHttpHealth,
|
|
63
|
+
resolveReadinessOptions,
|
|
64
|
+
runFailureCleanup,
|
|
65
|
+
} from '../readiness.js'
|
|
66
|
+
import { RemoteCancellationUnknownError } from '../remote-execution-controller.js'
|
|
57
67
|
|
|
58
68
|
/**
|
|
59
69
|
* Authentication callback. Caller returns a fresh Azure Resource
|
|
@@ -147,11 +157,20 @@ const DEFAULT_CONTAINER_NAME_PREFIX = 'namzu-task'
|
|
|
147
157
|
export function buildAciStandbyPoolBackend(
|
|
148
158
|
config: ACIStandbyPoolBackendInternalConfig,
|
|
149
159
|
): SandboxBackend {
|
|
160
|
+
const readiness = resolveReadinessOptions(
|
|
161
|
+
'aci-standby-pool',
|
|
162
|
+
config.readyTimeoutMs,
|
|
163
|
+
config.readyPollIntervalMs,
|
|
164
|
+
{
|
|
165
|
+
timeoutMs: DEFAULT_READY_TIMEOUT_MS,
|
|
166
|
+
pollIntervalMs: DEFAULT_READY_POLL_MS,
|
|
167
|
+
},
|
|
168
|
+
)
|
|
150
169
|
return {
|
|
151
170
|
tier: 'container',
|
|
152
171
|
name: 'aci-standby-pool',
|
|
153
172
|
async create(options: SandboxBackendOptions): Promise<Sandbox> {
|
|
154
|
-
return await spawnAciSandbox(config, options)
|
|
173
|
+
return await spawnAciSandbox(config, options, readiness)
|
|
155
174
|
},
|
|
156
175
|
}
|
|
157
176
|
}
|
|
@@ -219,7 +238,10 @@ function buildAzureFileVolumesFromLayout(layout: ResolvedContainerSandboxLayout)
|
|
|
219
238
|
let counter = 0
|
|
220
239
|
|
|
221
240
|
function add(
|
|
222
|
-
mount: {
|
|
241
|
+
mount: {
|
|
242
|
+
readonly source: ContainerSandboxMountSource
|
|
243
|
+
readonly containerPath: string
|
|
244
|
+
},
|
|
223
245
|
label: string,
|
|
224
246
|
readOnly: boolean,
|
|
225
247
|
): void {
|
|
@@ -280,8 +302,12 @@ async function armCall<T>(
|
|
|
280
302
|
method: 'GET' | 'PUT' | 'DELETE',
|
|
281
303
|
getToken: ArmTokenProvider,
|
|
282
304
|
body?: unknown,
|
|
305
|
+
signal?: AbortSignal,
|
|
306
|
+
acceptedStatuses: readonly number[] = [],
|
|
283
307
|
): Promise<T | undefined> {
|
|
308
|
+
signal?.throwIfAborted()
|
|
284
309
|
const token = await getToken()
|
|
310
|
+
signal?.throwIfAborted()
|
|
285
311
|
const init: RequestInit = {
|
|
286
312
|
method,
|
|
287
313
|
headers: {
|
|
@@ -292,15 +318,25 @@ async function armCall<T>(
|
|
|
292
318
|
if (body !== undefined) {
|
|
293
319
|
init.body = JSON.stringify(body)
|
|
294
320
|
}
|
|
321
|
+
if (signal !== undefined) init.signal = signal
|
|
295
322
|
const res = await fetch(url, init)
|
|
323
|
+
signal?.throwIfAborted()
|
|
296
324
|
if (!res.ok) {
|
|
325
|
+
if (acceptedStatuses.includes(res.status)) {
|
|
326
|
+
await res.text()
|
|
327
|
+
signal?.throwIfAborted()
|
|
328
|
+
return undefined
|
|
329
|
+
}
|
|
297
330
|
const text = await res.text()
|
|
331
|
+
signal?.throwIfAborted()
|
|
298
332
|
throw new Error(`ARM ${method} ${url} → ${res.status}: ${text}`)
|
|
299
333
|
}
|
|
300
334
|
if (res.status === 204 || res.status === 202) return undefined
|
|
301
335
|
const ct = res.headers.get('content-type') ?? ''
|
|
302
336
|
if (ct.includes('application/json')) {
|
|
303
|
-
|
|
337
|
+
const json = (await res.json()) as T
|
|
338
|
+
signal?.throwIfAborted()
|
|
339
|
+
return json
|
|
304
340
|
}
|
|
305
341
|
return undefined
|
|
306
342
|
}
|
|
@@ -389,7 +425,9 @@ export function assertNotPubliclyAddressed(config: {
|
|
|
389
425
|
async function spawnAciSandbox(
|
|
390
426
|
config: ACIStandbyPoolBackendInternalConfig,
|
|
391
427
|
options: SandboxBackendOptions,
|
|
428
|
+
readiness: { readonly timeoutMs: number; readonly pollIntervalMs: number },
|
|
392
429
|
): Promise<Sandbox> {
|
|
430
|
+
options.signal?.throwIfAborted()
|
|
393
431
|
assertEnforceable(options)
|
|
394
432
|
assertNotPubliclyAddressed(config)
|
|
395
433
|
const id = generateSandboxId()
|
|
@@ -439,8 +477,23 @@ async function spawnAciSandbox(
|
|
|
439
477
|
|
|
440
478
|
let claimed: ArmContainerGroup | undefined
|
|
441
479
|
try {
|
|
442
|
-
claimed = await armCall<ArmContainerGroup>(
|
|
480
|
+
claimed = await armCall<ArmContainerGroup>(
|
|
481
|
+
armUrl,
|
|
482
|
+
'PUT',
|
|
483
|
+
config.getArmToken,
|
|
484
|
+
body,
|
|
485
|
+
options.signal,
|
|
486
|
+
)
|
|
443
487
|
} catch (err) {
|
|
488
|
+
// The resource name is client-owned even when ARM never returns the
|
|
489
|
+
// claim response. Best-effort reconciliation is therefore possible.
|
|
490
|
+
// ARM still owns the create/delete ordering semantics; a resource that
|
|
491
|
+
// commits after this DELETE needs the deployment's normal resource reaper.
|
|
492
|
+
if (options.signal?.aborted) {
|
|
493
|
+
await runFailureCleanup(async (signal) => {
|
|
494
|
+
await armCall(armUrl, 'DELETE', config.getArmToken, undefined, signal)
|
|
495
|
+
})
|
|
496
|
+
}
|
|
444
497
|
throw new Error(
|
|
445
498
|
`aci-standby-pool: failed to claim from pool — ${err instanceof Error ? err.message : String(err)}`,
|
|
446
499
|
)
|
|
@@ -448,30 +501,116 @@ async function spawnAciSandbox(
|
|
|
448
501
|
|
|
449
502
|
const initialIp = claimed?.properties?.ipAddress?.ip
|
|
450
503
|
let ip = initialIp
|
|
504
|
+
const readinessDeadline = new OperationDeadline(
|
|
505
|
+
readiness.timeoutMs,
|
|
506
|
+
'aci-standby-pool readiness',
|
|
507
|
+
options.signal,
|
|
508
|
+
)
|
|
451
509
|
try {
|
|
452
510
|
if (!ip) {
|
|
453
511
|
ip = await pollForRunningIp(
|
|
454
512
|
armUrl,
|
|
455
513
|
config.getArmToken,
|
|
456
|
-
|
|
457
|
-
|
|
514
|
+
readinessDeadline,
|
|
515
|
+
readiness.pollIntervalMs,
|
|
516
|
+
readiness.timeoutMs,
|
|
458
517
|
)
|
|
459
518
|
}
|
|
460
519
|
|
|
461
520
|
const baseUrl = `http://${ip}:${workerPort}`
|
|
462
521
|
await waitForWorkerReady(
|
|
463
522
|
baseUrl,
|
|
464
|
-
|
|
465
|
-
|
|
523
|
+
readinessDeadline,
|
|
524
|
+
readiness.timeoutMs,
|
|
525
|
+
readiness.pollIntervalMs,
|
|
466
526
|
)
|
|
467
527
|
|
|
468
|
-
|
|
528
|
+
type Lifecycle = 'active' | 'retiring' | 'destroyed'
|
|
529
|
+
let activeExecutions = 0
|
|
530
|
+
let lifecycle: Lifecycle = 'active'
|
|
531
|
+
let retirementPromise:
|
|
532
|
+
| Promise<{ readonly accepted: boolean; readonly error?: Error }>
|
|
533
|
+
| undefined
|
|
534
|
+
let teardownPromise: Promise<void> | undefined
|
|
535
|
+
let teardownComplete = false
|
|
469
536
|
const rootDir = config.layout.outputs.containerPath
|
|
537
|
+
const workerClient = new HttpWorkerClient(baseUrl)
|
|
538
|
+
const assertActive = (): void => {
|
|
539
|
+
if (lifecycle !== 'active') {
|
|
540
|
+
throw new Error(`Sandbox ${id} is ${lifecycle}; no new worker operation can be admitted`)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const teardownSandbox = (signal?: AbortSignal): Promise<void> => {
|
|
544
|
+
lifecycle = 'retiring'
|
|
545
|
+
if (teardownComplete) return Promise.resolve()
|
|
546
|
+
if (teardownPromise) return teardownPromise
|
|
547
|
+
const attempt = armCall(
|
|
548
|
+
armUrl,
|
|
549
|
+
'DELETE',
|
|
550
|
+
config.getArmToken,
|
|
551
|
+
undefined,
|
|
552
|
+
signal,
|
|
553
|
+
[404, 410],
|
|
554
|
+
).then(() => undefined)
|
|
555
|
+
const shared = attempt.then(
|
|
556
|
+
() => {
|
|
557
|
+
teardownComplete = true
|
|
558
|
+
lifecycle = 'destroyed'
|
|
559
|
+
},
|
|
560
|
+
(error: unknown) => {
|
|
561
|
+
if (teardownPromise === shared) teardownPromise = undefined
|
|
562
|
+
throw error
|
|
563
|
+
},
|
|
564
|
+
)
|
|
565
|
+
teardownPromise = shared
|
|
566
|
+
return shared
|
|
567
|
+
}
|
|
568
|
+
const retire = (): Promise<{
|
|
569
|
+
readonly accepted: boolean
|
|
570
|
+
readonly error?: Error
|
|
571
|
+
}> => {
|
|
572
|
+
lifecycle = 'retiring'
|
|
573
|
+
if (retirementPromise) return retirementPromise
|
|
574
|
+
const deadline = new OperationDeadline(5_000, `ACI sandbox ${id} retirement`)
|
|
575
|
+
retirementPromise = deadline
|
|
576
|
+
.run(async (signal) => {
|
|
577
|
+
const joinedExistingAttempt = teardownPromise !== undefined
|
|
578
|
+
try {
|
|
579
|
+
await teardownSandbox(signal)
|
|
580
|
+
} catch (error) {
|
|
581
|
+
if (!joinedExistingAttempt || signal.aborted) throw error
|
|
582
|
+
await teardownSandbox(signal)
|
|
583
|
+
}
|
|
584
|
+
})
|
|
585
|
+
.then(() => {
|
|
586
|
+
return { accepted: true as const }
|
|
587
|
+
})
|
|
588
|
+
.catch((error: unknown) => ({
|
|
589
|
+
accepted: false as const,
|
|
590
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
591
|
+
}))
|
|
592
|
+
return retirementPromise
|
|
593
|
+
}
|
|
594
|
+
const runExecution = async <T>(operation: () => Promise<T>): Promise<T> => {
|
|
595
|
+
assertActive()
|
|
596
|
+
activeExecutions += 1
|
|
597
|
+
try {
|
|
598
|
+
return await operation()
|
|
599
|
+
} catch (error) {
|
|
600
|
+
if (error instanceof RemoteCancellationUnknownError) {
|
|
601
|
+
error.retirement = await retire()
|
|
602
|
+
}
|
|
603
|
+
throw error
|
|
604
|
+
} finally {
|
|
605
|
+
activeExecutions = Math.max(0, activeExecutions - 1)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
470
608
|
|
|
471
609
|
return {
|
|
472
610
|
id,
|
|
473
611
|
get status(): SandboxStatus {
|
|
474
|
-
return
|
|
612
|
+
if (lifecycle !== 'active') return 'destroyed'
|
|
613
|
+
return activeExecutions > 0 ? 'busy' : 'ready'
|
|
475
614
|
},
|
|
476
615
|
rootDir,
|
|
477
616
|
environment: detectEnvironment(),
|
|
@@ -481,15 +620,11 @@ async function spawnAciSandbox(
|
|
|
481
620
|
argv?: string[],
|
|
482
621
|
opts?: SandboxExecOptions,
|
|
483
622
|
): Promise<SandboxExecResult> {
|
|
484
|
-
|
|
485
|
-
try {
|
|
486
|
-
return await execViaWorker(baseUrl, command, argv, opts)
|
|
487
|
-
} finally {
|
|
488
|
-
status = 'ready'
|
|
489
|
-
}
|
|
623
|
+
return await runExecution(async () => await workerClient.exec(command, argv, opts))
|
|
490
624
|
},
|
|
491
625
|
|
|
492
626
|
async writeFile(path: string, content: string | Buffer): Promise<void> {
|
|
627
|
+
assertActive()
|
|
493
628
|
const buf = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf8')
|
|
494
629
|
const res = await fetch(`${baseUrl}/write-file`, {
|
|
495
630
|
method: 'POST',
|
|
@@ -506,6 +641,7 @@ async function spawnAciSandbox(
|
|
|
506
641
|
},
|
|
507
642
|
|
|
508
643
|
async readFile(path: string): Promise<Buffer> {
|
|
644
|
+
assertActive()
|
|
509
645
|
const res = await fetch(`${baseUrl}/read-file`, {
|
|
510
646
|
method: 'POST',
|
|
511
647
|
headers: { 'content-type': 'application/json' },
|
|
@@ -514,7 +650,11 @@ async function spawnAciSandbox(
|
|
|
514
650
|
if (!res.ok) {
|
|
515
651
|
throw new Error(`read-file failed: HTTP ${res.status} ${await res.text()}`)
|
|
516
652
|
}
|
|
517
|
-
const json = (await res.json()) as {
|
|
653
|
+
const json = (await res.json()) as {
|
|
654
|
+
ok: boolean
|
|
655
|
+
content?: string
|
|
656
|
+
error?: string
|
|
657
|
+
}
|
|
518
658
|
if (!json.ok || typeof json.content !== 'string') {
|
|
519
659
|
throw new Error(json.error ?? 'read-file: no content')
|
|
520
660
|
}
|
|
@@ -522,11 +662,15 @@ async function spawnAciSandbox(
|
|
|
522
662
|
},
|
|
523
663
|
|
|
524
664
|
async listFiles(rootPath: string): Promise<readonly SandboxFileEntry[]> {
|
|
525
|
-
return await listFilesViaWorker(
|
|
665
|
+
return await runExecution(async () => await listFilesViaWorker(workerClient, rootPath))
|
|
526
666
|
},
|
|
527
667
|
|
|
528
|
-
async destroy(): Promise<void> {
|
|
529
|
-
|
|
668
|
+
async destroy(options?: SandboxDestroyOptions): Promise<void> {
|
|
669
|
+
if (retirementPromise) {
|
|
670
|
+
const observation = await retirementPromise
|
|
671
|
+
if (observation.accepted) return
|
|
672
|
+
retirementPromise = undefined
|
|
673
|
+
}
|
|
530
674
|
// ARM DELETE — let failures propagate. The Vandal-side
|
|
531
675
|
// lifecycle wraps this in its own try/catch with logging,
|
|
532
676
|
// so a silently swallowed error here means orphan ACI
|
|
@@ -535,17 +679,13 @@ async function spawnAciSandbox(
|
|
|
535
679
|
// the WARM side topped up; that has nothing to do with
|
|
536
680
|
// cleaning up a CLAIMED instance, which is exclusively the
|
|
537
681
|
// claimer's responsibility.
|
|
538
|
-
await
|
|
682
|
+
await teardownSandbox(options?.signal)
|
|
539
683
|
},
|
|
540
684
|
}
|
|
541
685
|
} catch (err) {
|
|
542
|
-
|
|
543
|
-
await armCall(armUrl, 'DELETE', config.getArmToken)
|
|
544
|
-
}
|
|
545
|
-
// Preserve the readiness failure as the primary error; the
|
|
546
|
-
// caller cannot use a Sandbox handle yet, so best-effort ARM
|
|
547
|
-
// cleanup is the only reliable orphan-prevention hook here.
|
|
548
|
-
}
|
|
686
|
+
await runFailureCleanup(async (signal) => {
|
|
687
|
+
await armCall(armUrl, 'DELETE', config.getArmToken, undefined, signal)
|
|
688
|
+
})
|
|
549
689
|
throw err
|
|
550
690
|
}
|
|
551
691
|
}
|
|
@@ -553,115 +693,58 @@ async function spawnAciSandbox(
|
|
|
553
693
|
async function pollForRunningIp(
|
|
554
694
|
armUrl: string,
|
|
555
695
|
getToken: ArmTokenProvider,
|
|
696
|
+
deadline: OperationDeadline,
|
|
556
697
|
pollIntervalMs: number,
|
|
557
698
|
timeoutMs: number,
|
|
558
699
|
): Promise<string> {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
700
|
+
while (deadline.remainingMs() > 0) {
|
|
701
|
+
let cg: ArmContainerGroup | undefined
|
|
702
|
+
try {
|
|
703
|
+
cg = await deadline.run((signal) =>
|
|
704
|
+
armCall<ArmContainerGroup>(armUrl, 'GET', getToken, undefined, signal),
|
|
705
|
+
)
|
|
706
|
+
} catch (err) {
|
|
707
|
+
if (err instanceof OperationDeadlineExpired) break
|
|
708
|
+
throw err
|
|
709
|
+
}
|
|
562
710
|
const state = cg?.properties?.provisioningState
|
|
563
711
|
const ip = cg?.properties?.ipAddress?.ip
|
|
564
712
|
if (state === 'Succeeded' && ip) return ip
|
|
565
713
|
if (state === 'Failed') {
|
|
566
714
|
throw new Error('aci-standby-pool: container group provisioning failed')
|
|
567
715
|
}
|
|
568
|
-
|
|
716
|
+
try {
|
|
717
|
+
await deadline.delay(pollIntervalMs)
|
|
718
|
+
} catch (err) {
|
|
719
|
+
if (err instanceof OperationDeadlineExpired) break
|
|
720
|
+
throw err
|
|
721
|
+
}
|
|
569
722
|
}
|
|
570
723
|
throw new Error(`aci-standby-pool: timed out waiting for container group IP (${timeoutMs}ms)`)
|
|
571
724
|
}
|
|
572
725
|
|
|
573
726
|
async function waitForWorkerReady(
|
|
574
727
|
baseUrl: string,
|
|
728
|
+
deadline: OperationDeadline,
|
|
575
729
|
timeoutMs: number,
|
|
576
730
|
pollIntervalMs: number,
|
|
577
731
|
): Promise<void> {
|
|
578
|
-
|
|
579
|
-
while (Date.now() < deadline) {
|
|
732
|
+
while (deadline.remainingMs() > 0) {
|
|
580
733
|
try {
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
if (
|
|
585
|
-
} catch {
|
|
734
|
+
const result = await deadline.run((signal) => probeHttpHealth(`${baseUrl}/healthz`, signal))
|
|
735
|
+
if (result.ok) return
|
|
736
|
+
} catch (err) {
|
|
737
|
+
if (err instanceof OperationDeadlineExpired) break
|
|
586
738
|
// Network not ready yet, try again.
|
|
587
739
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
async function execViaWorker(
|
|
594
|
-
baseUrl: string,
|
|
595
|
-
command: string,
|
|
596
|
-
argv: string[] | undefined,
|
|
597
|
-
opts: SandboxExecOptions | undefined,
|
|
598
|
-
): Promise<SandboxExecResult> {
|
|
599
|
-
const start = Date.now()
|
|
600
|
-
const res = await fetch(`${baseUrl}/execute`, {
|
|
601
|
-
method: 'POST',
|
|
602
|
-
headers: { 'content-type': 'application/json' },
|
|
603
|
-
body: JSON.stringify({
|
|
604
|
-
command,
|
|
605
|
-
args: argv ?? [],
|
|
606
|
-
cwd: opts?.cwd,
|
|
607
|
-
env: opts?.env,
|
|
608
|
-
timeoutMs: opts?.timeout,
|
|
609
|
-
}),
|
|
610
|
-
})
|
|
611
|
-
if (!res.ok || !res.body) {
|
|
612
|
-
throw new Error(`execute failed: HTTP ${res.status} ${await res.text()}`)
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
let stdout = ''
|
|
616
|
-
let stderr = ''
|
|
617
|
-
let exitCode = -1
|
|
618
|
-
let timedOut = false
|
|
619
|
-
|
|
620
|
-
const decoder = new TextDecoder()
|
|
621
|
-
const reader = res.body.getReader()
|
|
622
|
-
let buffered = ''
|
|
623
|
-
for (;;) {
|
|
624
|
-
const { value, done } = await reader.read()
|
|
625
|
-
if (done) break
|
|
626
|
-
buffered += decoder.decode(value, { stream: true })
|
|
627
|
-
let newlineIdx = buffered.indexOf('\n')
|
|
628
|
-
while (newlineIdx !== -1) {
|
|
629
|
-
const line = buffered.slice(0, newlineIdx).trim()
|
|
630
|
-
buffered = buffered.slice(newlineIdx + 1)
|
|
631
|
-
if (line) {
|
|
632
|
-
try {
|
|
633
|
-
const event = JSON.parse(line) as
|
|
634
|
-
| { type: 'stdout_delta'; data: string }
|
|
635
|
-
| { type: 'stderr_delta'; data: string }
|
|
636
|
-
| { type: 'result'; exitCode: number; timedOut: boolean }
|
|
637
|
-
| { type: 'error'; error: string }
|
|
638
|
-
if (event.type === 'stdout_delta') {
|
|
639
|
-
stdout += event.data
|
|
640
|
-
opts?.onOutput?.({ stream: 'stdout', data: event.data })
|
|
641
|
-
} else if (event.type === 'stderr_delta') {
|
|
642
|
-
stderr += event.data
|
|
643
|
-
opts?.onOutput?.({ stream: 'stderr', data: event.data })
|
|
644
|
-
} else if (event.type === 'result') {
|
|
645
|
-
exitCode = event.exitCode
|
|
646
|
-
timedOut = event.timedOut
|
|
647
|
-
} else if (event.type === 'error') {
|
|
648
|
-
throw new Error(event.error)
|
|
649
|
-
}
|
|
650
|
-
} catch (err) {
|
|
651
|
-
if (!(err instanceof SyntaxError)) throw err
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
newlineIdx = buffered.indexOf('\n')
|
|
740
|
+
try {
|
|
741
|
+
await deadline.delay(pollIntervalMs)
|
|
742
|
+
} catch (err) {
|
|
743
|
+
if (err instanceof OperationDeadlineExpired) break
|
|
744
|
+
throw err
|
|
655
745
|
}
|
|
656
746
|
}
|
|
657
|
-
|
|
658
|
-
return {
|
|
659
|
-
stdout,
|
|
660
|
-
stderr,
|
|
661
|
-
exitCode,
|
|
662
|
-
timedOut,
|
|
663
|
-
durationMs: Date.now() - start,
|
|
664
|
-
} as SandboxExecResult
|
|
747
|
+
throw new Error(`aci-standby-pool: worker /healthz never responded (${timeoutMs}ms)`)
|
|
665
748
|
}
|
|
666
749
|
|
|
667
750
|
/**
|
|
@@ -673,11 +756,10 @@ async function execViaWorker(
|
|
|
673
756
|
* have produced anything in `rootPath` yet.
|
|
674
757
|
*/
|
|
675
758
|
async function listFilesViaWorker(
|
|
676
|
-
|
|
759
|
+
workerClient: HttpWorkerClient,
|
|
677
760
|
rootPath: string,
|
|
678
761
|
): Promise<readonly SandboxFileEntry[]> {
|
|
679
|
-
const result = await
|
|
680
|
-
baseUrl,
|
|
762
|
+
const result = await workerClient.exec(
|
|
681
763
|
'find',
|
|
682
764
|
[rootPath, '-type', 'f', '-printf', '%p\t%s\n'],
|
|
683
765
|
undefined,
|