mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:20:42 +00:00
fix: restart package updates through updated install
This commit is contained in:
@@ -4,6 +4,10 @@ import {
|
||||
buildGatewayInstallEntrypointCandidates as resolveGatewayInstallEntrypointCandidates,
|
||||
resolveGatewayInstallEntrypoint,
|
||||
} from "../../daemon/gateway-entrypoint.js";
|
||||
import {
|
||||
shouldPrepareUpdatedInstallRestart,
|
||||
shouldUseLegacyProcessRestartAfterUpdate,
|
||||
} from "./update-command.js";
|
||||
|
||||
describe("resolveGatewayInstallEntrypointCandidates", () => {
|
||||
it("prefers index.js before legacy entry.js", () => {
|
||||
@@ -39,3 +43,55 @@ describe("resolveGatewayInstallEntrypoint", () => {
|
||||
).resolves.toBe(entryPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldPrepareUpdatedInstallRestart", () => {
|
||||
it("prepares package update restarts when the service is installed but stopped", () => {
|
||||
expect(
|
||||
shouldPrepareUpdatedInstallRestart({
|
||||
updateMode: "npm",
|
||||
serviceInstalled: true,
|
||||
serviceLoaded: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not install a new service for package updates when no service exists", () => {
|
||||
expect(
|
||||
shouldPrepareUpdatedInstallRestart({
|
||||
updateMode: "npm",
|
||||
serviceInstalled: false,
|
||||
serviceLoaded: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps non-package updates tied to the loaded service state", () => {
|
||||
expect(
|
||||
shouldPrepareUpdatedInstallRestart({
|
||||
updateMode: "git",
|
||||
serviceInstalled: true,
|
||||
serviceLoaded: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldPrepareUpdatedInstallRestart({
|
||||
updateMode: "git",
|
||||
serviceInstalled: true,
|
||||
serviceLoaded: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldUseLegacyProcessRestartAfterUpdate", () => {
|
||||
it("never restarts package updates through the pre-update process", () => {
|
||||
expect(shouldUseLegacyProcessRestartAfterUpdate({ updateMode: "npm" })).toBe(false);
|
||||
expect(shouldUseLegacyProcessRestartAfterUpdate({ updateMode: "pnpm" })).toBe(false);
|
||||
expect(shouldUseLegacyProcessRestartAfterUpdate({ updateMode: "bun" })).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps the in-process restart path for non-package updates", () => {
|
||||
expect(shouldUseLegacyProcessRestartAfterUpdate({ updateMode: "git" })).toBe(true);
|
||||
expect(shouldUseLegacyProcessRestartAfterUpdate({ updateMode: "unknown" })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ import { formatConfigIssueLines } from "../../config/issue-format.js";
|
||||
import { asResolvedSourceConfig, asRuntimeConfig } from "../../config/materialize.js";
|
||||
import { resolveGatewayInstallEntrypoint } from "../../daemon/gateway-entrypoint.js";
|
||||
import { resolveGatewayRestartLogPath } from "../../daemon/restart-logs.js";
|
||||
import { resolveGatewayService } from "../../daemon/service.js";
|
||||
import { readGatewayServiceState, resolveGatewayService } from "../../daemon/service.js";
|
||||
import { createLowDiskSpaceWarning } from "../../infra/disk-space.js";
|
||||
import { runGlobalPackageUpdateSteps } from "../../infra/package-update-steps.js";
|
||||
import { nodeVersionSatisfiesEngine } from "../../infra/runtime-guard.js";
|
||||
@@ -133,6 +133,24 @@ function pickUpdateQuip(): string {
|
||||
function isPackageManagerUpdateMode(mode: UpdateRunResult["mode"]): mode is "npm" | "pnpm" | "bun" {
|
||||
return mode === "npm" || mode === "pnpm" || mode === "bun";
|
||||
}
|
||||
|
||||
export function shouldPrepareUpdatedInstallRestart(params: {
|
||||
updateMode: UpdateRunResult["mode"];
|
||||
serviceInstalled: boolean;
|
||||
serviceLoaded: boolean;
|
||||
}): boolean {
|
||||
if (isPackageManagerUpdateMode(params.updateMode)) {
|
||||
return params.serviceInstalled;
|
||||
}
|
||||
return params.serviceLoaded;
|
||||
}
|
||||
|
||||
export function shouldUseLegacyProcessRestartAfterUpdate(params: {
|
||||
updateMode: UpdateRunResult["mode"];
|
||||
}): boolean {
|
||||
return !isPackageManagerUpdateMode(params.updateMode);
|
||||
}
|
||||
|
||||
function formatCommandFailure(stdout: string, stderr: string): string {
|
||||
const detail = (stderr || stdout).trim();
|
||||
if (!detail) {
|
||||
@@ -267,6 +285,7 @@ async function refreshGatewayServiceEnv(params: {
|
||||
result: UpdateRunResult;
|
||||
jsonMode: boolean;
|
||||
invocationCwd?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<void> {
|
||||
const args = ["gateway", "install", "--force"];
|
||||
if (params.jsonMode) {
|
||||
@@ -277,7 +296,7 @@ async function refreshGatewayServiceEnv(params: {
|
||||
if (entrypoint) {
|
||||
const res = await runCommandWithTimeout([resolveNodeRunner(), entrypoint, ...args], {
|
||||
cwd: params.result.root,
|
||||
env: resolveServiceRefreshEnv(process.env, params.invocationCwd),
|
||||
env: resolveServiceRefreshEnv(params.env ?? process.env, params.invocationCwd),
|
||||
timeoutMs: SERVICE_REFRESH_TIMEOUT_MS,
|
||||
});
|
||||
if (res.code === 0) {
|
||||
@@ -288,9 +307,45 @@ async function refreshGatewayServiceEnv(params: {
|
||||
);
|
||||
}
|
||||
|
||||
if (isPackageManagerUpdateMode(params.result.mode)) {
|
||||
throw new Error(
|
||||
`updated install entrypoint not found under ${params.result.root ?? "unknown"}`,
|
||||
);
|
||||
}
|
||||
|
||||
await runDaemonInstall({ force: true, json: params.jsonMode || undefined });
|
||||
}
|
||||
|
||||
async function runUpdatedInstallGatewayRestart(params: {
|
||||
result: UpdateRunResult;
|
||||
jsonMode: boolean;
|
||||
invocationCwd?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<boolean> {
|
||||
const entrypoint = await resolveGatewayInstallEntrypoint(params.result.root);
|
||||
if (!entrypoint) {
|
||||
throw new Error(
|
||||
`updated install entrypoint not found under ${params.result.root ?? "unknown"}`,
|
||||
);
|
||||
}
|
||||
|
||||
const args = ["gateway", "restart"];
|
||||
if (params.jsonMode) {
|
||||
args.push("--json");
|
||||
}
|
||||
const res = await runCommandWithTimeout([resolveNodeRunner(), entrypoint, ...args], {
|
||||
cwd: params.result.root,
|
||||
env: resolveServiceRefreshEnv(params.env ?? process.env, params.invocationCwd),
|
||||
timeoutMs: SERVICE_REFRESH_TIMEOUT_MS,
|
||||
});
|
||||
if (res.code === 0) {
|
||||
return true;
|
||||
}
|
||||
throw new Error(
|
||||
`updated install restart failed (${entrypoint}): ${formatCommandFailure(res.stdout, res.stderr)}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function tryInstallShellCompletion(opts: {
|
||||
jsonMode: boolean;
|
||||
skipPrompt: boolean;
|
||||
@@ -739,11 +794,26 @@ async function maybeRestartService(params: {
|
||||
result: UpdateRunResult;
|
||||
opts: UpdateCommandOptions;
|
||||
refreshServiceEnv: boolean;
|
||||
serviceEnv?: NodeJS.ProcessEnv;
|
||||
gatewayPort: number;
|
||||
restartScriptPath?: string | null;
|
||||
invocationCwd?: string;
|
||||
}): Promise<boolean> {
|
||||
const verifyRestartedGateway = async (expectedGatewayVersion: string | undefined) => {
|
||||
const restartAfterStaleCleanup = async () => {
|
||||
if (params.refreshServiceEnv && isPackageManagerUpdateMode(params.result.mode)) {
|
||||
await runUpdatedInstallGatewayRestart({
|
||||
result: params.result,
|
||||
jsonMode: Boolean(params.opts.json),
|
||||
invocationCwd: params.invocationCwd,
|
||||
env: params.serviceEnv,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (shouldUseLegacyProcessRestartAfterUpdate({ updateMode: params.result.mode })) {
|
||||
await runDaemonRestart();
|
||||
}
|
||||
};
|
||||
const service = resolveGatewayService();
|
||||
let health = await waitForGatewayHealthyRestart({
|
||||
service,
|
||||
@@ -759,7 +829,7 @@ async function maybeRestartService(params: {
|
||||
);
|
||||
}
|
||||
await terminateStaleGatewayPids(health.staleGatewayPids);
|
||||
await runDaemonRestart();
|
||||
await restartAfterStaleCleanup();
|
||||
health = await waitForGatewayHealthyRestart({
|
||||
service,
|
||||
port: params.gatewayPort,
|
||||
@@ -799,6 +869,7 @@ async function maybeRestartService(params: {
|
||||
const expectedGatewayVersion = isPackageManagerUpdateMode(params.result.mode)
|
||||
? normalizeOptionalString(params.result.after?.version)
|
||||
: undefined;
|
||||
const isPackageUpdate = isPackageManagerUpdateMode(params.result.mode);
|
||||
let restarted = false;
|
||||
let restartInitiated = false;
|
||||
if (params.refreshServiceEnv) {
|
||||
@@ -807,6 +878,7 @@ async function maybeRestartService(params: {
|
||||
result: params.result,
|
||||
jsonMode: Boolean(params.opts.json),
|
||||
invocationCwd: params.invocationCwd,
|
||||
env: params.serviceEnv,
|
||||
});
|
||||
} catch (err) {
|
||||
// Always log the refresh failure so callers can detect it (issue #56772).
|
||||
@@ -818,7 +890,7 @@ async function maybeRestartService(params: {
|
||||
} else {
|
||||
defaultRuntime.log(theme.warn(message));
|
||||
}
|
||||
if (isPackageManagerUpdateMode(params.result.mode)) {
|
||||
if (isPackageUpdate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -826,8 +898,17 @@ async function maybeRestartService(params: {
|
||||
if (params.restartScriptPath) {
|
||||
await runRestartScript(params.restartScriptPath);
|
||||
restartInitiated = true;
|
||||
} else {
|
||||
} else if (params.refreshServiceEnv && isPackageUpdate) {
|
||||
restarted = await runUpdatedInstallGatewayRestart({
|
||||
result: params.result,
|
||||
jsonMode: Boolean(params.opts.json),
|
||||
invocationCwd: params.invocationCwd,
|
||||
env: params.serviceEnv,
|
||||
});
|
||||
} else if (shouldUseLegacyProcessRestartAfterUpdate({ updateMode: params.result.mode })) {
|
||||
restarted = await runDaemonRestart();
|
||||
} else if (!params.opts.json) {
|
||||
defaultRuntime.log(theme.muted("No installed gateway service found; skipped restart."));
|
||||
}
|
||||
|
||||
const shouldVerifyRestart =
|
||||
@@ -871,6 +952,9 @@ async function maybeRestartService(params: {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (isPackageManagerUpdateMode(params.result.mode)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1419,15 +1503,25 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
|
||||
let restartScriptPath: string | null = null;
|
||||
let refreshGatewayServiceEnv = false;
|
||||
let gatewayServiceEnv: NodeJS.ProcessEnv | undefined;
|
||||
const gatewayPort = resolveGatewayPort(
|
||||
postUpdateConfigSnapshot.valid ? postUpdateConfigSnapshot.config : undefined,
|
||||
process.env,
|
||||
);
|
||||
if (shouldRestart) {
|
||||
try {
|
||||
const loaded = await resolveGatewayService().isLoaded({ env: process.env });
|
||||
if (loaded) {
|
||||
restartScriptPath = await prepareRestartScript(process.env, gatewayPort);
|
||||
const serviceState = await readGatewayServiceState(resolveGatewayService(), {
|
||||
env: process.env,
|
||||
});
|
||||
if (
|
||||
shouldPrepareUpdatedInstallRestart({
|
||||
updateMode: resultWithPostUpdate.mode,
|
||||
serviceInstalled: serviceState.installed,
|
||||
serviceLoaded: serviceState.loaded,
|
||||
})
|
||||
) {
|
||||
gatewayServiceEnv = serviceState.env;
|
||||
restartScriptPath = await prepareRestartScript(serviceState.env, gatewayPort);
|
||||
refreshGatewayServiceEnv = true;
|
||||
}
|
||||
} catch {
|
||||
@@ -1446,6 +1540,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
result: resultWithPostUpdate,
|
||||
opts,
|
||||
refreshServiceEnv: refreshGatewayServiceEnv,
|
||||
serviceEnv: gatewayServiceEnv,
|
||||
gatewayPort,
|
||||
restartScriptPath,
|
||||
invocationCwd,
|
||||
|
||||
Reference in New Issue
Block a user