fix: block package updates from inside running gateway service

This commit is contained in:
masonxhuang
2026-05-02 00:00:05 +08:00
committed by Mason Huang
parent 473fc0aad8
commit ccdaa8120a
2 changed files with 107 additions and 20 deletions

View File

@@ -612,6 +612,24 @@ describe("update-cli", () => {
expect(runDaemonRestart).not.toHaveBeenCalled();
});
it("does not carry gateway service markers into the post-core update process", async () => {
setupUpdatedRootRefresh();
await withEnvAsync(
{
OPENCLAW_SERVICE_MARKER: "openclaw",
OPENCLAW_SERVICE_KIND: "gateway",
},
async () => {
await updateCommand({ yes: true });
},
);
const spawnEnv = (spawn.mock.calls[0]?.[2] as { env?: NodeJS.ProcessEnv } | undefined)?.env;
expect(spawnEnv?.OPENCLAW_SERVICE_MARKER).toBeUndefined();
expect(spawnEnv?.OPENCLAW_SERVICE_KIND).toBeUndefined();
});
it("respawns into the updated git root before requested channel persistence", async () => {
const { entrypoints } = setupUpdatedRootRefresh({
gatewayUpdateImpl: async (root) =>
@@ -1263,7 +1281,7 @@ describe("update-cli", () => {
).toContain("Low disk space near");
});
it("refuses package updates from inside the gateway service process", async () => {
it("allows package updates from inherited gateway service env when the managed gateway is not running", async () => {
mockPackageInstallStatus(createCaseDir("openclaw-update"));
await withEnvAsync(
@@ -1276,12 +1294,42 @@ describe("update-cli", () => {
},
);
expect(defaultRuntime.error).not.toHaveBeenCalledWith(
expect.stringContaining(
"Package updates cannot run from inside the gateway service process.",
),
);
expectPackageInstallSpec("openclaw@latest");
});
it("refuses package updates from inherited gateway service env when --no-restart leaves the gateway running", async () => {
mockPackageInstallStatus(createCaseDir("openclaw-update"));
serviceReadCommand.mockResolvedValue({
programArguments: ["openclaw", "gateway", "run"],
environment: {
OPENCLAW_SERVICE_MARKER: "openclaw",
OPENCLAW_SERVICE_KIND: "gateway",
},
});
serviceLoaded.mockResolvedValue(true);
await withEnvAsync(
{
OPENCLAW_SERVICE_MARKER: "openclaw",
OPENCLAW_SERVICE_KIND: "gateway",
},
async () => {
await updateCommand({ yes: true, restart: false });
},
);
expect(defaultRuntime.error).toHaveBeenCalledWith(
expect.stringContaining(
"Package updates cannot run from inside the gateway service process.",
),
);
expect(defaultRuntime.exit).toHaveBeenCalledWith(1);
expect(serviceStop).not.toHaveBeenCalled();
expect(runGatewayUpdate).not.toHaveBeenCalled();
expect(runCommandWithTimeout).not.toHaveBeenCalledWith(
["npm", "i", "-g", "openclaw@latest", "--no-fund", "--no-audit", "--loglevel=error"],
@@ -1605,7 +1653,15 @@ describe("update-cli", () => {
};
});
await updateCommand({ yes: true });
await withEnvAsync(
{
OPENCLAW_SERVICE_MARKER: "openclaw",
OPENCLAW_SERVICE_KIND: "gateway",
},
async () => {
await updateCommand({ yes: true });
},
);
const npmInstallCallIndex = vi
.mocked(runCommandWithTimeout)

View File

@@ -162,6 +162,8 @@ export function shouldUseLegacyProcessRestartAfterUpdate(params: {
type PrePackageServiceStop = {
stopped: boolean;
inspected: boolean;
running: boolean;
serviceEnv?: NodeJS.ProcessEnv;
};
@@ -175,11 +177,11 @@ async function maybeStopManagedServiceBeforePackageUpdate(params: {
service = resolveGatewayService();
serviceState = await readGatewayServiceState(service, { env: process.env });
} catch {
return { stopped: false };
return { stopped: false, inspected: false, running: false };
}
if (!serviceState.installed) {
return { stopped: false };
return { stopped: false, inspected: true, running: false };
}
if (!params.shouldRestart) {
@@ -190,18 +192,23 @@ async function maybeStopManagedServiceBeforePackageUpdate(params: {
),
);
}
return { stopped: false, serviceEnv: serviceState.env };
return {
stopped: false,
inspected: true,
running: serviceState.running,
serviceEnv: serviceState.env,
};
}
if (!serviceState.running) {
return { stopped: false, serviceEnv: serviceState.env };
return { stopped: false, inspected: true, running: false, serviceEnv: serviceState.env };
}
if (!params.jsonMode) {
defaultRuntime.log(theme.muted("Stopping managed gateway service before package update..."));
}
await service.stop({ env: serviceState.env, stdout: process.stdout });
return { stopped: true, serviceEnv: serviceState.env };
return { stopped: true, inspected: true, running: true, serviceEnv: serviceState.env };
}
async function maybeRestartServiceAfterFailedPackageUpdate(params: {
@@ -239,6 +246,22 @@ function isRunningInsideGatewayService(
return !serviceKind || serviceKind === GATEWAY_SERVICE_KIND;
}
function shouldBlockPackageUpdateFromGatewayServiceEnv(params: {
prePackageServiceStop: PrePackageServiceStop | undefined;
}): boolean {
if (!isRunningInsideGatewayService()) {
return false;
}
const stopState = params.prePackageServiceStop;
if (!stopState?.inspected) {
return true;
}
if (!stopState.running) {
return false;
}
return !stopState.stopped;
}
function formatCommandFailure(stdout: string, stderr: string): string {
const detail = (stderr || stdout).trim();
if (!detail) {
@@ -317,6 +340,13 @@ function disableUpdatedPackageCompileCacheEnv(env: NodeJS.ProcessEnv): NodeJS.Pr
};
}
function stripGatewayServiceMarkerEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const resolvedEnv = { ...env };
delete resolvedEnv.OPENCLAW_SERVICE_MARKER;
delete resolvedEnv.OPENCLAW_SERVICE_KIND;
return resolvedEnv;
}
function resolveUpdatedInstallCommandEnv(
env: NodeJS.ProcessEnv,
invocationCwd?: string,
@@ -1271,7 +1301,7 @@ async function continuePostCoreUpdateInFreshProcess(params: {
const child = spawn(resolveNodeRunner(), argv, {
stdio: "inherit",
env: {
...disableUpdatedPackageCompileCacheEnv(process.env),
...stripGatewayServiceMarkerEnv(disableUpdatedPackageCompileCacheEnv(process.env)),
[POST_CORE_UPDATE_ENV]: "1",
[POST_CORE_UPDATE_CHANNEL_ENV]: params.channel,
...(params.requestedChannel
@@ -1555,18 +1585,6 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
return;
}
if (updateInstallKind === "package" && isRunningInsideGatewayService()) {
defaultRuntime.error(
[
"Package updates cannot run from inside the gateway service process.",
"That path replaces the active OpenClaw dist tree while the live gateway may still lazy-load old chunks.",
`Run \`${replaceCliName(formatCliCommand("openclaw update"), CLI_NAME)}\` from a shell outside the gateway service, or stop the gateway service first and then update.`,
].join("\n"),
);
defaultRuntime.exit(1);
return;
}
if (downgradeRisk && !opts.yes) {
if (!process.stdin.isTTY || opts.json) {
defaultRuntime.error(
@@ -1634,6 +1652,19 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
defaultRuntime.exit(1);
return;
}
if (shouldBlockPackageUpdateFromGatewayServiceEnv({ prePackageServiceStop })) {
stop();
defaultRuntime.error(
[
"Package updates cannot run from inside the gateway service process.",
"That path replaces the active OpenClaw dist tree while the live gateway may still lazy-load old chunks.",
`Run \`${replaceCliName(formatCliCommand("openclaw update"), CLI_NAME)}\` from a shell outside the gateway service, or stop the gateway service first and then update.`,
].join("\n"),
);
defaultRuntime.exit(1);
return;
}
}
let result: UpdateRunResult;