Compare commits

...

9 Commits

Author SHA1 Message Date
andreas 999740bc99 version 5.0.2 2026-02-08 14:40:59 -05:00
andreas 4c53b7cec9 support SSL ws session 2026-02-08 14:32:12 -05:00
andreas 535b839bfc Add smtp auth 2026-02-06 20:04:12 -05:00
andreas e3dd461d04 fix order of AFs 2026-02-06 19:49:21 -05:00
andreas e55a81568f make socket listen on IPv4 and IPv6 2026-02-06 19:25:23 -05:00
andreas 83fbba433e use system packages 2026-02-06 17:04:15 -05:00
andreas a494b162cd remove 2026-02-06 16:54:49 -05:00
andreas 83b7139643 typo 2026-02-06 15:53:01 -05:00
andreas 5dca9369dd fix bumpminor.sh script 2026-02-06 15:51:48 -05:00
17 changed files with 132 additions and 78 deletions
+1
View File
@@ -8,3 +8,4 @@ __pycache__/
test/ test/
build/ build/
*.egg-info/ *.egg-info/
ssl/
+18 -1
View File
@@ -10,7 +10,9 @@ watchhosts:
# "localhost": # "localhost":
# "haschloss" : # "haschloss" :
# "cotgate": # "cotgate":
# "wentworth": "wentworth":
notify: +4915123456789
src: "signal"
"y": "y":
notify: +4915123456789 notify: +4915123456789
src: "signal" src: "signal"
@@ -25,3 +27,18 @@ pushover_user: "uDhH33UjQQDYtNzJb1ThRiWb9ingGK"
pushsrv: "pushover" pushsrv: "pushover"
dyndomains: {"wrede.org"} dyndomains: {"wrede.org"}
toemail: ["aew.hbd.notify@wrede.ca"]
fromemail: "aew.hbd@wrede.ca"
smtpserver: "smtp.fastmail.com"
smtpuser: "andreas@wrede.ca"
smtppassword: "r8psra6wj6gcakkp"
smtpport: 587
ws_port: 50005
wss_port: 50006
cert_path: "/usr/local/etc/letsencrypt/live/hbd.wrede.ca/"
cert_path: "ssl/"
# CERT_PATH = "./test/"
wss_pem: "fullchain.pem"
wss_key: "privkey.pem"
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -6,6 +6,6 @@ start moving functionality into the package.
""" """
__all__ = ["main", "__version__"] __all__ = ["main", "__version__"]
__version__ = "5.0" __version__ = 5.0.2
from .cli import main from .cli import main
+11
View File
@@ -27,6 +27,17 @@ DEFAULTS = {
"foreground": False, "foreground": False,
"verbose": False, "verbose": False,
"debug": 0, "debug": 0,
"smtpserver": "smtp.fastmail.com",
"smtpuser": "andreas@wrede.ca",
"smtppassword": "pvtvefyp5gbhnch2",
"smtpport": 587,
"toemail": ["aew.hbd.notify@wrede.ca"],
"fromemail": "aew.hbd@wrede.ca",
"ws_port": 50005,
"wss_port": None,
"cert_path": "/usr/local/etc/ssl/",
"wss_pem": "fullchain.pem",
"wss_key": "privkey.pem"
} }
+5 -5
View File
@@ -54,7 +54,7 @@ def nsupdate(hostname: str, newip: str, dyndomain: str, nsupdate_bin: str = "/us
return out return out
async def dns_update_worker(hbdclass, cfg: dict, async_queue=None, log: Optional[callable] = None, email: Optional[callable] = None, loop: Optional[asyncio.AbstractEventLoop] = None): async def dns_update_worker(hbdclass, cfg: dict, async_queue=None, log: Optional[callable] = None, pushmsg: Optional[callable] = None, loop: Optional[asyncio.AbstractEventLoop] = None):
"""Pure async DNS worker that processes updates from asyncio.Queue. """Pure async DNS worker that processes updates from asyncio.Queue.
Exits when it receives a None sentinel. Exits when it receives a None sentinel.
@@ -99,9 +99,9 @@ async def dns_update_worker(hbdclass, cfg: dict, async_queue=None, log: Optional
err = await loop.run_in_executor(None, nsupdate, name, addr, dyndomain, cfg.get("nsupdate_bin", "/usr/local/bin/nsupdate"), cfg.get("rndc_key", "/etc/dhcpc/rndc-key")) err = await loop.run_in_executor(None, nsupdate, name, addr, dyndomain, cfg.get("nsupdate_bin", "/usr/local/bin/nsupdate"), cfg.get("rndc_key", "/etc/dhcpc/rndc-key"))
if err: if err:
m += f", DNS update failed: {err}" m += f", DNS update failed: {err}"
if email: if pushmsg:
try: try:
await loop.run_in_executor(None, email, "error: nsupdate failed", f"{name}.dy.{dyndomain}: {m}") await loop.run_in_executor(None, pushmsg, "error: nsupdate failed", f"{name}.dy.{dyndomain}: {m}")
except Exception: except Exception:
pass pass
else: else:
@@ -125,7 +125,7 @@ async def dns_update_worker(hbdclass, cfg: dict, async_queue=None, log: Optional
pass pass
def start_dns_worker(hbdclass, cfg: dict, log: Optional[callable] = None, email: Optional[callable] = None, loop: Optional[asyncio.AbstractEventLoop] = None): def start_dns_worker(hbdclass, cfg: dict, log: Optional[callable] = None, pushmsg: Optional[callable] = None, loop: Optional[asyncio.AbstractEventLoop] = None):
"""Start the async DNS worker and return the Task. """Start the async DNS worker and return the Task.
Replaces Host.dnsQ with an asyncio.Queue wrapped in a thread-safe bridge Replaces Host.dnsQ with an asyncio.Queue wrapped in a thread-safe bridge
@@ -167,5 +167,5 @@ def start_dns_worker(hbdclass, cfg: dict, log: Optional[callable] = None, email:
bridge = _QueueBridge(loop, async_q) bridge = _QueueBridge(loop, async_q)
hbdclass.Host.dnsQ = bridge hbdclass.Host.dnsQ = bridge
task = loop.create_task(dns_update_worker(hbdclass, cfg, async_queue=async_q, log=log, email=email, loop=loop)) task = loop.create_task(dns_update_worker(hbdclass, cfg, async_queue=async_q, log=log, pushmsg=pushmsg, loop=loop))
return task return task
+3 -1
View File
@@ -221,7 +221,9 @@ class Host:
for d in self.__dict__: for d in self.__dict__:
if d == "connections": if d == "connections":
cl = [] cl = []
for c in self.connections: for c in ["IPv4", "IPv6"]:
if c not in self.connections:
continue
# dirty ugly hack: fix conn to host backpointer # dirty ugly hack: fix conn to host backpointer
cld = copy.deepcopy(self.connections[c].__dict__) cld = copy.deepcopy(self.connections[c].__dict__)
cld["host"] = cld["host"].name cld["host"] = cld["host"].name
+14 -1
View File
@@ -139,7 +139,10 @@ async def start(
host = config.get("hb_host", "localhost") host = config.get("hb_host", "localhost")
extra_scripts = config.get("http_extra_scripts", "") extra_scripts = config.get("http_extra_scripts", "")
host = request.host.split(":")[0] host = request.host.split(":")[0]
heartbeat_ws_url = f"ws://{host}:{config.get('ws_port', 50005)}/hbd" if config.get("wss_port"):
heartbeat_ws_url = f"wss://{host}:{config['wss_port']}/hbd"
else:
heartbeat_ws_url = f"ws://{host}:{config.get('ws_port', 50005)}/hbd"
tmpl = env.get_template("live.html") tmpl = env.get_template("live.html")
body = tmpl.render( body = tmpl.render(
title="Heartbeat", title="Heartbeat",
@@ -158,6 +161,7 @@ async def start(
URL form: /static/<path> URL form: /static/<path>
""" """
p = request.match_info.get("path", "") p = request.match_info.get("path", "")
logger.debug("static file requested: %s", p)
base = os.path.abspath(os.path.join(os.path.dirname(__file__), "static")) base = os.path.abspath(os.path.join(os.path.dirname(__file__), "static"))
# normalize and prevent directory traversal # normalize and prevent directory traversal
target = os.path.abspath(os.path.normpath(os.path.join(base, p))) target = os.path.abspath(os.path.normpath(os.path.join(base, p)))
@@ -168,6 +172,14 @@ async def start(
logger.info("serving static file: %s", target) logger.info("serving static file: %s", target)
return web.FileResponse(path=target) return web.FileResponse(path=target)
async def favicon(request):
"""Serve favicon.ico from the package static directory."""
base = os.path.abspath(os.path.join(os.path.dirname(__file__), "static/images"))
target = os.path.join(base, "favicon.ico")
if not os.path.exists(target) or not os.path.isfile(target):
return web.Response(status=404, text="Not Found")
return web.FileResponse(path=target)
app = web.Application() app = web.Application()
app.add_routes( app.add_routes(
[ [
@@ -181,6 +193,7 @@ async def start(
web.get("/r", restart), web.get("/r", restart),
web.get("/live", live), web.get("/live", live),
web.get("/static/{path:.*}", static), web.get("/static/{path:.*}", static),
web.get("/favicon.ico", favicon),
] ]
) )
+2 -4
View File
@@ -9,7 +9,7 @@ from typing import Optional
from . import hbdclass from . import hbdclass
DROPOVERDUE = 7 * 24 * 3600 DROPOVERDUE = 7 * 24 * 3600
def checkoverdue(config: dict, hbdclass, log: callable, email: callable, pushmsg: callable, msg_to_websockets: callable): def checkoverdue(config: dict, hbdclass, log: callable, pushmsg: callable, msg_to_websockets: callable):
now = time.time() now = time.time()
for h in list(hbdclass.Host.hosts.keys()): for h in list(hbdclass.Host.hosts.keys()):
pmsg = [] pmsg = []
@@ -27,7 +27,6 @@ def checkoverdue(config: dict, hbdclass, log: callable, email: callable, pushmsg
conn.newstate(hbdclass.Connection.UNKNOWN, conn.lastbeat) conn.newstate(hbdclass.Connection.UNKNOWN, conn.lastbeat)
if pmsg != []: if pmsg != []:
if h in config.get("watchhosts", []): if h in config.get("watchhosts", []):
email("overdue", "%s overdue" % " and ".join(pmsg))
pushmsg("%s %s overdue" % (h, " and ".join(pmsg))) pushmsg("%s %s overdue" % (h, " and ".join(pmsg)))
log(h, "%s overdue" % " and ".join(pmsg)) log(h, "%s overdue" % " and ".join(pmsg))
msg_to_websockets("host", hbdclass.Host.hosts[h].stateinfo()) msg_to_websockets("host", hbdclass.Host.hosts[h].stateinfo())
@@ -36,11 +35,10 @@ async def start(
config: dict, config: dict,
hbdclass: callable, hbdclass: callable,
log=None, log=None,
email=None,
pushmsg=None, pushmsg=None,
msg_to_websockets=None, msg_to_websockets=None,
): ):
""" start a monitor loop that checks for overdue hosts every minute """ """ start a monitor loop that checks for overdue hosts every minute """
while True: while True:
await asyncio.sleep(15) # 15 seconds between checks await asyncio.sleep(15) # 15 seconds between checks
checkoverdue(config, hbdclass, log, email, pushmsg, msg_to_websockets) checkoverdue(config, hbdclass, log, pushmsg, msg_to_websockets)
+20 -6
View File
@@ -21,13 +21,21 @@ def setup(cfg: dict):
_config = dict(cfg) _config = dict(cfg)
def send_email(aemail, smtpserver, sender, subject, body, debug=0): def send_email(toaddrs, smtpserver, sender, subject, body, debug=0):
"""Send a plain email via SMTP. Returns True on success.""" """Send a plain email via SMTP. Returns True on success."""
try: try:
server = smtplib.SMTP(smtpserver) smtpport = _config.get("smtpport", 587)
server = smtplib.SMTP(smtpserver, smtpport)
if debug > 0: if debug > 0:
server.set_debuglevel(1) server.set_debuglevel(1)
server.sendmail(sender, aemail, body) if smtpport == 587:
server.starttls()
server.ehlo()
smtpuser = _config.get("smtpuser", None)
smtppassword = _config.get("smtppassword", None)
if smtpuser and smtppassword:
server.login(smtpuser, smtppassword)
server.sendmail(sender, toaddrs, body)
except Exception as e: except Exception as e:
logger.warning("email send failed: %s", e) logger.warning("email send failed: %s", e)
try: try:
@@ -48,9 +56,12 @@ def email(subject: str, msg: str, debug: int = 0) -> bool:
Uses module-level configuration to supply recipient list, smtp server Uses module-level configuration to supply recipient list, smtp server
and sender address. and sender address.
""" """
toaddrs = _config.get("AEMAIL") or _config.get("aemail") or _config.get("email_to") or [] toaddrs = _config.get("toemail")
fromemail = _config.get("fromemail") or _config.get("sender") or f"aew.heartbeat@{_config.get('domain','local') }" fromemail = _config.get("fromemail")
smtpserver = _config.get("SMTPSERVER") or _config.get("smtpserver") or _config.get("SMTPSERVER", "localhost") smtpserver = _config.get("smtpserver")
if not toaddrs or not fromemail or not smtpserver:
logger.warning("email config incomplete: toemail=%s, fromemail=%s, smtpserver=%s", toaddrs, fromemail, smtpserver)
return False
date = time.strftime("%a, %d %b %Y %H:%M:%S %z", time.localtime()) date = time.strftime("%a, %d %b %Y %H:%M:%S %z", time.localtime())
body = "To: %s\nFrom: %s\nSubject: %s\nDate: %s\n\n%s" % ( body = "To: %s\nFrom: %s\nSubject: %s\nDate: %s\n\n%s" % (
toaddrs[0] if toaddrs else "", toaddrs[0] if toaddrs else "",
@@ -145,6 +156,9 @@ def pushmsg(cfg: dict, msg: str, debug: int = 0):
if p in ("all", "signal"): if p in ("all", "signal"):
ok = pushsignal(cfg.get("signal_cli", "/usr/local/bin/signal-cli"), cfg.get("signal_user", ""), cfg.get("signal_recipient", ""), msg, debug=debug) ok = pushsignal(cfg.get("signal_cli", "/usr/local/bin/signal-cli"), cfg.get("signal_user", ""), cfg.get("signal_recipient", ""), msg, debug=debug)
results["signal"] = ok results["signal"] = ok
if p in ("all", "email"):
ok = email("Heartbeat notification", msg, debug=debug)
results["email"] = ok
logger.debug("push results: %s", results) logger.debug("push results: %s", results)
return results return results
+34 -10
View File
@@ -1,10 +1,12 @@
"""Server runtime: starts UDP listener, HTTP server and websocket stubs.""" """Server runtime: starts UDP listener, HTTP server and websocket stubs."""
import asyncio import asyncio
import logging import logging
import atexit import socket
import time import time
import signal import signal
import sys import sys
import ssl
import pathlib
from . import __version__ from . import __version__
from . import udp from . import udp
@@ -82,11 +84,22 @@ async def _run_async(config):
notify_mod.setup(config) notify_mod.setup(config)
email = notify_mod.email
pushmsg = notify_mod.pushmsg_from_config pushmsg = notify_mod.pushmsg_from_config
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
# Disable IPV6_V6ONLY option to enable dual-stack (listen on IPv4 as well)
# This option is system-dependent; on many systems, setting it to False enables
# the socket to handle both IPv4 and IPv6 traffic.
try:
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False)
except OSError as e:
logger.error(f"Warning: Could not set IPV6_V6ONLY to False. System may not support dual-stack or option is unavailable. Error: {e}")
# 3. Bind to all interfaces (::) on a specific port
# UDP server endpoint (handler wired to handle_datagram with context) # UDP server endpoint (handler wired to handle_datagram with context)
bind_addr = ("0.0.0.0", config.get("hb_port", 50003)) bind_addr = ("::", config.get("hb_port", 50003))
sock.bind(bind_addr)
logger.info("Starting UDP server on %s:%s", *bind_addr) logger.info("Starting UDP server on %s:%s", *bind_addr)
def udp_handler(msg, addr, transport): def udp_handler(msg, addr, transport):
@@ -94,7 +107,6 @@ async def _run_async(config):
config=config, config=config,
hbdclass=hbdclass, hbdclass=hbdclass,
log=log, log=log,
email=email,
pushmsg=pushmsg, pushmsg=pushmsg,
msg_to_websockets=msg_to_websockets, msg_to_websockets=msg_to_websockets,
DEBUG=config.get("debug", 0), DEBUG=config.get("debug", 0),
@@ -104,7 +116,7 @@ async def _run_async(config):
transport, protocol = await loop.create_datagram_endpoint( transport, protocol = await loop.create_datagram_endpoint(
lambda: udp.EchoServerProtocol(config=config, handler=udp_handler), lambda: udp.EchoServerProtocol(config=config, handler=udp_handler),
local_addr=bind_addr, sock=sock,
) )
# HTTP server (asyncio-based via aiohttp) # HTTP server (asyncio-based via aiohttp)
@@ -117,7 +129,6 @@ async def _run_async(config):
hbdclass=hbdclass, hbdclass=hbdclass,
msgs_getter=lambda: msgs, msgs_getter=lambda: msgs,
log=log, log=log,
email=email,
pushmsg=pushmsg, pushmsg=pushmsg,
msg_to_websockets=msg_to_websockets, msg_to_websockets=msg_to_websockets,
tcss=None, tcss=None,
@@ -134,19 +145,33 @@ async def _run_async(config):
# start dns update worker (async) # start dns update worker (async)
dns_task = None dns_task = None
try: try:
dns_task = dns_mod.start_dns_worker(hbdclass, config, log=log, email=email, loop=loop) dns_task = dns_mod.start_dns_worker(hbdclass, config, log=log, pushmsg=pushmsg, loop=loop)
logger.info("dns update worker started") logger.info("dns update worker started")
except Exception as e: except Exception as e:
logger.exception("dns worker failed to start: %s", e) logger.exception("dns worker failed to start: %s", e)
# Start the websocket servers as a background task # Start the websocket servers as a background task
if config.get("wss_port", None):
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_path = config.get("cert_path", "")
wss_pem = ssl_path + config.get("wss_pem", "")
wss_key = ssl_path + config.get("wss_key", "")
try:
ssl_context.load_cert_chain(wss_pem, keyfile=wss_key)
except FileNotFoundError:
logger.error("error: missing SSL keys %s or %s", wss_pem, wss_key)
sys.exit(1)
logger.info("Starting secure WebSocket server on port %s with cert %s", config.get("wss_port", None), wss_pem)
else:
ssl_context = None
try: try:
ws_task = asyncio.create_task( ws_task = asyncio.create_task(
ws_mod.start( ws_mod.start(
host=config.get("hbd_host", ""), host=config.get("hbd_host", ""),
ws_port=config.get("ws_port", 50005), ws_port=config.get("ws_port", None),
wss_port=config.get("wss_port", None), wss_port=config.get("wss_port", None),
ssl_context=None, ssl_context=ssl_context,
get_hosts=lambda: [hbdclass.Host.hosts[h].stateinfo() for h in sorted(hbdclass.Host.hosts)], get_hosts=lambda: [hbdclass.Host.hosts[h].stateinfo() for h in sorted(hbdclass.Host.hosts)],
get_msgs=lambda: msgs, get_msgs=lambda: msgs,
verbose=config.get("verbose", False), verbose=config.get("verbose", False),
@@ -163,7 +188,6 @@ async def _run_async(config):
config=config, config=config,
hbdclass=hbdclass, hbdclass=hbdclass,
log=log, log=log,
email=email,
pushmsg=pushmsg, pushmsg=pushmsg,
msg_to_websockets=msg_to_websockets, msg_to_websockets=msg_to_websockets,
) )
+2 -2
View File
@@ -185,11 +185,11 @@
function WS_Connect() { function WS_Connect() {
if ("WebSocket" in window) { if ("WebSocket" in window) {
//N.B: subprotocol field causes chrome to error 1006 //N.B: subprotocol field causes chrome to error 1006
var ws_hbd = new WebSocket("{{heartbeat_ws_url}}" /*, "hdb" */); var ws_hbd = new WebSocket("{{heartbeat_ws_url}}", /* "hdb" */ );
ws_hbd.onopen = function () { ws_hbd.onopen = function () {
// Web Socket is connected, send data using send() // Web Socket is connected, send data using send()
console.log("ws connect"); console.log("ws connect {{heartbeat_ws_url}}");
// Hide modal window if visible // Hide modal window if visible
var modal = document.getElementById("connectionModal"); var modal = document.getElementById("connectionModal");
if (modal) { if (modal) {
-14
View File
@@ -68,7 +68,6 @@ def handle_datagram(msg: dict, addr, transport, ctx: dict):
- config: dict of configuration - config: dict of configuration
- hbdclass: module providing Host/Connection classes - hbdclass: module providing Host/Connection classes
- log: callable(loghost, message) - log: callable(loghost, message)
- email: callable(subject, message)
- pushmsg: callable(message) - pushmsg: callable(message)
- msg_to_websockets: callable(typ, data) - msg_to_websockets: callable(typ, data)
- DEBUG, verbose - DEBUG, verbose
@@ -79,7 +78,6 @@ def handle_datagram(msg: dict, addr, transport, ctx: dict):
cfg = ctx.get("config", {}) cfg = ctx.get("config", {})
hbdcls = ctx.get("hbdclass") hbdcls = ctx.get("hbdclass")
log = ctx.get("log") log = ctx.get("log")
email = ctx.get("email")
pushmsg = ctx.get("pushmsg") pushmsg = ctx.get("pushmsg")
msg_to_websockets = ctx.get("msg_to_websockets") msg_to_websockets = ctx.get("msg_to_websockets")
DEBUG = ctx.get("DEBUG", 0) DEBUG = ctx.get("DEBUG", 0)
@@ -122,8 +120,6 @@ def handle_datagram(msg: dict, addr, transport, ctx: dict):
if log: if log:
log(uname, res) log(uname, res)
if uname in cfg.get("watchhosts", []): if uname in cfg.get("watchhosts", []):
if email:
email("address change", "%s %s" % (host.name, res))
if pushmsg: if pushmsg:
pushmsg("%s %s" % (host.name, res)) pushmsg("%s %s" % (host.name, res))
@@ -138,16 +134,12 @@ def handle_datagram(msg: dict, addr, transport, ctx: dict):
log(uname, "booted") log(uname, "booted")
if uname in cfg.get("watchhosts", []): if uname in cfg.get("watchhosts", []):
m = "%s booted" % (host.name) m = "%s booted" % (host.name)
if email:
email("booted", m)
if pushmsg: if pushmsg:
pushmsg(m) pushmsg(m)
if message: if message:
if log: if log:
log(uname, "msg: %s" % message, service=service) log(uname, "msg: %s" % message, service=service)
if uname in cfg.get("watchhosts", []): if uname in cfg.get("watchhosts", []):
if email:
email("msg", message)
if pushmsg: if pushmsg:
pushmsg(message) pushmsg(message)
@@ -158,8 +150,6 @@ def handle_datagram(msg: dict, addr, transport, ctx: dict):
if log: if log:
log(uname, m) log(uname, m)
if uname in cfg.get("watchhosts", []): if uname in cfg.get("watchhosts", []):
if email:
email("%s back" % conn.afam, uname)
if pushmsg: if pushmsg:
pushmsg("%s %s is back" % (uname, conn.afam)) pushmsg("%s %s is back" % (uname, conn.afam))
@@ -172,8 +162,6 @@ def handle_datagram(msg: dict, addr, transport, ctx: dict):
if log: if log:
log(uname, "%s shutdown" % conn.afam) log(uname, "%s shutdown" % conn.afam)
if uname in cfg.get("watchhosts", []): if uname in cfg.get("watchhosts", []):
if email:
email("shutdown", "%s %s shutdown" % (uname, conn.afam))
if pushmsg: if pushmsg:
pushmsg("%s %s shutdown" % (uname, conn.afam)) pushmsg("%s %s shutdown" % (uname, conn.afam))
conn.newstate(hbdcls.Connection.DOWN, now) conn.newstate(hbdcls.Connection.DOWN, now)
@@ -197,8 +185,6 @@ def handle_datagram(msg: dict, addr, transport, ctx: dict):
while len(host.cmds): while len(host.cmds):
op, rmsg = host.cmds[0] op, rmsg = host.cmds[0]
if op == "CMD": if op == "CMD":
if email:
email("%s cmd exec" % uname, "command '%s' sent" % rmsg)
del host.cmds[0] del host.cmds[0]
if log: if log:
log(uname, "command sent") log(uname, "command sent")
+11 -24
View File
@@ -20,11 +20,9 @@ _verbose = False
async def _handler(websocket, path=None): async def _handler(websocket, path=None):
# Some versions of the websockets library call handler(connection) only;
# accept optional path and fall back to websocket.path when missing.
global _connections global _connections
_connections.add(websocket) _connections.add(websocket)
remote_address = getattr(websocket, "remote_address", None) remote_address = websocket.remote_address
if path is None: if path is None:
path = getattr(websocket, "path", None) path = getattr(websocket, "path", None)
if _verbose: if _verbose:
@@ -76,36 +74,25 @@ async def start(host: str, ws_port: int, wss_port: Optional[int] = None, ssl_con
servers = [] servers = []
# plain WebSocket # plain WebSocket
ws_server = websockets.serve(_handler, host, ws_port) #, subprotocols=["hbd"])
websockets_logger = logging.getLogger("websockets.server") websockets_logger = logging.getLogger("websockets.server")
websockets_logger.setLevel(logging.INFO) websockets_logger.setLevel(logging.DEBUG if verbose else logging.INFO)
# regular WebSocket
ws_server = websockets.serve(_handler, host, ws_port) #, subprotocols=["hbd"])
servers.append(ws_server) servers.append(ws_server)
# secure WebSocket (optional) # secure WebSocket (optional)
if wss_port and ssl_context: if wss_port and ssl_context:
wss_server = websockets.serve(_handler, host, wss_port, ssl=ssl_context) #, subprotocols=["hbd"]) wss_server = websockets.serve(_handler, host, wss_port, ssl=ssl_context ) #, subprotocols=["hbd"])
servers.append(wss_server) servers.append(wss_server)
# await starting of all servers # await starting of all servers
try: for srv in servers:
for srv in servers: await srv
await srv
if _verbose: if _verbose:
logger.info("WebSocket server started on port %s (wss %s)", ws_port, wss_port) logger.info("WebSocket server(s) started on port %s (wss %s)", ws_port, wss_port)
# block forever (until loop is stopped or cancelled) # block forever (until loop is stopped or cancelled)
await asyncio.Future() await asyncio.Future()
except asyncio.CancelledError:
logger.info("WebSocket server shutting down...")
# Close all active connections
for conn in list(_connections):
try:
await conn.close()
except Exception:
pass
_connections.clear()
raise
def broadcast(typ: str, data) -> bool: def broadcast(typ: str, data) -> bool:
+1 -1
View File
@@ -5,7 +5,7 @@
set -e set -e
if [ ! -d ~/venvs/hbd ]; then if [ ! -d ~/venvs/hbd ]; then
mkdir -p ~/venvs mkdir -p ~/venvs
python3 -m venv ~/venvs/hbd python3 -m venv ~/venvs/hbd --system-site-packages
fi fi
. ~/venvs/hbd/bin/activate . ~/venvs/hbd/bin/activate
pip install 'git+ssh://git@git.wrede.ca/andreas/heartbeat.git' pip install 'git+ssh://git@git.wrede.ca/andreas/heartbeat.git'
+2 -2
View File
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "hbd" name = "hbd"
version = "5.0" version = "5.0.2"
description = "Heartbeat daemon (hbd) — receive heartbeats and act on them" description = "Heartbeat daemon (hbd) — receive heartbeats and act on them"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
license = { text = "MIT" } license = "MIT"
keywords = ["heartbeat", "monitoring", "dns", "websocket"] keywords = ["heartbeat", "monitoring", "dns", "websocket"]
authors = [ authors = [
{ name = "heartbeat contributors" } { name = "heartbeat contributors" }
+4 -3
View File
@@ -1,12 +1,13 @@
#!/bin/sh #!/bin/sh
set -e
uv version --bump patch uv version --bump patch
VER=$(uv version --short) VER=$(uv version --short)
sed -i "s/__version__ = \"[0-9.]*\"\(.*\)$/__version__ = \"$VER\"\1/" moninbox/const.py sed -i "s/__version__ = \"[0-9.]*\"\(.*\)$/__version__ = \"$VER\"\1/" hbd/__init__.py
# commit pyproject.toml # commit pyproject.toml
git commit -m "version $VER" pyproject.toml moninbox/const.py git commit -m "version $VER" pyproject.toml hbd/__init__.py
git push git push
# tag version # tag version
git tag -a v$VER -m "Version $VER" git tag -a v$VER -m "Version $VER"
git push --tags git push --tags