mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix: recover delayed live Mac bundle restores (#104613)
This commit is contained in:
committed by
GitHub
parent
9ad59ae788
commit
43c40a3b3e
@@ -524,7 +524,21 @@ function assertExactBuild(checkout, expectedSha) {
|
||||
return state;
|
||||
}
|
||||
|
||||
function runBuildWithPreservedMacApp(runCommand, checkout) {
|
||||
function isOriginalMacBundle(bundlePath, originalStat) {
|
||||
try {
|
||||
const currentStat = lstatSync(bundlePath);
|
||||
return (
|
||||
currentStat.isDirectory() &&
|
||||
!currentStat.isSymbolicLink() &&
|
||||
currentStat.dev === originalStat.dev &&
|
||||
currentStat.ino === originalStat.ino
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function runBuildWithPreservedMacApp(runCommand, checkout, sleep = defaultSleep) {
|
||||
const appBundle = path.join(checkout, "dist/OpenClaw.app");
|
||||
if (!existsSync(appBundle)) {
|
||||
runCommand("pnpm", ["build"], checkout);
|
||||
@@ -546,20 +560,59 @@ function runBuildWithPreservedMacApp(runCommand, checkout) {
|
||||
try {
|
||||
runCommand("pnpm", ["build"], checkout);
|
||||
} finally {
|
||||
if (!existsSync(preservedBundle)) {
|
||||
throw new UpdateInvariantError(
|
||||
"missing_preserved_mac_bundle",
|
||||
`preserved Mac app bundle disappeared: ${preservedBundle}`,
|
||||
);
|
||||
// A running app or external file coordinator can temporarily relocate and
|
||||
// restore the exact bundle while the JS build runs. Allow that move to settle, but
|
||||
// require the original inode so an unrelated replacement still fails closed.
|
||||
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||
if (existsSync(preservedBundle) || existsSync(appBundle)) {
|
||||
break;
|
||||
}
|
||||
sleep(100);
|
||||
}
|
||||
if (existsSync(appBundle)) {
|
||||
const alreadyRestored = isOriginalMacBundle(appBundle, appStat);
|
||||
if (!alreadyRestored && existsSync(appBundle)) {
|
||||
throw new UpdateInvariantError(
|
||||
"mac_bundle_restore_conflict",
|
||||
`build unexpectedly created ${appBundle}; preserved bundle remains at ${preservedBundle}`,
|
||||
);
|
||||
}
|
||||
mkdirSync(path.dirname(appBundle), { recursive: true });
|
||||
renameSync(preservedBundle, appBundle);
|
||||
if (!alreadyRestored) {
|
||||
mkdirSync(path.dirname(appBundle), { recursive: true });
|
||||
try {
|
||||
renameSync(preservedBundle, appBundle);
|
||||
} catch (error) {
|
||||
if (!isOriginalMacBundle(appBundle, appStat)) {
|
||||
if (existsSync(appBundle)) {
|
||||
throw new UpdateInvariantError(
|
||||
"mac_bundle_restore_conflict",
|
||||
`build unexpectedly created ${appBundle}; preserved bundle remains at ${preservedBundle}`,
|
||||
);
|
||||
}
|
||||
if (existsSync(preservedBundle)) {
|
||||
throw new UpdateInvariantError(
|
||||
"mac_bundle_restore_failed",
|
||||
`failed to restore Mac app bundle: ${String(error)}`,
|
||||
);
|
||||
}
|
||||
throw new UpdateInvariantError(
|
||||
"missing_preserved_mac_bundle",
|
||||
`preserved Mac app bundle disappeared: ${preservedBundle}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isOriginalMacBundle(appBundle, appStat)) {
|
||||
throw new UpdateInvariantError(
|
||||
"missing_preserved_mac_bundle",
|
||||
`original Mac app bundle was not restored to ${appBundle}`,
|
||||
);
|
||||
}
|
||||
if (existsSync(preservedBundle)) {
|
||||
throw new UpdateInvariantError(
|
||||
"mac_bundle_restore_conflict",
|
||||
`original Mac app bundle exists at both ${appBundle} and ${preservedBundle}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -819,7 +872,7 @@ export function maintainMain(options, dependencies = {}) {
|
||||
// Use the existing built CLI directly. Source launchers may auto-build a
|
||||
// stale dist before dispatching `gateway stop`, recreating the live-import race.
|
||||
runCommand(process.execPath, ["dist/index.js", "gateway", "stop"], update.checkout);
|
||||
runBuildWithPreservedMacApp(runCommand, update.checkout);
|
||||
runBuildWithPreservedMacApp(runCommand, update.checkout, sleep);
|
||||
assertExactBuild(update.checkout, update.afterSha);
|
||||
const restartStartedAt = restartGateway(runCommand, update.checkout, update.afterSha);
|
||||
gatewayLogAudit = verifyAndAuditGateway({
|
||||
|
||||
@@ -508,6 +508,80 @@ describe("openclaw live updater", () => {
|
||||
expect(readFileSync(appMarker, "utf8")).toBe("signed\n");
|
||||
});
|
||||
|
||||
test("accepts a delayed external restore of the exact preserved Mac bundle", () => {
|
||||
const { root, mirror } = makeFixture();
|
||||
mkdirSync(path.join(mirror, "node_modules"));
|
||||
const appBundle = path.join(mirror, "dist/OpenClaw.app");
|
||||
const appMarker = path.join(appBundle, "Contents/signature-marker");
|
||||
mkdirSync(path.dirname(appMarker), { recursive: true });
|
||||
writeFileSync(appMarker, "signed\n");
|
||||
const commands = fakeCommands(mirror);
|
||||
const delayedBundle = path.join(root, "delayed-openclaw.app");
|
||||
let restored = false;
|
||||
|
||||
maintainFixture(
|
||||
{ checkout: mirror, remote: "origin", lockPath: path.join(root, "maintenance.lock") },
|
||||
{
|
||||
runCommand(command: string, args: string[]) {
|
||||
if (command === "pnpm" && args[0] === "build") {
|
||||
expect(existsSync(appBundle)).toBe(false);
|
||||
}
|
||||
commands.runCommand(command, args);
|
||||
if (command === "pnpm" && args[0] === "build") {
|
||||
const preserved = readdirSync(path.join(mirror, ".git")).find((entry) =>
|
||||
entry.startsWith(".openclaw-live-mac-"),
|
||||
);
|
||||
expect(preserved).toBeDefined();
|
||||
renameSync(path.join(mirror, ".git", preserved!), delayedBundle);
|
||||
}
|
||||
},
|
||||
sleep() {
|
||||
if (restored) {
|
||||
return;
|
||||
}
|
||||
renameSync(delayedBundle, appBundle);
|
||||
restored = true;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(restored).toBe(true);
|
||||
expect(readFileSync(appMarker, "utf8")).toBe("signed\n");
|
||||
expect(
|
||||
readdirSync(path.join(mirror, ".git")).filter((entry) =>
|
||||
entry.startsWith(".openclaw-live-mac-"),
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test("preserves a build failure after an external Mac bundle restore", () => {
|
||||
const { root, mirror } = makeFixture();
|
||||
mkdirSync(path.join(mirror, "node_modules"));
|
||||
const appBundle = path.join(mirror, "dist/OpenClaw.app");
|
||||
const appMarker = path.join(appBundle, "Contents/signature-marker");
|
||||
mkdirSync(path.dirname(appMarker), { recursive: true });
|
||||
writeFileSync(appMarker, "signed\n");
|
||||
|
||||
expect(() =>
|
||||
maintainFixture(
|
||||
{ checkout: mirror, remote: "origin", lockPath: path.join(root, "maintenance.lock") },
|
||||
{
|
||||
runCommand(command: string, args: string[]) {
|
||||
if (command === "pnpm" && args[0] === "build") {
|
||||
const preserved = readdirSync(path.join(mirror, ".git")).find((entry) =>
|
||||
entry.startsWith(".openclaw-live-mac-"),
|
||||
);
|
||||
expect(preserved).toBeDefined();
|
||||
renameSync(path.join(mirror, ".git", preserved!), appBundle);
|
||||
throw new Error("build failed after external restore");
|
||||
}
|
||||
},
|
||||
},
|
||||
),
|
||||
).toThrow("build failed after external restore");
|
||||
expect(readFileSync(appMarker, "utf8")).toBe("signed\n");
|
||||
});
|
||||
|
||||
test("proves a current exact-SHA Gateway on a no-op heartbeat", () => {
|
||||
const { root, mirror } = makeFixture();
|
||||
mkdirSync(path.join(mirror, "node_modules"));
|
||||
|
||||
Reference in New Issue
Block a user