style: clear whitespace, unused-import and blank-line lint

Strip trailing whitespace, drop unused imports/variables/global statements,
split multi-imports, fix E3xx/E1xx spacing and E712 comparisons. No behaviour
change; only E501 long lines remain in flake8.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 04:59:34 -04:00
co-authored by Claude Sonnet 5.5
parent 85c6a4eebe
commit 748349a7b9
35 changed files with 705 additions and 719 deletions
+59 -60
View File
@@ -16,7 +16,7 @@ import sys
import time
from logging.handlers import SysLogHandler
from pathlib import Path
from typing import Dict, List, Optional
from typing import List, Optional
# Import protocol and config
from .config import load_config
@@ -46,20 +46,20 @@ allow_remote_command = False
class AsyncConnection:
"""Async UDP connection to a heartbeat server."""
def __init__(self, conn_id: int, addr: str, port: int, af: int, name: str):
self.conn_id = conn_id
self.addr = addr
self.port = port
self.af = af
self.name = name
self.ackcount = 0
self.lastack = 0.0
self.send_count = 0
self.lastsend = 0.0
self.rtts = [0.0]
self.transport: Optional[asyncio.DatagramTransport] = None
self.protocol: Optional[asyncio.DatagramProtocol] = None
self._dead = False
@@ -89,7 +89,7 @@ class AsyncConnection:
except Exception as e:
self.logger.error(f"Failed to open connection: {e}")
return False
def close(self):
"""Close the connection."""
if self.transport:
@@ -104,10 +104,10 @@ class AsyncConnection:
self.af = af
self.addr = addr
self.logger = logging.getLogger(f"hbc.conn.{addr}")
async def sendto(self, msg: dict, msg_id: str = "HTB"):
"""Send a message to the server.
Args:
msg: Message dictionary
msg_id: Message ID (HTB, PLG, etc.)
@@ -121,36 +121,36 @@ class AsyncConnection:
if not self.transport:
self.logger.error("Cannot send - no transport")
return
# Add standard fields
msg["name"] = shortname(self.name)
msg["id"] = self.conn_id
msg["time"] = time.time()
# Encode message
data = dicttos(msg_id, msg)
# Send
self.transport.sendto(data, (self.addr, self.port))
self.send_count += 1
self.lastsend = time.time()
self.logger.debug(f"Sent {msg_id} message ({len(data)} bytes)")
def handle_ack(self, msg: dict, now: float):
"""Handle ACK message from server.
RTT is calculated as: (time ACK received) - (time HTB sent)
"""
self.lastack = now
# Calculate RTT: time ACK received minus time HTB sent
rtt = (now - self.lastsend) * 1000.0 # Convert to ms
self.rtts.append(rtt)
if len(self.rtts) > 10:
self.rtts.pop(0)
self.ackcount += 1
self.logger.debug(f"ACK received, RTT: {rtt:.1f}ms")
if msg.get("request_update"):
@@ -160,11 +160,11 @@ class AsyncConnection:
class HeartbeatProtocol(asyncio.DatagramProtocol):
"""Protocol handler for incoming UDP messages."""
def __init__(self, connection: AsyncConnection):
self.connection = connection
self.logger = logging.getLogger("hbc.protocol")
def datagram_received(self, data: bytes, addr):
"""Handle incoming datagram."""
try:
@@ -172,10 +172,10 @@ class HeartbeatProtocol(asyncio.DatagramProtocol):
if not msg:
self.logger.warning(f"Failed to parse message from {addr}")
return
now = time.time()
msg_id = msg.get("ID")
if msg_id == "ACK":
self.connection.handle_ack(msg, now)
elif msg_id == "CMD":
@@ -186,10 +186,10 @@ class HeartbeatProtocol(asyncio.DatagramProtocol):
asyncio.create_task(handle_update(self.connection, msg))
else:
self.logger.warning(f"Unknown message type: {msg_id}")
except Exception as e:
self.logger.error(f"Error processing datagram: {e}", exc_info=True)
def error_received(self, exc):
"""Handle protocol errors — close transport so the heartbeat sender retries."""
self.logger.warning(f"Protocol error on {self.connection.addr}: {exc} — will retry")
@@ -230,7 +230,7 @@ async def handle_command(conn: AsyncConnection, msg: dict):
except Exception as e:
result = str(e)
status = "Error"
# Send response
response = {
"service": "command",
@@ -446,14 +446,14 @@ async def plugin_collector_interval(
interval: int
):
"""Collect plugins on a specific interval.
Args:
conn: Connection to send on
plugins: List of plugins to collect
interval: Collection interval in seconds
"""
logger = logging.getLogger(f"hbc.plugins.{interval}s")
while running:
for plugin in plugins:
try:
@@ -471,7 +471,7 @@ async def plugin_collector_interval(
f"Error collecting {plugin.name}: {e}",
exc_info=True
)
# Wait for next interval or shutdown event
try:
if shutdown_event:
@@ -498,11 +498,11 @@ def stop():
"""Stop the event loop."""
global running
running = False
# Set shutdown event to wake up sleeping tasks
if shutdown_event:
shutdown_event.set()
# Cancel all active tasks
for task in active_tasks:
if not task.done():
@@ -555,7 +555,7 @@ async def cleanup(connections: List[AsyncConnection]):
"""Cleanup connections on shutdown."""
logger = logging.getLogger("hbc.cleanup")
logger.info("Cleaning up connections")
target = next((c for c in connections if c.transport), connections[0] if connections else None)
# A SIGHUP restart is not a host shutdown, so don't announce one.
if target and send_shutdown and not dorestart:
@@ -565,35 +565,35 @@ async def cleanup(connections: List[AsyncConnection]):
logger.error(f"Error sending shutdown: {e}")
for conn in connections:
conn.close()
# Give messages time to send
await asyncio.sleep(0.5)
async def async_main(args, config):
"""Async main function."""
global running, shutdown_event, active_tasks, send_shutdown, allow_remote_command
global shutdown_event, active_tasks, send_shutdown, allow_remote_command
# Create shutdown event
shutdown_event = asyncio.Event()
active_tasks = []
allow_remote_command = bool(config.get("allow_remote_command", False))
logger = logging.getLogger("hbc.main")
# Setup
iam = socket.gethostname()
if args.name:
iam = args.name
hb_hosts = args.hosts
hb_port = config.get("hb_port", PORT)
interval = config.get("interval", INTERVAL)
logger.info(f"hbc {__version__} on {iam} -> {hb_hosts} port={hb_port}, interval={interval}s")
if allow_remote_command:
logger.warning("allow_remote_command is true — CMD packets from the server will be executed")
af_filter = (socket.AF_INET if getattr(args, "ipv4_only", False)
else socket.AF_INET6 if getattr(args, "ipv6_only", False)
else 0)
@@ -632,7 +632,7 @@ async def async_main(args, config):
return 1
logger.info(f"Created {len(connections)} connections")
# Send boot/message if requested
send_shutdown = False
if args.boot or args.message:
@@ -643,27 +643,27 @@ async def async_main(args, config):
if args.message:
boot_msg["service"] = "service"
boot_msg["msg"] = args.message
boot_msg["acks"] = 0
target = next((c for c in connections if c.transport), connections[0])
await target.sendto(boot_msg)
if args.message and not args.daemon:
# Message-only mode
await cleanup(connections)
return 0
# Load plugins
registry = PluginRegistry()
loader = PluginLoader(registry)
plugin_dir = Path(__file__).parent / "plugins"
if plugin_dir.exists():
count = await loader.load_from_directory(plugin_dir, config)
logger.info(f"Loaded {count} plugins")
else:
logger.warning(f"Plugin directory not found: {plugin_dir}")
# Setup signal handlers
loop = asyncio.get_event_loop()
for sig in (signal.SIGTERM, signal.SIGINT):
@@ -675,7 +675,7 @@ async def async_main(args, config):
stop()
loop.add_signal_handler(signal.SIGHUP, _sighup)
# Start async tasks
# Per-protocol internet presence probe (shared with the network_monitor plugin)
net_cfg = dict((config.get("plugins") or {}).get("network_monitor")
@@ -690,7 +690,7 @@ async def async_main(args, config):
for conn in connections:
task = asyncio.create_task(heartbeat_sender(conn, interval, connectivity))
active_tasks.append(task)
active_tasks.append(asyncio.create_task(
dns_refresher(connections, hb_hosts, hb_port, af_filter, iam, interval, connectivity, conn_id)))
@@ -698,18 +698,18 @@ async def async_main(args, config):
if connections and registry.get_enabled():
task = asyncio.create_task(plugin_collector(connections[0], registry))
active_tasks.append(task)
# Wait for stop or tasks to complete
try:
await asyncio.gather(*active_tasks, return_exceptions=True)
except asyncio.CancelledError:
logger.info("Tasks cancelled")
# Cleanup
logger.info("Shutting down...")
await cleanup(connections)
await loader.unload_all()
return 0
@@ -727,11 +727,11 @@ def daemonize(
except OSError as e:
sys.stderr.write(f"fork #1 failed: {e}\n")
os._exit(1)
os.chdir(working_dir)
os.setsid()
os.umask(0)
try:
pid = os.fork()
if pid > 0:
@@ -739,14 +739,14 @@ def daemonize(
except OSError as e:
sys.stderr.write(f"fork #2 failed: {e}\n")
sys.exit(1)
sys.stdout.flush()
sys.stderr.flush()
si = open(stdin, "r")
so = open(stdout, "a+")
se = open(stderr, "a+")
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
@@ -838,18 +838,17 @@ def build_parser():
def main(argv=None):
"""Main entry point."""
global running, dorestart
parser = build_parser()
args = parser.parse_args(argv)
# Setup logging
log_level = logging.WARNING
if args.verbose:
log_level = logging.INFO
if args.debug:
log_level = logging.DEBUG
logging.basicConfig(
level=log_level,
format="%(asctime)s %(name)s %(levelname)s: %(message)s",
@@ -858,14 +857,14 @@ def main(argv=None):
# Load config
config = load_config(args.configfile)
# Daemonize if requested
if args.daemon:
logging.info("Daemonizing...")
daemonize()
_reconfigure_logging_for_daemon(log_level)
logging.info(f"hbc starting, sending heartbeat to {', '.join(args.hosts)}")
# Run async main
try:
exit_code = asyncio.run(async_main(args, config))
@@ -875,7 +874,7 @@ def main(argv=None):
except Exception as e:
logging.error(f"Fatal error: {e}", exc_info=True)
exit_code = 1
# Handle restart
if dorestart:
logging.info("Restarting...")
@@ -883,7 +882,7 @@ def main(argv=None):
# a SIGHUP restart is not a host reboot.
restart_argv = [sys.argv[0]] + [a for a in sys.argv[1:] if a not in ("-b", "--boot")]
os.execv(restart_argv[0], restart_argv)
sys.exit(exit_code)