mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(daemon): ignore recursive Windows gateway wrapper
Fixes #86007. Release note: Windows gateway install/update now ignores a persisted OPENCLAW_WRAPPER when it points back at the generated gateway.cmd task script, preventing recursive gateway startup while keeping valid wrapper installs intact. Credit: thanks @luoyanglang for the fix and proof.
This commit is contained in:
@@ -275,6 +275,42 @@ describe("buildGatewayInstallPlan", () => {
|
||||
expect(plan.environment.OPENCLAW_WRAPPER).toBe(wrapperPath);
|
||||
});
|
||||
|
||||
it("clears a Windows wrapper env that points at the generated gateway.cmd script", async () => {
|
||||
const selfWrapperPath = path.join(isolatedHome, ".openclaw", "gateway.cmd");
|
||||
const warn = vi.fn();
|
||||
mockNodeGatewayPlanFixture({
|
||||
serviceEnvironment: {
|
||||
OPENCLAW_PORT: "3000",
|
||||
},
|
||||
});
|
||||
|
||||
const plan = await buildGatewayInstallPlan({
|
||||
env: isolatedPlanEnv({
|
||||
OPENCLAW_WRAPPER: selfWrapperPath,
|
||||
}),
|
||||
port: 3000,
|
||||
runtime: "node",
|
||||
platform: "win32",
|
||||
warn,
|
||||
});
|
||||
|
||||
expect(mocks.resolveGatewayProgramArguments).toHaveBeenCalledOnce();
|
||||
expect(
|
||||
firstMockArg(mocks.resolveGatewayProgramArguments, "resolveGatewayProgramArguments")
|
||||
.wrapperPath,
|
||||
).toBeUndefined();
|
||||
expect(mocks.buildServiceEnvironment).toHaveBeenCalledOnce();
|
||||
expect(
|
||||
firstMockArg(mocks.buildServiceEnvironment, "buildServiceEnvironment").env?.OPENCLAW_WRAPPER,
|
||||
).toBeUndefined();
|
||||
expect(plan.environment.OPENCLAW_WRAPPER).toBeUndefined();
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"Ignoring OPENCLAW_WRAPPER because it points to the Windows task script",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("tracks safe config env keys without embedding literal values", async () => {
|
||||
mockNodeGatewayPlanFixture({
|
||||
serviceEnvironment: {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { collectDurableServiceEnvVarSources } from "../config/state-dir-dotenv.j
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import { resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js";
|
||||
import { resolveGatewayStateDir } from "../daemon/paths.js";
|
||||
import { resolveGatewayStateDir, resolveGatewayTaskScriptPath } from "../daemon/paths.js";
|
||||
import {
|
||||
OPENCLAW_WRAPPER_ENV_KEY,
|
||||
resolveGatewayProgramArguments,
|
||||
@@ -519,12 +519,24 @@ export async function buildGatewayInstallPlan(params: {
|
||||
devMode: params.devMode,
|
||||
nodePath: params.nodePath,
|
||||
});
|
||||
const wrapperPath = await resolveOpenClawWrapperPath(
|
||||
params.wrapperPath ?? params.env[OPENCLAW_WRAPPER_ENV_KEY],
|
||||
);
|
||||
const wrapperInput = params.wrapperPath ?? params.env[OPENCLAW_WRAPPER_ENV_KEY];
|
||||
const wrapperPointsAtWindowsTaskScript =
|
||||
Boolean(wrapperInput?.trim()) &&
|
||||
platform === "win32" &&
|
||||
isSameServicePath(wrapperInput, resolveGatewayTaskScriptPath(params.env), platform);
|
||||
if (wrapperPointsAtWindowsTaskScript) {
|
||||
params.warn?.(
|
||||
`Ignoring ${OPENCLAW_WRAPPER_ENV_KEY} because it points to the Windows task script; using the OpenClaw gateway entrypoint directly to avoid a recursive gateway.cmd wrapper.`,
|
||||
);
|
||||
}
|
||||
const wrapperPath = wrapperPointsAtWindowsTaskScript
|
||||
? undefined
|
||||
: await resolveOpenClawWrapperPath(wrapperInput);
|
||||
const serviceInputEnv: Record<string, string | undefined> = wrapperPath
|
||||
? { ...params.env, [OPENCLAW_WRAPPER_ENV_KEY]: wrapperPath }
|
||||
: params.env;
|
||||
: wrapperPointsAtWindowsTaskScript
|
||||
? omitEnvKey(params.env, OPENCLAW_WRAPPER_ENV_KEY)
|
||||
: params.env;
|
||||
const { programArguments, workingDirectory } = await resolveGatewayProgramArguments({
|
||||
port: params.port,
|
||||
dev: devMode,
|
||||
@@ -578,6 +590,36 @@ export async function buildGatewayInstallPlan(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeServicePathForCompare(
|
||||
value: string | undefined,
|
||||
platform: NodeJS.Platform,
|
||||
): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
return platform === "win32" ? path.win32.resolve(trimmed).toLowerCase() : path.resolve(trimmed);
|
||||
}
|
||||
|
||||
function isSameServicePath(
|
||||
left: string | undefined,
|
||||
right: string | undefined,
|
||||
platform: NodeJS.Platform,
|
||||
): boolean {
|
||||
const normalizedLeft = normalizeServicePathForCompare(left, platform);
|
||||
const normalizedRight = normalizeServicePathForCompare(right, platform);
|
||||
return Boolean(normalizedLeft && normalizedRight && normalizedLeft === normalizedRight);
|
||||
}
|
||||
|
||||
function omitEnvKey(
|
||||
env: Record<string, string | undefined>,
|
||||
key: string,
|
||||
): Record<string, string | undefined> {
|
||||
const next = { ...env };
|
||||
delete next[key];
|
||||
return next;
|
||||
}
|
||||
|
||||
export function gatewayInstallErrorHint(platform = process.platform): string {
|
||||
return platform === "win32"
|
||||
? "Tip: native Windows now falls back to a per-user Startup-folder login item when Scheduled Task creation is denied; if install still fails, rerun from an elevated PowerShell or skip service install."
|
||||
|
||||
@@ -41,3 +41,17 @@ export function resolveGatewayStateDir(env: Record<string, string | undefined>):
|
||||
const suffix = resolveGatewayProfileSuffix(env.OPENCLAW_PROFILE);
|
||||
return path.join(home, `.openclaw${suffix}`);
|
||||
}
|
||||
|
||||
export function resolveGatewayTaskScriptPath(env: Record<string, string | undefined>): string {
|
||||
const override = normalizeOptionalString(env.OPENCLAW_TASK_SCRIPT);
|
||||
if (override) {
|
||||
return override;
|
||||
}
|
||||
const scriptName = normalizeOptionalString(env.OPENCLAW_TASK_SCRIPT_NAME) || "gateway.cmd";
|
||||
if (/[/\\]|\.\./.test(scriptName)) {
|
||||
throw new Error(
|
||||
`OPENCLAW_TASK_SCRIPT_NAME must be a file name only, not a path: ${scriptName}`,
|
||||
);
|
||||
}
|
||||
return path.join(resolveGatewayStateDir(env), scriptName);
|
||||
}
|
||||
|
||||
+2
-13
@@ -13,7 +13,7 @@ import { parseCmdScriptCommandLine, quoteCmdScriptArg } from "./cmd-argv.js";
|
||||
import { assertNoCmdLineBreak, parseCmdSetAssignment, renderCmdSetAssignment } from "./cmd-set.js";
|
||||
import { resolveGatewayServiceDescription, resolveGatewayWindowsTaskName } from "./constants.js";
|
||||
import { formatLine, writeFormattedLines } from "./output.js";
|
||||
import { resolveGatewayStateDir } from "./paths.js";
|
||||
import { resolveGatewayTaskScriptPath } from "./paths.js";
|
||||
import { parseKeyValueOutput } from "./runtime-parse.js";
|
||||
import { execSchtasks } from "./schtasks-exec.js";
|
||||
import type { GatewayServiceRuntime } from "./service-runtime.js";
|
||||
@@ -47,18 +47,7 @@ function shouldFallbackToStartupEntry(params: { code: number; detail: string }):
|
||||
}
|
||||
|
||||
export function resolveTaskScriptPath(env: GatewayServiceEnv): string {
|
||||
const override = env.OPENCLAW_TASK_SCRIPT?.trim();
|
||||
if (override) {
|
||||
return override;
|
||||
}
|
||||
const scriptName = env.OPENCLAW_TASK_SCRIPT_NAME?.trim() || "gateway.cmd";
|
||||
if (/[/\\]|\.\./.test(scriptName)) {
|
||||
throw new Error(
|
||||
`OPENCLAW_TASK_SCRIPT_NAME must be a file name only, not a path: ${scriptName}`,
|
||||
);
|
||||
}
|
||||
const stateDir = resolveGatewayStateDir(env);
|
||||
return path.join(stateDir, scriptName);
|
||||
return resolveGatewayTaskScriptPath(env);
|
||||
}
|
||||
|
||||
function resolveWindowsStartupDir(env: GatewayServiceEnv): string {
|
||||
|
||||
Reference in New Issue
Block a user