fix(release): close cross-os artifact sockets

This commit is contained in:
Peter Steinberger
2026-05-28 05:42:28 +01:00
parent ca1829c3f4
commit 97717277c4
2 changed files with 42 additions and 1 deletions

View File

@@ -20,6 +20,7 @@ import {
import { mkdtempSync } from "node:fs";
import { createServer } from "node:http";
import { createConnection as createNetConnection, createServer as createNetServer } from "node:net";
import type { Socket } from "node:net";
import { tmpdir } from "node:os";
import { dirname, join, relative, resolve, win32 as pathWin32 } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
@@ -3686,6 +3687,7 @@ export async function startStaticFileServer(params) {
const logStream = createWriteStream(params.logPath, { flags: "a" });
const fileName = String(params.filePath.split(/[/\\]/u).at(-1) ?? "artifact");
const fileStat = statSync(params.filePath);
const sockets = new Set<Socket>();
const server = createServer((request, response) => {
logStream.write(`${new Date().toISOString()} ${request.method} ${request.url}\n`);
if (request.url !== `/${fileName}`) {
@@ -3710,6 +3712,12 @@ export async function startStaticFileServer(params) {
});
fileStream.pipe(response);
});
server.on("connection", (socket) => {
sockets.add(socket);
socket.once("close", () => {
sockets.delete(socket);
});
});
await new Promise((resolvePromise, rejectPromise) => {
server.once("error", rejectPromise);
server.listen(0, "127.0.0.1", resolvePromise);
@@ -3731,6 +3739,9 @@ export async function startStaticFileServer(params) {
}
resolvePromise();
});
for (const socket of sockets) {
socket.destroy();
}
}),
};
}

View File

@@ -7,7 +7,7 @@ import {
symlinkSync,
writeFileSync,
} from "node:fs";
import { createServer as createNetServer } from "node:net";
import { createConnection as createNetConnection, createServer as createNetServer } from "node:net";
import { tmpdir } from "node:os";
import { join, win32 } from "node:path";
import { setTimeout as delay } from "node:timers/promises";
@@ -685,6 +685,36 @@ describe("scripts/openclaw-cross-os-release-checks", () => {
}
});
it("closes static release artifact sockets left by aborted clients", async () => {
const dir = mkdtempSync(join(tmpdir(), "openclaw-cross-os-static-server-close-"));
const filePath = join(dir, "openclaw-2026.4.14.tgz");
const logPath = join(dir, "server.log");
let server: Awaited<ReturnType<typeof startStaticFileServer>> | undefined;
try {
writeFileSync(filePath, Buffer.alloc(1024 * 1024, "x"));
server = await startStaticFileServer({ filePath, logPath });
const url = new URL(server.url);
const socket = createNetConnection(Number(url.port), url.hostname);
await new Promise<void>((resolve, reject) => {
socket.once("connect", resolve);
socket.once("error", reject);
});
socket.write(`GET ${url.pathname} HTTP/1.1\r\nHost: ${url.host}\r\n\r\n`);
await Promise.race([
server.close(),
delay(1_000).then(() => Promise.reject(new Error("close timed out"))),
]);
await Promise.race([
new Promise<void>((resolve) => socket.once("close", resolve)),
delay(1_000).then(() => Promise.reject(new Error("socket close timed out"))),
]);
} finally {
await server?.close().catch(() => {});
rmSync(dir, { recursive: true, force: true });
}
});
it("does not preload static release artifacts before serving them", () => {
const source = readFileSync("scripts/openclaw-cross-os-release-checks.ts", "utf8");
const serverSource = source.slice(