From 9522df26e9b88a66115675a216c397dc9d3ed24d Mon Sep 17 00:00:00 2001 From: Pratik Bhavsar Date: Fri, 6 Nov 2026 12:58:21 +0530 Subject: [PATCH] fix installer menu bar readiness timeout --- scripts/install | 13 ++++-- scripts/install-linux | 12 +++-- tests/test_installer_modes.py | 86 +++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 6 deletions(-) diff --git a/scripts/install b/scripts/install index fb5c7657..42f60a27 100755 --- a/scripts/install +++ b/scripts/install @@ -387,9 +387,16 @@ if payload.get("runtime_matches") is not True: if (( BACKEND_ONLY )); then printf '%s\n' "backend-only" > "$INSTALL_ROOT/INSTALL_MODE" else - curl -fsS --max-time 5 "$MENUBAR_URL" \ - | python3 -c 'import json, sys; json.load(sys.stdin)' \ - || fail "the menu bar endpoint did not return valid JSON." + # A ready index does not guarantee that the first menu-bar projection is + # cheap. Allow the same bounded startup window for this separate request. + progress_start "Checking the menu bar endpoint" "Still checking the menu bar endpoint…" + if ! menubar_json="$(curl -fsS --max-time "$READINESS_TIMEOUT_SECONDS" "$MENUBAR_URL" 2>/dev/null)"; then + fail "the menu bar endpoint did not respond within ${READINESS_TIMEOUT_SECONDS}s or returned an HTTP error." + fi + if ! printf '%s' "$menubar_json" | python3 -c 'import json, sys; json.load(sys.stdin)' 2>/dev/null; then + fail "the menu bar endpoint did not return valid JSON." + fi + progress_done "Menu bar endpoint ready" if [[ -n "$MENUBAR_BUILD_PID" ]]; then echo "Finishing the menu bar companion…" else diff --git a/scripts/install-linux b/scripts/install-linux index 9be82134..5e6f7769 100755 --- a/scripts/install-linux +++ b/scripts/install-linux @@ -296,9 +296,15 @@ else [[ "$tray_fragment" == "${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/token-meter-tray.service" ]] \ || fail "a different unit owns token-meter-tray.service." - curl -fsS --max-time 5 "$MENUBAR_URL" \ - | python3 -c 'import json, sys; json.load(sys.stdin)' \ - || fail "the menu bar endpoint did not return valid JSON." + # Index readiness does not bound the time to build the first tray payload. + progress_start "Checking the menu bar endpoint" "Still checking the menu bar endpoint…" + if ! menubar_json="$(curl -fsS --max-time "$READINESS_TIMEOUT_SECONDS" "$MENUBAR_URL" 2>/dev/null)"; then + fail "the menu bar endpoint did not respond within ${READINESS_TIMEOUT_SECONDS}s or returned an HTTP error." + fi + if ! printf '%s' "$menubar_json" | python3 -c 'import json, sys; json.load(sys.stdin)' 2>/dev/null; then + fail "the menu bar endpoint did not return valid JSON." + fi + progress_done "Menu bar endpoint ready" printf '%s\n' "full" > "$INSTALL_ROOT/INSTALL_MODE" fi diff --git a/tests/test_installer_modes.py b/tests/test_installer_modes.py index 5d790070..fe9b7a2b 100644 --- a/tests/test_installer_modes.py +++ b/tests/test_installer_modes.py @@ -924,6 +924,92 @@ def test_macos_default_install_keeps_the_native_companion(self): ), ) + def test_macos_install_waits_for_slow_menubar_response(self): + with tempfile.TemporaryDirectory() as tmp: + workspace = Path(tmp) + env, install_root, _ = self.installer_environment(workspace, "Darwin") + env.pop("BASH_ENV") + env["TOKEN_METER_READINESS_TIMEOUT_SECONDS"] = "12" + self.write_executable( + workspace / "bin" / "curl", + """#!/usr/bin/env bash + if [[ "${*: -1}" == */health ]]; then + owned=false; [[ "$*" == *"runtime=$(cd "$TEST_INSTALL_ROOT" 2>/dev/null && pwd -P)"* ]] && owned=true + printf '{"ok": true, "state_ready": true, "runtime_matches": %s}\n' "$owned" + else + timeout=0 + while (($#)); do + if [[ "$1" == --max-time ]]; then timeout="$2"; shift; fi + shift + done + if (( timeout <= 5 )); then exit 28; fi + sleep 6 + printf '{"ok": true}\n' + fi + """, + ) + result = subprocess.run( + ["bash", str(ROOT / "scripts" / "install")], + capture_output=True, text=True, env=env, timeout=30, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual((install_root / "INSTALL_MODE").read_text(), "full\n") + + def test_linux_install_waits_for_slow_menubar_response(self): + with tempfile.TemporaryDirectory() as tmp: + workspace = Path(tmp) + env, install_root, _ = self.installer_environment(workspace, "Linux") + env["TOKEN_METER_READINESS_TIMEOUT_SECONDS"] = "12" + self.write_executable( + workspace / "bin" / "curl", + """#!/usr/bin/env bash + if [[ "${*: -1}" == */health ]]; then + owned=false; [[ "$*" == *"runtime=$(cd "$TEST_INSTALL_ROOT" 2>/dev/null && pwd -P)"* ]] && owned=true + printf '{"ok": true, "state_ready": true, "runtime_matches": %s}\n' "$owned" + else + timeout=0 + while (($#)); do + if [[ "$1" == --max-time ]]; then timeout="$2"; shift; fi + shift + done + if (( timeout <= 5 )); then exit 28; fi + sleep 6 + printf '{"ok": true}\n' + fi + """, + ) + result = subprocess.run( + ["bash", str(ROOT / "scripts" / "install")], + capture_output=True, text=True, env=env, timeout=30, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual((install_root / "INSTALL_MODE").read_text(), "full\n") + + def test_macos_install_rejects_invalid_menubar_json_without_traceback(self): + with tempfile.TemporaryDirectory() as tmp: + workspace = Path(tmp) + env, install_root, _ = self.installer_environment(workspace, "Darwin") + env.pop("BASH_ENV") + self.write_executable( + workspace / "bin" / "curl", + """#!/usr/bin/env bash + if [[ "${*: -1}" == */health ]]; then + owned=false; [[ "$*" == *"runtime=$(cd "$TEST_INSTALL_ROOT" 2>/dev/null && pwd -P)"* ]] && owned=true + printf '{"ok": true, "state_ready": true, "runtime_matches": %s}\n' "$owned" + else + printf 'invalid json\n' + fi + """, + ) + result = subprocess.run( + ["bash", str(ROOT / "scripts" / "install")], + capture_output=True, text=True, env=env, timeout=30, + ) + self.assertNotEqual(result.returncode, 0) + self.assertIn("menu bar endpoint did not return valid JSON", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertFalse((install_root / "INSTALL_MODE").exists()) + def test_linux_default_install_keeps_the_tray_companion(self): with tempfile.TemporaryDirectory() as tmp: workspace = Path(tmp)