Compare commits

...

10 Commits

Author SHA1 Message Date
Andreas Wrede 65c4267847 version 5.1.7
Release / release (push) Successful in 5s
2026-04-30 17:50:46 -04:00
Andreas Wrede 462a445235 feat: add hbc_mini single-file client; drop dead connections on protocol error
- scripts/hbc_mini.py: self-contained hbc with no external deps; uses
  /proc for CPU/memory/network on Linux, df for disk, JSON config
- hbc + hbc_mini: mark connection _dead and stop sending on protocol error
- README: document hbc_mini usage, config, and plugin availability
- pyproject.toml: include hbc_mini.py in script-files

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 17:50:19 -04:00
Andreas Wrede 368e178f93 install the hb_install.sh script 2026-04-30 17:03:37 -04:00
Andreas Wrede 6905bf266a version 5.1.6
Release / release (push) Successful in 5s
2026-04-30 15:39:11 -04:00
Andreas Wrede b6dcce4f35 simplify eventlog usage, fix arguments 2026-04-30 15:38:46 -04:00
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
9 changed files with 1261 additions and 67 deletions
+63 -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
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):
@@ -441,6 +441,68 @@ plugins:
All monitoring plugins default to 5-minute (300 second) intervals, but can be customized as needed.
### hbc_mini — single-file client (no external dependencies)
`scripts/hbc_mini.py` is a self-contained version of the heartbeat client that requires only Python 3.8+ and no external packages. Copy it to any host and run it directly — no virtualenv, no `pip install`.
```bash
# Basic usage
python3 hbc_mini.py your-server.example.com
# Run as daemon
python3 hbc_mini.py -d your-server.example.com
# Send a boot message
python3 hbc_mini.py -b your-server.example.com
# Send a one-off message
python3 hbc_mini.py -m "maintenance starting" your-server.example.com
```
**Config:** `~/.hbc.json` (same keys as `~/.hbc.yaml`, JSON format). Example:
```json
{
"hb_port": 50003,
"interval": 30,
"plugins": {
"ping_monitor": {
"interval": 60,
"hosts": ["8.8.8.8", "192.168.1.1"]
},
"nagios_runner": {
"interval": 300,
"commands": [
{"name": "check_load", "command": "/usr/lib/nagios/plugins/check_load -w 5,4,3 -c 10,8,6"}
]
}
}
}
```
**Plugin availability:**
| Plugin | Platform | Data source |
|---|---|---|
| `os_info` | all | `platform` stdlib |
| `ping_monitor` | all | `ping` subprocess |
| `nagios_runner` | all (not Windows) | subprocess |
| `cpu_monitor` | Linux | `/proc/stat` |
| `memory_monitor` | Linux | `/proc/meminfo` |
| `disk_monitor` | Linux, macOS, BSD | `df -P` subprocess |
| `network_monitor` | Linux | `/proc/net/dev` |
**What is not available compared to the full `hbc`:**
- No YAML config (use JSON instead)
- No `filesystem_info` plugin
- `cpu_monitor` does not report per-core usage or CPU frequency (no psutil)
- Plugins cannot be loaded from external `.py` files — all plugins are compiled in
Everything else — heartbeat protocol, ACK/CMD/UPD handling, `hb_install.sh`-based self-update, daemonize, syslog — is identical to the full client.
---
## 🐞 Debugging in VS Code
This repository includes a ready-to-use `.vscode/launch.json` with configurations to run or attach the VS Code debugger to `hbd`.
+1 -1
View File
@@ -14,4 +14,4 @@ Install options:
"""
__all__ = ["__version__"]
__version__ = "5.1.4"
__version__ = "5.1.7"
+34 -32
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
@@ -56,6 +55,7 @@ class AsyncConnection:
self.transport: Optional[asyncio.DatagramTransport] = None
self.protocol: Optional[asyncio.DatagramProtocol] = None
self._dead = False
self.logger = logging.getLogger(f"hbc.conn.{addr}")
@@ -93,6 +93,9 @@ class AsyncConnection:
msg: Message dictionary
msg_id: Message ID (HTB, PLG, etc.)
"""
if self._dead:
return
if not self.transport:
await self.open()
@@ -167,7 +170,9 @@ class HeartbeatProtocol(asyncio.DatagramProtocol):
def error_received(self, exc):
"""Handle protocol errors."""
self.logger.error(f"Protocol error: {exc}")
self.logger.warning(f"Protocol error on {self.connection.addr}: {exc} — dropping connection")
self.connection._dead = True
self.connection.close()
async def handle_command(conn: AsyncConnection, msg: dict):
@@ -204,48 +209,45 @@ 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")
try:
code = codecs.decode(msg["code"], "base64").decode()
csum = msg["csum"]
except Exception as e:
error = f"Missing code/csum: {e}"
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
# Verify checksum
m = md5()
m.update(code.encode())
if m.hexdigest() != csum:
error = "Checksum mismatch"
logger.info(f"Running installer: {installer}")
try:
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"Installer failed: {e}"
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}"
if proc.returncode != 0:
error = f"Installer exited {proc.returncode}: {out.decode().strip()}"
logger.error(error)
await conn.sendto({"service": "update", "msg": error})
return
+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'}")
-2
View File
@@ -210,7 +210,6 @@ async def _run_async(config, config_path=None):
ctx = dict(
config=config,
hbdclass=hbdclass,
log=eventlog,
msg_to_websockets=msg_to_websockets,
msg_journal=msg_journal,
threshold_checker=threshold_checker,
@@ -237,7 +236,6 @@ async def _run_async(config, config_path=None):
restore_ctx = dict(
config=config,
hbdclass=hbdclass,
log=eventlog,
msg_to_websockets=msg_to_websockets,
threshold_checker=threshold_checker,
)
+2 -5
View File
@@ -315,7 +315,6 @@ def handle_datagram(msg: dict, addr, transport, ctx: dict):
cfg = ctx.get("config", {})
hbdcls = ctx.get("hbdclass")
log = ctx.get("log")
msg_to_websockets = ctx.get("msg_to_websockets")
DEBUG = ctx.get("DEBUG", 0)
verbose = ctx.get("verbose", False)
@@ -491,12 +490,10 @@ def handle_datagram(msg: dict, addr, transport, ctx: dict):
op, rmsg = host.cmds[0]
if op == "CMD":
del host.cmds[0]
if log:
log(uname, "command sent")
eventlog(uname, "INFO", "command sent")
elif op == "UPD":
del host.cmds[0]
if log:
log(uname, "update initiated")
eventlog(uname, "INFO", "update initiated")
opkt = dicttos(op, rmsg)
try:
transport.sendto(opkt, addr)
+4 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "hbd"
version = "5.1.4"
version = "5.1.7"
description = "Heartbeat monitoring system — client (hbc) and server (hbd)"
readme = "README.md"
requires-python = ">=3.11"
@@ -54,6 +54,9 @@ dev = [
hbd = "hbd.server.cli:main"
hbc = "hbd.client.main:main"
[tool.setuptools]
script-files = ["scripts/hb_install.sh", "scripts/hbc_mini.py"]
[tool.setuptools.packages.find]
where = ["."]
include = ["hbd*"]
+15 -6
View File
@@ -1,6 +1,6 @@
#!/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
# 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
@@ -9,18 +9,21 @@
# reused. The script will also remove any existing symlinks for hbd and hbc
# in ~/bin before creating new ones.
# hbd/hbc from wheel and create symlinks for hbd and hbc in ~/bin
set -e
what=$1
on_ha=0
[ -z "$what" ] && what="client"
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 $@\""
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"
where="/config/bin"
@@ -46,8 +49,11 @@ fi
echo "Installing heartbeat $what"
if [ ! -d $venv/hbd ]; then
set +e
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
echo "pip is not installed, fetching get-pip.py and installing pip"
arg="--without-pip"
@@ -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://')
+1128
View File
File diff suppressed because it is too large Load Diff