fix: hide bundled runtime npm windows

This commit is contained in:
Peter Steinberger
2026-04-27 08:30:59 +01:00
parent 720ea766e6
commit d23ee2f702
7 changed files with 74 additions and 2 deletions

View File

@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
- Agents/bootstrap: refresh cached workspace bootstrap snapshots on long-lived main-session turns when `AGENTS.md`, `SOUL.md`, `MEMORY.md`, or `TOOLS.md` change on disk, while preserving unchanged snapshot identity through the workspace file cache. (#64871; related #43901, #26497, #28594, #30896) Thanks @aimqwest and @mikejuyoon.
- macOS Gateway: detect installed-but-unloaded LaunchAgent split-brain states during status, doctor, and restart, and re-bootstrap launchd supervision before falling back to unmanaged listener restarts. Fixes #67335, #53475, and #71060; refs #58890, #60885, and #70801. Thanks @ze1tgeist88, @dafacto, and @vishutdhar.
- Plugins/install: treat mirrored core logger dependencies as staged bundled runtime deps so packaged Gateway starts do not crash when the external plugin-runtime-deps root is missing `tslog`. Fixes #72228; supersedes #72493. Thanks @deepujain.
- Plugins/install: hide bundled runtime-dependency npm child windows on Windows across Gateway startup, postinstall, and packaged staging paths so Telegram/Anthropic dependency repair no longer flashes shell windows. Fixes #72315. Thanks @athuljayaram and @joshfeng.
- Plugins/install: stage bundled plugin runtime dependencies before Gateway startup, drain update restarts, and materialize plugin-owned root chunks in external mirrors so staged deps resolve under native ESM. Fixes #72058; supersedes #72084. Thanks @amnesia106 and @drvoss.
- TTS/SecretRef: resolve `messages.tts.providers.*.apiKey` from the active runtime snapshot so SecretRef-backed MiniMax and other TTS provider keys work in runtime reply/audio paths. Fixes #68690. Thanks @joshavant.
- Gateway/install: surface systemd user-bus recovery hints during Linux service activation and retry via the machine user scope when `systemctl --user` reports no-medium bus failures. Fixes #39673; refs #44417 and #63561. Thanks @Arbor4, @myrsu, and @mssteuer.

View File

@@ -817,6 +817,7 @@ export function runBundledPluginPostinstall(params = {}) {
encoding: "utf8",
env: npmRunner.env ?? installEnv,
stdio: "pipe",
windowsHide: true,
shell: npmRunner.shell,
windowsVerbatimArguments: npmRunner.windowsVerbatimArguments,
});

View File

@@ -877,13 +877,15 @@ function runNpmInstall(params) {
npm_config_save: "false",
npm_config_yes: "true",
};
const result = spawnSync(params.npmRunner.command, params.npmRunner.args, {
const runSpawnSync = params.spawnSyncImpl ?? spawnSync;
const result = runSpawnSync(params.npmRunner.command, params.npmRunner.args, {
cwd: params.cwd,
encoding: "utf8",
env: npmEnv,
shell: params.npmRunner.shell,
stdio: ["ignore", "pipe", "pipe"],
timeout: params.timeoutMs ?? 5 * 60 * 1000,
windowsHide: true,
windowsVerbatimArguments: params.npmRunner.windowsVerbatimArguments,
});
if (result.status === 0) {
@@ -1240,6 +1242,10 @@ export function stageBundledPluginRuntimeDeps(params = {}) {
}
}
export const __testing = {
runNpmInstall,
};
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
stageBundledPluginRuntimeDeps();
}

View File

@@ -1,5 +1,6 @@
import { spawnSync } from "node:child_process";
import { spawn, spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { EventEmitter } from "node:events";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
@@ -27,9 +28,11 @@ import {
vi.mock("node:child_process", async (importOriginal) => ({
...(await importOriginal<typeof import("node:child_process")>()),
spawn: vi.fn(),
spawnSync: vi.fn(),
}));
const spawnMock = vi.mocked(spawn);
const spawnSyncMock = vi.mocked(spawnSync);
const tempDirs: string[] = [];
@@ -91,6 +94,7 @@ function statfsFixture(params: {
afterEach(() => {
vi.restoreAllMocks();
spawnMock.mockReset();
spawnSyncMock.mockReset();
bundledRuntimeDepsActivityTesting.resetBundledRuntimeDepsInstallActivity();
for (const dir of tempDirs.splice(0)) {
@@ -312,6 +316,7 @@ describe("installBundledRuntimeDeps", () => {
["C:\\node\\node_modules\\npm\\bin\\npm-cli.js", "install", "--ignore-scripts", "acpx@0.5.3"],
expect.objectContaining({
cwd: installRoot,
windowsHide: true,
env: expect.objectContaining({
npm_config_legacy_peer_deps: "true",
npm_config_package_lock: "false",
@@ -330,6 +335,36 @@ describe("installBundledRuntimeDeps", () => {
);
});
it("hides async npm child windows for startup repair installs", async () => {
const installRoot = makeTempDir();
spawnMock.mockImplementation((_command, _args, options) => {
writeInstalledPackage(String(options?.cwd ?? ""), "acpx", "0.5.3");
const child = new EventEmitter() as ReturnType<typeof spawn>;
Object.assign(child, {
stdout: new EventEmitter(),
stderr: new EventEmitter(),
});
queueMicrotask(() => child.emit("close", 0, null));
return child;
});
await repairBundledRuntimeDepsInstallRootAsync({
installRoot,
missingSpecs: ["acpx@0.5.3"],
installSpecs: ["acpx@0.5.3"],
env: {},
});
expect(spawnMock).toHaveBeenCalledWith(
expect.any(String),
expect.any(Array),
expect.objectContaining({
cwd: installRoot,
windowsHide: true,
}),
);
});
it("anchors non-isolated external install roots with a package manifest", () => {
const parentRoot = makeTempDir();
const installRoot = path.join(parentRoot, ".openclaw", "plugin-runtime-deps", "openclaw-test");

View File

@@ -1409,6 +1409,7 @@ async function spawnBundledRuntimeDepsInstall(params: {
cwd: params.cwd,
env: params.env,
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true,
});
const stdout: Buffer[] = [];
const stderr: Buffer[] = [];
@@ -1480,6 +1481,7 @@ export function installBundledRuntimeDeps(params: {
encoding: "utf8",
env: npmRunner.env ?? installEnv,
stdio: "pipe",
windowsHide: true,
});
if (result.status !== 0 || result.error) {
throw new Error(formatBundledRuntimeDepsInstallError(result));

View File

@@ -79,6 +79,7 @@ describe("bundled plugin postinstall", () => {
},
shell: false,
stdio: "pipe",
windowsHide: true,
windowsVerbatimArguments: undefined,
});
}

View File

@@ -2,6 +2,7 @@ import fs from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
__testing as stageBundledPluginRuntimeDepsTesting,
collectRuntimeDependencyInstallManifest,
collectRuntimeDependencyInstallSpecs,
stageBundledPluginRuntimeDeps,
@@ -129,6 +130,31 @@ describe("stageBundledPluginRuntimeDeps", () => {
});
});
it("hides npm child windows during fallback runtime installs", () => {
const spawnSyncImpl = vi.fn(() => ({ status: 0, stderr: "", stdout: "" }));
stageBundledPluginRuntimeDepsTesting.runNpmInstall({
cwd: "C:\\openclaw\\dist\\extensions\\telegram\\.openclaw-install-stage",
npmRunner: {
command: "npm.cmd",
args: ["install", "--silent"],
env: { PATH: "C:\\node" },
shell: false,
windowsVerbatimArguments: true,
},
spawnSyncImpl,
});
expect(spawnSyncImpl).toHaveBeenCalledWith(
"npm.cmd",
["install", "--silent"],
expect.objectContaining({
windowsHide: true,
windowsVerbatimArguments: true,
}),
);
});
it("skips restaging when runtime deps stamp matches the sanitized manifest", () => {
const { pluginDir, repoRoot } = createBundledPluginFixture({
packageJson: {