mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
fix: start Docker daemon on sandbox startup, not just after install (#3129)
The sandbox mode now starts the Docker daemon whenever it's not running, not only after a fresh install. This handles the common case where OrbStack/Docker is installed but the daemon isn't started yet. Flow: check daemon → if down, check binary → if missing, install → start daemon (open -a OrbStack / systemctl start docker) → poll up to 30s Co-authored-by: spawn-bot <spawn-bot@openrouter.ai> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c1d8acb73e
commit
426ebc9b76
3 changed files with 140 additions and 36 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openrouter/spawn",
|
||||
"version": "0.30.0",
|
||||
"version": "0.30.1",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"spawn": "cli.js"
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ describe("ensureDocker", () => {
|
|||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it("attempts brew install on macOS when docker unavailable", async () => {
|
||||
it("attempts brew install on macOS when docker not installed", async () => {
|
||||
const origPlatform = Object.getOwnPropertyDescriptor(process, "platform");
|
||||
Object.defineProperty(process, "platform", {
|
||||
value: "darwin",
|
||||
|
|
@ -82,19 +82,7 @@ describe("ensureDocker", () => {
|
|||
let callCount = 0;
|
||||
const spy = spyOn(Bun, "spawnSync").mockImplementation((..._args: unknown[]) => {
|
||||
callCount++;
|
||||
// First call: docker info → fail, second: brew install → succeed, third: docker info → succeed
|
||||
if (callCount === 1) {
|
||||
return {
|
||||
exitCode: 1,
|
||||
stdout: new Uint8Array(),
|
||||
stderr: new Uint8Array(),
|
||||
success: false,
|
||||
signalCode: null,
|
||||
resourceUsage: undefined,
|
||||
pid: 1234,
|
||||
} satisfies ReturnType<typeof Bun.spawnSync>;
|
||||
}
|
||||
return {
|
||||
const ok = {
|
||||
exitCode: 0,
|
||||
stdout: new Uint8Array(),
|
||||
stderr: new Uint8Array(),
|
||||
|
|
@ -103,16 +91,37 @@ describe("ensureDocker", () => {
|
|||
resourceUsage: undefined,
|
||||
pid: 1234,
|
||||
} satisfies ReturnType<typeof Bun.spawnSync>;
|
||||
const fail = {
|
||||
exitCode: 1,
|
||||
stdout: new Uint8Array(),
|
||||
stderr: new Uint8Array(),
|
||||
success: false,
|
||||
signalCode: null,
|
||||
resourceUsage: undefined,
|
||||
pid: 1234,
|
||||
} satisfies ReturnType<typeof Bun.spawnSync>;
|
||||
// 1: docker info → fail, 2: which docker → fail (not installed),
|
||||
// 3: brew install → ok, 4: open -a OrbStack → ok, 5: docker info → ok
|
||||
if (callCount <= 2) {
|
||||
return fail;
|
||||
}
|
||||
return ok;
|
||||
});
|
||||
|
||||
await ensureDocker();
|
||||
|
||||
// Second call should be brew install orbstack
|
||||
expect(spy.mock.calls[1][0]).toEqual([
|
||||
// Call 1: docker info, 2: which docker, 3: brew install orbstack
|
||||
expect(spy.mock.calls[2][0]).toEqual([
|
||||
"brew",
|
||||
"install",
|
||||
"orbstack",
|
||||
]);
|
||||
// Call 4: open -a OrbStack (starts daemon)
|
||||
expect(spy.mock.calls[3][0]).toEqual([
|
||||
"open",
|
||||
"-a",
|
||||
"OrbStack",
|
||||
]);
|
||||
|
||||
spy.mockRestore();
|
||||
if (origPlatform) {
|
||||
|
|
|
|||
|
|
@ -68,31 +68,129 @@ export async function interactiveSession(cmd: string): Promise<number> {
|
|||
|
||||
// ─── Docker Sandbox ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Check whether Docker (or OrbStack) is available on the host. */
|
||||
/** Check whether the Docker daemon is running and responsive. */
|
||||
export function isDockerAvailable(): boolean {
|
||||
const result = Bun.spawnSync(
|
||||
[
|
||||
"docker",
|
||||
"info",
|
||||
],
|
||||
{
|
||||
stdio: [
|
||||
"ignore",
|
||||
"ignore",
|
||||
"ignore",
|
||||
return (
|
||||
Bun.spawnSync(
|
||||
[
|
||||
"docker",
|
||||
"info",
|
||||
],
|
||||
},
|
||||
{
|
||||
stdio: [
|
||||
"ignore",
|
||||
"ignore",
|
||||
"ignore",
|
||||
],
|
||||
},
|
||||
).exitCode === 0
|
||||
);
|
||||
return result.exitCode === 0;
|
||||
}
|
||||
|
||||
/** Install Docker if not present, or exit with guidance if install fails. */
|
||||
/** Check whether the docker binary exists (installed but daemon may be stopped). */
|
||||
function isDockerInstalled(): boolean {
|
||||
return (
|
||||
Bun.spawnSync(
|
||||
[
|
||||
"which",
|
||||
"docker",
|
||||
],
|
||||
{
|
||||
stdio: [
|
||||
"ignore",
|
||||
"ignore",
|
||||
"ignore",
|
||||
],
|
||||
},
|
||||
).exitCode === 0
|
||||
);
|
||||
}
|
||||
|
||||
/** Try to start the Docker daemon and wait up to 30s for it to respond. */
|
||||
function startAndWaitForDocker(isMac: boolean): void {
|
||||
if (isMac) {
|
||||
logStep("Starting OrbStack...");
|
||||
Bun.spawnSync(
|
||||
[
|
||||
"open",
|
||||
"-a",
|
||||
"OrbStack",
|
||||
],
|
||||
{
|
||||
stdio: [
|
||||
"ignore",
|
||||
"ignore",
|
||||
"ignore",
|
||||
],
|
||||
},
|
||||
);
|
||||
} else {
|
||||
logStep("Starting Docker daemon...");
|
||||
const hasSudo =
|
||||
Bun.spawnSync(
|
||||
[
|
||||
"which",
|
||||
"sudo",
|
||||
],
|
||||
{
|
||||
stdio: [
|
||||
"ignore",
|
||||
"ignore",
|
||||
"ignore",
|
||||
],
|
||||
},
|
||||
).exitCode === 0;
|
||||
if (hasSudo) {
|
||||
Bun.spawnSync(
|
||||
[
|
||||
"sudo",
|
||||
"systemctl",
|
||||
"start",
|
||||
"docker",
|
||||
],
|
||||
{
|
||||
stdio: [
|
||||
"ignore",
|
||||
"inherit",
|
||||
"inherit",
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait up to 30s for the daemon to be ready
|
||||
logStep("Waiting for Docker daemon...");
|
||||
for (let i = 0; i < 30; i++) {
|
||||
if (isDockerAvailable()) {
|
||||
logInfo("Docker is ready");
|
||||
return;
|
||||
}
|
||||
Bun.sleepSync(1000);
|
||||
}
|
||||
logInfo("Docker daemon did not start within 30s.");
|
||||
if (isMac) {
|
||||
logInfo("Open OrbStack.app manually, then retry.");
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/** Ensure Docker is installed and the daemon is running. Installs and starts if needed. */
|
||||
export async function ensureDocker(): Promise<void> {
|
||||
// Fast path: daemon already running
|
||||
if (isDockerAvailable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isMac = process.platform === "darwin";
|
||||
|
||||
// Docker binary exists but daemon not running — just start it
|
||||
if (isDockerInstalled()) {
|
||||
startAndWaitForDocker(isMac);
|
||||
return;
|
||||
}
|
||||
|
||||
// Not installed at all — install first
|
||||
if (isMac) {
|
||||
logStep("Docker not found — installing OrbStack...");
|
||||
const result = Bun.spawnSync(
|
||||
|
|
@ -150,11 +248,8 @@ export async function ensureDocker(): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
// Verify Docker works after install
|
||||
if (!isDockerAvailable()) {
|
||||
logInfo("Docker installed but not responding. You may need to start the Docker daemon.");
|
||||
process.exit(1);
|
||||
}
|
||||
// Start the daemon after fresh install
|
||||
startAndWaitForDocker(isMac);
|
||||
}
|
||||
|
||||
/** Pull the agent Docker image and start a container. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue