Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -2305,15 +2305,27 @@ async function createSandbox(
run(`rm -rf "${buildCtx}"`, { ignoreError: true });

if (createResult.status !== 0) {
console.error("");
console.error(` Sandbox creation failed (exit ${createResult.status}).`);
if (createResult.output) {
const failure = classifySandboxCreateFailure(createResult.output);
if (failure.kind === "sandbox_create_incomplete") {
// The sandbox was created in the gateway but the create stream exited
// with a non-zero code (e.g. SSH 255). Fall through to the ready-wait
// loop — the sandbox may still reach Ready on its own.
console.warn("");
console.warn(
` Create stream exited with code ${createResult.status} after sandbox was created.`,
);
console.warn(" Checking whether the sandbox reaches Ready state...");
} else {
console.error("");
console.error(createResult.output);
console.error(` Sandbox creation failed (exit ${createResult.status}).`);
if (createResult.output) {
console.error("");
console.error(createResult.output);
}
console.error(" Try: openshell sandbox list # check gateway state");
printSandboxCreateRecoveryHints(createResult.output);
process.exit(createResult.status || 1);
}
console.error(" Try: openshell sandbox list # check gateway state");
printSandboxCreateRecoveryHints(createResult.output);
process.exit(createResult.status || 1);
}

// Wait for sandbox to reach Ready state in k3s before registering.
Expand Down
44 changes: 44 additions & 0 deletions src/lib/sandbox-create-stream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,50 @@ describe("sandbox-create-stream", () => {
});
});

it("recovers when sandbox is ready at the moment the stream exits non-zero", async () => {
const child = new FakeChild();
const logLine = vi.fn();
const promise = streamSandboxCreate("echo create", process.env, {
spawnImpl: () => child as never,
readyCheck: () => true, // sandbox is already Ready
pollIntervalMs: 60_000, // large interval so the poll doesn't fire first
heartbeatIntervalMs: 1_000,
silentPhaseMs: 10_000,
logLine,
});

child.stdout.emit("data", Buffer.from("Created sandbox: demo\n"));
// SSH 255 — stream exits non-zero after sandbox was created
child.emit("close", 255);

await expect(promise).resolves.toMatchObject({
status: 0,
forcedReady: true,
sawProgress: true,
});
});

it("returns non-zero when readyCheck is false at close time", async () => {
const child = new FakeChild();
const promise = streamSandboxCreate("echo create", process.env, {
spawnImpl: () => child as never,
readyCheck: () => false, // sandbox is NOT ready
pollIntervalMs: 60_000,
heartbeatIntervalMs: 1_000,
silentPhaseMs: 10_000,
logLine: vi.fn(),
});

child.stdout.emit("data", Buffer.from("Created sandbox: demo\n"));
child.emit("close", 255);

await expect(promise).resolves.toMatchObject({
status: 255,
sawProgress: true,
});
expect((await promise).forcedReady).toBeUndefined();
});

it("reports spawn errors cleanly", async () => {
const child = new FakeChild();
const promise = streamSandboxCreate("echo create", process.env, {
Expand Down
12 changes: 12 additions & 0 deletions src/lib/sandbox-create-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,18 @@ export function streamSandboxCreate(
});

child.on("close", (code) => {
// One last ready-check: the sandbox may have become Ready between the
// last poll tick and the stream exit (e.g. SSH 255 after "Created sandbox:").
if (code && code !== 0 && options.readyCheck) {
try {
if (options.readyCheck()) {
finish(0, { forcedReady: true });
return;
}
} catch {
// Ignore — fall through to normal exit handling.
}
}
finish(code ?? 1);
});
});
Expand Down
Loading