@cloudflare/sandbox 0.12.1 → 0.12.3
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/dist/bridge/index.js +22 -18
- package/dist/bridge/index.js.map +1 -1
- package/dist/{contexts-B0_bcx9f.d.ts → contexts-mjA8ZsDG.d.ts} +65 -2
- package/dist/contexts-mjA8ZsDG.d.ts.map +1 -0
- package/dist/{errors-aRUdk9K8.js → errors-CpkKPbWC.js} +8 -2
- package/dist/errors-CpkKPbWC.js.map +1 -0
- package/dist/index.d.ts +42 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/openai/index.d.ts +1 -1
- package/dist/opencode/index.d.ts +2 -2
- package/dist/opencode/index.js +1 -1
- package/dist/{sandbox-C8l-pMlL.d.ts → sandbox-BhIQBik-.d.ts} +78 -8
- package/dist/sandbox-BhIQBik-.d.ts.map +1 -0
- package/dist/{sandbox-DKG3H156.js → sandbox-DI6suZAc.js} +965 -241
- package/dist/sandbox-DI6suZAc.js.map +1 -0
- package/package.json +3 -3
- package/dist/contexts-B0_bcx9f.d.ts.map +0 -1
- package/dist/errors-aRUdk9K8.js.map +0 -1
- package/dist/sandbox-C8l-pMlL.d.ts.map +0 -1
- package/dist/sandbox-DKG3H156.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { _ as GitLogger, b as getEnvString, c as parseSSEFrames, d as createNoOpLogger, f as TraceContext, g as DEFAULT_GIT_CLONE_TIMEOUT_MS, h as ResultImpl, i as isWSStreamChunk, l as shellEscape, m as Execution, n as isWSError, p as logCanonicalEvent, r as isWSResponse, t as generateRequestId, u as createLogger, v as extractRepoName, x as partitionEnvVars, y as filterEnvVars } from "./dist-B_eXrP83.js";
|
|
2
|
-
import { n as getHttpStatus, r as ErrorCode, t as getSuggestion } from "./errors-
|
|
2
|
+
import { n as getHttpStatus, r as ErrorCode, t as getSuggestion } from "./errors-CpkKPbWC.js";
|
|
3
3
|
import { Container, ContainerProxy, getContainer, switchPort } from "@cloudflare/containers";
|
|
4
4
|
import { AwsClient } from "aws4fetch";
|
|
5
5
|
import { RpcSession, RpcTarget } from "capnweb";
|
|
@@ -630,6 +630,26 @@ var BackupRestoreError = class extends SandboxError {
|
|
|
630
630
|
}
|
|
631
631
|
};
|
|
632
632
|
/**
|
|
633
|
+
* Raised when the sandbox container cannot accept an incoming RPC connection.
|
|
634
|
+
* The container may be starting up, temporarily unhealthy, or was replaced
|
|
635
|
+
* while the connection attempt was in progress.
|
|
636
|
+
*
|
|
637
|
+
* Always retryable: the operation that triggered the connection attempt can
|
|
638
|
+
* be retried once the container is ready.
|
|
639
|
+
*/
|
|
640
|
+
var ContainerUnavailableError = class extends SandboxError {
|
|
641
|
+
constructor(errorResponse) {
|
|
642
|
+
super(errorResponse);
|
|
643
|
+
this.name = "ContainerUnavailableError";
|
|
644
|
+
}
|
|
645
|
+
get reason() {
|
|
646
|
+
return this.context.reason;
|
|
647
|
+
}
|
|
648
|
+
get retryAfterMs() {
|
|
649
|
+
return this.context.retryAfterMs;
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
/**
|
|
633
653
|
* Raised when the capnweb WebSocket session itself fails on the SDK side.
|
|
634
654
|
* Unlike the rest of the SandboxError tree, the container never produces
|
|
635
655
|
* this error — it is synthesised by `translateRPCError` from the plain
|
|
@@ -653,6 +673,20 @@ var RPCTransportError = class extends SandboxError {
|
|
|
653
673
|
return this.errorResponse.context.originalMessage;
|
|
654
674
|
}
|
|
655
675
|
};
|
|
676
|
+
/**
|
|
677
|
+
* Raised when a sandbox-owned operation was interrupted by a runtime
|
|
678
|
+
* replacement or sandbox lifetime change after the operation was admitted.
|
|
679
|
+
* The caller can retry the full operation when `context.retryable` is true.
|
|
680
|
+
*/
|
|
681
|
+
var OperationInterruptedError = class extends SandboxError {
|
|
682
|
+
constructor(errorResponse, options) {
|
|
683
|
+
super(errorResponse, options);
|
|
684
|
+
this.name = "OperationInterruptedError";
|
|
685
|
+
}
|
|
686
|
+
get reason() {
|
|
687
|
+
return this.context.reason;
|
|
688
|
+
}
|
|
689
|
+
};
|
|
656
690
|
|
|
657
691
|
//#endregion
|
|
658
692
|
//#region src/errors/adapter.ts
|
|
@@ -710,6 +744,8 @@ function createErrorFromResponse(errorResponse, options) {
|
|
|
710
744
|
case ErrorCode.INTERPRETER_NOT_READY: return new InterpreterNotReadyError(errorResponse);
|
|
711
745
|
case ErrorCode.CONTEXT_NOT_FOUND: return new ContextNotFoundError(errorResponse);
|
|
712
746
|
case ErrorCode.CODE_EXECUTION_ERROR: return new CodeExecutionError(errorResponse);
|
|
747
|
+
case ErrorCode.OPERATION_INTERRUPTED: return new OperationInterruptedError(errorResponse, options);
|
|
748
|
+
case ErrorCode.CONTAINER_UNAVAILABLE: return new ContainerUnavailableError(errorResponse);
|
|
713
749
|
case ErrorCode.RPC_TRANSPORT_ERROR: return new RPCTransportError(errorResponse, options);
|
|
714
750
|
case ErrorCode.VALIDATION_FAILED: return new ValidationFailedError(errorResponse);
|
|
715
751
|
case ErrorCode.INVALID_JSON_RESPONSE:
|
|
@@ -2451,6 +2487,52 @@ function createTunnelsNotImplemented() {
|
|
|
2451
2487
|
} });
|
|
2452
2488
|
}
|
|
2453
2489
|
|
|
2490
|
+
//#endregion
|
|
2491
|
+
//#region src/platform-errors.ts
|
|
2492
|
+
const SUPERSEDED_ISOLATE_PATTERN = /reset because its code was updated|this script has been upgraded/i;
|
|
2493
|
+
const CONNECTION_LOST_PATTERN = /network connection lost/i;
|
|
2494
|
+
const DO_STORAGE_STARTUP_RESET_PATTERN = /internal error while starting up durable object storage caused object to be reset/i;
|
|
2495
|
+
function errorMessageOf(error) {
|
|
2496
|
+
return error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
2497
|
+
}
|
|
2498
|
+
function* selfAndCauses(error) {
|
|
2499
|
+
let current = error;
|
|
2500
|
+
for (let depth = 0; depth < 8 && current != null; depth += 1) {
|
|
2501
|
+
yield current;
|
|
2502
|
+
current = typeof current === "object" ? current.cause : void 0;
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
function isErrorRetryable(error) {
|
|
2506
|
+
if (typeof error !== "object" || error === null) return false;
|
|
2507
|
+
const message = String(error);
|
|
2508
|
+
const typed = error;
|
|
2509
|
+
return typed.retryable === true && typed.overloaded !== true && !message.includes("Durable Object is overloaded");
|
|
2510
|
+
}
|
|
2511
|
+
/**
|
|
2512
|
+
* Whether an error was raised because the current Durable Object isolate was
|
|
2513
|
+
* superseded by a deploy/code update. In-process retries are futile for this
|
|
2514
|
+
* class because the invocation keeps running on the old isolate; callers
|
|
2515
|
+
* should return control to the platform or retry from a fresh request.
|
|
2516
|
+
*/
|
|
2517
|
+
function isDurableObjectCodeUpdateReset(error) {
|
|
2518
|
+
for (const candidate of selfAndCauses(error)) if (SUPERSEDED_ISOLATE_PATTERN.test(errorMessageOf(candidate))) return true;
|
|
2519
|
+
return false;
|
|
2520
|
+
}
|
|
2521
|
+
/**
|
|
2522
|
+
* Whether an error represents a transient platform lifecycle/storage failure,
|
|
2523
|
+
* not an application-level sandbox failure.
|
|
2524
|
+
*/
|
|
2525
|
+
function isPlatformTransientError(error) {
|
|
2526
|
+
for (const candidate of selfAndCauses(error)) {
|
|
2527
|
+
const message = errorMessageOf(candidate);
|
|
2528
|
+
if (SUPERSEDED_ISOLATE_PATTERN.test(message)) return true;
|
|
2529
|
+
if (CONNECTION_LOST_PATTERN.test(message)) return true;
|
|
2530
|
+
if (DO_STORAGE_STARTUP_RESET_PATTERN.test(message)) return true;
|
|
2531
|
+
if (isErrorRetryable(candidate)) return true;
|
|
2532
|
+
}
|
|
2533
|
+
return false;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2454
2536
|
//#endregion
|
|
2455
2537
|
//#region ../shared/src/backup.ts
|
|
2456
2538
|
/**
|
|
@@ -2476,8 +2558,484 @@ function normalizeBackupExcludePattern(pattern) {
|
|
|
2476
2558
|
//#region ../shared/src/internal.ts
|
|
2477
2559
|
const DISABLE_SESSION_TOKEN = "__DISABLE_SESSION__";
|
|
2478
2560
|
|
|
2561
|
+
//#endregion
|
|
2562
|
+
//#region src/current-runtime-identity.ts
|
|
2563
|
+
const CURRENT_RUNTIME_IDENTITY_STORAGE_KEY = "currentRuntimeIdentity";
|
|
2564
|
+
var RuntimeIdentityInactiveError = class extends Error {
|
|
2565
|
+
constructor() {
|
|
2566
|
+
super("Runtime identity is no longer active");
|
|
2567
|
+
this.name = "RuntimeIdentityInactiveError";
|
|
2568
|
+
}
|
|
2569
|
+
};
|
|
2570
|
+
var RuntimeIdentity = class {
|
|
2571
|
+
id;
|
|
2572
|
+
constructor(record) {
|
|
2573
|
+
this.id = record.id;
|
|
2574
|
+
}
|
|
2575
|
+
owns(record) {
|
|
2576
|
+
return record.runtimeIdentityID === this.id;
|
|
2577
|
+
}
|
|
2578
|
+
scope(value) {
|
|
2579
|
+
return {
|
|
2580
|
+
...value,
|
|
2581
|
+
runtimeIdentityID: this.id
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
};
|
|
2585
|
+
var CurrentRuntimeIdentity = class {
|
|
2586
|
+
/**
|
|
2587
|
+
* Runtime identity is stored in Durable Object storage so a reconstructed DO
|
|
2588
|
+
* can still recognize the live container runtime it owns. In-memory state is
|
|
2589
|
+
* only a cache and cannot define runtime-scoped correctness.
|
|
2590
|
+
*/
|
|
2591
|
+
constructor(storage, getContainerState, isContainerRunning) {
|
|
2592
|
+
this.storage = storage;
|
|
2593
|
+
this.getContainerState = getContainerState;
|
|
2594
|
+
this.isContainerRunning = isContainerRunning;
|
|
2595
|
+
}
|
|
2596
|
+
async get() {
|
|
2597
|
+
const status = await this.getStatus();
|
|
2598
|
+
return status.status === "active" ? status.runtime : null;
|
|
2599
|
+
}
|
|
2600
|
+
async getStatus() {
|
|
2601
|
+
const state = await this.getContainerState();
|
|
2602
|
+
if (state.status !== "healthy") return {
|
|
2603
|
+
status: "inactive",
|
|
2604
|
+
reason: "runtime-not-healthy",
|
|
2605
|
+
containerStatus: state.status
|
|
2606
|
+
};
|
|
2607
|
+
if (!this.isContainerRunning()) return {
|
|
2608
|
+
status: "inactive",
|
|
2609
|
+
reason: "runtime-not-running",
|
|
2610
|
+
containerStatus: state.status
|
|
2611
|
+
};
|
|
2612
|
+
const runtime = await this.getStored();
|
|
2613
|
+
if (!runtime) return {
|
|
2614
|
+
status: "inactive",
|
|
2615
|
+
reason: "missing-runtime-id",
|
|
2616
|
+
containerStatus: state.status
|
|
2617
|
+
};
|
|
2618
|
+
return {
|
|
2619
|
+
status: "active",
|
|
2620
|
+
runtime,
|
|
2621
|
+
containerStatus: state.status
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
async getStored(storage = this.storage) {
|
|
2625
|
+
const record = await storage.get(CURRENT_RUNTIME_IDENTITY_STORAGE_KEY) ?? null;
|
|
2626
|
+
return record ? new RuntimeIdentity(record) : null;
|
|
2627
|
+
}
|
|
2628
|
+
async markStarted() {
|
|
2629
|
+
const record = { id: crypto.randomUUID() };
|
|
2630
|
+
await this.storage.put(CURRENT_RUNTIME_IDENTITY_STORAGE_KEY, record);
|
|
2631
|
+
return new RuntimeIdentity(record);
|
|
2632
|
+
}
|
|
2633
|
+
async clear() {
|
|
2634
|
+
await this.storage.delete(CURRENT_RUNTIME_IDENTITY_STORAGE_KEY);
|
|
2635
|
+
}
|
|
2636
|
+
async isActive(runtime) {
|
|
2637
|
+
return (await this.get())?.id === runtime.id;
|
|
2638
|
+
}
|
|
2639
|
+
async assertActive(runtime) {
|
|
2640
|
+
if (!await this.isActive(runtime)) throw new RuntimeIdentityInactiveError();
|
|
2641
|
+
}
|
|
2642
|
+
};
|
|
2643
|
+
|
|
2644
|
+
//#endregion
|
|
2645
|
+
//#region src/sandbox-lifetime.ts
|
|
2646
|
+
const SANDBOX_LIFETIME_STORAGE_KEY = "sandbox:lifetime";
|
|
2647
|
+
var SandboxLifetimeChangedError = class extends Error {
|
|
2648
|
+
constructor() {
|
|
2649
|
+
super("Sandbox lifetime is no longer current");
|
|
2650
|
+
this.name = "SandboxLifetimeChangedError";
|
|
2651
|
+
}
|
|
2652
|
+
};
|
|
2653
|
+
var SandboxLifetime = class {
|
|
2654
|
+
id;
|
|
2655
|
+
generation;
|
|
2656
|
+
createdAt;
|
|
2657
|
+
updatedAt;
|
|
2658
|
+
constructor(record) {
|
|
2659
|
+
this.record = record;
|
|
2660
|
+
this.id = record.id;
|
|
2661
|
+
this.generation = record.generation;
|
|
2662
|
+
this.createdAt = record.createdAt;
|
|
2663
|
+
this.updatedAt = record.updatedAt;
|
|
2664
|
+
}
|
|
2665
|
+
owns(record) {
|
|
2666
|
+
return record.sandboxLifetimeID === this.id;
|
|
2667
|
+
}
|
|
2668
|
+
scope(value) {
|
|
2669
|
+
return {
|
|
2670
|
+
...value,
|
|
2671
|
+
sandboxLifetimeID: this.id
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
};
|
|
2675
|
+
var CurrentSandboxLifetime = class {
|
|
2676
|
+
constructor(storage) {
|
|
2677
|
+
this.storage = storage;
|
|
2678
|
+
}
|
|
2679
|
+
async get() {
|
|
2680
|
+
const record = await this.storage.get(SANDBOX_LIFETIME_STORAGE_KEY) ?? null;
|
|
2681
|
+
return record ? new SandboxLifetime(record) : null;
|
|
2682
|
+
}
|
|
2683
|
+
/**
|
|
2684
|
+
* Returns the current lifetime if one exists, or creates and persists a new
|
|
2685
|
+
* one. The id is stable for the lifetime of the sandbox — it only changes
|
|
2686
|
+
* when `rotate()` is called (on `destroy()`).
|
|
2687
|
+
*/
|
|
2688
|
+
async getOrCreate() {
|
|
2689
|
+
const existing = await this.get();
|
|
2690
|
+
if (existing) return existing;
|
|
2691
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2692
|
+
const record = {
|
|
2693
|
+
id: crypto.randomUUID(),
|
|
2694
|
+
generation: 1,
|
|
2695
|
+
createdAt: now,
|
|
2696
|
+
updatedAt: now
|
|
2697
|
+
};
|
|
2698
|
+
await this.storage.put(SANDBOX_LIFETIME_STORAGE_KEY, record);
|
|
2699
|
+
return new SandboxLifetime(record);
|
|
2700
|
+
}
|
|
2701
|
+
/**
|
|
2702
|
+
* Creates and persists a new lifetime id, incrementing the generation
|
|
2703
|
+
* counter. Called during `sandbox.destroy()` before runtime state is
|
|
2704
|
+
* cleared so in-flight operations can detect the lifetime change.
|
|
2705
|
+
*/
|
|
2706
|
+
async rotate() {
|
|
2707
|
+
const existing = await this.get();
|
|
2708
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2709
|
+
const record = {
|
|
2710
|
+
id: crypto.randomUUID(),
|
|
2711
|
+
generation: (existing?.generation ?? 0) + 1,
|
|
2712
|
+
createdAt: now,
|
|
2713
|
+
updatedAt: now
|
|
2714
|
+
};
|
|
2715
|
+
await this.storage.put(SANDBOX_LIFETIME_STORAGE_KEY, record);
|
|
2716
|
+
return new SandboxLifetime(record);
|
|
2717
|
+
}
|
|
2718
|
+
async isCurrent(lifetime) {
|
|
2719
|
+
return (await this.get())?.id === lifetime.id;
|
|
2720
|
+
}
|
|
2721
|
+
/**
|
|
2722
|
+
* Throws `SandboxLifetimeChangedError` if the given lifetime is no longer
|
|
2723
|
+
* the current one. Used by operation runners to abort operations that
|
|
2724
|
+
* started before a `destroy()` call.
|
|
2725
|
+
*/
|
|
2726
|
+
async assertCurrent(lifetime) {
|
|
2727
|
+
if (!await this.isCurrent(lifetime)) throw new SandboxLifetimeChangedError();
|
|
2728
|
+
}
|
|
2729
|
+
};
|
|
2730
|
+
|
|
2731
|
+
//#endregion
|
|
2732
|
+
//#region src/backup/restore-operation-store.ts
|
|
2733
|
+
const OPERATION_STORAGE_PREFIX = "operations:";
|
|
2734
|
+
function backupRestoreOperationKey(backupId, dir) {
|
|
2735
|
+
return `restore:${backupId}:${dir}`;
|
|
2736
|
+
}
|
|
2737
|
+
function createBackupRestoreOperationRecord(params) {
|
|
2738
|
+
return {
|
|
2739
|
+
operationKey: backupRestoreOperationKey(params.backupId, params.dir),
|
|
2740
|
+
kind: "backup.restore",
|
|
2741
|
+
sandboxLifetimeID: params.sandboxLifetimeID,
|
|
2742
|
+
phase: "validating",
|
|
2743
|
+
status: "running",
|
|
2744
|
+
payload: {
|
|
2745
|
+
backupId: params.backupId,
|
|
2746
|
+
dir: params.dir
|
|
2747
|
+
},
|
|
2748
|
+
createdAt: params.now,
|
|
2749
|
+
updatedAt: params.now
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
var BackupRestoreOperationStore = class {
|
|
2753
|
+
constructor(storage) {
|
|
2754
|
+
this.storage = storage;
|
|
2755
|
+
}
|
|
2756
|
+
async get(operationKey) {
|
|
2757
|
+
return await this.storage.get(this.storageKey(operationKey)) ?? null;
|
|
2758
|
+
}
|
|
2759
|
+
async put(record) {
|
|
2760
|
+
await this.storage.put(this.storageKey(record.operationKey), record);
|
|
2761
|
+
}
|
|
2762
|
+
storageKey(operationKey) {
|
|
2763
|
+
return `${OPERATION_STORAGE_PREFIX}${operationKey}`;
|
|
2764
|
+
}
|
|
2765
|
+
};
|
|
2766
|
+
|
|
2767
|
+
//#endregion
|
|
2768
|
+
//#region src/backup/restore-lifecycle.ts
|
|
2769
|
+
const BACKUP_RESTORE_MAX_RECOVERY_ATTEMPTS = 2;
|
|
2770
|
+
var RestoreLifecycleRunner = class {
|
|
2771
|
+
operationRecords;
|
|
2772
|
+
constructor(deps) {
|
|
2773
|
+
this.deps = deps;
|
|
2774
|
+
this.operationRecords = new BackupRestoreOperationStore(deps.storage);
|
|
2775
|
+
}
|
|
2776
|
+
async execute(params) {
|
|
2777
|
+
return await this.runWithRecovery(async () => {
|
|
2778
|
+
const { lifetime, operation } = await this.startOperation(params.backupId, params.dir);
|
|
2779
|
+
let currentOperation = operation;
|
|
2780
|
+
let runtime;
|
|
2781
|
+
const context = {
|
|
2782
|
+
lifetime,
|
|
2783
|
+
runtimeReady: async () => {
|
|
2784
|
+
try {
|
|
2785
|
+
runtime = await this.captureRuntime();
|
|
2786
|
+
await this.deps.currentLifetime.assertCurrent(lifetime);
|
|
2787
|
+
} catch (error) {
|
|
2788
|
+
throw await this.translateFenceError(error, currentOperation, "validating", "unknown") ?? error;
|
|
2789
|
+
}
|
|
2790
|
+
currentOperation = await this.markRuntimeReady(currentOperation, runtime);
|
|
2791
|
+
return {
|
|
2792
|
+
runtime,
|
|
2793
|
+
operation: currentOperation
|
|
2794
|
+
};
|
|
2795
|
+
},
|
|
2796
|
+
archiveReady: async (archiveSize) => {
|
|
2797
|
+
if (!runtime) throw new Error("Backup restore archiveReady requires runtimeReady()");
|
|
2798
|
+
try {
|
|
2799
|
+
await this.assertFences(runtime, lifetime);
|
|
2800
|
+
} catch (error) {
|
|
2801
|
+
throw await this.translateFenceError(error, currentOperation, currentOperation.phase, true) ?? error;
|
|
2802
|
+
}
|
|
2803
|
+
currentOperation = await this.markArchiveReady(currentOperation, runtime, archiveSize);
|
|
2804
|
+
return {
|
|
2805
|
+
runtime,
|
|
2806
|
+
operation: currentOperation
|
|
2807
|
+
};
|
|
2808
|
+
},
|
|
2809
|
+
verify: async (result) => {
|
|
2810
|
+
if (!runtime) throw new Error("Backup restore verification requires runtimeReady()");
|
|
2811
|
+
try {
|
|
2812
|
+
await this.assertFences(runtime, lifetime);
|
|
2813
|
+
} catch (error) {
|
|
2814
|
+
throw await this.translateFenceError(error, currentOperation, "archive_ready", true) ?? error;
|
|
2815
|
+
}
|
|
2816
|
+
currentOperation = await this.markVerified(currentOperation, runtime, result);
|
|
2817
|
+
return currentOperation;
|
|
2818
|
+
}
|
|
2819
|
+
};
|
|
2820
|
+
try {
|
|
2821
|
+
return await params.attempt(context);
|
|
2822
|
+
} catch (error) {
|
|
2823
|
+
const translatedRPC = await this.translateRPCError(error, currentOperation);
|
|
2824
|
+
if (translatedRPC) throw translatedRPC;
|
|
2825
|
+
const translatedFence = await this.translateFenceError(error, currentOperation, currentOperation.phase, "unknown");
|
|
2826
|
+
if (translatedFence) throw translatedFence;
|
|
2827
|
+
if (!(error instanceof OperationInterruptedError)) await this.markFailed(currentOperation, error);
|
|
2828
|
+
throw error;
|
|
2829
|
+
}
|
|
2830
|
+
});
|
|
2831
|
+
}
|
|
2832
|
+
async runWithRecovery(attempt) {
|
|
2833
|
+
let recoveryAttempts = 0;
|
|
2834
|
+
while (true) try {
|
|
2835
|
+
return await attempt();
|
|
2836
|
+
} catch (error) {
|
|
2837
|
+
if (!(error instanceof OperationInterruptedError)) throw error;
|
|
2838
|
+
if (!error.context.retryable) throw error;
|
|
2839
|
+
if (recoveryAttempts >= BACKUP_RESTORE_MAX_RECOVERY_ATTEMPTS) throw this.createRecoveryExhaustedError(error, recoveryAttempts);
|
|
2840
|
+
recoveryAttempts++;
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
async startOperation(backupId, dir) {
|
|
2844
|
+
const lifetime = await this.deps.currentLifetime.getOrCreate();
|
|
2845
|
+
const operation = createBackupRestoreOperationRecord({
|
|
2846
|
+
sandboxLifetimeID: lifetime.id,
|
|
2847
|
+
backupId,
|
|
2848
|
+
dir,
|
|
2849
|
+
now: (/* @__PURE__ */ new Date()).toISOString()
|
|
2850
|
+
});
|
|
2851
|
+
await this.operationRecords.put(operation);
|
|
2852
|
+
return {
|
|
2853
|
+
lifetime,
|
|
2854
|
+
operation
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
async captureRuntime() {
|
|
2858
|
+
const runtime = await this.deps.currentRuntime.get() ?? await this.deps.currentRuntime.markStarted();
|
|
2859
|
+
await this.deps.currentRuntime.assertActive(runtime);
|
|
2860
|
+
return runtime;
|
|
2861
|
+
}
|
|
2862
|
+
async assertFences(runtime, lifetime) {
|
|
2863
|
+
await this.deps.currentRuntime.assertActive(runtime);
|
|
2864
|
+
await this.deps.currentLifetime.assertCurrent(lifetime);
|
|
2865
|
+
}
|
|
2866
|
+
async markRuntimeReady(operation, runtime) {
|
|
2867
|
+
const next = {
|
|
2868
|
+
...operation,
|
|
2869
|
+
phase: "runtime_ready",
|
|
2870
|
+
runtimeIdentityID: runtime.id,
|
|
2871
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2872
|
+
};
|
|
2873
|
+
await this.operationRecords.put(next);
|
|
2874
|
+
return next;
|
|
2875
|
+
}
|
|
2876
|
+
async markArchiveReady(operation, runtime, archiveSize) {
|
|
2877
|
+
const next = {
|
|
2878
|
+
...operation,
|
|
2879
|
+
phase: "archive_ready",
|
|
2880
|
+
runtimeIdentityID: runtime.id,
|
|
2881
|
+
payload: {
|
|
2882
|
+
...operation.payload,
|
|
2883
|
+
...archiveSize !== void 0 && { archiveSize }
|
|
2884
|
+
},
|
|
2885
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2886
|
+
};
|
|
2887
|
+
await this.operationRecords.put(next);
|
|
2888
|
+
return next;
|
|
2889
|
+
}
|
|
2890
|
+
async markVerified(operation, runtime, result) {
|
|
2891
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2892
|
+
const next = {
|
|
2893
|
+
...operation,
|
|
2894
|
+
phase: "verified",
|
|
2895
|
+
status: "committed",
|
|
2896
|
+
runtimeIdentityID: runtime.id,
|
|
2897
|
+
result,
|
|
2898
|
+
completedAt,
|
|
2899
|
+
updatedAt: completedAt
|
|
2900
|
+
};
|
|
2901
|
+
await this.operationRecords.put(next);
|
|
2902
|
+
return next;
|
|
2903
|
+
}
|
|
2904
|
+
async markFailed(operation, error) {
|
|
2905
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2906
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2907
|
+
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
|
|
2908
|
+
const next = {
|
|
2909
|
+
...operation,
|
|
2910
|
+
phase: "failed",
|
|
2911
|
+
status: "failed",
|
|
2912
|
+
error: {
|
|
2913
|
+
code,
|
|
2914
|
+
message,
|
|
2915
|
+
retryable: false
|
|
2916
|
+
},
|
|
2917
|
+
completedAt,
|
|
2918
|
+
updatedAt: completedAt
|
|
2919
|
+
};
|
|
2920
|
+
await this.operationRecords.put(next);
|
|
2921
|
+
return next;
|
|
2922
|
+
}
|
|
2923
|
+
async markInterrupted(operation, message, retryable) {
|
|
2924
|
+
const next = {
|
|
2925
|
+
...operation,
|
|
2926
|
+
phase: "interrupted",
|
|
2927
|
+
status: "interrupted",
|
|
2928
|
+
error: {
|
|
2929
|
+
code: ErrorCode.OPERATION_INTERRUPTED,
|
|
2930
|
+
message,
|
|
2931
|
+
retryable
|
|
2932
|
+
},
|
|
2933
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2934
|
+
};
|
|
2935
|
+
await this.operationRecords.put(next);
|
|
2936
|
+
return next;
|
|
2937
|
+
}
|
|
2938
|
+
async translateFenceError(error, operation, phase, admitted) {
|
|
2939
|
+
if (!(error instanceof RuntimeIdentityInactiveError || error instanceof SandboxLifetimeChangedError)) return null;
|
|
2940
|
+
const reason = error instanceof RuntimeIdentityInactiveError ? "runtime_replaced" : "sandbox_lifetime_changed";
|
|
2941
|
+
const retryable = reason !== "sandbox_lifetime_changed";
|
|
2942
|
+
const message = "Backup restore was interrupted before completion could be verified";
|
|
2943
|
+
const interrupted = await this.markInterrupted(operation, message, retryable);
|
|
2944
|
+
return this.createInterruptedError({
|
|
2945
|
+
reason,
|
|
2946
|
+
phase,
|
|
2947
|
+
admitted,
|
|
2948
|
+
retryable,
|
|
2949
|
+
message,
|
|
2950
|
+
operation: interrupted
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
async translateRPCError(error, operation) {
|
|
2954
|
+
if (!(error instanceof RPCTransportError)) return null;
|
|
2955
|
+
const message = "Backup restore was interrupted before completion could be verified";
|
|
2956
|
+
const interruptedPhase = operation.phase;
|
|
2957
|
+
const interrupted = await this.markInterrupted(operation, message, true);
|
|
2958
|
+
return this.createInterruptedError({
|
|
2959
|
+
reason: "transport_disposed",
|
|
2960
|
+
phase: interruptedPhase,
|
|
2961
|
+
admitted: "unknown",
|
|
2962
|
+
retryable: true,
|
|
2963
|
+
message,
|
|
2964
|
+
operation: interrupted
|
|
2965
|
+
});
|
|
2966
|
+
}
|
|
2967
|
+
createInterruptedError(params) {
|
|
2968
|
+
return new OperationInterruptedError({
|
|
2969
|
+
message: params.message,
|
|
2970
|
+
code: ErrorCode.OPERATION_INTERRUPTED,
|
|
2971
|
+
httpStatus: 409,
|
|
2972
|
+
context: {
|
|
2973
|
+
reason: params.reason,
|
|
2974
|
+
operation: "backup.restore",
|
|
2975
|
+
phase: params.phase,
|
|
2976
|
+
admitted: params.admitted,
|
|
2977
|
+
retryable: params.retryable
|
|
2978
|
+
},
|
|
2979
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2980
|
+
suggestion: this.suggestionFor(params.retryable)
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2983
|
+
createRecoveryExhaustedError(error, recoveryAttempts) {
|
|
2984
|
+
return new OperationInterruptedError({
|
|
2985
|
+
message: "Backup restore recovery attempts were exhausted",
|
|
2986
|
+
code: ErrorCode.OPERATION_INTERRUPTED,
|
|
2987
|
+
httpStatus: 409,
|
|
2988
|
+
context: {
|
|
2989
|
+
reason: "recovery_exhausted",
|
|
2990
|
+
operation: error.context.operation,
|
|
2991
|
+
phase: "interrupted",
|
|
2992
|
+
admitted: error.context.admitted,
|
|
2993
|
+
retryable: false,
|
|
2994
|
+
recoveryAttempts,
|
|
2995
|
+
maxRecoveryAttempts: BACKUP_RESTORE_MAX_RECOVERY_ATTEMPTS
|
|
2996
|
+
},
|
|
2997
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2998
|
+
suggestion: this.suggestionFor(false)
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
suggestionFor(retryable) {
|
|
3002
|
+
return retryable ? "Retry restoreBackup() with the same backup handle so the SDK can reconcile the restore operation." : "Start a new restoreBackup() call only if restoring this backup is still desired for the current sandbox.";
|
|
3003
|
+
}
|
|
3004
|
+
};
|
|
3005
|
+
|
|
2479
3006
|
//#endregion
|
|
2480
3007
|
//#region src/container-control/connection.ts
|
|
3008
|
+
/**
|
|
3009
|
+
* Attempt to parse a structured CONTAINER_UNAVAILABLE JSON body from a
|
|
3010
|
+
* non-101 upgrade response. Returns a typed ContainerUnavailableError when
|
|
3011
|
+
* the body contains a recognized code, or null if the response is not
|
|
3012
|
+
* JSON / not a container availability error.
|
|
3013
|
+
*
|
|
3014
|
+
* The response body is consumed only once via `clone()` so the caller still
|
|
3015
|
+
* holds the original Response.
|
|
3016
|
+
*/
|
|
3017
|
+
async function tryParseContainerUnavailable(response) {
|
|
3018
|
+
if (!(response.headers.get("content-type") ?? "").includes("application/json")) return null;
|
|
3019
|
+
try {
|
|
3020
|
+
const body = await response.clone().json();
|
|
3021
|
+
if (body.code !== ErrorCode.CONTAINER_UNAVAILABLE) return null;
|
|
3022
|
+
const context = {
|
|
3023
|
+
reason: body.context?.reason === "container_starting" || body.context?.reason === "container_unhealthy" || body.context?.reason === "container_replaced" || body.context?.reason === "rpc_upgrade_failed" ? body.context.reason : "container_replaced",
|
|
3024
|
+
retryable: true,
|
|
3025
|
+
...typeof body.context?.retryAfterMs === "number" && { retryAfterMs: body.context.retryAfterMs }
|
|
3026
|
+
};
|
|
3027
|
+
return createErrorFromResponse({
|
|
3028
|
+
code: ErrorCode.CONTAINER_UNAVAILABLE,
|
|
3029
|
+
message: body.message ?? "Container is unavailable",
|
|
3030
|
+
context,
|
|
3031
|
+
httpStatus: getHttpStatus(ErrorCode.CONTAINER_UNAVAILABLE),
|
|
3032
|
+
suggestion: getSuggestion(ErrorCode.CONTAINER_UNAVAILABLE, context),
|
|
3033
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3034
|
+
});
|
|
3035
|
+
} catch {
|
|
3036
|
+
return null;
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
2481
3039
|
const DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
|
|
2482
3040
|
const DEFAULT_RETRY_TIMEOUT_MS = 12e4;
|
|
2483
3041
|
const MIN_TIME_FOR_RETRY_MS = 15e3;
|
|
@@ -2604,7 +3162,22 @@ var ContainerControlConnection = class {
|
|
|
2604
3162
|
async doConnect() {
|
|
2605
3163
|
try {
|
|
2606
3164
|
const response = await this.fetchUpgradeWithRetry();
|
|
2607
|
-
if (response.status !== 101)
|
|
3165
|
+
if (response.status !== 101) {
|
|
3166
|
+
const structured = await tryParseContainerUnavailable(response);
|
|
3167
|
+
if (structured) throw structured;
|
|
3168
|
+
if (isRetryableWebSocketUpgradeResponse(response)) throw createErrorFromResponse({
|
|
3169
|
+
code: ErrorCode.CONTAINER_UNAVAILABLE,
|
|
3170
|
+
message: `Container was unavailable after exhausting upgrade retry budget (status ${response.status})`,
|
|
3171
|
+
context: {
|
|
3172
|
+
reason: "rpc_upgrade_failed",
|
|
3173
|
+
retryable: true
|
|
3174
|
+
},
|
|
3175
|
+
httpStatus: getHttpStatus(ErrorCode.CONTAINER_UNAVAILABLE),
|
|
3176
|
+
suggestion: getSuggestion(ErrorCode.CONTAINER_UNAVAILABLE, {}),
|
|
3177
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3178
|
+
});
|
|
3179
|
+
throw new Error(`WebSocket upgrade failed: ${response.status} ${response.statusText}`);
|
|
3180
|
+
}
|
|
2608
3181
|
const ws = response.webSocket;
|
|
2609
3182
|
if (!ws) throw new Error("No WebSocket in upgrade response");
|
|
2610
3183
|
ws.accept();
|
|
@@ -2617,6 +3190,7 @@ var ContainerControlConnection = class {
|
|
|
2617
3190
|
} catch (error) {
|
|
2618
3191
|
this.connected = false;
|
|
2619
3192
|
this.transport.abort(error);
|
|
3193
|
+
this.fireOnClose();
|
|
2620
3194
|
this.logger.error("ContainerControlConnection failed", error instanceof Error ? error : new Error(String(error)));
|
|
2621
3195
|
throw error;
|
|
2622
3196
|
}
|
|
@@ -2749,32 +3323,17 @@ const BUSY_POLL_INTERVAL_MS = 1e3;
|
|
|
2749
3323
|
*/
|
|
2750
3324
|
const IDLE_IMPORT_THRESHOLD = 1;
|
|
2751
3325
|
const IDLE_EXPORT_THRESHOLD = 1;
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
*
|
|
2755
|
-
* Two wire formats are supported for backward compatibility with older
|
|
2756
|
-
* container images:
|
|
2757
|
-
*
|
|
2758
|
-
* 1. Propagated error properties (capnweb >= 0.8.0). The container throws a
|
|
2759
|
-
* `ServiceError`-shaped object with own enumerable `code` and `details`
|
|
2760
|
-
* properties. capnweb walks `Object.keys()` and reconstructs those fields
|
|
2761
|
-
* on the SDK side.
|
|
2762
|
-
* 2. Legacy JSON-encoded message. Older containers encoded the structured
|
|
2763
|
-
* payload as a JSON string in `error.message`.
|
|
2764
|
-
*
|
|
2765
|
-
* The JSON-fallback branch can be removed once all older container images are
|
|
2766
|
-
* no longer in service.
|
|
2767
|
-
*/
|
|
2768
|
-
function translateRPCError(error) {
|
|
3326
|
+
function translateRPCError(error, context = {}) {
|
|
3327
|
+
if (error instanceof SandboxError) throw error;
|
|
2769
3328
|
if (error instanceof Error) {
|
|
2770
3329
|
const propagated = error;
|
|
2771
3330
|
if (typeof propagated.code === "string" && Object.hasOwn(ErrorCode, propagated.code)) {
|
|
2772
3331
|
const code = propagated.code;
|
|
2773
|
-
const context = propagated.details && typeof propagated.details === "object" ? propagated.details : {};
|
|
3332
|
+
const context$1 = propagated.details && typeof propagated.details === "object" ? propagated.details : {};
|
|
2774
3333
|
throw createErrorFromResponse({
|
|
2775
3334
|
code,
|
|
2776
3335
|
message: error.message,
|
|
2777
|
-
context,
|
|
3336
|
+
context: context$1,
|
|
2778
3337
|
httpStatus: getHttpStatus(code),
|
|
2779
3338
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2780
3339
|
});
|
|
@@ -2790,7 +3349,8 @@ function translateRPCError(error) {
|
|
|
2790
3349
|
httpStatus: getHttpStatus(payload.code),
|
|
2791
3350
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2792
3351
|
});
|
|
2793
|
-
|
|
3352
|
+
const transportResponse = buildTransportErrorResponse(error);
|
|
3353
|
+
throw createErrorFromResponse(buildInterruptedOperationResponse(transportResponse, context) ?? transportResponse, { cause: error });
|
|
2794
3354
|
}
|
|
2795
3355
|
throw createErrorFromResponse(buildTransportErrorResponse(new Error(String(error))), { cause: error });
|
|
2796
3356
|
}
|
|
@@ -2838,6 +3398,27 @@ function buildTransportErrorResponse(error) {
|
|
|
2838
3398
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2839
3399
|
};
|
|
2840
3400
|
}
|
|
3401
|
+
function buildInterruptedOperationResponse(transportResponse, context) {
|
|
3402
|
+
if (!context.operation) return null;
|
|
3403
|
+
const { kind } = transportResponse.context;
|
|
3404
|
+
if (kind !== "session_disposed" && kind !== "peer_closed" && kind !== "connection_failed") return null;
|
|
3405
|
+
const interruptedContext = {
|
|
3406
|
+
reason: kind === "session_disposed" ? "transport_disposed" : "runtime_replaced",
|
|
3407
|
+
operation: context.operation,
|
|
3408
|
+
phase: "rpc_call",
|
|
3409
|
+
admitted: "unknown",
|
|
3410
|
+
retryable: false
|
|
3411
|
+
};
|
|
3412
|
+
const action = kind === "session_disposed" ? "was closing" : "closed unexpectedly";
|
|
3413
|
+
return {
|
|
3414
|
+
code: ErrorCode.OPERATION_INTERRUPTED,
|
|
3415
|
+
message: `Sandbox operation ${context.operation} was interrupted while the runtime connection ${action}`,
|
|
3416
|
+
context: interruptedContext,
|
|
3417
|
+
httpStatus: getHttpStatus(ErrorCode.OPERATION_INTERRUPTED),
|
|
3418
|
+
suggestion: getSuggestion(ErrorCode.OPERATION_INTERRUPTED, interruptedContext),
|
|
3419
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
2841
3422
|
/**
|
|
2842
3423
|
* Wrap a capnweb RPC stub so that every method call translates errors
|
|
2843
3424
|
* from the JSON wire format into typed SandboxError instances and signals
|
|
@@ -2853,18 +3434,19 @@ function buildTransportErrorResponse(error) {
|
|
|
2853
3434
|
* settles — capnweb keeps the export alive until the stream ends. The
|
|
2854
3435
|
* busy/idle poll on `getStats()` is the source of truth for that.
|
|
2855
3436
|
*/
|
|
2856
|
-
function wrapStub(stub, onCallStarted) {
|
|
3437
|
+
function wrapStub(stub, domain, onCallStarted) {
|
|
2857
3438
|
return new Proxy(stub, { get(target, prop, receiver) {
|
|
2858
3439
|
const value = Reflect.get(target, prop, receiver);
|
|
2859
3440
|
if (typeof value !== "function") return value;
|
|
2860
3441
|
return (...args) => {
|
|
2861
3442
|
onCallStarted();
|
|
3443
|
+
const operation = typeof prop === "string" ? `${domain}.${prop}` : domain;
|
|
2862
3444
|
try {
|
|
2863
3445
|
const result = Reflect.apply(value, target, args);
|
|
2864
|
-
if (result != null && typeof result.then === "function") return result.catch(translateRPCError);
|
|
3446
|
+
if (result != null && typeof result.then === "function") return result.catch((err) => translateRPCError(err, { operation }));
|
|
2865
3447
|
return result;
|
|
2866
3448
|
} catch (err) {
|
|
2867
|
-
translateRPCError(err);
|
|
3449
|
+
translateRPCError(err, { operation });
|
|
2868
3450
|
}
|
|
2869
3451
|
};
|
|
2870
3452
|
} });
|
|
@@ -3003,34 +3585,34 @@ var ContainerControlClient = class {
|
|
|
3003
3585
|
}
|
|
3004
3586
|
}
|
|
3005
3587
|
get commands() {
|
|
3006
|
-
return wrapStub(this.getConnection().rpc().commands, this.renewActivity);
|
|
3588
|
+
return wrapStub(this.getConnection().rpc().commands, "commands", this.renewActivity);
|
|
3007
3589
|
}
|
|
3008
3590
|
get files() {
|
|
3009
|
-
return wrapStub(this.getConnection().rpc().files, this.renewActivity);
|
|
3591
|
+
return wrapStub(this.getConnection().rpc().files, "files", this.renewActivity);
|
|
3010
3592
|
}
|
|
3011
3593
|
get processes() {
|
|
3012
|
-
return wrapStub(this.getConnection().rpc().processes, this.renewActivity);
|
|
3594
|
+
return wrapStub(this.getConnection().rpc().processes, "processes", this.renewActivity);
|
|
3013
3595
|
}
|
|
3014
3596
|
get ports() {
|
|
3015
|
-
return wrapStub(this.getConnection().rpc().ports, this.renewActivity);
|
|
3597
|
+
return wrapStub(this.getConnection().rpc().ports, "ports", this.renewActivity);
|
|
3016
3598
|
}
|
|
3017
3599
|
get git() {
|
|
3018
|
-
return wrapStub(this.getConnection().rpc().git, this.renewActivity);
|
|
3600
|
+
return wrapStub(this.getConnection().rpc().git, "git", this.renewActivity);
|
|
3019
3601
|
}
|
|
3020
3602
|
get utils() {
|
|
3021
|
-
return wrapStub(this.getConnection().rpc().utils, this.renewActivity);
|
|
3603
|
+
return wrapStub(this.getConnection().rpc().utils, "utils", this.renewActivity);
|
|
3022
3604
|
}
|
|
3023
3605
|
get backup() {
|
|
3024
|
-
return wrapStub(this.getConnection().rpc().backup, this.renewActivity);
|
|
3606
|
+
return wrapStub(this.getConnection().rpc().backup, "backup", this.renewActivity);
|
|
3025
3607
|
}
|
|
3026
3608
|
get watch() {
|
|
3027
|
-
return wrapStub(this.getConnection().rpc().watch, this.renewActivity);
|
|
3609
|
+
return wrapStub(this.getConnection().rpc().watch, "watch", this.renewActivity);
|
|
3028
3610
|
}
|
|
3029
3611
|
get tunnels() {
|
|
3030
|
-
return wrapStub(this.getConnection().rpc().tunnels, this.renewActivity);
|
|
3612
|
+
return wrapStub(this.getConnection().rpc().tunnels, "tunnels", this.renewActivity);
|
|
3031
3613
|
}
|
|
3032
3614
|
get interpreter() {
|
|
3033
|
-
return wrapStub(this.getConnection().rpc().interpreter, this.renewActivity);
|
|
3615
|
+
return wrapStub(this.getConnection().rpc().interpreter, "interpreter", this.renewActivity);
|
|
3034
3616
|
}
|
|
3035
3617
|
/**
|
|
3036
3618
|
* Update the upgrade retry budget. Applies to the current connection
|
|
@@ -3055,89 +3637,6 @@ var ContainerControlClient = class {
|
|
|
3055
3637
|
}
|
|
3056
3638
|
};
|
|
3057
3639
|
|
|
3058
|
-
//#endregion
|
|
3059
|
-
//#region src/current-runtime-identity.ts
|
|
3060
|
-
const CURRENT_RUNTIME_IDENTITY_STORAGE_KEY = "currentRuntimeIdentity";
|
|
3061
|
-
var RuntimeIdentityInactiveError = class extends Error {
|
|
3062
|
-
constructor() {
|
|
3063
|
-
super("Runtime identity is no longer active");
|
|
3064
|
-
this.name = "RuntimeIdentityInactiveError";
|
|
3065
|
-
}
|
|
3066
|
-
};
|
|
3067
|
-
var RuntimeIdentity = class {
|
|
3068
|
-
id;
|
|
3069
|
-
constructor(record) {
|
|
3070
|
-
this.id = record.id;
|
|
3071
|
-
}
|
|
3072
|
-
owns(record) {
|
|
3073
|
-
return record.runtimeIdentityID === this.id;
|
|
3074
|
-
}
|
|
3075
|
-
scope(value) {
|
|
3076
|
-
return {
|
|
3077
|
-
...value,
|
|
3078
|
-
runtimeIdentityID: this.id
|
|
3079
|
-
};
|
|
3080
|
-
}
|
|
3081
|
-
};
|
|
3082
|
-
var CurrentRuntimeIdentity = class {
|
|
3083
|
-
/**
|
|
3084
|
-
* Runtime identity is stored in Durable Object storage so a reconstructed DO
|
|
3085
|
-
* can still recognize the live container runtime it owns. In-memory state is
|
|
3086
|
-
* only a cache and cannot define runtime-scoped correctness.
|
|
3087
|
-
*/
|
|
3088
|
-
constructor(storage, getContainerState, isContainerRunning) {
|
|
3089
|
-
this.storage = storage;
|
|
3090
|
-
this.getContainerState = getContainerState;
|
|
3091
|
-
this.isContainerRunning = isContainerRunning;
|
|
3092
|
-
}
|
|
3093
|
-
async get() {
|
|
3094
|
-
const status = await this.getStatus();
|
|
3095
|
-
return status.status === "active" ? status.runtime : null;
|
|
3096
|
-
}
|
|
3097
|
-
async getStatus() {
|
|
3098
|
-
const state = await this.getContainerState();
|
|
3099
|
-
if (state.status !== "healthy") return {
|
|
3100
|
-
status: "inactive",
|
|
3101
|
-
reason: "runtime-not-healthy",
|
|
3102
|
-
containerStatus: state.status
|
|
3103
|
-
};
|
|
3104
|
-
if (!this.isContainerRunning()) return {
|
|
3105
|
-
status: "inactive",
|
|
3106
|
-
reason: "runtime-not-running",
|
|
3107
|
-
containerStatus: state.status
|
|
3108
|
-
};
|
|
3109
|
-
const runtime = await this.getStored();
|
|
3110
|
-
if (!runtime) return {
|
|
3111
|
-
status: "inactive",
|
|
3112
|
-
reason: "missing-runtime-id",
|
|
3113
|
-
containerStatus: state.status
|
|
3114
|
-
};
|
|
3115
|
-
return {
|
|
3116
|
-
status: "active",
|
|
3117
|
-
runtime,
|
|
3118
|
-
containerStatus: state.status
|
|
3119
|
-
};
|
|
3120
|
-
}
|
|
3121
|
-
async getStored(storage = this.storage) {
|
|
3122
|
-
const record = await storage.get(CURRENT_RUNTIME_IDENTITY_STORAGE_KEY) ?? null;
|
|
3123
|
-
return record ? new RuntimeIdentity(record) : null;
|
|
3124
|
-
}
|
|
3125
|
-
async markStarted() {
|
|
3126
|
-
const record = { id: crypto.randomUUID() };
|
|
3127
|
-
await this.storage.put(CURRENT_RUNTIME_IDENTITY_STORAGE_KEY, record);
|
|
3128
|
-
return new RuntimeIdentity(record);
|
|
3129
|
-
}
|
|
3130
|
-
async clear() {
|
|
3131
|
-
await this.storage.delete(CURRENT_RUNTIME_IDENTITY_STORAGE_KEY);
|
|
3132
|
-
}
|
|
3133
|
-
async isActive(runtime) {
|
|
3134
|
-
return (await this.get())?.id === runtime.id;
|
|
3135
|
-
}
|
|
3136
|
-
async assertActive(runtime) {
|
|
3137
|
-
if (!await this.isActive(runtime)) throw new RuntimeIdentityInactiveError();
|
|
3138
|
-
}
|
|
3139
|
-
};
|
|
3140
|
-
|
|
3141
3640
|
//#endregion
|
|
3142
3641
|
//#region src/file-stream.ts
|
|
3143
3642
|
/**
|
|
@@ -4143,6 +4642,18 @@ async function proxyTerminal(stub, sessionId, request, options) {
|
|
|
4143
4642
|
return stub.fetch(switchPort(ptyRequest, 3e3));
|
|
4144
4643
|
}
|
|
4145
4644
|
|
|
4645
|
+
//#endregion
|
|
4646
|
+
//#region src/session-init.ts
|
|
4647
|
+
var SessionInitInvalidatedError = class extends Error {
|
|
4648
|
+
constructor() {
|
|
4649
|
+
super("Default session initialization was invalidated by a container stop");
|
|
4650
|
+
this.name = "SessionInitInvalidatedError";
|
|
4651
|
+
}
|
|
4652
|
+
};
|
|
4653
|
+
function isSessionInitInvalidated(error) {
|
|
4654
|
+
return error instanceof SessionInitInvalidatedError;
|
|
4655
|
+
}
|
|
4656
|
+
|
|
4146
4657
|
//#endregion
|
|
4147
4658
|
//#region src/storage-mount/r2-egress-handler.ts
|
|
4148
4659
|
const XML_NS = "xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"";
|
|
@@ -5058,17 +5569,18 @@ var SandboxControlCallbackImpl = class extends RpcTarget {
|
|
|
5058
5569
|
this.getHandler = getHandler;
|
|
5059
5570
|
this.logger = logger;
|
|
5060
5571
|
}
|
|
5061
|
-
async onTunnelExit(id, port, exitCode) {
|
|
5572
|
+
async onTunnelExit(id, port, exitCode, runId) {
|
|
5062
5573
|
const handler = this.getHandler();
|
|
5063
5574
|
if (!handler) {
|
|
5064
5575
|
this.logger.debug("onTunnelExit: no handler bound; ignoring", {
|
|
5065
5576
|
id,
|
|
5066
5577
|
port,
|
|
5067
|
-
exitCode
|
|
5578
|
+
exitCode,
|
|
5579
|
+
runId
|
|
5068
5580
|
});
|
|
5069
5581
|
return;
|
|
5070
5582
|
}
|
|
5071
|
-
await handler(id, port, exitCode);
|
|
5583
|
+
await handler(id, port, exitCode, runId);
|
|
5072
5584
|
}
|
|
5073
5585
|
};
|
|
5074
5586
|
|
|
@@ -5295,6 +5807,111 @@ async function deleteDNSRecord(args) {
|
|
|
5295
5807
|
});
|
|
5296
5808
|
}
|
|
5297
5809
|
|
|
5810
|
+
//#endregion
|
|
5811
|
+
//#region src/tunnels/random-id.ts
|
|
5812
|
+
const ID_ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
5813
|
+
/**
|
|
5814
|
+
* Generate a URL- and DNS-safe random id. The default 20 characters give
|
|
5815
|
+
* ~100 bits of entropy, which keeps collisions negligible at any realistic
|
|
5816
|
+
* volume without a uniqueness retry loop. 32 divides 256 evenly, so masking
|
|
5817
|
+
* the low 5 bits of each random byte stays uniform.
|
|
5818
|
+
*/
|
|
5819
|
+
function randomId(size = 20) {
|
|
5820
|
+
const bytes = new Uint8Array(size);
|
|
5821
|
+
crypto.getRandomValues(bytes);
|
|
5822
|
+
let id = "";
|
|
5823
|
+
for (let i = 0; i < size; i += 1) id += ID_ALPHABET[bytes[i] & 31];
|
|
5824
|
+
return id;
|
|
5825
|
+
}
|
|
5826
|
+
|
|
5827
|
+
//#endregion
|
|
5828
|
+
//#region src/tunnels/lifecycle.ts
|
|
5829
|
+
const TUNNEL_GET_MAX_RECOVERY_ATTEMPTS = 2;
|
|
5830
|
+
function runtimeRunId() {
|
|
5831
|
+
return `run-${randomId()}`;
|
|
5832
|
+
}
|
|
5833
|
+
function createTunnelInterruptedError(params) {
|
|
5834
|
+
return new OperationInterruptedError({
|
|
5835
|
+
message: params.reason === "recovery_exhausted" ? "Tunnel recovery attempts were exhausted" : "Tunnel operation was interrupted by a sandbox runtime change",
|
|
5836
|
+
code: ErrorCode.OPERATION_INTERRUPTED,
|
|
5837
|
+
httpStatus: 409,
|
|
5838
|
+
context: {
|
|
5839
|
+
reason: params.reason,
|
|
5840
|
+
operation: "tunnel.get",
|
|
5841
|
+
phase: params.phase,
|
|
5842
|
+
admitted: params.admitted,
|
|
5843
|
+
retryable: params.retryable,
|
|
5844
|
+
...params.recoveryAttempts !== void 0 && { recoveryAttempts: params.recoveryAttempts },
|
|
5845
|
+
...params.maxRecoveryAttempts !== void 0 && { maxRecoveryAttempts: params.maxRecoveryAttempts }
|
|
5846
|
+
},
|
|
5847
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5848
|
+
suggestion: params.retryable ? "Retry tunnels.get() for the same port so the SDK can establish a fresh tunnel run." : "Start a new tunnels.get() call only if this tunnel is still desired for the current sandbox."
|
|
5849
|
+
});
|
|
5850
|
+
}
|
|
5851
|
+
function translateTunnelInterruption(error, phase, admitted) {
|
|
5852
|
+
if (error instanceof OperationInterruptedError) return error;
|
|
5853
|
+
if (error instanceof RuntimeIdentityInactiveError) return createTunnelInterruptedError({
|
|
5854
|
+
reason: "runtime_replaced",
|
|
5855
|
+
phase,
|
|
5856
|
+
admitted,
|
|
5857
|
+
retryable: true
|
|
5858
|
+
});
|
|
5859
|
+
if (error instanceof SandboxLifetimeChangedError) return createTunnelInterruptedError({
|
|
5860
|
+
reason: "sandbox_lifetime_changed",
|
|
5861
|
+
phase,
|
|
5862
|
+
admitted,
|
|
5863
|
+
retryable: false
|
|
5864
|
+
});
|
|
5865
|
+
if (error instanceof RPCTransportError) return createTunnelInterruptedError({
|
|
5866
|
+
reason: "transport_disposed",
|
|
5867
|
+
phase,
|
|
5868
|
+
admitted: "unknown",
|
|
5869
|
+
retryable: true
|
|
5870
|
+
});
|
|
5871
|
+
return null;
|
|
5872
|
+
}
|
|
5873
|
+
|
|
5874
|
+
//#endregion
|
|
5875
|
+
//#region src/tunnels/storage.ts
|
|
5876
|
+
const STORAGE_KEY = "tunnels";
|
|
5877
|
+
const META_STORAGE_KEY = "tunnels:meta";
|
|
5878
|
+
async function readMap(storage) {
|
|
5879
|
+
return await storage.get(STORAGE_KEY) ?? {};
|
|
5880
|
+
}
|
|
5881
|
+
async function readMetaMap(storage) {
|
|
5882
|
+
return await storage.get(META_STORAGE_KEY) ?? {};
|
|
5883
|
+
}
|
|
5884
|
+
function computeOptionsHash(options) {
|
|
5885
|
+
if (!options || !options.name) return "v1:quick";
|
|
5886
|
+
return `v1:named:${options.name}`;
|
|
5887
|
+
}
|
|
5888
|
+
function normaliseHash(hash) {
|
|
5889
|
+
return hash.startsWith("v1:") ? hash.slice(3) : hash;
|
|
5890
|
+
}
|
|
5891
|
+
function optionsHashesEqual(a, b) {
|
|
5892
|
+
return normaliseHash(a) === normaliseHash(b);
|
|
5893
|
+
}
|
|
5894
|
+
|
|
5895
|
+
//#endregion
|
|
5896
|
+
//#region src/tunnels/restart.ts
|
|
5897
|
+
async function pruneTunnelsForRestart(storage) {
|
|
5898
|
+
await storage.transaction(async (txn) => {
|
|
5899
|
+
const map = await readMap(txn);
|
|
5900
|
+
const meta = await readMetaMap(txn);
|
|
5901
|
+
const nextMap = {};
|
|
5902
|
+
const nextMeta = {};
|
|
5903
|
+
for (const [portKey, info] of Object.entries(map)) if (info.name) {
|
|
5904
|
+
nextMap[portKey] = info;
|
|
5905
|
+
nextMeta[portKey] = {
|
|
5906
|
+
...meta[portKey] ?? { optionsHash: `v1:named:${info.name}` },
|
|
5907
|
+
needsRespawn: true
|
|
5908
|
+
};
|
|
5909
|
+
}
|
|
5910
|
+
await txn.put(STORAGE_KEY, nextMap);
|
|
5911
|
+
await txn.put(META_STORAGE_KEY, nextMeta);
|
|
5912
|
+
});
|
|
5913
|
+
}
|
|
5914
|
+
|
|
5298
5915
|
//#endregion
|
|
5299
5916
|
//#region src/tunnels/tunnels-handler.ts
|
|
5300
5917
|
/**
|
|
@@ -5302,21 +5919,11 @@ async function deleteDNSRecord(args) {
|
|
|
5302
5919
|
* `createTunnelsHandler(host)` and exposed as `sandbox.tunnels`.
|
|
5303
5920
|
*
|
|
5304
5921
|
* Storage is the source of truth. The DO holds a `Record<portString, TunnelInfo>`
|
|
5305
|
-
* under the `tunnels` storage key
|
|
5306
|
-
*
|
|
5307
|
-
*
|
|
5308
|
-
*
|
|
5309
|
-
*/
|
|
5310
|
-
/** DO storage key for the `port → TunnelInfo` map. */
|
|
5311
|
-
const STORAGE_KEY = "tunnels";
|
|
5312
|
-
/**
|
|
5313
|
-
* Sidecar storage key for per-port metadata the handler needs but the
|
|
5314
|
-
* public `TunnelInfo` shape does not carry: the options hash used to
|
|
5315
|
-
* detect divergent retries, and (for named tunnels) the DNS record id
|
|
5316
|
-
* needed for cleanup. Kept under a separate key so the existing
|
|
5317
|
-
* `tunnels` shape remains a clean `Record<port, TunnelInfo>`.
|
|
5922
|
+
* under the `tunnels` storage key, with internal lifecycle metadata under
|
|
5923
|
+
* `tunnels:meta`. Container restarts drop quick tunnel records and mark named
|
|
5924
|
+
* tunnels for respawn, so public list/get calls only return records that are
|
|
5925
|
+
* usable for the current runtime.
|
|
5318
5926
|
*/
|
|
5319
|
-
const META_STORAGE_KEY = "tunnels:meta";
|
|
5320
5927
|
function validateTunnelPort(port) {
|
|
5321
5928
|
if (!validatePort(port)) throw new SandboxSecurityError(`Invalid port number: ${port}. Must be 1024-65535, excluding reserved ports.`);
|
|
5322
5929
|
}
|
|
@@ -5332,9 +5939,9 @@ function shortId() {
|
|
|
5332
5939
|
* the nested `errorResponse.code`. Used for the few error codes the SDK
|
|
5333
5940
|
* recognises and recovers from (TUNNEL_NOT_FOUND, TUNNEL_ALREADY_RUNNING).
|
|
5334
5941
|
*
|
|
5335
|
-
*
|
|
5336
|
-
*
|
|
5337
|
-
*
|
|
5942
|
+
* Message strings may quote error-code tokens for diagnostics, so matching
|
|
5943
|
+
* only structured fields keeps recovery branches tied to explicit error
|
|
5944
|
+
* contracts.
|
|
5338
5945
|
*/
|
|
5339
5946
|
function hasErrorCode(error, code) {
|
|
5340
5947
|
if (!error || typeof error !== "object") return false;
|
|
@@ -5349,35 +5956,6 @@ function isTunnelNotFoundError(error) {
|
|
|
5349
5956
|
function isTunnelAlreadyRunningError(error) {
|
|
5350
5957
|
return hasErrorCode(error, "TUNNEL_ALREADY_RUNNING");
|
|
5351
5958
|
}
|
|
5352
|
-
async function readMap(storage) {
|
|
5353
|
-
return await storage.get(STORAGE_KEY) ?? {};
|
|
5354
|
-
}
|
|
5355
|
-
async function readMetaMap(storage) {
|
|
5356
|
-
return await storage.get(META_STORAGE_KEY) ?? {};
|
|
5357
|
-
}
|
|
5358
|
-
/**
|
|
5359
|
-
* Stable hash of `options`. Empty/undefined options collapse to the same
|
|
5360
|
-
* hash so `get(port)`, `get(port, {})`, and `get(port, { name: undefined })`
|
|
5361
|
-
* all hit the same cache entry. Named tunnels hash on `name` alone (the
|
|
5362
|
-
* only option today).
|
|
5363
|
-
*
|
|
5364
|
-
* The `v1:` prefix exists so a future addition of a second option (e.g.
|
|
5365
|
-
* `subdomain`) can change the canonical form without colliding with an
|
|
5366
|
-
* older record's hash. Comparison goes through `optionsHashesEqual`, which
|
|
5367
|
-
* normalises legacy unversioned hashes (`quick`, `named:foo`) to their v1
|
|
5368
|
-
* form before equality, so upgrading does not invalidate cached records.
|
|
5369
|
-
*/
|
|
5370
|
-
function computeOptionsHash(options) {
|
|
5371
|
-
if (!options || !options.name) return "v1:quick";
|
|
5372
|
-
return `v1:named:${options.name}`;
|
|
5373
|
-
}
|
|
5374
|
-
/** Strip the optional `v1:` prefix so legacy hashes compare equal. */
|
|
5375
|
-
function normaliseHash(hash) {
|
|
5376
|
-
return hash.startsWith("v1:") ? hash.slice(3) : hash;
|
|
5377
|
-
}
|
|
5378
|
-
function optionsHashesEqual(a, b) {
|
|
5379
|
-
return normaliseHash(a) === normaliseHash(b);
|
|
5380
|
-
}
|
|
5381
5959
|
/**
|
|
5382
5960
|
* Concrete `TunnelsHandler` implementation.
|
|
5383
5961
|
*
|
|
@@ -5442,11 +6020,15 @@ var TunnelsRpcTarget = class extends RpcTarget$1 {
|
|
|
5442
6020
|
validateTunnelPort(port);
|
|
5443
6021
|
if (options?.name !== void 0) validateTunnelName(options.name);
|
|
5444
6022
|
const requestedHash = computeOptionsHash(options);
|
|
5445
|
-
const info = await this.#withPortLock(port, async () => {
|
|
6023
|
+
const info = await this.#withPortLock(port, () => this.#getWithRecovery(port, options, async (recovery) => {
|
|
5446
6024
|
const existing = (await readMap(this.#host.storage))[port.toString()];
|
|
5447
6025
|
if (existing) {
|
|
5448
6026
|
const metaEntry = (await readMetaMap(this.#host.storage))[port.toString()];
|
|
5449
6027
|
if (!optionsHashesEqual(metaEntry?.optionsHash ?? (existing.name ? `v1:named:${existing.name}` : "v1:quick"), requestedHash)) throw new Error(`Tunnel on port ${port} was created with different options. Call destroy(${port}) before changing tunnel options.`);
|
|
6028
|
+
if (!existing.name && await this.#quickTunnelRecordIsStale(metaEntry)) {
|
|
6029
|
+
await this.#stopStaleQuickTunnel(existing, metaEntry);
|
|
6030
|
+
return await this.#provisionQuickTunnel(port, recovery);
|
|
6031
|
+
}
|
|
5450
6032
|
if (metaEntry?.needsRespawn && existing.name) return await this.#provisionNamedTunnel(port, existing.name);
|
|
5451
6033
|
if (existing.name && this.#host.getNamedTunnelConfig) {
|
|
5452
6034
|
const currentConfig = await this.#host.getNamedTunnelConfig();
|
|
@@ -5461,8 +6043,8 @@ var TunnelsRpcTarget = class extends RpcTarget$1 {
|
|
|
5461
6043
|
return existing;
|
|
5462
6044
|
}
|
|
5463
6045
|
if (options?.name) return await this.#provisionNamedTunnel(port, options.name);
|
|
5464
|
-
return await this.#provisionQuickTunnel(port);
|
|
5465
|
-
});
|
|
6046
|
+
return await this.#provisionQuickTunnel(port, recovery);
|
|
6047
|
+
}));
|
|
5466
6048
|
outcome = "success";
|
|
5467
6049
|
return info;
|
|
5468
6050
|
} catch (error) {
|
|
@@ -5479,6 +6061,58 @@ var TunnelsRpcTarget = class extends RpcTarget$1 {
|
|
|
5479
6061
|
});
|
|
5480
6062
|
}
|
|
5481
6063
|
}
|
|
6064
|
+
async #getWithRecovery(port, options, run) {
|
|
6065
|
+
let recoveryAttempts = 0;
|
|
6066
|
+
const recovery = {};
|
|
6067
|
+
while (true) try {
|
|
6068
|
+
return await run(recovery);
|
|
6069
|
+
} catch (error) {
|
|
6070
|
+
const interrupted = translateTunnelInterruption(error, "provisioning", "unknown");
|
|
6071
|
+
if (!interrupted || options?.name || !interrupted.context.retryable) throw error;
|
|
6072
|
+
if (recoveryAttempts >= TUNNEL_GET_MAX_RECOVERY_ATTEMPTS) {
|
|
6073
|
+
await this.#clearQuickTunnelStorage(port);
|
|
6074
|
+
throw createTunnelInterruptedError({
|
|
6075
|
+
reason: "recovery_exhausted",
|
|
6076
|
+
phase: "interrupted",
|
|
6077
|
+
admitted: interrupted.context.admitted,
|
|
6078
|
+
retryable: false,
|
|
6079
|
+
recoveryAttempts,
|
|
6080
|
+
maxRecoveryAttempts: TUNNEL_GET_MAX_RECOVERY_ATTEMPTS
|
|
6081
|
+
});
|
|
6082
|
+
}
|
|
6083
|
+
recoveryAttempts++;
|
|
6084
|
+
if (interrupted.context.reason === "runtime_replaced") recovery.quickRun = void 0;
|
|
6085
|
+
await this.#clearQuickTunnelStorage(port);
|
|
6086
|
+
}
|
|
6087
|
+
}
|
|
6088
|
+
async #clearQuickTunnelStorage(port) {
|
|
6089
|
+
await this.#host.storage.transaction(async (txn) => {
|
|
6090
|
+
const map = await readMap(txn);
|
|
6091
|
+
delete map[port.toString()];
|
|
6092
|
+
await txn.put(STORAGE_KEY, map);
|
|
6093
|
+
const meta = await readMetaMap(txn);
|
|
6094
|
+
delete meta[port.toString()];
|
|
6095
|
+
await txn.put(META_STORAGE_KEY, meta);
|
|
6096
|
+
});
|
|
6097
|
+
}
|
|
6098
|
+
async #quickTunnelRecordIsStale(metaEntry) {
|
|
6099
|
+
if (!this.#host.currentRuntime || !this.#host.currentLifetime) return false;
|
|
6100
|
+
if (!metaEntry?.runtimeIdentityID || !metaEntry.sandboxLifetimeID) return true;
|
|
6101
|
+
const runtime = await this.#host.currentRuntime.get();
|
|
6102
|
+
const lifetime = await this.#host.currentLifetime.getOrCreate();
|
|
6103
|
+
return runtime?.id !== metaEntry.runtimeIdentityID || lifetime.id !== metaEntry.sandboxLifetimeID;
|
|
6104
|
+
}
|
|
6105
|
+
async #stopStaleQuickTunnel(existing, metaEntry) {
|
|
6106
|
+
try {
|
|
6107
|
+
if (metaEntry?.tunnelRunId) await this.#host.client.tunnels.stopTunnelRun({
|
|
6108
|
+
tunnelId: existing.id,
|
|
6109
|
+
runId: metaEntry.tunnelRunId
|
|
6110
|
+
});
|
|
6111
|
+
else await this.#host.client.tunnels.destroyTunnel(existing.id);
|
|
6112
|
+
} catch (error) {
|
|
6113
|
+
if (!isTunnelNotFoundError(error)) throw error;
|
|
6114
|
+
}
|
|
6115
|
+
}
|
|
5482
6116
|
/**
|
|
5483
6117
|
* Provision a fresh quick tunnel and persist it. Caller holds the
|
|
5484
6118
|
* per-port lock.
|
|
@@ -5490,7 +6124,8 @@ var TunnelsRpcTarget = class extends RpcTarget$1 {
|
|
|
5490
6124
|
* surfacing the confusing error — the retry budget caps the loop so a
|
|
5491
6125
|
* persistent failure still surfaces.
|
|
5492
6126
|
*/
|
|
5493
|
-
async #provisionQuickTunnel(port) {
|
|
6127
|
+
async #provisionQuickTunnel(port, recovery) {
|
|
6128
|
+
if (this.#host.currentRuntime && this.#host.currentLifetime) return await this.#provisionQuickTunnelRun(port, recovery);
|
|
5494
6129
|
const MAX_ID_RETRIES = 3;
|
|
5495
6130
|
let lastError;
|
|
5496
6131
|
for (let attempt = 0; attempt < MAX_ID_RETRIES; attempt += 1) {
|
|
@@ -5513,6 +6148,65 @@ var TunnelsRpcTarget = class extends RpcTarget$1 {
|
|
|
5513
6148
|
}
|
|
5514
6149
|
throw lastError ?? /* @__PURE__ */ new Error("Failed to mint a unique quick-tunnel id");
|
|
5515
6150
|
}
|
|
6151
|
+
async #provisionQuickTunnelRun(port, recovery) {
|
|
6152
|
+
if (!this.#host.currentRuntime || !this.#host.currentLifetime) throw new Error("Quick tunnel runtime fences are not configured");
|
|
6153
|
+
const lifetime = await this.#host.currentLifetime.getOrCreate();
|
|
6154
|
+
const runtimeBeforeAdmission = await this.#host.currentRuntime.get();
|
|
6155
|
+
try {
|
|
6156
|
+
await this.#host.currentLifetime.assertCurrent(lifetime);
|
|
6157
|
+
} catch (error) {
|
|
6158
|
+
throw translateTunnelInterruption(error, "runtime_ready", "unknown") ?? error;
|
|
6159
|
+
}
|
|
6160
|
+
recovery.quickRun ??= {
|
|
6161
|
+
tunnelId: `quick-${randomId()}`,
|
|
6162
|
+
runId: runtimeRunId()
|
|
6163
|
+
};
|
|
6164
|
+
const { tunnelId, runId } = recovery.quickRun;
|
|
6165
|
+
let ensureResult;
|
|
6166
|
+
let runtime = runtimeBeforeAdmission;
|
|
6167
|
+
try {
|
|
6168
|
+
ensureResult = await this.#host.client.tunnels.ensureTunnelRun({
|
|
6169
|
+
tunnelId,
|
|
6170
|
+
runId,
|
|
6171
|
+
mode: "quick",
|
|
6172
|
+
port
|
|
6173
|
+
});
|
|
6174
|
+
runtime ??= await this.#host.currentRuntime.get() ?? await this.#host.currentRuntime.markStarted();
|
|
6175
|
+
await this.#host.currentRuntime.assertActive(runtime);
|
|
6176
|
+
await this.#host.currentLifetime.assertCurrent(lifetime);
|
|
6177
|
+
} catch (error) {
|
|
6178
|
+
throw translateTunnelInterruption(error, "runtime_ready", "unknown") ?? error;
|
|
6179
|
+
}
|
|
6180
|
+
const { run } = ensureResult;
|
|
6181
|
+
if (!run.url || !run.hostname) throw new Error("Quick tunnel run did not produce a public URL");
|
|
6182
|
+
const spawned = {
|
|
6183
|
+
id: run.tunnelId,
|
|
6184
|
+
port: run.port,
|
|
6185
|
+
url: run.url,
|
|
6186
|
+
hostname: run.hostname,
|
|
6187
|
+
createdAt: run.startedAt
|
|
6188
|
+
};
|
|
6189
|
+
await this.#host.storage.transaction(async (txn) => {
|
|
6190
|
+
const nextMap = await readMap(txn);
|
|
6191
|
+
nextMap[port.toString()] = spawned;
|
|
6192
|
+
await txn.put(STORAGE_KEY, nextMap);
|
|
6193
|
+
const nextMeta = await readMetaMap(txn);
|
|
6194
|
+
nextMeta[port.toString()] = {
|
|
6195
|
+
optionsHash: "v1:quick",
|
|
6196
|
+
runtimeIdentityID: runtime.id,
|
|
6197
|
+
sandboxLifetimeID: lifetime.id,
|
|
6198
|
+
tunnelRunId: run.runId
|
|
6199
|
+
};
|
|
6200
|
+
await txn.put(META_STORAGE_KEY, nextMeta);
|
|
6201
|
+
});
|
|
6202
|
+
try {
|
|
6203
|
+
await this.#host.currentRuntime.assertActive(runtime);
|
|
6204
|
+
await this.#host.currentLifetime.assertCurrent(lifetime);
|
|
6205
|
+
} catch (error) {
|
|
6206
|
+
throw translateTunnelInterruption(error, "storage_committed", true) ?? error;
|
|
6207
|
+
}
|
|
6208
|
+
return spawned;
|
|
6209
|
+
}
|
|
5516
6210
|
/**
|
|
5517
6211
|
* Provision a named tunnel end-to-end:
|
|
5518
6212
|
* 1. resolve credentials + zone name
|
|
@@ -5623,7 +6317,11 @@ var TunnelsRpcTarget = class extends RpcTarget$1 {
|
|
|
5623
6317
|
await txn.put(META_STORAGE_KEY, currentMeta);
|
|
5624
6318
|
});
|
|
5625
6319
|
try {
|
|
5626
|
-
await this.#host.client.tunnels.
|
|
6320
|
+
if (metaBefore?.tunnelRunId) await this.#host.client.tunnels.stopTunnelRun({
|
|
6321
|
+
tunnelId: existing.id,
|
|
6322
|
+
runId: metaBefore.tunnelRunId
|
|
6323
|
+
});
|
|
6324
|
+
else await this.#host.client.tunnels.destroyTunnel(existing.id);
|
|
5627
6325
|
} catch (error) {
|
|
5628
6326
|
if (isTunnelNotFoundError(error)) {} else if (metaBefore?.dnsRecordId) this.#host.logger.warn("tunnel.destroy: container tunnel cleanup failed", {
|
|
5629
6327
|
port,
|
|
@@ -5704,7 +6402,7 @@ function createTunnelsHandler(host) {
|
|
|
5704
6402
|
return next;
|
|
5705
6403
|
};
|
|
5706
6404
|
const tunnels = new TunnelsRpcTarget(host, withPortLock);
|
|
5707
|
-
const handleTunnelExit = async (id, port, exitCode) => {
|
|
6405
|
+
const handleTunnelExit = async (id, port, exitCode, runId) => {
|
|
5708
6406
|
const startTime = Date.now();
|
|
5709
6407
|
let outcome = "error";
|
|
5710
6408
|
let caughtError;
|
|
@@ -5714,19 +6412,20 @@ function createTunnelsHandler(host) {
|
|
|
5714
6412
|
const map = await readMap(txn);
|
|
5715
6413
|
const existing = map[port.toString()];
|
|
5716
6414
|
if (existing?.id !== id) return;
|
|
6415
|
+
const meta = await readMetaMap(txn);
|
|
6416
|
+
const metaEntry = meta[port.toString()];
|
|
6417
|
+
if (runId && metaEntry?.tunnelRunId && metaEntry.tunnelRunId !== runId) return;
|
|
5717
6418
|
if (existing.name) {
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
optionsHash: meta$1[port.toString()]?.optionsHash ?? `v1:named:${existing.name}`,
|
|
6419
|
+
meta[port.toString()] = {
|
|
6420
|
+
...metaEntry,
|
|
6421
|
+
optionsHash: metaEntry?.optionsHash ?? `v1:named:${existing.name}`,
|
|
5722
6422
|
needsRespawn: true
|
|
5723
6423
|
};
|
|
5724
|
-
await txn.put(META_STORAGE_KEY, meta
|
|
6424
|
+
await txn.put(META_STORAGE_KEY, meta);
|
|
5725
6425
|
return;
|
|
5726
6426
|
}
|
|
5727
6427
|
delete map[port.toString()];
|
|
5728
6428
|
await txn.put(STORAGE_KEY, map);
|
|
5729
|
-
const meta = await readMetaMap(txn);
|
|
5730
6429
|
delete meta[port.toString()];
|
|
5731
6430
|
await txn.put(META_STORAGE_KEY, meta);
|
|
5732
6431
|
});
|
|
@@ -5780,52 +6479,6 @@ function createTunnelsHandler(host) {
|
|
|
5780
6479
|
destroyAll
|
|
5781
6480
|
};
|
|
5782
6481
|
}
|
|
5783
|
-
/**
|
|
5784
|
-
* Reconcile storage with a fresh container.
|
|
5785
|
-
*
|
|
5786
|
-
* Called from `Sandbox.onStart()` after every container restart. The
|
|
5787
|
-
* `cloudflared` processes the container was running all died with it, so
|
|
5788
|
-
* any stored record is *not* currently backed by a running tunnel.
|
|
5789
|
-
*
|
|
5790
|
-
* Two tunnel flavours, two recovery stories:
|
|
5791
|
-
*
|
|
5792
|
-
* - Quick tunnels: the `*.trycloudflare.com` URL is bound to the dead
|
|
5793
|
-
* `cloudflared` process. Nothing on Cloudflare's side outlives the
|
|
5794
|
-
* container, and the URL is unrecoverable. Drop the record from both
|
|
5795
|
-
* maps so the next `get(port)` takes the miss branch and mints a new
|
|
5796
|
-
* URL.
|
|
5797
|
-
* - Named tunnels: the Cloudflare-side tunnel + DNS record survive.
|
|
5798
|
-
* The hostname is stable, the DNS still resolves to
|
|
5799
|
-
* `<tunnelId>.cfargotunnel.com`, and the next caller can reuse both
|
|
5800
|
-
* by walking the same `findTunnelByName` / `upsertCNAME` path the
|
|
5801
|
-
* SDK uses for retries. Keep the record in storage and mark the
|
|
5802
|
-
* meta entry `needsRespawn: true`; the next `get(port, { name })`
|
|
5803
|
-
* cache hit falls through to `#provisionNamedTunnel` to respawn
|
|
5804
|
-
* `cloudflared`.
|
|
5805
|
-
*
|
|
5806
|
-
* Crucially, named-tunnel metadata (including `dnsRecordId`) is
|
|
5807
|
-
* preserved so `destroy(port)` and `sandbox.destroy()` can still clean
|
|
5808
|
-
* up the Cloudflare-side resources after a restart. Wiping meta
|
|
5809
|
-
* unconditionally — the previous behaviour — silently leaked the tunnel
|
|
5810
|
-
* and DNS record on every restart.
|
|
5811
|
-
*/
|
|
5812
|
-
async function pruneTunnelsForRestart(storage) {
|
|
5813
|
-
await storage.transaction(async (txn) => {
|
|
5814
|
-
const map = await readMap(txn);
|
|
5815
|
-
const meta = await readMetaMap(txn);
|
|
5816
|
-
const nextMap = {};
|
|
5817
|
-
const nextMeta = {};
|
|
5818
|
-
for (const [portKey, info] of Object.entries(map)) if (info.name) {
|
|
5819
|
-
nextMap[portKey] = info;
|
|
5820
|
-
nextMeta[portKey] = {
|
|
5821
|
-
...meta[portKey] ?? { optionsHash: `v1:named:${info.name}` },
|
|
5822
|
-
needsRespawn: true
|
|
5823
|
-
};
|
|
5824
|
-
}
|
|
5825
|
-
await txn.put(STORAGE_KEY, nextMap);
|
|
5826
|
-
await txn.put(META_STORAGE_KEY, nextMeta);
|
|
5827
|
-
});
|
|
5828
|
-
}
|
|
5829
6482
|
|
|
5830
6483
|
//#endregion
|
|
5831
6484
|
//#region src/version.ts
|
|
@@ -5834,7 +6487,7 @@ async function pruneTunnelsForRestart(storage) {
|
|
|
5834
6487
|
* This file is auto-updated by .github/changeset-version.ts during releases
|
|
5835
6488
|
* DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
|
|
5836
6489
|
*/
|
|
5837
|
-
const SDK_VERSION = "0.12.
|
|
6490
|
+
const SDK_VERSION = "0.12.3";
|
|
5838
6491
|
|
|
5839
6492
|
//#endregion
|
|
5840
6493
|
//#region src/sandbox.ts
|
|
@@ -5997,6 +6650,38 @@ function applySandboxConfiguration(stub, configuration) {
|
|
|
5997
6650
|
if (configuration.transport !== void 0) operations.push(stub.setTransport?.(configuration.transport) ?? Promise.resolve());
|
|
5998
6651
|
return Promise.all(operations).then(() => void 0);
|
|
5999
6652
|
}
|
|
6653
|
+
function createPlatformInterruptedError(error, operation) {
|
|
6654
|
+
if (!isPlatformTransientError(error)) return null;
|
|
6655
|
+
const context = {
|
|
6656
|
+
reason: "runtime_replaced",
|
|
6657
|
+
operation,
|
|
6658
|
+
phase: "durable_object_call",
|
|
6659
|
+
admitted: "unknown",
|
|
6660
|
+
retryable: false
|
|
6661
|
+
};
|
|
6662
|
+
return new OperationInterruptedError({
|
|
6663
|
+
code: ErrorCode.OPERATION_INTERRUPTED,
|
|
6664
|
+
message: `Sandbox operation ${operation} was interrupted while the platform was updating the sandbox runtime`,
|
|
6665
|
+
context,
|
|
6666
|
+
httpStatus: getHttpStatus(ErrorCode.OPERATION_INTERRUPTED),
|
|
6667
|
+
suggestion: getSuggestion(ErrorCode.OPERATION_INTERRUPTED, context),
|
|
6668
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6669
|
+
}, { cause: error });
|
|
6670
|
+
}
|
|
6671
|
+
function translatePlatformInterruption(error, operation) {
|
|
6672
|
+
throw createPlatformInterruptedError(error, operation) ?? error;
|
|
6673
|
+
}
|
|
6674
|
+
function withSandboxOperationContext(operation, fn) {
|
|
6675
|
+
return (...args) => {
|
|
6676
|
+
try {
|
|
6677
|
+
const result = fn(...args);
|
|
6678
|
+
if (result != null && typeof result.then === "function") return result.catch((error) => translatePlatformInterruption(error, operation));
|
|
6679
|
+
return result;
|
|
6680
|
+
} catch (error) {
|
|
6681
|
+
translatePlatformInterruption(error, operation);
|
|
6682
|
+
}
|
|
6683
|
+
};
|
|
6684
|
+
}
|
|
6000
6685
|
function getSandbox(ns, id, options) {
|
|
6001
6686
|
const sanitizedId = sanitizeSandboxId(id);
|
|
6002
6687
|
const effectiveId = options?.normalizeId ? sanitizedId.toLowerCase() : sanitizedId;
|
|
@@ -6079,11 +6764,15 @@ function getSandbox(ns, id, options) {
|
|
|
6079
6764
|
wsConnect: connect(stub),
|
|
6080
6765
|
tunnels: new Proxy({}, { get: (_, method) => {
|
|
6081
6766
|
if (typeof method !== "string" || method === "then") return void 0;
|
|
6082
|
-
return (...args) => stub.callTunnels(method, args);
|
|
6767
|
+
return withSandboxOperationContext(`sandbox.tunnels.${method}`, (...args) => stub.callTunnels(method, args));
|
|
6083
6768
|
} })
|
|
6084
6769
|
};
|
|
6085
6770
|
return new Proxy(stub, { get(target, prop) {
|
|
6086
|
-
if (typeof prop === "string" && prop in enhancedMethods)
|
|
6771
|
+
if (typeof prop === "string" && prop in enhancedMethods) {
|
|
6772
|
+
const method = enhancedMethods[prop];
|
|
6773
|
+
if (typeof method === "function") return withSandboxOperationContext(`sandbox.${prop}`, method);
|
|
6774
|
+
return method;
|
|
6775
|
+
}
|
|
6087
6776
|
return target[prop];
|
|
6088
6777
|
} });
|
|
6089
6778
|
}
|
|
@@ -6120,6 +6809,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6120
6809
|
activeMounts = /* @__PURE__ */ new Map();
|
|
6121
6810
|
mountOperationQueue = Promise.resolve();
|
|
6122
6811
|
currentRuntime;
|
|
6812
|
+
currentLifetime;
|
|
6123
6813
|
transport = "http";
|
|
6124
6814
|
/**
|
|
6125
6815
|
* True once transport has been written to storage at least once (either
|
|
@@ -6271,6 +6961,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6271
6961
|
sandboxId: this.ctx.id.toString()
|
|
6272
6962
|
});
|
|
6273
6963
|
this.currentRuntime = new CurrentRuntimeIdentity(this.ctx.storage, () => this.getState(), () => this.ctx.container?.running === true);
|
|
6964
|
+
this.currentLifetime = new CurrentSandboxLifetime(this.ctx.storage);
|
|
6274
6965
|
const transportEnv = envObj?.SANDBOX_TRANSPORT;
|
|
6275
6966
|
if (transportEnv === "websocket" || transportEnv === "rpc") this.transport = transportEnv;
|
|
6276
6967
|
else if (transportEnv != null && transportEnv !== "http") this.logger.warn(`Invalid SANDBOX_TRANSPORT value: "${transportEnv}". Must be "http", "websocket", or "rpc". Defaulting to "http".`);
|
|
@@ -7096,6 +7787,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
7096
7787
|
try {
|
|
7097
7788
|
await this.ctx.storage.delete(PORT_TOKENS_STORAGE_KEY);
|
|
7098
7789
|
await this.clearActivePreviewPorts();
|
|
7790
|
+
await this.currentLifetime.rotate();
|
|
7099
7791
|
await this.currentRuntime.clear();
|
|
7100
7792
|
for (const [mountPath, mountInfo] of this.activeMounts.entries()) {
|
|
7101
7793
|
mountsProcessed++;
|
|
@@ -7610,6 +8302,14 @@ var Sandbox = class Sandbox extends Container {
|
|
|
7610
8302
|
this.defaultSessionInit = init;
|
|
7611
8303
|
try {
|
|
7612
8304
|
return await promise;
|
|
8305
|
+
} catch (err) {
|
|
8306
|
+
if (isSessionInitInvalidated(err)) {
|
|
8307
|
+
if (this.defaultSession === sessionId) return this.defaultSession;
|
|
8308
|
+
const freshPending = this.defaultSessionInit;
|
|
8309
|
+
if (freshPending != null && freshPending !== init && freshPending.sessionId === sessionId && freshPending.generation === this.containerGeneration) return freshPending.promise;
|
|
8310
|
+
return await this.initializeDefaultSession(sessionId, this.containerGeneration);
|
|
8311
|
+
}
|
|
8312
|
+
throw err;
|
|
7613
8313
|
} finally {
|
|
7614
8314
|
if (this.defaultSessionInit === init) this.defaultSessionInit = null;
|
|
7615
8315
|
}
|
|
@@ -7627,7 +8327,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
7627
8327
|
placementId = error.containerPlacementId;
|
|
7628
8328
|
this.logger.debug("Session exists in container but not in DO state, syncing", { sessionId });
|
|
7629
8329
|
}
|
|
7630
|
-
if (generation !== this.containerGeneration) throw new
|
|
8330
|
+
if (generation !== this.containerGeneration) throw new SessionInitInvalidatedError();
|
|
7631
8331
|
await this.ctx.storage.put("defaultSession", sessionId);
|
|
7632
8332
|
await this.capturePlacementId(placementId);
|
|
7633
8333
|
this.defaultSession = sessionId;
|
|
@@ -8499,6 +9199,8 @@ var Sandbox = class Sandbox extends Container {
|
|
|
8499
9199
|
storage: this.ctx.storage,
|
|
8500
9200
|
logger: this.logger,
|
|
8501
9201
|
sandboxId: this.ctx.id.toString(),
|
|
9202
|
+
currentRuntime: this.currentRuntime,
|
|
9203
|
+
currentLifetime: this.currentLifetime,
|
|
8502
9204
|
getNamedTunnelConfig: async () => {
|
|
8503
9205
|
const envObj = this.env;
|
|
8504
9206
|
const token = getEnvString(envObj, "CLOUDFLARE_API_TOKEN");
|
|
@@ -9665,11 +10367,24 @@ var Sandbox = class Sandbox extends Container {
|
|
|
9665
10367
|
* Concurrent backup/restore calls on the same sandbox are serialized.
|
|
9666
10368
|
*/
|
|
9667
10369
|
async restoreBackup(backup) {
|
|
9668
|
-
if (backup.localBucket)
|
|
9669
|
-
this.
|
|
9670
|
-
|
|
10370
|
+
if (!backup.localBucket) this.requireBackupBucket();
|
|
10371
|
+
return await this.enqueueBackupOp(() => this.doRestoreBackupWithRecovery(backup));
|
|
10372
|
+
}
|
|
10373
|
+
async doRestoreBackupWithRecovery(backup) {
|
|
10374
|
+
return await new RestoreLifecycleRunner({
|
|
10375
|
+
storage: this.ctx.storage,
|
|
10376
|
+
currentRuntime: this.currentRuntime,
|
|
10377
|
+
currentLifetime: this.currentLifetime
|
|
10378
|
+
}).execute({
|
|
10379
|
+
backupId: backup.id,
|
|
10380
|
+
dir: backup.dir,
|
|
10381
|
+
attempt: async (lifecycle) => {
|
|
10382
|
+
if (backup.localBucket) return await this.doRestoreBackupLocal(backup, lifecycle);
|
|
10383
|
+
return await this.doRestoreBackup(backup, lifecycle);
|
|
10384
|
+
}
|
|
10385
|
+
});
|
|
9671
10386
|
}
|
|
9672
|
-
async doRestoreBackup(backup) {
|
|
10387
|
+
async doRestoreBackup(backup, lifecycle) {
|
|
9673
10388
|
const restoreStartTime = Date.now();
|
|
9674
10389
|
const bucket = this.requireBackupBucket();
|
|
9675
10390
|
this.requirePresignedURLSupport();
|
|
@@ -9736,12 +10451,14 @@ var Sandbox = class Sandbox extends Container {
|
|
|
9736
10451
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9737
10452
|
});
|
|
9738
10453
|
backupSession = await this.ensureBackupSession();
|
|
10454
|
+
await lifecycle.runtimeReady();
|
|
9739
10455
|
const archivePath = `${BACKUP_CONTAINER_DIR}/${id}.sqsh`;
|
|
9740
10456
|
const mountGlob = `${BACKUP_CONTAINER_DIR}/mounts/${id}`;
|
|
9741
10457
|
await this.execWithSession(`/usr/bin/fusermount3 -uz ${shellEscape(dir)} 2>/dev/null || true`, backupSession, { origin: "internal" }).catch(() => {});
|
|
9742
10458
|
await this.execWithSession(`for d in ${shellEscape(mountGlob)}_*/lower ${shellEscape(mountGlob)}/lower; do [ -d "$d" ] && /usr/bin/fusermount3 -uz "$d" 2>/dev/null; done; true`, backupSession, { origin: "internal" }).catch(() => {});
|
|
9743
10459
|
const sizeCheck = await this.execWithSession(`stat -c %s ${shellEscape(archivePath)} 2>/dev/null || echo 0`, backupSession, { origin: "internal" }).catch(() => ({ stdout: "0" }));
|
|
9744
10460
|
if (Number.parseInt((sizeCheck.stdout ?? "0").trim(), 10) !== archiveHead.size) await this.downloadBackupParallel(archivePath, r2Key, archiveHead.size, id, dir, backupSession);
|
|
10461
|
+
await lifecycle.archiveReady(archiveHead.size);
|
|
9745
10462
|
if (!(await this.client.backup.restoreArchive(dir, archivePath, backupSession)).success) throw new BackupRestoreError({
|
|
9746
10463
|
message: "Container failed to restore backup archive",
|
|
9747
10464
|
code: ErrorCode.BACKUP_RESTORE_FAILED,
|
|
@@ -9752,12 +10469,14 @@ var Sandbox = class Sandbox extends Container {
|
|
|
9752
10469
|
},
|
|
9753
10470
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9754
10471
|
});
|
|
9755
|
-
|
|
9756
|
-
return {
|
|
10472
|
+
const result = {
|
|
9757
10473
|
success: true,
|
|
9758
10474
|
dir,
|
|
9759
10475
|
id
|
|
9760
10476
|
};
|
|
10477
|
+
await lifecycle.verify(result);
|
|
10478
|
+
outcome = "success";
|
|
10479
|
+
return result;
|
|
9761
10480
|
} catch (error) {
|
|
9762
10481
|
caughtError = error instanceof Error ? error : new Error(String(error));
|
|
9763
10482
|
if (id && backupSession) {
|
|
@@ -9782,7 +10501,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
9782
10501
|
* Uses the R2 binding directly instead of presigned URLs, and
|
|
9783
10502
|
* unsquashfs for extraction instead of squashfuse + fuse-overlayfs.
|
|
9784
10503
|
*/
|
|
9785
|
-
async doRestoreBackupLocal(backup) {
|
|
10504
|
+
async doRestoreBackupLocal(backup, lifecycle) {
|
|
9786
10505
|
const restoreStartTime = Date.now();
|
|
9787
10506
|
const { id, dir } = backup;
|
|
9788
10507
|
let outcome = "error";
|
|
@@ -9854,7 +10573,9 @@ var Sandbox = class Sandbox extends Container {
|
|
|
9854
10573
|
context: { backupId: id },
|
|
9855
10574
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9856
10575
|
});
|
|
10576
|
+
const archiveSize = metadata.sizeBytes;
|
|
9857
10577
|
backupSession = await this.ensureBackupSession();
|
|
10578
|
+
await lifecycle.runtimeReady();
|
|
9858
10579
|
const archivePath = `${BACKUP_CONTAINER_DIR}/${id}.sqsh`;
|
|
9859
10580
|
await this.execWithSession(`mkdir -p ${BACKUP_CONTAINER_DIR}`, backupSession, { origin: "internal" });
|
|
9860
10581
|
if (this.transport === "rpc") {
|
|
@@ -9885,6 +10606,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
9885
10606
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9886
10607
|
});
|
|
9887
10608
|
}
|
|
10609
|
+
await lifecycle.archiveReady(archiveSize);
|
|
9888
10610
|
const extractResult = await this.execWithSession(`/usr/bin/unsquashfs -f -d ${shellEscape(dir)} ${shellEscape(archivePath)}`, backupSession, { origin: "internal" });
|
|
9889
10611
|
if (extractResult.exitCode !== 0) throw new BackupRestoreError({
|
|
9890
10612
|
message: `unsquashfs extraction failed (exit code ${extractResult.exitCode}): ${extractResult.stderr}`,
|
|
@@ -9896,13 +10618,15 @@ var Sandbox = class Sandbox extends Container {
|
|
|
9896
10618
|
},
|
|
9897
10619
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9898
10620
|
});
|
|
9899
|
-
|
|
9900
|
-
outcome = "success";
|
|
9901
|
-
return {
|
|
10621
|
+
const result = {
|
|
9902
10622
|
success: true,
|
|
9903
10623
|
dir,
|
|
9904
10624
|
id
|
|
9905
10625
|
};
|
|
10626
|
+
await lifecycle.verify(result);
|
|
10627
|
+
await this.execWithSession(`rm -f ${shellEscape(archivePath)}`, backupSession, { origin: "internal" }).catch(() => {});
|
|
10628
|
+
outcome = "success";
|
|
10629
|
+
return result;
|
|
9906
10630
|
} catch (error) {
|
|
9907
10631
|
caughtError = error instanceof Error ? error : new Error(String(error));
|
|
9908
10632
|
if (id && backupSession) {
|
|
@@ -9984,5 +10708,5 @@ var Sandbox = class Sandbox extends Container {
|
|
|
9984
10708
|
};
|
|
9985
10709
|
|
|
9986
10710
|
//#endregion
|
|
9987
|
-
export {
|
|
9988
|
-
//# sourceMappingURL=sandbox-
|
|
10711
|
+
export { PortClient as A, InvalidBackupConfigError as B, collectFile as C, SandboxClient as D, isPlatformTransientError as E, BackupCreateError as F, SessionTerminatedError as G, ProcessExitedBeforeReadyError as H, BackupExpiredError as I, BackupNotFoundError as L, FileClient as M, CommandClient as N, UtilityClient as O, BackupClient as P, BackupRestoreError as R, validateTunnelName as S, isDurableObjectCodeUpdateReset as T, ProcessReadyTimeoutError as U, OperationInterruptedError as V, RPCTransportError as W, responseToAsyncIterable as _, PREVIEW_PROXY_HEADER as a, sanitizeSandboxId as b, PREVIEW_PROXY_SANDBOX_ID_HEADER as c, BucketUnmountError as d, InvalidMountConfigError as f, parseSSEStream as g, asyncIterableToSSEStream as h, proxyTerminal as i, GitClient as j, ProcessClient as k, PREVIEW_PROXY_TOKEN_HEADER as l, S3FSMountError as m, Sandbox as n, PREVIEW_PROXY_HEADERS as o, MissingCredentialsError as p, getSandbox as r, PREVIEW_PROXY_PORT_HEADER as s, ContainerProxy$1 as t, BucketMountError as u, CodeInterpreter as v, streamFile as w, validatePort as x, SandboxSecurityError as y, ContainerUnavailableError as z };
|
|
10712
|
+
//# sourceMappingURL=sandbox-DI6suZAc.js.map
|