mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
Desktop stream teardown discarded the observed WebSocket close code and the preceding owner cleanup trigger, leaving normal observer disposal indistinguishable from an unexpected transport close. Record bounded diagnostics once at the observer, broker, and node transport owners without changing forwarding or teardown behavior or logging peer payloads and credentials. Move authenticated macOS snapshot mounts to controller-owned private Darwin temporary storage so external-volume builds use the same verified mount paths for build and cleanup. Preserve read-only source, signing, and cleanup ownership. Repair two native test synchronization boundaries: wait for an admitted reconnect before requesting its lease, and flush post-resize settings layout before sampling the prior pane. Regression and sibling proof covers real socket closes, cancellation/backpressure, snapshot mount cleanup, signed Intel native installation, and real Linux, Windows, and macOS CUA/WebVNC flows. Successful runs do not explain the previously observed intermittent Mac close or historical-marker response; those remain named follow-ups with improved diagnostics. Follow-up to #136297 and #137724. Required exact-head CI passed: https://github.com/openclaw/openclaw/actions/runs/33849738091 . Native proof includes the original ordered SettingsWindowLayoutTests pair, all 2,082 app Swift Testing cases, isolated AppState cases, and 1,627 OpenClawKit cases. Both native build/test jobs passed. Separate optional shared-kit scans were still queued at landing preparation.
152 lines
5.3 KiB
TypeScript
152 lines
5.3 KiB
TypeScript
#!/usr/bin/env node
|
|
import { execFileSync } from "node:child_process";
|
|
import { mkdir, mkdtemp, readFile, realpath, rm, rmdir, writeFile } from "node:fs/promises";
|
|
import { availableParallelism } from "node:os";
|
|
import path from "node:path";
|
|
import { runManagedCommand, signalExitCode } from "./lib/managed-child-process.mts";
|
|
|
|
const [root, configuration, peekabooCommit, skipMlx, resultsRoot, ...architectures] =
|
|
process.argv.slice(2);
|
|
if (
|
|
!root ||
|
|
!configuration ||
|
|
!peekabooCommit ||
|
|
!skipMlx ||
|
|
!resultsRoot ||
|
|
architectures.length === 0 ||
|
|
architectures.length > 2 ||
|
|
new Set(architectures).size !== architectures.length ||
|
|
architectures.some((arch) => arch !== "arm64" && arch !== "x86_64")
|
|
) {
|
|
throw new Error(
|
|
"Expected build root, configuration, source revision, MLX flag, result directory and unique macOS architectures",
|
|
);
|
|
}
|
|
const concurrency = Math.min(architectures.length, availableParallelism());
|
|
const jobs = Math.max(1, Math.floor(availableParallelism() / concurrency));
|
|
const controller = new AbortController();
|
|
const owned: Array<{ arch: string; lock: string; work: string; treeSafe: boolean }> = [];
|
|
const failures: unknown[] = [];
|
|
let parentSignal: NodeJS.Signals | undefined;
|
|
const signals = ["SIGINT", "SIGTERM", "SIGHUP"] as const;
|
|
const signalHandlers = signals.map((signal) => {
|
|
const handler = () => {
|
|
parentSignal ??= signal;
|
|
controller.abort(new Error(`Swift build interrupted by ${signal}`));
|
|
};
|
|
process.on(signal, handler);
|
|
return { signal, handler };
|
|
});
|
|
await mkdir(resultsRoot, { recursive: true });
|
|
// A checkout may live on a volume that rejects nested mounts. Only the mount
|
|
// directories use Darwin's private temp storage; images and builds stay in work.
|
|
const temporaryRoot = execFileSync("getconf", ["DARWIN_USER_TEMP_DIR"], {
|
|
encoding: "utf8",
|
|
}).trim();
|
|
const mountRoot = await mkdtemp(path.join(await realpath(temporaryRoot), "openclaw-swift-mounts-"));
|
|
const worker = path.join(root, "scripts/lib/mac-swift-build.sh");
|
|
const workerArgs = (operation: string, arch: string, work: string) => [
|
|
worker,
|
|
operation,
|
|
root,
|
|
arch,
|
|
configuration,
|
|
String(jobs),
|
|
peekabooCommit,
|
|
skipMlx,
|
|
work,
|
|
path.join(mountRoot, arch),
|
|
];
|
|
|
|
try {
|
|
for (let offset = 0; offset < architectures.length; offset += concurrency) {
|
|
if (controller.signal.aborted || parentSignal) {
|
|
break;
|
|
}
|
|
await Promise.allSettled(
|
|
architectures.slice(offset, offset + concurrency).map(async (arch) => {
|
|
const lock = path.join(root, "apps/macos/.build", `.openclaw-package-${arch}.lock`);
|
|
const work = path.join(resultsRoot, arch);
|
|
let ownership: (typeof owned)[number] | undefined;
|
|
try {
|
|
await mkdir(path.dirname(lock), { recursive: true });
|
|
// SwiftPM's lock does not cover our checkout resource patches.
|
|
await mkdir(lock);
|
|
ownership = { arch, lock, work, treeSafe: true };
|
|
owned.push(ownership);
|
|
await mkdir(work);
|
|
const code = await runManagedCommand({
|
|
bin: "/bin/bash",
|
|
args: workerArgs("build", arch, work),
|
|
requireProcessTreeExit: true,
|
|
signal: controller.signal,
|
|
});
|
|
if (code !== 0) {
|
|
throw new Error(`Swift ${arch} build failed with exit code ${code}`);
|
|
}
|
|
const compiled = (await readFile(path.join(work, "peekaboo-commit"), "utf8")).trim();
|
|
if (compiled !== peekabooCommit) {
|
|
throw new Error(`Swift ${arch} compiled a different Peekaboo source`);
|
|
}
|
|
} catch (error) {
|
|
if (
|
|
ownership &&
|
|
error &&
|
|
typeof error === "object" &&
|
|
"code" in error &&
|
|
error.code === "EPROCESSGROUP_CLEANUP_FAILED"
|
|
) {
|
|
ownership.treeSafe = false;
|
|
}
|
|
failures.push(error);
|
|
if (!controller.signal.aborted) {
|
|
controller.abort(error);
|
|
}
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
} finally {
|
|
// All writers have settled before mounts, patched sources or locks are retired.
|
|
for (const owner of owned) {
|
|
if (!owner.treeSafe) {
|
|
continue;
|
|
}
|
|
try {
|
|
const code = await runManagedCommand({
|
|
bin: "/bin/bash",
|
|
args: workerArgs("cleanup", owner.arch, owner.work),
|
|
requireProcessTreeExit: true,
|
|
});
|
|
if (code !== 0) {
|
|
owner.treeSafe = false;
|
|
failures.push(new Error(`Swift ${owner.arch} cleanup failed with exit code ${code}`));
|
|
} else {
|
|
await rm(owner.lock, { recursive: true });
|
|
}
|
|
} catch (error) {
|
|
owner.treeSafe = false;
|
|
failures.push(error);
|
|
}
|
|
}
|
|
}
|
|
if (owned.every((owner) => owner.treeSafe)) {
|
|
await rmdir(mountRoot);
|
|
await writeFile(path.join(resultsRoot, "cleanup-complete"), "verified\n", { flag: "wx" });
|
|
}
|
|
for (const { signal, handler } of signalHandlers) {
|
|
process.off(signal, handler);
|
|
}
|
|
for (const failure of failures) {
|
|
console.error(failure);
|
|
}
|
|
if (owned.some((owner) => !owner.treeSafe)) {
|
|
console.error(
|
|
`Swift cleanup could not be verified; retained work and ownership locks under ${resultsRoot}, snapshot mounts under ${mountRoot}`,
|
|
);
|
|
process.exitCode = 2;
|
|
} else if (parentSignal) {
|
|
process.exitCode = signalExitCode(parentSignal);
|
|
} else if (failures.length > 0) {
|
|
process.exitCode = 1;
|
|
}
|