mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
fix(cli): stop Node 24/26 process exits from hanging after output (#163788)
On Node 24 and 26, process.exit() joins V8's platform workers without disposing the isolate. A concurrent Maglev or Sparkplug compile job parked waiting for a main-thread GC then never finishes, so CLI commands, the hook relay, or a stopping Gateway can sit at 0% CPU forever after printing their output (nodejs/node#64274; fix pending in nodejs/node#66171). Every OpenClaw executable entrypoint (CLI entry, Gateway/service index, native hook relay, macOS node worker) now turns off Maglev and concurrent Sparkplug at startup when it runs as main, matching Node 22's tiering and the existing Vitest policy. Library imports keep stock V8; explicit --maglev or --concurrent-sparkplug flags still win. Testbox A/B (4 vCPU, Node 24.19, e2e CLI child replay, 8-way): 6/1019 hung with defaults, 0/1019 with the policy; re-enabling only Maglev hung 2/1019 and only concurrent Sparkplug 12/944, so both flags are needed.
This commit is contained in:
parent
d1148df0af
commit
1aa0aeb549
6 changed files with 46 additions and 6 deletions
|
|
@ -43,6 +43,10 @@ The **SQLite WAL-reset corruption bug** requires a safe loaded library: SQLite *
|
|||
|
||||
Separately, the **`node:sqlite` TEXT decoder** in Node 22.23.x, 24.15.0, 25.9.0, and 26.0.0 silently truncates values at embedded NUL characters. The first fixed releases are Node 24.16.0 and 26.1.0; a WAL-safe SQLite library does not fix this decoder. Node 23 was excluded earlier for incompatible `node:sqlite` behavior.
|
||||
|
||||
## V8 compiler settings
|
||||
|
||||
On Node 24 and 26, `process.exit()` can hang forever after a command has printed its output: Node joins V8's background threads while a Maglev or concurrent Sparkplug compile job waits for a garbage collection the exiting main thread never runs ([nodejs/node#64274](https://github.com/nodejs/node/issues/64274)). OpenClaw's CLI, Gateway, hook relay, and macOS node worker therefore start with Maglev and concurrent Sparkplug turned off, the tiering Node 22 used; TurboFan still optimizes hot code. Passing `--maglev` or `--concurrent-sparkplug` to `node` keeps that compiler enabled.
|
||||
|
||||
## Platform consequences
|
||||
|
||||
Official Node 24+ macOS binaries are built for **macOS 13.5+**, the oldest release Node supports. macOS does not block them on older releases, and the CLI and Gateway have been observed running on macOS 12 with official Node 24. OpenClaw does not test or support macOS 11 through 13.4, so features that ship their own native binaries can still fail there. The companion app has separate [macOS requirements](/platforms/macos).
|
||||
|
|
|
|||
25
src/bootstrap/node-exit-safe-compilers.ts
Normal file
25
src/bootstrap/node-exit-safe-compilers.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// V8 compiler tiering for OpenClaw process entrypoints that end in process.exit().
|
||||
import { setFlagsFromString } from "node:v8";
|
||||
|
||||
// process.exit() joins V8's platform workers without disposing the isolate. A
|
||||
// concurrent Maglev or Sparkplug job parked waiting for a main-thread GC then
|
||||
// never finishes, so the process hangs after its output (nodejs/node#64274;
|
||||
// fix pending in nodejs/node#66171). Node 22 ran without both; switch them off
|
||||
// before commands start compiling hot code. An explicit operator flag wins.
|
||||
const EXIT_UNSAFE_COMPILERS = [
|
||||
{ disable: "--no-maglev", enable: "--maglev" },
|
||||
{ disable: "--no-concurrent-sparkplug", enable: "--concurrent-sparkplug" },
|
||||
] as const;
|
||||
|
||||
/** Keeps V8 compile jobs that can deadlock process.exit() off background threads. */
|
||||
export function disableExitUnsafeCompilers(): void {
|
||||
if (process.versions.bun) {
|
||||
return;
|
||||
}
|
||||
const explicit = new Set(process.execArgv.map((arg) => arg.replaceAll("_", "-")));
|
||||
for (const { disable, enable } of EXIT_UNSAFE_COMPILERS) {
|
||||
if (!explicit.has(enable)) {
|
||||
setFlagsFromString(disable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
#!/usr/bin/env node
|
||||
// Dedicated cold-process entrypoint for native provider hook relays.
|
||||
import process from "node:process";
|
||||
import { disableExitUnsafeCompilers } from "../bootstrap/node-exit-safe-compilers.js";
|
||||
import { drainProcessOutput } from "../process/output-drain.js";
|
||||
import { runNativeHookRelayCliFromArgv } from "./native-hook-relay-cli.js";
|
||||
|
||||
disableExitUnsafeCompilers();
|
||||
process.title = "openclaw-hooks";
|
||||
let exitCode = 1;
|
||||
try {
|
||||
|
|
|
|||
15
src/entry.ts
15
src/entry.ts
|
|
@ -4,6 +4,7 @@
|
|||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { format } from "node:util";
|
||||
import { disableExitUnsafeCompilers } from "./bootstrap/node-exit-safe-compilers.js";
|
||||
import { resolveCliArgvInvocation } from "./cli/argv-invocation.js";
|
||||
import { isRootHelpInvocation } from "./cli/argv.js";
|
||||
import { parseCliContainerArgs, resolveCliContainerTarget } from "./cli/container-target.js";
|
||||
|
|
@ -122,12 +123,14 @@ const gatewayEntryStartupTrace = createGatewayDispatchStartupTrace(process.argv,
|
|||
// is the actual entry point; without this guard the top-level code below
|
||||
// would call runCli a second time, starting a duplicate gateway that fails
|
||||
// on the lock / port and crashes the process.
|
||||
if (
|
||||
!isMainModule({
|
||||
currentFile: fileURLToPath(import.meta.url),
|
||||
wrapperEntryPairs: [...ENTRY_WRAPPER_PAIRS],
|
||||
})
|
||||
) {
|
||||
const isEntryMain = isMainModule({
|
||||
currentFile: fileURLToPath(import.meta.url),
|
||||
wrapperEntryPairs: [...ENTRY_WRAPPER_PAIRS],
|
||||
});
|
||||
if (isEntryMain) {
|
||||
disableExitUnsafeCompilers();
|
||||
}
|
||||
if (!isEntryMain) {
|
||||
// Imported as a dependency — skip all entry-point side effects.
|
||||
} else if (isUpdateAdmissionInvocation(resolveCliArgvInvocation(process.argv))) {
|
||||
await tryRunUpdateAdmissionBeforeStartup(resolveCliArgvInvocation(process.argv));
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { existsSync } from "node:fs";
|
|||
// Package executable entrypoint that forwards to the CLI bootstrap.
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { disableExitUnsafeCompilers } from "./bootstrap/node-exit-safe-compilers.js";
|
||||
import { resolveCliArgvInvocation } from "./cli/argv-invocation.js";
|
||||
import { tryRunUpdateAdmissionBeforeStartup } from "./cli/run-main-update-admission.js";
|
||||
import {
|
||||
|
|
@ -16,6 +17,9 @@ import { isMainModule } from "./infra/is-main.js";
|
|||
const isMain = isMainModule({
|
||||
currentFile: fileURLToPath(import.meta.url),
|
||||
});
|
||||
if (isMain) {
|
||||
disableExitUnsafeCompilers();
|
||||
}
|
||||
const handledAdmission =
|
||||
isMain && (await tryRunUpdateAdmissionBeforeStartup(resolveCliArgvInvocation(process.argv)));
|
||||
const packageRootUrl = new URL("../", import.meta.url);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Sealed CLI composition root for the private macOS app node-host worker.
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { disableExitUnsafeCompilers } from "../bootstrap/node-exit-safe-compilers.js";
|
||||
import { ensureCliExecutionBootstrap } from "../cli/command-execution-startup.js";
|
||||
import { resolveCliStartupPolicy } from "../cli/command-startup-policy.js";
|
||||
import { loadCliDotEnv } from "../cli/dotenv.js";
|
||||
|
|
@ -82,6 +83,7 @@ async function runMacNodeWorkerEntry(argv: string[] = process.argv): Promise<voi
|
|||
}
|
||||
|
||||
if (isMainModule({ currentFile: fileURLToPath(import.meta.url) })) {
|
||||
disableExitUnsafeCompilers();
|
||||
// The worker records its exit request after draining runtime-owned resources.
|
||||
// Finalize it here so plugin-owned pipes cannot pin shutdown or startup failure.
|
||||
await runCliWithExitFinalization({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue