mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(daemon): prevent macOS node startup hangs (#130097)
Prevent macOS node LaunchAgents from activating inherited Node compile caches while keeping the managed cache for worker processes. Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
@@ -5,6 +5,7 @@ import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { resolveAutoNodeExtraCaCerts } from "../bootstrap/node-extra-ca-certs.js";
|
||||
import { inspectGatewayHeapLimit } from "./gateway-heap.js";
|
||||
import { buildLaunchAgentPlist } from "./launchd-plist.js";
|
||||
import { resolveGatewayStateDir } from "./paths.js";
|
||||
import {
|
||||
buildNodeServiceEnvironment,
|
||||
@@ -784,6 +785,44 @@ describe("buildNodeServiceEnvironment", () => {
|
||||
expect(env.OPENCLAW_LAUNCHD_LABEL).toBe("ai.openclaw.node");
|
||||
});
|
||||
|
||||
it("fences inherited Node compile cache in macOS node LaunchAgents", () => {
|
||||
const environment = buildNodeServiceEnvironment({
|
||||
env: {
|
||||
HOME: "/Users/user",
|
||||
NODE_COMPILE_CACHE: "/tmp/ambient-node-compile-cache",
|
||||
},
|
||||
platform: "darwin",
|
||||
});
|
||||
const plist = buildLaunchAgentPlist({
|
||||
label: "ai.openclaw.node",
|
||||
programArguments: ["/usr/local/bin/node", "dist/index.js", "node", "run"],
|
||||
stdoutPath: "/tmp/openclaw-node.log",
|
||||
stderrPath: "/tmp/openclaw-node.err.log",
|
||||
environment,
|
||||
});
|
||||
|
||||
expect(environment.NODE_DISABLE_COMPILE_CACHE).toBe("1");
|
||||
expect(environment.NODE_COMPILE_CACHE).toBeUndefined();
|
||||
expect(plist).toContain("<key>NODE_DISABLE_COMPILE_CACHE</key>");
|
||||
expect(plist).not.toContain("<key>NODE_COMPILE_CACHE</key>");
|
||||
});
|
||||
|
||||
it.each(["linux", "win32"] as const)(
|
||||
"does not force Node compile cache off for %s node services",
|
||||
(platform) => {
|
||||
const environment = buildNodeServiceEnvironment({
|
||||
env: {
|
||||
HOME: platform === "win32" ? "C:\\Users\\user" : "/home/user",
|
||||
NODE_COMPILE_CACHE: "/tmp/ambient-node-compile-cache",
|
||||
},
|
||||
platform,
|
||||
});
|
||||
|
||||
expect(environment.NODE_DISABLE_COMPILE_CACHE).toBeUndefined();
|
||||
expect(environment.NODE_COMPILE_CACHE).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
it("passes through OPENCLAW_GATEWAY_TOKEN for node services", () => {
|
||||
const env = buildNodeServiceEnvironment({
|
||||
env: { HOME: "/home/user", OPENCLAW_GATEWAY_TOKEN: " node-token " },
|
||||
|
||||
@@ -392,6 +392,9 @@ export function buildNodeServiceEnvironment(params: {
|
||||
CF_ACCESS_CLIENT_ID: cloudflareAccessClientId,
|
||||
CF_ACCESS_CLIENT_SECRET: cloudflareAccessClientSecret,
|
||||
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS: allowInsecurePrivateWs,
|
||||
// launchd manager variables outlive the installer. Worker snapshots scope
|
||||
// this host fence by the canonical managed-node service identity.
|
||||
NODE_DISABLE_COMPILE_CACHE: platform === "darwin" ? "1" : undefined,
|
||||
...resolveNodeServiceIdentityEnvironment(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -180,6 +180,31 @@ describe("node worker bundle installer", () => {
|
||||
await expect(fs.readFile(prewarmMarker, "utf8")).resolves.toBe("ready");
|
||||
});
|
||||
|
||||
it("prewarms bundles with managed cache behind a host compile-cache fence", async () => {
|
||||
const prewarmMarker = path.join(root, "worker-prewarmed-with-managed-cache");
|
||||
const fixture = await bundleFixture({
|
||||
prewarmMarker,
|
||||
bundlePrewarm: 1,
|
||||
compileCacheDisabled: false,
|
||||
});
|
||||
const served = await serve(fixture.archive, fixture.input.archive.token);
|
||||
const installer = new NodeWorkerBundleInstaller({
|
||||
root,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_COMPILE_CACHE: "/tmp/ambient-host-compile-cache",
|
||||
NODE_DISABLE_COMPILE_CACHE: "1",
|
||||
OPENCLAW_LAUNCHD_LABEL: "ai.openclaw.node",
|
||||
OPENCLAW_SERVICE_KIND: "node",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
installer.ensure({ input: fixture.input, gatewayUrl: served.gatewayUrl }),
|
||||
).resolves.toEqual(fixture.input.build);
|
||||
await expect(fs.readFile(prewarmMarker, "utf8")).resolves.toBe("ready");
|
||||
});
|
||||
|
||||
it("reuses a v1 install when Windows cannot retain Unix artifact modes", async () => {
|
||||
const fixture = await bundleFixture();
|
||||
const served = await serve(fixture.archive, fixture.input.archive.token);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from "node:path";
|
||||
import { NODE_SERVICE_KIND, resolveNodeLaunchAgentLabel } from "../daemon/constants.js";
|
||||
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
|
||||
|
||||
const POSIX_WORKER_ENV_KEYS = new Set([
|
||||
@@ -50,10 +51,15 @@ export function snapshotNodeWorkerEnv(source: NodeJS.ProcessEnv): NodeJS.Process
|
||||
}
|
||||
snapshot[key] = value;
|
||||
}
|
||||
if (source.NODE_DISABLE_COMPILE_CACHE === undefined) {
|
||||
const hostCacheFenced =
|
||||
source.NODE_DISABLE_COMPILE_CACHE !== undefined &&
|
||||
source.OPENCLAW_SERVICE_KIND === NODE_SERVICE_KIND &&
|
||||
source.OPENCLAW_LAUNCHD_LABEL === resolveNodeLaunchAgentLabel();
|
||||
const workerCacheDisabled = source.NODE_DISABLE_COMPILE_CACHE !== undefined && !hostCacheFenced;
|
||||
if (!workerCacheDisabled) {
|
||||
const requestedCache = hostCacheFenced ? undefined : source.NODE_COMPILE_CACHE?.trim();
|
||||
snapshot.NODE_COMPILE_CACHE =
|
||||
source.NODE_COMPILE_CACHE?.trim() ||
|
||||
path.join(resolvePreferredOpenClawTmpDir(), "node-worker-compile-cache");
|
||||
requestedCache || path.join(resolvePreferredOpenClawTmpDir(), "node-worker-compile-cache");
|
||||
} else {
|
||||
snapshot.NODE_DISABLE_COMPILE_CACHE = "1";
|
||||
}
|
||||
|
||||
@@ -445,9 +445,13 @@ describe("node worker supervisor", () => {
|
||||
HOME: path.join(root, "worker-home"),
|
||||
LANG: "en_US.UTF-8",
|
||||
LC_TIME: "de_DE.UTF-8",
|
||||
NODE_COMPILE_CACHE: path.join(root, "host-compile-cache"),
|
||||
NODE_DISABLE_COMPILE_CACHE: "1",
|
||||
NODE_EXTRA_CA_CERTS: path.join(root, "private-ca.pem"),
|
||||
NODE_USE_SYSTEM_CA: "1",
|
||||
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS: "1",
|
||||
OPENCLAW_LAUNCHD_LABEL: "ai.openclaw.node",
|
||||
OPENCLAW_SERVICE_KIND: "node",
|
||||
OPENCLAW_SUPPLIED_SECRET: "supplied-openclaw-secret",
|
||||
NODE_OPTIONS: "--title=forbidden-worker-title",
|
||||
BASH_ENV: path.join(root, "forbidden-shell-init"),
|
||||
@@ -486,8 +490,11 @@ describe("node worker supervisor", () => {
|
||||
expect(workerEnv).toMatchObject(expectedWorkerEnv);
|
||||
expect(workerEnv).not.toHaveProperty("AMBIENT_SECRET");
|
||||
expect(workerEnv).not.toHaveProperty("OPENCLAW_AMBIENT_SECRET");
|
||||
expect(workerEnv).not.toHaveProperty("OPENCLAW_LAUNCHD_LABEL");
|
||||
expect(workerEnv).not.toHaveProperty("OPENCLAW_SERVICE_KIND");
|
||||
expect(workerEnv).not.toHaveProperty("OPENCLAW_STATE_DIR");
|
||||
expect(workerEnv).not.toHaveProperty("OPENCLAW_SUPPLIED_SECRET");
|
||||
expect(workerEnv).not.toHaveProperty("NODE_DISABLE_COMPILE_CACHE");
|
||||
expect(workerEnv).not.toHaveProperty("NODE_OPTIONS");
|
||||
expect(workerEnv).not.toHaveProperty("BASH_ENV");
|
||||
expect(workerEnv).not.toHaveProperty("DYLD_INSERT_LIBRARIES");
|
||||
|
||||
Reference in New Issue
Block a user