mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-17 12:11:20 +00:00
fix: harden qa lab docker launcher startup
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { createServer } from "node:net";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
@@ -82,4 +83,42 @@ describe("runQaDockerUp", () => {
|
||||
await rm(outputDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to free host ports when defaults are already occupied", async () => {
|
||||
const gatewayServer = createServer();
|
||||
const labServer = createServer();
|
||||
const outputDir = await mkdtemp(path.join(os.tmpdir(), "qa-docker-up-"));
|
||||
|
||||
await new Promise<void>((resolve) => gatewayServer.listen(18789, "127.0.0.1", () => resolve()));
|
||||
await new Promise<void>((resolve) => labServer.listen(43124, "127.0.0.1", () => resolve()));
|
||||
|
||||
try {
|
||||
const result = await runQaDockerUp(
|
||||
{
|
||||
repoRoot: "/repo/openclaw",
|
||||
outputDir,
|
||||
skipUiBuild: true,
|
||||
usePrebuiltImage: true,
|
||||
},
|
||||
{
|
||||
async runCommand() {
|
||||
return { stdout: "", stderr: "" };
|
||||
},
|
||||
fetchImpl: vi.fn(async () => ({ ok: true })),
|
||||
sleepImpl: vi.fn(async () => {}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.gatewayUrl).not.toBe("http://127.0.0.1:18789/");
|
||||
expect(result.qaLabUrl).not.toBe("http://127.0.0.1:43124");
|
||||
} finally {
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
gatewayServer.close((error) => (error ? reject(error) : resolve())),
|
||||
);
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
labServer.close((error) => (error ? reject(error) : resolve())),
|
||||
);
|
||||
await rm(outputDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { createServer } from "node:net";
|
||||
import path from "node:path";
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
import { writeQaDockerHarnessFiles } from "./docker-harness.js";
|
||||
@@ -31,22 +32,83 @@ function describeError(error: unknown) {
|
||||
return JSON.stringify(error);
|
||||
}
|
||||
|
||||
async function execCommand(command: string, args: string[], cwd: string) {
|
||||
return await new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
|
||||
execFile(command, args, { cwd, encoding: "utf8" }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(
|
||||
new Error(
|
||||
stderr.trim() || stdout.trim() || `Command failed: ${[command, ...args].join(" ")}`,
|
||||
),
|
||||
);
|
||||
async function isPortFree(port: number) {
|
||||
return await new Promise<boolean>((resolve) => {
|
||||
const server = createServer();
|
||||
server.once("error", () => resolve(false));
|
||||
server.listen(port, () => {
|
||||
server.close(() => resolve(true));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function findFreePort() {
|
||||
return await new Promise<number>((resolve, reject) => {
|
||||
const server = createServer();
|
||||
server.once("error", reject);
|
||||
server.listen(0, () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
server.close();
|
||||
reject(new Error("failed to find free port"));
|
||||
return;
|
||||
}
|
||||
resolve({ stdout, stderr });
|
||||
server.close((error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(address.port);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveHostPort(preferredPort: number, pinned: boolean) {
|
||||
if (pinned || (await isPortFree(preferredPort))) {
|
||||
return preferredPort;
|
||||
}
|
||||
return await findFreePort();
|
||||
}
|
||||
|
||||
function trimCommandOutput(output: string) {
|
||||
const trimmed = output.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
const lines = trimmed.split("\n");
|
||||
return lines.length <= 120 ? trimmed : lines.slice(-120).join("\n");
|
||||
}
|
||||
|
||||
async function execCommand(command: string, args: string[], cwd: string) {
|
||||
return await new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
|
||||
execFile(
|
||||
command,
|
||||
args,
|
||||
{ cwd, encoding: "utf8", maxBuffer: 10 * 1024 * 1024 },
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
const renderedStdout = trimCommandOutput(stdout);
|
||||
const renderedStderr = trimCommandOutput(stderr);
|
||||
reject(
|
||||
new Error(
|
||||
[
|
||||
`Command failed: ${[command, ...args].join(" ")}`,
|
||||
renderedStderr ? `stderr:\n${renderedStderr}` : "",
|
||||
renderedStdout ? `stdout:\n${renderedStdout}` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
resolve({ stdout, stderr });
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForHealth(
|
||||
url: string,
|
||||
deps: {
|
||||
@@ -98,8 +160,11 @@ export async function runQaDockerUp(
|
||||
): Promise<QaDockerUpResult> {
|
||||
const repoRoot = path.resolve(params.repoRoot ?? process.cwd());
|
||||
const outputDir = path.resolve(params.outputDir ?? DEFAULT_QA_DOCKER_DIR);
|
||||
const gatewayPort = params.gatewayPort ?? 18789;
|
||||
const qaLabPort = params.qaLabPort ?? 43124;
|
||||
const gatewayPort = await resolveHostPort(
|
||||
params.gatewayPort ?? 18789,
|
||||
params.gatewayPort != null,
|
||||
);
|
||||
const qaLabPort = await resolveHostPort(params.qaLabPort ?? 43124, params.qaLabPort != null);
|
||||
const runCommand = deps?.runCommand ?? execCommand;
|
||||
const fetchImpl =
|
||||
deps?.fetchImpl ??
|
||||
|
||||
Reference in New Issue
Block a user