mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
fix(runtime): re-exec under an available supported Node before refusing (#143464)
* fix(runtime): reuse an available compatible Node at startup Share startup recovery between the launcher and legacy CLI runtime admission so older updaters can run target Doctor through dist/index.js under an already installed compatible Node. Preserve process-contract exclusions, arguments, environment, standard streams, and exit status. Refs #140465 * test(runtime): include recovery proof in E2E routing * fix(runtime): secure Node discovery and decode service scripts Reject relative candidates and cwd-resolved runtimes before probing, except for explicit absolute PATH directories. Parse generated Windows command quoting and recorded code pages without loading application dependencies. Skip CP850 and CP949 with a diagnostic instead of guessing executable paths. Use real task-writer fixtures for encoding, quoting, and fallback coverage. Refs #140465 * fix(runtime): reject cwd-local manager symlinks * test(runtime): keep recovery home outside launcher cwd * fix(runtime): isolate recovery from dotenv environment * fix(runtime): canonicalize discovery paths before use * fix(runtime): preserve private Node recovery from home * refactor(runtime): trim Node recovery comments and aliases Behavior-neutral cleanup of the recovery launcher module: fold the serviceHome and managerHome aliases into homeDir and shorten five comment blocks to the invariant they protect.
This commit is contained in:
parent
4bc64e7957
commit
05963f8b5e
26 changed files with 2052 additions and 249 deletions
|
|
@ -1,30 +1,9 @@
|
|||
// This module must run on unsupported Node versions, before importing dist or dependencies.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createInterface } from "node:readline";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { nodeRuntimeFailure, SQLITE_CAPABILITY_PROBE } from "./node-sqlite.mjs";
|
||||
|
||||
function isUsableNode(nodePath) {
|
||||
if (!existsSync(nodePath)) {
|
||||
return false;
|
||||
}
|
||||
const result = spawnSync(
|
||||
nodePath,
|
||||
[
|
||||
"-e",
|
||||
`const probe = ${SQLITE_CAPABILITY_PROBE}; process.stdout.write(JSON.stringify({ version: process.versions.node, probe }));`,
|
||||
],
|
||||
{ encoding: "utf8", timeout: 10_000, windowsHide: true },
|
||||
);
|
||||
try {
|
||||
const details = JSON.parse(result.stdout);
|
||||
return result.status === 0 && !nodeRuntimeFailure(details.version, details.probe);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
import { isUsableNode, resolveRecoveryPath } from "./node-runtime-recovery.mjs";
|
||||
|
||||
function canInstallPrivateNode() {
|
||||
if (!["x64", "arm64"].includes(process.arch)) {
|
||||
|
|
@ -56,26 +35,38 @@ function confirmNodeUpdate() {
|
|||
}
|
||||
|
||||
/** Returns a verified private runtime, or null when recovery was declined/unavailable. */
|
||||
export async function resolveUpdatedNodeRuntime(homeDir, { allowInstall = true } = {}) {
|
||||
if (process.env.OPENCLAW_NODE_UPDATE_RESPAWNED === "1") {
|
||||
export async function resolveUpdatedNodeRuntime(
|
||||
recoveryRoot,
|
||||
{ allowInstall = true, env = process.env } = {},
|
||||
) {
|
||||
if (env.OPENCLAW_NODE_UPDATE_RESPAWNED === "1") {
|
||||
return null;
|
||||
}
|
||||
const privatePaths = { allowMissing: true, trustedRoot: recoveryRoot };
|
||||
const prefix = resolveRecoveryPath(
|
||||
path.join(recoveryRoot, "tools", "cli-node"),
|
||||
undefined,
|
||||
privatePaths,
|
||||
);
|
||||
const nodeRoot =
|
||||
prefix && resolveRecoveryPath(path.join(prefix, "tools", "node"), undefined, privatePaths);
|
||||
if (!prefix || !nodeRoot) {
|
||||
return null;
|
||||
}
|
||||
const prefix = path.join(homeDir, ".openclaw", "tools", "cli-node");
|
||||
const nodeRoot = path.join(prefix, "tools", "node");
|
||||
const nodePath =
|
||||
process.platform === "win32"
|
||||
? path.join(nodeRoot, "node.exe")
|
||||
: path.join(nodeRoot, "bin", "node");
|
||||
|
||||
// An earlier explicit opt-in is durable, but an incompatible cache is never trusted.
|
||||
if (isUsableNode(nodePath)) {
|
||||
if (isUsableNode(nodePath, { env, trustedRoot: recoveryRoot })) {
|
||||
return nodePath;
|
||||
}
|
||||
if (
|
||||
!allowInstall ||
|
||||
!process.stdin.isTTY ||
|
||||
!process.stderr.isTTY ||
|
||||
process.env.CI ||
|
||||
env.CI ||
|
||||
process.argv.some((arg) => ["--non-interactive", "--json", "--yes"].includes(arg)) ||
|
||||
!canInstallPrivateNode()
|
||||
) {
|
||||
|
|
@ -95,7 +86,7 @@ export async function resolveUpdatedNodeRuntime(homeDir, { allowInstall = true }
|
|||
new URL(windows ? "./scripts/install.ps1" : "./scripts/install-cli.sh", import.meta.url),
|
||||
);
|
||||
const command = windows
|
||||
? (await import("./scripts/windows-cmd-helpers.mjs")).resolveWindowsPowerShellPath()
|
||||
? (await import("./scripts/windows-cmd-helpers.mjs")).resolveWindowsPowerShellPath(env)
|
||||
: process.platform === "darwin"
|
||||
? "/bin/bash"
|
||||
: "bash";
|
||||
|
|
@ -112,8 +103,8 @@ export async function resolveUpdatedNodeRuntime(homeDir, { allowInstall = true }
|
|||
nodeRoot,
|
||||
]
|
||||
: [installer, "--node-only", "--prefix", prefix];
|
||||
const result = spawnSync(command, args, { stdio: "inherit" });
|
||||
if (result.status !== 0 || !isUsableNode(nodePath)) {
|
||||
const result = spawnSync(command, args, { stdio: "inherit", env });
|
||||
if (result.status !== 0 || !isUsableNode(nodePath, { env, trustedRoot: recoveryRoot })) {
|
||||
process.stderr.write(
|
||||
"openclaw: Node.js update failed; install a compatible Node.js manually.\n",
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue