Skip to content

Commit bf1a5c3

Browse files
authored
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!
1 parent 119d235 commit bf1a5c3

2 files changed

Lines changed: 119 additions & 8 deletions

File tree

scripts/install.sh

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2269,15 +2269,15 @@ ensure_user_local_bin_on_path() {
22692269

22702270
npm_global_bin_dir() {
22712271
local prefix=""
2272-
prefix="$(npm prefix -g 2>/dev/null || true)"
2272+
prefix="$(bounded_probe_output "npm prefix -g" npm prefix -g || true)"
22732273
if [[ -n "$prefix" ]]; then
22742274
if [[ "$prefix" == /* ]]; then
22752275
echo "${prefix%/}/bin"
22762276
return 0
22772277
fi
22782278
fi
22792279

2280-
prefix="$(npm config get prefix 2>/dev/null || true)"
2280+
prefix="$(bounded_probe_output "npm config get prefix" npm config get prefix || true)"
22812281
if [[ -n "$prefix" && "$prefix" != "undefined" && "$prefix" != "null" ]]; then
22822282
if [[ "$prefix" == /* ]]; then
22832283
echo "${prefix%/}/bin"
@@ -2496,6 +2496,51 @@ maybe_nodenv_rehash() {
24962496
fi
24972497
}
24982498

2499+
bounded_probe_output() {
2500+
local label="$1"
2501+
shift
2502+
local timeout_seconds="${OPENCLAW_INSTALL_PROBE_TIMEOUT_SECONDS:-5}"
2503+
local output_file status_file timeout_file pid watchdog status
2504+
output_file="$(mktemp)"
2505+
status_file="$(mktemp)"
2506+
timeout_file="$(mktemp)"
2507+
TMPFILES+=("$output_file" "$status_file" "$timeout_file")
2508+
2509+
(
2510+
"$@" >"$output_file" 2>/dev/null
2511+
printf '%s' "$?" >"$status_file"
2512+
) &
2513+
pid="$!"
2514+
2515+
(
2516+
sleep "$timeout_seconds"
2517+
if kill -0 "$pid" 2>/dev/null; then
2518+
printf '1' >"$timeout_file"
2519+
kill "$pid" 2>/dev/null || true
2520+
sleep 0.1
2521+
kill -9 "$pid" 2>/dev/null || true
2522+
printf 'timeout' >"$status_file"
2523+
fi
2524+
) &
2525+
watchdog="$!"
2526+
2527+
wait "$pid" 2>/dev/null || true
2528+
kill "$watchdog" 2>/dev/null || true
2529+
wait "$watchdog" 2>/dev/null || true
2530+
2531+
status="$(cat "$status_file" 2>/dev/null || true)"
2532+
if [[ -s "$timeout_file" || "$status" == "timeout" ]]; then
2533+
echo "Warning: timed out during installer finalization probe: ${label}" >&2
2534+
return 124
2535+
fi
2536+
2537+
cat "$output_file" 2>/dev/null || true
2538+
if [[ -n "$status" && "$status" =~ ^[0-9]+$ ]]; then
2539+
return "$status"
2540+
fi
2541+
return 1
2542+
}
2543+
24992544
warn_openclaw_not_found() {
25002545
ui_warn "Installed, but openclaw is not discoverable on PATH in this shell"
25012546
echo " Try: hash -r (bash) or rehash (zsh), then retry."
@@ -2509,7 +2554,7 @@ warn_openclaw_not_found() {
25092554
fi
25102555

25112556
local npm_prefix=""
2512-
npm_prefix="$(npm prefix -g 2>/dev/null || true)"
2557+
npm_prefix="$(bounded_probe_output "npm prefix -g" npm prefix -g || true)"
25132558
local npm_bin=""
25142559
npm_bin="$(npm_global_bin_dir 2>/dev/null || true)"
25152560
if [[ -n "$npm_prefix" ]]; then
@@ -2906,7 +2951,7 @@ is_gateway_daemon_loaded() {
29062951
fi
29072952

29082953
local status_json=""
2909-
status_json="$("$claw" daemon status --json 2>/dev/null || true)"
2954+
status_json="$(bounded_probe_output "openclaw daemon status --json" "$claw" daemon status --json || true)"
29102955
if [[ -z "$status_json" ]]; then
29112956
return 1
29122957
fi

test/scripts/install-sh.test.ts

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,9 @@ describe("install.sh", () => {
138138
expect(script).toContain(
139139
'run_quiet_step "Installing Node.js" sudo apk add --no-cache nodejs npm',
140140
);
141-
expect(script).toContain('run_quiet_step "Installing nodejs-current" apk add --no-cache nodejs-current npm');
141+
expect(script).toContain(
142+
'run_quiet_step "Installing nodejs-current" apk add --no-cache nodejs-current npm',
143+
);
142144
expect(script).toContain("if ! node_is_at_least_required; then");
143145

144146
const apkIndex = script.indexOf("if command -v apk &> /dev/null && is_alpine_linux; then");
@@ -208,7 +210,9 @@ describe("install.sh", () => {
208210
expect(result.status).toBe(0);
209211
expect(result.stdout).toContain("step:Installing Node.js|apk add --no-cache nodejs npm");
210212
expect(result.stdout).toContain("warn:Alpine nodejs package installed v20.15.1");
211-
expect(result.stdout).toContain("step:Installing nodejs-current|apk add --no-cache nodejs-current npm");
213+
expect(result.stdout).toContain(
214+
"step:Installing nodejs-current|apk add --no-cache nodejs-current npm",
215+
);
212216
expect(result.stdout).toContain("finish-linux-node");
213217
});
214218

@@ -247,8 +251,12 @@ describe("install.sh", () => {
247251

248252
expect(result.status).toBe(1);
249253
expect(result.stdout).toContain("warn:Alpine nodejs package installed v20.15.1");
250-
expect(result.stdout).toContain("step:Installing nodejs-current|apk add --no-cache nodejs-current npm");
251-
expect(result.stdout).toContain("error:Alpine apk repositories did not provide Node.js v22.19+");
254+
expect(result.stdout).toContain(
255+
"step:Installing nodejs-current|apk add --no-cache nodejs-current npm",
256+
);
257+
expect(result.stdout).toContain(
258+
"error:Alpine apk repositories did not provide Node.js v22.19+",
259+
);
252260
expect(result.stdout).toContain("Use Alpine 3.21+ or install Node.js 24 manually");
253261
});
254262

@@ -758,6 +766,64 @@ describe("install.sh", () => {
758766
expect(result.stdout).not.toContain("[4/3] Verifying installation");
759767
});
760768

769+
it("bounds installer npm prefix probes during finalization helpers", () => {
770+
const result = runInstallShell(
771+
[
772+
`source ${JSON.stringify(SCRIPT_PATH)}`,
773+
"npm() {",
774+
' if [[ "$1" == "prefix" && "$2" == "-g" ]]; then sleep 2; return 0; fi',
775+
' if [[ "$1" == "config" && "$2" == "get" && "$3" == "prefix" ]]; then printf "/tmp/openclaw-npm\\n"; return 0; fi',
776+
" return 1",
777+
"}",
778+
"npm_global_bin_dir",
779+
].join("\n"),
780+
{ OPENCLAW_INSTALL_PROBE_TIMEOUT_SECONDS: "0.1" },
781+
);
782+
783+
expect(result.status).toBe(0);
784+
expect(result.stdout.trim()).toBe("/tmp/openclaw-npm/bin");
785+
expect(result.stderr).toContain("timed out during installer finalization probe: npm prefix -g");
786+
});
787+
788+
it("bounds daemon status probes during finalization helpers", () => {
789+
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-probe-"));
790+
const claw = join(tmp, "openclaw");
791+
writeFileSync(
792+
claw,
793+
[
794+
"#!/usr/bin/env bash",
795+
'if [[ "$1" == "daemon" && "$2" == "status" && "$3" == "--json" ]]; then',
796+
" sleep 2",
797+
" exit 0",
798+
"fi",
799+
"exit 1",
800+
"",
801+
].join("\n"),
802+
);
803+
chmodSync(claw, 0o755);
804+
try {
805+
const result = runInstallShell(
806+
[
807+
`source ${JSON.stringify(SCRIPT_PATH)}`,
808+
`if is_gateway_daemon_loaded ${JSON.stringify(claw)}; then`,
809+
' printf "loaded\\n"',
810+
"else",
811+
' printf "not-loaded\\n"',
812+
"fi",
813+
].join("\n"),
814+
{ OPENCLAW_INSTALL_PROBE_TIMEOUT_SECONDS: "0.1" },
815+
);
816+
817+
expect(result.status).toBe(0);
818+
expect(result.stdout.trim()).toBe("not-loaded");
819+
expect(result.stderr).toContain(
820+
"timed out during installer finalization probe: openclaw daemon status --json",
821+
);
822+
} finally {
823+
rmSync(tmp, { force: true, recursive: true });
824+
}
825+
});
826+
761827
it("loads nvm before checking Node.js so stale system Node does not win", () => {
762828
expect(script).toMatch(
763829
/# Step 2: Node\.js\s+load_nvm_for_node_detection\s+if ! check_node; then/,

0 commit comments

Comments
 (0)