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) <noreply@anthropic.com>
This commit is contained in:
Andreas Wrede
2026-04-30 13:55:15 -04:00
parent 26ca0c095f
commit c5ce41762e
3 changed files with 49 additions and 49 deletions
+34 -38
View File
@@ -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
+4 -9
View File
@@ -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'}")
+11 -2
View File
@@ -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://')