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:
+34
-38
@@ -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
@@ -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
@@ -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://')
|
||||
|
||||
Reference in New Issue
Block a user