mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:40:43 +00:00
fix(sandbox): gracefully handle Docker daemon unavailability when sandbox mode is off (#73671)
Merged via squash.
Prepared head SHA: 378851cf40
Co-authored-by: kaseonedge <15183881+kaseonedge@users.noreply.github.com>
Co-authored-by: sallyom <11166065+sallyom@users.noreply.github.com>
Reviewed-by: @sallyom
This commit is contained in:
@@ -18,7 +18,7 @@ export {
|
|||||||
requireSandboxBackendFactory,
|
requireSandboxBackendFactory,
|
||||||
} from "./sandbox/backend.js";
|
} from "./sandbox/backend.js";
|
||||||
|
|
||||||
export { buildSandboxCreateArgs } from "./sandbox/docker.js";
|
export { buildSandboxCreateArgs, isDockerDaemonUnavailable } from "./sandbox/docker.js";
|
||||||
export {
|
export {
|
||||||
listSandboxBrowsers,
|
listSandboxBrowsers,
|
||||||
listSandboxContainers,
|
listSandboxContainers,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
buildSandboxCreateArgs,
|
buildSandboxCreateArgs,
|
||||||
dockerContainerState,
|
dockerContainerState,
|
||||||
execDocker,
|
execDocker,
|
||||||
|
isDockerDaemonUnavailable,
|
||||||
readDockerContainerEnvVar,
|
readDockerContainerEnvVar,
|
||||||
readDockerContainerLabel,
|
readDockerContainerLabel,
|
||||||
readDockerNetworkDriver,
|
readDockerNetworkDriver,
|
||||||
@@ -130,6 +131,12 @@ async function ensureSandboxBrowserImage(image: string) {
|
|||||||
if (result.code === 0) {
|
if (result.code === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const stderr = result.stderr.trim();
|
||||||
|
// When Docker daemon is unavailable, silently return instead of throwing.
|
||||||
|
// This allows sandbox.mode="off" sessions to start without Docker errors.
|
||||||
|
if (isDockerDaemonUnavailable(stderr)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Sandbox browser image not found: ${image}. Build it with scripts/sandbox-browser-setup.sh.`,
|
`Sandbox browser image not found: ${image}. Build it with scripts/sandbox-browser-setup.sh.`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type MockDockerChild = EventEmitter & {
|
|||||||
const spawnState = vi.hoisted(() => ({
|
const spawnState = vi.hoisted(() => ({
|
||||||
calls: [] as SpawnCall[],
|
calls: [] as SpawnCall[],
|
||||||
imageExists: true,
|
imageExists: true,
|
||||||
|
inspectError: "",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function createMockDockerChild(): MockDockerChild {
|
function createMockDockerChild(): MockDockerChild {
|
||||||
@@ -40,7 +41,9 @@ function spawnDockerProcess(command: string, args: string[]) {
|
|||||||
stderr = `unexpected command: ${command}`;
|
stderr = `unexpected command: ${command}`;
|
||||||
} else if (args[0] === "image" && args[1] === "inspect") {
|
} else if (args[0] === "image" && args[1] === "inspect") {
|
||||||
code = spawnState.imageExists ? 0 : 1;
|
code = spawnState.imageExists ? 0 : 1;
|
||||||
stderr = spawnState.imageExists ? "" : `Error response from daemon: No such image: ${args[2]}`;
|
stderr = spawnState.imageExists
|
||||||
|
? ""
|
||||||
|
: spawnState.inspectError || `Error response from daemon: No such image: ${args[2]}`;
|
||||||
} else if (args[0] === "pull" || args[0] === "tag") {
|
} else if (args[0] === "pull" || args[0] === "tag") {
|
||||||
code = 0;
|
code = 0;
|
||||||
} else {
|
} else {
|
||||||
@@ -79,6 +82,7 @@ describe("ensureDockerImage", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
spawnState.calls.length = 0;
|
spawnState.calls.length = 0;
|
||||||
spawnState.imageExists = true;
|
spawnState.imageExists = true;
|
||||||
|
spawnState.inspectError = "";
|
||||||
await loadFreshDockerModuleForTest();
|
await loadFreshDockerModuleForTest();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -113,4 +117,19 @@ describe("ensureDockerImage", () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns when the Docker daemon is unavailable during image inspection", async () => {
|
||||||
|
spawnState.imageExists = false;
|
||||||
|
spawnState.inspectError =
|
||||||
|
"Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?";
|
||||||
|
|
||||||
|
await ensureDockerImage(DEFAULT_SANDBOX_IMAGE);
|
||||||
|
|
||||||
|
expect(spawnState.calls).toEqual([
|
||||||
|
{
|
||||||
|
command: "docker",
|
||||||
|
args: ["image", "inspect", DEFAULT_SANDBOX_IMAGE],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -272,23 +272,40 @@ export async function readDockerPort(containerName: string, port: number) {
|
|||||||
return Number.isFinite(mapped) ? mapped : null;
|
return Number.isFinite(mapped) ? mapped : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dockerImageExists(image: string) {
|
const DOCKER_DAEMON_UNAVAILABLE_MARKERS = [
|
||||||
|
"cannot connect to the docker daemon",
|
||||||
|
"dial unix",
|
||||||
|
"docker daemon is not running",
|
||||||
|
"connection refused",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function isDockerDaemonUnavailable(stderr: string): boolean {
|
||||||
|
return DOCKER_DAEMON_UNAVAILABLE_MARKERS.some((marker) => stderr.toLowerCase().includes(marker));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inspectDockerImage(image: string): Promise<"exists" | "missing" | "unavailable"> {
|
||||||
const result = await execDocker(["image", "inspect", image], {
|
const result = await execDocker(["image", "inspect", image], {
|
||||||
allowFailure: true,
|
allowFailure: true,
|
||||||
});
|
});
|
||||||
if (result.code === 0) {
|
if (result.code === 0) {
|
||||||
return true;
|
return "exists";
|
||||||
}
|
}
|
||||||
const stderr = result.stderr.trim();
|
const stderr = result.stderr.trim();
|
||||||
if (stderr.includes("No such image")) {
|
if (stderr.toLowerCase().includes("no such image")) {
|
||||||
return false;
|
return "missing";
|
||||||
|
}
|
||||||
|
// When Docker daemon is unavailable, treat the image as unavailable
|
||||||
|
// rather than throwing. This allows sandbox.mode="off" sessions to
|
||||||
|
// start without a running Docker daemon.
|
||||||
|
if (isDockerDaemonUnavailable(stderr)) {
|
||||||
|
return "unavailable";
|
||||||
}
|
}
|
||||||
throw new Error(`Failed to inspect sandbox image: ${stderr}`);
|
throw new Error(`Failed to inspect sandbox image: ${stderr}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureDockerImage(image: string) {
|
export async function ensureDockerImage(image: string) {
|
||||||
const exists = await dockerImageExists(image);
|
const imageState = await inspectDockerImage(image);
|
||||||
if (exists) {
|
if (imageState === "exists" || imageState === "unavailable") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (image === DEFAULT_SANDBOX_IMAGE) {
|
if (image === DEFAULT_SANDBOX_IMAGE) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
DEFAULT_SANDBOX_BROWSER_IMAGE,
|
DEFAULT_SANDBOX_BROWSER_IMAGE,
|
||||||
DEFAULT_SANDBOX_COMMON_IMAGE,
|
DEFAULT_SANDBOX_COMMON_IMAGE,
|
||||||
DEFAULT_SANDBOX_IMAGE,
|
DEFAULT_SANDBOX_IMAGE,
|
||||||
|
isDockerDaemonUnavailable,
|
||||||
resolveSandboxScope,
|
resolveSandboxScope,
|
||||||
} from "../agents/sandbox.js";
|
} from "../agents/sandbox.js";
|
||||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||||
@@ -85,6 +86,9 @@ async function dockerImageExists(image: string): Promise<boolean> {
|
|||||||
if (stderr.includes("No such image")) {
|
if (stderr.includes("No such image")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (isDockerDaemonUnavailable(stderr)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user