From 64d42663cd22cdb613baf6a99bd811cfcd15f678 Mon Sep 17 00:00:00 2001 From: taylanbakircioglu Date: Sat, 27 Jun 2026 13:49:59 +0300 Subject: [PATCH] fix(agent): stop installer self-kill in pre-installation cleanup (v1.8.4) The Linux/macOS agent installer could abort during "pre-installation cleanup" (terminal showed `Killing processes matching: haproxy-agent` then `Killed`, returning to the prompt) when the install script's own command line contained "haproxy-agent". The cleanup killed processes via `pgrep -f "$pattern"` starting with the bare string "haproxy-agent", which also matched the running installer's own command line and a sudo/PAM ancestor that the $$/$PPID self-exclusion did not cover, so the installer terminated itself before installing. - linux_install.sh / macos_install.sh: the cleanup kill loop now targets ONLY the installed agent - "$INSTALL_DIR/haproxy-agent" (the daemon binary path) and the agent service/label ("haproxy-agent.service" / "com.haproxy.agent") - never the bare "haproxy-agent" substring. Neither pattern can match the installer's own command line. The redundant bare pattern is dropped (the service is stopped separately, and the binary-path pattern still catches a running daemon). - frontend (AgentManagement.js): name the downloaded scripts install-agent-.sh / uninstall-agent-.sh (matching the backend's suggested filename) - defense in depth so this cannot resurface. Installer-only change. The running agent and its privilege model are unchanged (it runs as root for HAProxy reload, config writes, keepalived, and self-upgrade). The cleanup runs only on a full interactive install (gated by SKIP_TO_DAEMON), so daemon mode, self-upgrade, and config/version apply are unaffected. Both agent scripts kept in sync. Scripts parse on bash 4.2-5.2; full backend suite green. Addresses #31. --- README.md | 1 + backend/main.py | 2 +- backend/tests/test_agent_script_heartbeat_json.py | 15 +++++++++++++++ backend/utils/agent_scripts/linux_install.sh | 7 ++++++- backend/utils/agent_scripts/macos_install.sh | 6 +++++- frontend/package.json | 2 +- frontend/src/components/AgentManagement.js | 6 +++--- version.json | 6 +++--- 8 files changed, 35 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d763e82..d82e2a1 100644 --- a/README.md +++ b/README.md @@ -2415,6 +2415,7 @@ Developed with ❤️ for the HAProxy community ## Release Notes +- **v1.8.4** (2026-06-27) — **Agent installer self-kill fix** (Issue #31): the Linux/macOS agent installer could abort during "pre-installation cleanup" (terminal showed `Killing processes matching: haproxy-agent` then `Killed`) when the install script's own filename contained "haproxy-agent". The cleanup killed processes by matching the bare string "haproxy-agent" against full command lines, which also matched the running installer (and a `sudo`/PAM ancestor the self-exclusion did not cover), so the installer terminated itself. Cleanup now targets only the installed agent (the `$INSTALL_DIR/haproxy-agent` binary and the agent service), never the bare string, and the UI now names the downloaded scripts `install-agent-.sh` / `uninstall-agent-.sh`. Installer-only change; the running agent and its privilege model (it runs as root for HAProxy reload, config writes, keepalived, and self-upgrade) are unchanged. - **v1.8.3** (2026-06-25) — **Agent heartbeat JSON fix** (Issue #31): a self-hosted agent could fail every heartbeat with `HTTP 400 Invalid JSON: Expecting property name enclosed in double quotes` when the system-info block it collects came back empty on an unusual host, leaving a stray comma in the hand-built heartbeat JSON. The agent script now substitutes a valid placeholder when that block is empty so it can no longer emit a stray comma, and the backend heartbeat endpoint now parses valid payloads as-is and, only when a body fails to parse, tolerates that specific malformed pattern (a leading or doubled comma) so an already-deployed agent recovers on its next heartbeat after this build is deployed. Backend + agent-script only; healthy agents of every version are byte-for-byte unaffected. - **v1.8.2** (2026-06-25) — **ACME nonce fix** (Issue #35 follow-up): the ACME client now scopes the anti-replay nonce **per certificate authority** so a nonce issued by one CA is never sent to another. This fixes ZeroSSL/Google account registration failing with `malformed: The Replay Nonce could not be base64url-decoded` (the client previously shared one nonce across CAs and only auto-retried on `badNonce`). Account registration now always uses a fresh nonce from the target CA, and the retry covers this case too. Backend-only; HTTP-01 and Let's Encrypt are unaffected. - **v1.8.1** (2026-06-24) — **ACME DNS-01 fixes** (Issue #35 follow-up): Cloudflare API tokens are now sanitized so a pasted token with quotes/spaces no longer fails with "Invalid request headers"; ZeroSSL/Google **External Account Binding (EAB)** can be entered per-account in the register dialog and EAB-required failures show a clear message; and **Apply Management** now categorizes cluster ACME enable/disable changes under their own "ACME Challenge Routing" section and **Apply/Reject All** correctly process them (previously "Rejected 0 HA/VIP change(s)"), consistent with every other entity. Fully backward compatible. diff --git a/backend/main.py b/backend/main.py index ecd7ac1..bab3fd1 100644 --- a/backend/main.py +++ b/backend/main.py @@ -9,7 +9,7 @@ import asyncio from datetime import datetime, timedelta # Build/deploy marker for the v1.8.x (Issue #35, DNS-01) rollout — ensures the pipeline ships this commit's image. -_version_info = {"version": "1.8.3", "releaseName": "Agent heartbeat JSON fix", "releaseDate": "2026-06-25"} +_version_info = {"version": "1.8.4", "releaseName": "Agent installer self-kill fix", "releaseDate": "2026-06-27"} for _vpath in ["/app/version.json", os.path.join(os.path.dirname(__file__), "..", "version.json")]: try: with open(_vpath) as _vf: diff --git a/backend/tests/test_agent_script_heartbeat_json.py b/backend/tests/test_agent_script_heartbeat_json.py index 32104a1..ec303aa 100644 --- a/backend/tests/test_agent_script_heartbeat_json.py +++ b/backend/tests/test_agent_script_heartbeat_json.py @@ -44,3 +44,18 @@ def test_b2_guard_precedes_every_fragment_system_info_use(): for name, script in (("linux", LINUX), ("macos", MACOS)): assert script.count(" $system_info,") >= 1, f"{name}: fragment heartbeat form unexpectedly gone" assert script.count(_B2_GUARD) == 2, f"{name}: each fragment call site must carry the guard" + + +def test_cleanup_does_not_self_kill_via_bare_haproxy_agent_pattern(): + # Issue #31 (v1.8.4): the pre-installation cleanup kills processes by pgrep -f "$pattern". A bare + # "haproxy-agent" pattern also matches the installer's OWN path (install-haproxy-agent-*.sh) and a + # sudo/PAM ancestor, so the installer killed itself. The kill loop must target ONLY the installed + # agent (binary path + service/label), never the bare string. + for name, script in (("linux", LINUX), ("macos", MACOS)): + assert 'for pattern in "haproxy-agent"' not in script, ( + f"{name}: pre-install cleanup uses the bare 'haproxy-agent' kill pattern -> self-kill (issue #31)" + ) + # The narrowed, installer-safe pattern must be present (binary path via $INSTALL_DIR). + assert 'for pattern in "$INSTALL_DIR/haproxy-agent"' in script, ( + f"{name}: cleanup must match the installed binary path, not a bare substring" + ) diff --git a/backend/utils/agent_scripts/linux_install.sh b/backend/utils/agent_scripts/linux_install.sh index aade227..2b8240c 100644 --- a/backend/utils/agent_scripts/linux_install.sh +++ b/backend/utils/agent_scripts/linux_install.sh @@ -470,7 +470,12 @@ safe_remove() { [[ "$QUIET_MODE" != "true" ]] && echo "Terminating existing HAProxy Agent processes..." KILLED_COUNT=0 INSTALLER_PID=$$ -for pattern in "haproxy-agent" "/usr/local/bin/haproxy-agent" "haproxy-agent.service"; do +# issue #31: match ONLY the installed agent (binary path + service), never the bare string +# "haproxy-agent". With pgrep -f, that bare string can also match the installer's OWN command line +# or a sudo/PAM ancestor (which the $$/$PPID guard does not fully cover), making the cleanup kill +# the installer itself ("Killed", install aborts). The systemd service is also stopped below; the +# "$INSTALL_DIR/haproxy-agent" path still catches any running daemon. +for pattern in "$INSTALL_DIR/haproxy-agent" "haproxy-agent.service"; do PIDS=$(pgrep -f "$pattern" 2>/dev/null || true) if [[ -n "$PIDS" ]]; then FILTERED="" diff --git a/backend/utils/agent_scripts/macos_install.sh b/backend/utils/agent_scripts/macos_install.sh index 1a147ba..19470b7 100644 --- a/backend/utils/agent_scripts/macos_install.sh +++ b/backend/utils/agent_scripts/macos_install.sh @@ -318,7 +318,11 @@ safe_remove() { [[ "$QUIET_MODE" != "true" ]] && echo "Terminating existing HAProxy Agent processes..." KILLED_COUNT=0 INSTALLER_PID=$$ -for pattern in "haproxy-agent" "/usr/local/bin/haproxy-agent" "com.haproxy.agent"; do +# issue #31: match ONLY the installed agent (binary path + LaunchDaemon label), never the bare +# string "haproxy-agent". With pgrep -f, that bare string can also match the installer's OWN command +# line or a sudo ancestor (which the $$/$PPID guard does not fully cover), making the cleanup kill +# the installer itself. The "$INSTALL_DIR/haproxy-agent" path still catches any running daemon. +for pattern in "$INSTALL_DIR/haproxy-agent" "com.haproxy.agent"; do PIDS=$(pgrep -f "$pattern" 2>/dev/null || true) if [[ -n "$PIDS" ]]; then FILTERED="" diff --git a/frontend/package.json b/frontend/package.json index 1ddd78d..9b222b3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "haproxy-openmanager-frontend", - "version": "1.8.3", + "version": "1.8.4", "description": "HAProxy Load Balancer Management UI", "license": "AGPL-3.0-or-later", "dependencies": { diff --git a/frontend/src/components/AgentManagement.js b/frontend/src/components/AgentManagement.js index db25c6b..bddf81a 100644 --- a/frontend/src/components/AgentManagement.js +++ b/frontend/src/components/AgentManagement.js @@ -2378,7 +2378,7 @@ const AgentManagement = () => { const element = document.createElement('a'); const file = new Blob([installScript], { type: 'text/plain' }); element.href = URL.createObjectURL(file); - element.download = `install-haproxy-agent-${selectedPlatform}.sh`; + element.download = `install-agent-${selectedPlatform}.sh`; document.body.appendChild(element); element.click(); document.body.removeChild(element); @@ -2516,7 +2516,7 @@ const AgentManagement = () => { const element = document.createElement('a'); const file = new Blob([uninstallScript], { type: 'text/plain' }); element.href = URL.createObjectURL(file); - element.download = `uninstall-haproxy-agent-${selectedPlatform}.sh`; + element.download = `uninstall-agent-${selectedPlatform}.sh`; document.body.appendChild(element); element.click(); document.body.removeChild(element); @@ -3147,7 +3147,7 @@ const AgentManagement = () => { const element = document.createElement('a'); const file = new Blob([deleteUninstallScript], { type: 'text/plain' }); element.href = URL.createObjectURL(file); - element.download = `uninstall-haproxy-agent-${agentToDelete.platform || 'linux'}.sh`; + element.download = `uninstall-agent-${agentToDelete.platform || 'linux'}.sh`; document.body.appendChild(element); element.click(); document.body.removeChild(element); diff --git a/version.json b/version.json index 20084bc..eeee28c 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "version": "1.8.3", - "releaseName": "Agent heartbeat JSON fix", - "releaseDate": "2026-06-25" + "version": "1.8.4", + "releaseName": "Agent installer self-kill fix", + "releaseDate": "2026-06-27" }