From c5ce41762e2ce47cb5914029f199d4866b418688 Mon Sep 17 00:00:00 2001 From: Andreas Wrede Date: Thu, 30 Apr 2026 13:55:15 -0400 Subject: [PATCH] feat: update hbc via hb_install.sh instead of code patching Server now sends a bare UPD command; client runs hb_install.sh to reinstall from the package registry, then restarts. hb_install.sh also copies itself alongside hbc on client installs. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- hbd/client/main.py | 72 ++++++++++++++++++++----------------------- hbd/server/http.py | 13 +++----- scripts/hb_install.sh | 13 ++++++-- 3 files changed, 49 insertions(+), 49 deletions(-) diff --git a/hbd/client/main.py b/hbd/client/main.py index f519fbd..be3ccc7 100644 --- a/hbd/client/main.py +++ b/hbd/client/main.py @@ -14,7 +14,6 @@ import signal import socket import sys import time -from hashlib import md5 from logging.handlers import SysLogHandler from pathlib import Path from typing import Dict, List, Optional @@ -204,55 +203,52 @@ async def handle_command(conn: AsyncConnection, msg: dict): await conn.sendto(response) -async def handle_update(conn: AsyncConnection, msg: dict): - """Handle self-update from server.""" - import codecs +async def handle_update(conn: AsyncConnection, _msg: dict): # pyright: ignore[reportUnusedParameter] + """Handle self-update by running hb_install.sh.""" import shutil - + logger = logging.getLogger("hbc.update") - + + installer = shutil.which("hb_install.sh") + if installer is None: + candidate = Path(sys.argv[0]).parent / "hb_install.sh" + if candidate.exists(): + installer = str(candidate) + + if installer is None: + error = "hb_install.sh not found in PATH or alongside hbc" + logger.error(error) + await conn.sendto({"service": "update", "msg": error}) + return + + logger.info(f"Running installer: {installer}") try: - code = codecs.decode(msg["code"], "base64").decode() - csum = msg["csum"] + proc = await asyncio.create_subprocess_exec( + installer, "client", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + ) + out, _ = await asyncio.wait_for(proc.communicate(), timeout=120) + except asyncio.TimeoutError: + error = "Installer timed out" + logger.error(error) + await conn.sendto({"service": "update", "msg": error}) + return except Exception as e: - error = f"Missing code/csum: {e}" + error = f"Installer failed: {e}" logger.error(error) await conn.sendto({"service": "update", "msg": error}) return - - # Verify checksum - m = md5() - m.update(code.encode()) - if m.hexdigest() != csum: - error = "Checksum mismatch" + + if proc.returncode != 0: + error = f"Installer exited {proc.returncode}: {out.decode().strip()}" logger.error(error) await conn.sendto({"service": "update", "msg": error}) return - - # Backup current file - fn = sys.argv[0] - ofn = f"{fn}.sav" - try: - shutil.copy2(fn, ofn) - except Exception as e: - error = f"Backup failed: {e}" - logger.error(error) - await conn.sendto({"service": "update", "msg": error}) - return - - # Write new code - try: - with open(fn, "w") as fh: - fh.write(code) - except Exception as e: - error = f"Write failed: {e}" - logger.error(error) - await conn.sendto({"service": "update", "msg": error}) - return - + logger.info("Update successful, restart required") await conn.sendto({"service": "update", "msg": "OK"}) - + # Trigger restart global dorestart dorestart = True diff --git a/hbd/server/http.py b/hbd/server/http.py index ab4212b..eee99b5 100644 --- a/hbd/server/http.py +++ b/hbd/server/http.py @@ -210,15 +210,11 @@ async def start( return err qa = request.rel_url.query uname = urllib.parse.unquote(qa.get("h", "")) - ucode = qa.get("c") - if not ucode or not uname: - return web.Response(status=400, text="need h= and c= arguments") + if not uname: + return web.Response(status=400, text="need h= argument") if uname != "All" and uname not in hbdclass.Host.hosts: return web.Response(status=400, text=f"h={uname} not found") - if uname != "All": - names = [uname] - else: - names = [n for n in hbdclass.Host.hosts] + names = [uname] if uname != "All" else list(hbdclass.Host.hosts) out = [] for n in names: host = hbdclass.Host.hosts[n] @@ -227,8 +223,7 @@ async def start( continue op_err = None try: - r = {"csum": None, "code": ucode} - host.cmds.append(("UPD", r)) + host.cmds.append(("UPD", {})) except Exception as e: op_err = str(e) out.append(f"update started for {n}: {op_err if op_err else 'OK'}") diff --git a/scripts/hb_install.sh b/scripts/hb_install.sh index 1912380..a2a88ed 100755 --- a/scripts/hb_install.sh +++ b/scripts/hb_install.sh @@ -15,8 +15,14 @@ on_ha=0 [ -z "$what" ] && what="client" if [ -d /homeassistant ]; then - echo "cannot install in HA, run \"docker exec -it homeassistant $0 $@\"" - exit 1 + echo "cannot install in HA, running \"docker exec homeassistant $0 $@\"" + docker exec homeassistant $0 $@ + rc=$? + if [ $rc -ne 0 ]; then + echo "Failed to install heartbeat in HA, please check the logs for more details" + exit 1 + fi + exit 0 fi if [ -d /config ]; then echo "Installing on HA" @@ -78,6 +84,9 @@ if [ "$what" = "server" ]; then else rm -f $where/hbc ln -sf $(which hbc) $where/hbc + rm -f $where/hb_install.sh + cp "$0" $where/hb_install.sh + chmod +x $where/hb_install.sh if [ $on_ha -eq 1 ]; then echo "restarting hbc " job=$(grep run_hbc configuration.yaml | sed 's/run_hbc://')