Compare commits

...

5 Commits

Author SHA1 Message Date
Andreas Wrede e6436fc236 version 5.1.5
Release / release (push) Successful in 5s
2026-04-30 13:55:21 -04:00
Andreas Wrede c5ce41762e 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>
2026-04-30 13:55:15 -04:00
Andreas Wrede 26ca0c095f install.sh --> hb_innstall.sh 2026-04-30 09:54:48 -04:00
Andreas Wrede 1eecd67594 update docu 2026-04-30 09:19:11 -04:00
Andreas Wrede caf3c2c0ac don't error exit on pip insttalled test 2026-04-30 09:16:22 -04:00
6 changed files with 57 additions and 57 deletions
+1 -1
View File
@@ -377,7 +377,7 @@ This project now declares its dependencies in `pyproject.toml`. Instead
of the old `requirements.txt` flow, install the package into a virtualenv of the old `requirements.txt` flow, install the package into a virtualenv
using `pip`: using `pip`:
See `scripts/install.sh` for a way to install. See `scripts/hb_install.sh` for a way to install.
Run the daemon (example): Run the daemon (example):
+1 -1
View File
@@ -14,4 +14,4 @@ Install options:
""" """
__all__ = ["__version__"] __all__ = ["__version__"]
__version__ = "5.1.4" __version__ = "5.1.5"
+34 -38
View File
@@ -14,7 +14,6 @@ import signal
import socket import socket
import sys import sys
import time import time
from hashlib import md5
from logging.handlers import SysLogHandler from logging.handlers import SysLogHandler
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional from typing import Dict, List, Optional
@@ -204,55 +203,52 @@ async def handle_command(conn: AsyncConnection, msg: dict):
await conn.sendto(response) await conn.sendto(response)
async def handle_update(conn: AsyncConnection, msg: dict): async def handle_update(conn: AsyncConnection, _msg: dict): # pyright: ignore[reportUnusedParameter]
"""Handle self-update from server.""" """Handle self-update by running hb_install.sh."""
import codecs
import shutil import shutil
logger = logging.getLogger("hbc.update") 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: try:
code = codecs.decode(msg["code"], "base64").decode() proc = await asyncio.create_subprocess_exec(
csum = msg["csum"] 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: except Exception as e:
error = f"Missing code/csum: {e}" error = f"Installer failed: {e}"
logger.error(error) logger.error(error)
await conn.sendto({"service": "update", "msg": error}) await conn.sendto({"service": "update", "msg": error})
return return
# Verify checksum if proc.returncode != 0:
m = md5() error = f"Installer exited {proc.returncode}: {out.decode().strip()}"
m.update(code.encode())
if m.hexdigest() != csum:
error = "Checksum mismatch"
logger.error(error) logger.error(error)
await conn.sendto({"service": "update", "msg": error}) await conn.sendto({"service": "update", "msg": error})
return 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") logger.info("Update successful, restart required")
await conn.sendto({"service": "update", "msg": "OK"}) await conn.sendto({"service": "update", "msg": "OK"})
# Trigger restart # Trigger restart
global dorestart global dorestart
dorestart = True dorestart = True
+4 -9
View File
@@ -210,15 +210,11 @@ async def start(
return err return err
qa = request.rel_url.query qa = request.rel_url.query
uname = urllib.parse.unquote(qa.get("h", "")) uname = urllib.parse.unquote(qa.get("h", ""))
ucode = qa.get("c") if not uname:
if not ucode or not uname: return web.Response(status=400, text="need h= argument")
return web.Response(status=400, text="need h= and c= arguments")
if uname != "All" and uname not in hbdclass.Host.hosts: if uname != "All" and uname not in hbdclass.Host.hosts:
return web.Response(status=400, text=f"h={uname} not found") return web.Response(status=400, text=f"h={uname} not found")
if uname != "All": names = [uname] if uname != "All" else list(hbdclass.Host.hosts)
names = [uname]
else:
names = [n for n in hbdclass.Host.hosts]
out = [] out = []
for n in names: for n in names:
host = hbdclass.Host.hosts[n] host = hbdclass.Host.hosts[n]
@@ -227,8 +223,7 @@ async def start(
continue continue
op_err = None op_err = None
try: try:
r = {"csum": None, "code": ucode} host.cmds.append(("UPD", {}))
host.cmds.append(("UPD", r))
except Exception as e: except Exception as e:
op_err = str(e) op_err = str(e)
out.append(f"update started for {n}: {op_err if op_err else 'OK'}") out.append(f"update started for {n}: {op_err if op_err else 'OK'}")
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "hbd" name = "hbd"
version = "5.1.4" version = "5.1.5"
description = "Heartbeat monitoring system — client (hbc) and server (hbd)" description = "Heartbeat monitoring system — client (hbc) and server (hbd)"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
+16 -7
View File
@@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
# install the heartbeat client, hbc. The server is installed when the arg 'server' is passed # Helper script to install the heartbeat tools. By default, it will only
# install the heartbeat client, hbc. The server is installed when the arg 'server' is passed # install the heartbeat client, hbc. The server is installed when the arg 'server' is passed
# to the script. The script will install the heartbeat tools in a python # to the script. The script will install the heartbeat tools in a python
# virtual environment in ~/venvs/hbd. The hbd and hbc commands will be # virtual environment in ~/venvs/hbd. The hbd and hbc commands will be
@@ -9,17 +9,20 @@
# reused. The script will also remove any existing symlinks for hbd and hbc # reused. The script will also remove any existing symlinks for hbd and hbc
# in ~/bin before creating new ones. # in ~/bin before creating new ones.
# hbd/hbc from wheel and create symlinks for hbd and hbc in ~/bin
set -e set -e
what=$1 what=$1
on_ha=0 on_ha=0
[ -z "$what" ] && what="client" [ -z "$what" ] && what="client"
if [ -d /homeassistant ]; then if [ -d /homeassistant ]; then
echo "cannot install in HA, run \"docker exec -it homeassistant $0 $@\"" echo "cannot install in HA, running \"docker exec homeassistant $0 $@\""
exit 1 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 fi
if [ -d /config ]; then if [ -d /config ]; then
echo "Installing on HA" echo "Installing on HA"
@@ -46,8 +49,11 @@ fi
echo "Installing heartbeat $what" echo "Installing heartbeat $what"
if [ ! -d $venv/hbd ]; then if [ ! -d $venv/hbd ]; then
set +e
python3 -m pip --version > /dev/null 2>&1 python3 -m pip --version > /dev/null 2>&1
if [ $? -ne 0 ]; then rc=$?
set -e
if [ $rc -ne 0 ]; then
# truenas does not have pip installed by default, so we need to fetch get-pip.py and install pip # truenas does not have pip installed by default, so we need to fetch get-pip.py and install pip
echo "pip is not installed, fetching get-pip.py and installing pip" echo "pip is not installed, fetching get-pip.py and installing pip"
arg="--without-pip" arg="--without-pip"
@@ -78,6 +84,9 @@ if [ "$what" = "server" ]; then
else else
rm -f $where/hbc rm -f $where/hbc
ln -sf $(which hbc) $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 if [ $on_ha -eq 1 ]; then
echo "restarting hbc " echo "restarting hbc "
job=$(grep run_hbc configuration.yaml | sed 's/run_hbc://') job=$(grep run_hbc configuration.yaml | sed 's/run_hbc://')