fix(respawn): rewrite pnpm versioned entry paths to stable wrapper (fixes #52313) (#93671)

* fix(respawn): rewrite pnpm versioned entry paths to stable wrapper

During self-update the pnpm versioned directory (node_modules/.pnpm/openclaw@<ver>/)
may be removed. If process.argv contains the versioned path, the respawned child
fails to start because the entrypoint no longer exists.

Detect pnpm versioned realpaths in spawnDetachedGatewayProcess and rewrite them
to the stable node_modules/<pkg>/openclaw.mjs wrapper before spawning.

Fixes #52313

* fix(respawn): scope pnpm entry rewrite to openclaw

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
This commit is contained in:
liuhao1024
2026-06-17 01:15:24 +08:00
committed by GitHub
parent 62503c4b48
commit 0278b59d0e
2 changed files with 64 additions and 3 deletions

View File

@@ -298,7 +298,7 @@ describe("respawnGatewayProcessForUpdate", () => {
process.execArgv = [];
process.argv = [
"C:\\Program Files\\node.exe",
"C:\\openclaw\\dist\\index.js",
"C:\\openclaw\\node_modules\\.pnpm\\openclaw@2026.6.5\\node_modules\\openclaw\\dist\\index.js",
"gateway",
"run",
];
@@ -310,7 +310,7 @@ describe("respawnGatewayProcessForUpdate", () => {
expect(result.pid).toBe(5151);
expect(spawnMock).toHaveBeenCalledWith(
process.execPath,
["C:\\openclaw\\dist\\index.js", "gateway", "run"],
["C:\\openclaw\\node_modules\\openclaw\\openclaw.mjs", "gateway", "run"],
{
detached: true,
env: process.env,
@@ -319,6 +319,50 @@ describe("respawnGatewayProcessForUpdate", () => {
);
});
it("rewrites a pnpm-versioned OpenClaw entry before detached update respawn", () => {
clearSupervisorHints();
setPlatform("linux");
process.execArgv = [];
process.argv = [
"/usr/local/bin/node",
"/app/node_modules/.pnpm/openclaw@2026.6.5/node_modules/openclaw/dist/entry.js",
"gateway",
"run",
];
spawnMock.mockReturnValue({ pid: 7171, unref: vi.fn(), kill: vi.fn() });
const result = respawnGatewayProcessForUpdate();
expect(result.mode).toBe("spawned");
expect(spawnMock).toHaveBeenCalledWith(
process.execPath,
["/app/node_modules/openclaw/openclaw.mjs", "gateway", "run"],
{
detached: true,
env: process.env,
stdio: "inherit",
},
);
});
it("does not rewrite another package's pnpm-versioned entry", () => {
clearSupervisorHints();
setPlatform("linux");
process.execArgv = [];
const entry =
"/app/node_modules/.pnpm/@anthropic+sdk@1.0.0/node_modules/@anthropic/sdk/dist/index.js";
process.argv = ["/usr/local/bin/node", entry, "gateway", "run"];
spawnMock.mockReturnValue({ pid: 8181, unref: vi.fn(), kill: vi.fn() });
respawnGatewayProcessForUpdate();
expect(spawnMock).toHaveBeenCalledWith(process.execPath, [entry, "gateway", "run"], {
detached: true,
env: process.env,
stdio: "inherit",
});
});
it("spawns a detached update process when macOS only has inherited XPC state", () => {
clearSupervisorHints();
setPlatform("darwin");

View File

@@ -26,11 +26,28 @@ function isTruthy(value: string | undefined): boolean {
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
}
const PNPM_VERSIONED_OPENCLAW_ENTRY_PATTERN =
/^(.*?)([\\/])node_modules\2\.pnpm\2openclaw@[^\\/]+\2node_modules\2openclaw\2.+$/;
function rewritePnpmVersionedOpenClawEntryPath(entryPath: string): string {
// pnpm can expose argv[1] as a versioned realpath that self-update removes.
// Respawn through the stable OpenClaw package wrapper instead.
return entryPath.replace(
PNPM_VERSIONED_OPENCLAW_ENTRY_PATTERN,
"$1$2node_modules$2openclaw$2openclaw.mjs",
);
}
function spawnDetachedGatewayProcess(opts: GatewayRespawnOptions = {}): {
child: ChildProcess;
pid?: number;
} {
const args = [...process.execArgv, ...process.argv.slice(1)];
const [entryArg, ...entryArgs] = process.argv.slice(1);
const args = [
...process.execArgv,
...(entryArg ? [rewritePnpmVersionedOpenClawEntryPath(entryArg)] : []),
...entryArgs,
];
const child = spawn(process.execPath, args, {
env: opts.env ? { ...process.env, ...opts.env } : process.env,
detached: true,