fix(install): bound finalization probes (#86997)

Bounds nonessential installer finalization probes so npm prefix and daemon-status checks warn and fall back instead of hanging setup.

Thanks @giodl73-repo!
This commit is contained in:
Gio Della-Libera
2026-05-26 21:39:05 -07:00
committed by GitHub
parent 119d2359f3
commit bf1a5c3303
2 changed files with 119 additions and 8 deletions
+49 -4
View File
@@ -2269,7 +2269,7 @@ ensure_user_local_bin_on_path() {
npm_global_bin_dir() {
local prefix=""
prefix="$(npm prefix -g 2>/dev/null || true)"
prefix="$(bounded_probe_output "npm prefix -g" npm prefix -g || true)"
if [[ -n "$prefix" ]]; then
if [[ "$prefix" == /* ]]; then
echo "${prefix%/}/bin"
@@ -2277,7 +2277,7 @@ npm_global_bin_dir() {
fi
fi
prefix="$(npm config get prefix 2>/dev/null || true)"
prefix="$(bounded_probe_output "npm config get prefix" npm config get prefix || true)"
if [[ -n "$prefix" && "$prefix" != "undefined" && "$prefix" != "null" ]]; then
if [[ "$prefix" == /* ]]; then
echo "${prefix%/}/bin"
@@ -2496,6 +2496,51 @@ maybe_nodenv_rehash() {
fi
}
bounded_probe_output() {
local label="$1"
shift
local timeout_seconds="${OPENCLAW_INSTALL_PROBE_TIMEOUT_SECONDS:-5}"
local output_file status_file timeout_file pid watchdog status
output_file="$(mktemp)"
status_file="$(mktemp)"
timeout_file="$(mktemp)"
TMPFILES+=("$output_file" "$status_file" "$timeout_file")
(
"$@" >"$output_file" 2>/dev/null
printf '%s' "$?" >"$status_file"
) &
pid="$!"
(
sleep "$timeout_seconds"
if kill -0 "$pid" 2>/dev/null; then
printf '1' >"$timeout_file"
kill "$pid" 2>/dev/null || true
sleep 0.1
kill -9 "$pid" 2>/dev/null || true
printf 'timeout' >"$status_file"
fi
) &
watchdog="$!"
wait "$pid" 2>/dev/null || true
kill "$watchdog" 2>/dev/null || true
wait "$watchdog" 2>/dev/null || true
status="$(cat "$status_file" 2>/dev/null || true)"
if [[ -s "$timeout_file" || "$status" == "timeout" ]]; then
echo "Warning: timed out during installer finalization probe: ${label}" >&2
return 124
fi
cat "$output_file" 2>/dev/null || true
if [[ -n "$status" && "$status" =~ ^[0-9]+$ ]]; then
return "$status"
fi
return 1
}
warn_openclaw_not_found() {
ui_warn "Installed, but openclaw is not discoverable on PATH in this shell"
echo " Try: hash -r (bash) or rehash (zsh), then retry."
@@ -2509,7 +2554,7 @@ warn_openclaw_not_found() {
fi
local npm_prefix=""
npm_prefix="$(npm prefix -g 2>/dev/null || true)"
npm_prefix="$(bounded_probe_output "npm prefix -g" npm prefix -g || true)"
local npm_bin=""
npm_bin="$(npm_global_bin_dir 2>/dev/null || true)"
if [[ -n "$npm_prefix" ]]; then
@@ -2906,7 +2951,7 @@ is_gateway_daemon_loaded() {
fi
local status_json=""
status_json="$("$claw" daemon status --json 2>/dev/null || true)"
status_json="$(bounded_probe_output "openclaw daemon status --json" "$claw" daemon status --json || true)"
if [[ -z "$status_json" ]]; then
return 1
fi
+70 -4
View File
@@ -138,7 +138,9 @@ describe("install.sh", () => {
expect(script).toContain(
'run_quiet_step "Installing Node.js" sudo apk add --no-cache nodejs npm',
);
expect(script).toContain('run_quiet_step "Installing nodejs-current" apk add --no-cache nodejs-current npm');
expect(script).toContain(
'run_quiet_step "Installing nodejs-current" apk add --no-cache nodejs-current npm',
);
expect(script).toContain("if ! node_is_at_least_required; then");
const apkIndex = script.indexOf("if command -v apk &> /dev/null && is_alpine_linux; then");
@@ -208,7 +210,9 @@ describe("install.sh", () => {
expect(result.status).toBe(0);
expect(result.stdout).toContain("step:Installing Node.js|apk add --no-cache nodejs npm");
expect(result.stdout).toContain("warn:Alpine nodejs package installed v20.15.1");
expect(result.stdout).toContain("step:Installing nodejs-current|apk add --no-cache nodejs-current npm");
expect(result.stdout).toContain(
"step:Installing nodejs-current|apk add --no-cache nodejs-current npm",
);
expect(result.stdout).toContain("finish-linux-node");
});
@@ -247,8 +251,12 @@ describe("install.sh", () => {
expect(result.status).toBe(1);
expect(result.stdout).toContain("warn:Alpine nodejs package installed v20.15.1");
expect(result.stdout).toContain("step:Installing nodejs-current|apk add --no-cache nodejs-current npm");
expect(result.stdout).toContain("error:Alpine apk repositories did not provide Node.js v22.19+");
expect(result.stdout).toContain(
"step:Installing nodejs-current|apk add --no-cache nodejs-current npm",
);
expect(result.stdout).toContain(
"error:Alpine apk repositories did not provide Node.js v22.19+",
);
expect(result.stdout).toContain("Use Alpine 3.21+ or install Node.js 24 manually");
});
@@ -758,6 +766,64 @@ describe("install.sh", () => {
expect(result.stdout).not.toContain("[4/3] Verifying installation");
});
it("bounds installer npm prefix probes during finalization helpers", () => {
const result = runInstallShell(
[
`source ${JSON.stringify(SCRIPT_PATH)}`,
"npm() {",
' if [[ "$1" == "prefix" && "$2" == "-g" ]]; then sleep 2; return 0; fi',
' if [[ "$1" == "config" && "$2" == "get" && "$3" == "prefix" ]]; then printf "/tmp/openclaw-npm\\n"; return 0; fi',
" return 1",
"}",
"npm_global_bin_dir",
].join("\n"),
{ OPENCLAW_INSTALL_PROBE_TIMEOUT_SECONDS: "0.1" },
);
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe("/tmp/openclaw-npm/bin");
expect(result.stderr).toContain("timed out during installer finalization probe: npm prefix -g");
});
it("bounds daemon status probes during finalization helpers", () => {
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-probe-"));
const claw = join(tmp, "openclaw");
writeFileSync(
claw,
[
"#!/usr/bin/env bash",
'if [[ "$1" == "daemon" && "$2" == "status" && "$3" == "--json" ]]; then',
" sleep 2",
" exit 0",
"fi",
"exit 1",
"",
].join("\n"),
);
chmodSync(claw, 0o755);
try {
const result = runInstallShell(
[
`source ${JSON.stringify(SCRIPT_PATH)}`,
`if is_gateway_daemon_loaded ${JSON.stringify(claw)}; then`,
' printf "loaded\\n"',
"else",
' printf "not-loaded\\n"',
"fi",
].join("\n"),
{ OPENCLAW_INSTALL_PROBE_TIMEOUT_SECONDS: "0.1" },
);
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe("not-loaded");
expect(result.stderr).toContain(
"timed out during installer finalization probe: openclaw daemon status --json",
);
} finally {
rmSync(tmp, { force: true, recursive: true });
}
});
it("loads nvm before checking Node.js so stale system Node does not win", () => {
expect(script).toMatch(
/# Step 2: Node\.js\s+load_nvm_for_node_detection\s+if ! check_node; then/,