mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
fix(update): finish post-core package updates
This commit is contained in:
@@ -623,6 +623,33 @@ describe("update-cli", () => {
|
||||
expect(runDaemonRestart).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("finishes package updates when the post-core process writes a result but keeps handles open", async () => {
|
||||
setupUpdatedRootRefresh();
|
||||
const kill = vi.fn();
|
||||
spawn.mockImplementationOnce((_command: unknown, _argv: unknown, options: unknown) => {
|
||||
const resultPath = (options as { env?: NodeJS.ProcessEnv }).env
|
||||
?.OPENCLAW_UPDATE_POST_CORE_RESULT_PATH;
|
||||
if (!resultPath) {
|
||||
throw new Error("missing post-core result path");
|
||||
}
|
||||
queueMicrotask(() => {
|
||||
void fs.writeFile(resultPath, `${JSON.stringify({ status: "ok" })}\n`, "utf-8");
|
||||
});
|
||||
const child = new EventEmitter() as EventEmitter & {
|
||||
kill: typeof kill;
|
||||
once: EventEmitter["once"];
|
||||
};
|
||||
child.kill = kill;
|
||||
return child;
|
||||
});
|
||||
|
||||
await updateCommand({ yes: true, restart: false });
|
||||
|
||||
expect(kill).toHaveBeenCalledTimes(1);
|
||||
expect(updateNpmInstalledPlugins).not.toHaveBeenCalled();
|
||||
expect(defaultRuntime.exit).not.toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it("does not carry gateway service markers into the post-core update process", async () => {
|
||||
setupUpdatedRootRefresh();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
@@ -110,6 +110,7 @@ const POST_CORE_UPDATE_ENV = "OPENCLAW_UPDATE_POST_CORE";
|
||||
const POST_CORE_UPDATE_CHANNEL_ENV = "OPENCLAW_UPDATE_POST_CORE_CHANNEL";
|
||||
const POST_CORE_UPDATE_REQUESTED_CHANNEL_ENV = "OPENCLAW_UPDATE_POST_CORE_REQUESTED_CHANNEL";
|
||||
const POST_CORE_UPDATE_RESULT_PATH_ENV = "OPENCLAW_UPDATE_POST_CORE_RESULT_PATH";
|
||||
const POST_CORE_UPDATE_RESULT_POLL_MS = 100;
|
||||
const UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV =
|
||||
"OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE";
|
||||
const SERVICE_REFRESH_PATH_ENV_KEYS = [
|
||||
@@ -1608,6 +1609,25 @@ async function readPostCorePluginUpdateResultFile(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function stopPostCoreUpdateChild(child: ChildProcess): void {
|
||||
if (process.platform === "win32" && child.pid) {
|
||||
try {
|
||||
const killer = spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"], {
|
||||
stdio: "ignore",
|
||||
windowsHide: true,
|
||||
});
|
||||
killer.once("error", () => {
|
||||
child.kill();
|
||||
});
|
||||
return;
|
||||
} catch {
|
||||
child.kill();
|
||||
return;
|
||||
}
|
||||
}
|
||||
child.kill();
|
||||
}
|
||||
|
||||
async function continuePostCoreUpdateInFreshProcess(params: {
|
||||
root: string;
|
||||
channel: "stable" | "beta" | "dev";
|
||||
@@ -1632,11 +1652,8 @@ async function continuePostCoreUpdateInFreshProcess(params: {
|
||||
if (params.opts.timeout) {
|
||||
argv.push("--timeout", params.opts.timeout);
|
||||
}
|
||||
const resultDir =
|
||||
params.opts.json === true
|
||||
? await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-post-core-"))
|
||||
: null;
|
||||
const resultPath = resultDir ? path.join(resultDir, "plugins.json") : null;
|
||||
const resultDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-post-core-"));
|
||||
const resultPath = path.join(resultDir, "plugins.json");
|
||||
|
||||
try {
|
||||
const child = spawn(resolveNodeRunner(), argv, {
|
||||
@@ -1648,24 +1665,65 @@ async function continuePostCoreUpdateInFreshProcess(params: {
|
||||
...(params.requestedChannel
|
||||
? { [POST_CORE_UPDATE_REQUESTED_CHANNEL_ENV]: params.requestedChannel }
|
||||
: {}),
|
||||
...(resultPath ? { [POST_CORE_UPDATE_RESULT_PATH_ENV]: resultPath } : {}),
|
||||
[POST_CORE_UPDATE_RESULT_PATH_ENV]: resultPath,
|
||||
},
|
||||
});
|
||||
|
||||
const exitCode = await new Promise<number>((resolve, reject) => {
|
||||
child.once("error", reject);
|
||||
const childResult = await new Promise<
|
||||
| { kind: "exit"; exitCode: number }
|
||||
| { kind: "plugin-update"; pluginUpdate: PostCorePluginUpdateResult }
|
||||
>((resolve, reject) => {
|
||||
let settled = false;
|
||||
const finish = (
|
||||
result:
|
||||
| { kind: "exit"; exitCode: number }
|
||||
| { kind: "plugin-update"; pluginUpdate: PostCorePluginUpdateResult },
|
||||
) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearInterval(resultPoll);
|
||||
resolve(result);
|
||||
};
|
||||
const resultPoll = setInterval(() => {
|
||||
void readPostCorePluginUpdateResultFile(resultPath)
|
||||
.then((pluginUpdate) => {
|
||||
if (!pluginUpdate) {
|
||||
return;
|
||||
}
|
||||
stopPostCoreUpdateChild(child);
|
||||
finish({ kind: "plugin-update", pluginUpdate });
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}, POST_CORE_UPDATE_RESULT_POLL_MS);
|
||||
child.once("error", (error) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearInterval(resultPoll);
|
||||
reject(error);
|
||||
});
|
||||
child.once("exit", (code, signal) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
if (signal) {
|
||||
settled = true;
|
||||
clearInterval(resultPoll);
|
||||
reject(new Error(`post-update process terminated by signal ${signal}`));
|
||||
return;
|
||||
}
|
||||
resolve(code ?? 1);
|
||||
finish({ kind: "exit", exitCode: code ?? 1 });
|
||||
});
|
||||
});
|
||||
|
||||
const pluginUpdate = resultPath
|
||||
? await readPostCorePluginUpdateResultFile(resultPath)
|
||||
: undefined;
|
||||
const pluginUpdate =
|
||||
childResult.kind === "plugin-update"
|
||||
? childResult.pluginUpdate
|
||||
: await readPostCorePluginUpdateResultFile(resultPath);
|
||||
const exitCode = childResult.kind === "exit" ? childResult.exitCode : 0;
|
||||
if (exitCode !== 0) {
|
||||
if (pluginUpdate) {
|
||||
return { resumed: true, pluginUpdate };
|
||||
@@ -1675,9 +1733,7 @@ async function continuePostCoreUpdateInFreshProcess(params: {
|
||||
}
|
||||
return { resumed: true, ...(pluginUpdate ? { pluginUpdate } : {}) };
|
||||
} finally {
|
||||
if (resultDir) {
|
||||
await fs.rm(resultDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
}
|
||||
await fs.rm(resultDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1752,11 +1808,13 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
opts,
|
||||
timeoutMs: updateStepTimeoutMs,
|
||||
});
|
||||
if (opts.json) {
|
||||
if (process.env[POST_CORE_UPDATE_RESULT_PATH_ENV]) {
|
||||
await writePostCorePluginUpdateResultFile(
|
||||
process.env[POST_CORE_UPDATE_RESULT_PATH_ENV],
|
||||
pluginUpdate,
|
||||
);
|
||||
}
|
||||
if (opts.json) {
|
||||
if (!process.env[POST_CORE_UPDATE_RESULT_PATH_ENV]) {
|
||||
const result: UpdateRunResult = {
|
||||
status: pluginUpdate.status === "error" ? "error" : "ok",
|
||||
|
||||
Reference in New Issue
Block a user