Complete uni-wizard implementation with unified tool registry: **Core Architecture:** - harness.py - Single entry point for all capabilities - tools/registry.py - Central tool registry with schema generation - Elegant routing: One harness, infinite capabilities **Tool Categories (13 tools total):** - System: system_info, process_list, service_status, service_control, health_check, disk_usage - Git: git_status, git_log, git_pull, git_commit, git_push, git_checkout, git_branch_list - Network: http_get, http_post, gitea_create_issue, gitea_comment, gitea_list_issues, gitea_get_issue **Daemons:** - health_daemon.py - HTTP endpoint on :8082, writes to ~/timmy/logs/health.json - task_router.py - Polls Gitea for assigned issues, routes to tools, posts results **Systemd Services:** - timmy-health.service - Health monitoring daemon - timmy-task-router.service - Gitea task router daemon **Testing:** - test_harness.py - Exercises all tool categories **Design Principles:** - Local-first: No cloud dependencies - Self-healing: Tools can restart, reconnect, recover - Unified: One consciousness, all capabilities Closes #76, #77, #78
181 lines
5.1 KiB
Python
181 lines
5.1 KiB
Python
"""
|
|
Health Check Daemon for Uni-Wizard
|
|
Monitors VPS status and exposes health endpoint
|
|
"""
|
|
|
|
import json
|
|
import time
|
|
import threading
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
# Add parent to path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from harness import get_harness
|
|
|
|
|
|
class HealthCheckHandler(BaseHTTPRequestHandler):
|
|
"""HTTP handler for health endpoint"""
|
|
|
|
def log_message(self, format, *args):
|
|
# Suppress default logging
|
|
pass
|
|
|
|
def do_GET(self):
|
|
"""Handle GET requests"""
|
|
if self.path == '/health':
|
|
self.send_health_response()
|
|
elif self.path == '/status':
|
|
self.send_full_status()
|
|
else:
|
|
self.send_error(404)
|
|
|
|
def send_health_response(self):
|
|
"""Send simple health check"""
|
|
harness = get_harness()
|
|
result = harness.execute("health_check")
|
|
|
|
try:
|
|
health_data = json.loads(result)
|
|
status_code = 200 if health_data.get("overall") == "healthy" else 503
|
|
except:
|
|
status_code = 503
|
|
health_data = {"error": "Health check failed"}
|
|
|
|
self.send_response(status_code)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps(health_data).encode())
|
|
|
|
def send_full_status(self):
|
|
"""Send full system status"""
|
|
harness = get_harness()
|
|
|
|
status = {
|
|
"timestamp": datetime.now().isoformat(),
|
|
"harness": json.loads(harness.get_status()),
|
|
"system": json.loads(harness.execute("system_info")),
|
|
"health": json.loads(harness.execute("health_check"))
|
|
}
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps(status, indent=2).encode())
|
|
|
|
|
|
class HealthDaemon:
|
|
"""
|
|
Health monitoring daemon.
|
|
|
|
Runs continuously, monitoring:
|
|
- System resources
|
|
- Service status
|
|
- Inference endpoint
|
|
|
|
Exposes:
|
|
- HTTP endpoint on port 8082
|
|
- JSON status file at ~/timmy/logs/health.json
|
|
"""
|
|
|
|
def __init__(self, port: int = 8082, check_interval: int = 60):
|
|
self.port = port
|
|
self.check_interval = check_interval
|
|
self.running = False
|
|
self.server = None
|
|
self.monitor_thread = None
|
|
self.last_health = None
|
|
|
|
# Ensure log directory exists
|
|
self.log_path = Path.home() / "timmy" / "logs"
|
|
self.log_path.mkdir(parents=True, exist_ok=True)
|
|
self.health_file = self.log_path / "health.json"
|
|
|
|
def start(self):
|
|
"""Start the health daemon"""
|
|
self.running = True
|
|
|
|
# Start HTTP server
|
|
self.server = HTTPServer(('127.0.0.1', self.port), HealthCheckHandler)
|
|
server_thread = threading.Thread(target=self.server.serve_forever)
|
|
server_thread.daemon = True
|
|
server_thread.start()
|
|
|
|
# Start monitoring loop
|
|
self.monitor_thread = threading.Thread(target=self._monitor_loop)
|
|
self.monitor_thread.daemon = True
|
|
self.monitor_thread.start()
|
|
|
|
print(f"Health daemon started on http://127.0.0.1:{self.port}")
|
|
print(f" - /health - Quick health check")
|
|
print(f" - /status - Full system status")
|
|
print(f"Health file: {self.health_file}")
|
|
|
|
def stop(self):
|
|
"""Stop the health daemon"""
|
|
self.running = False
|
|
if self.server:
|
|
self.server.shutdown()
|
|
print("Health daemon stopped")
|
|
|
|
def _monitor_loop(self):
|
|
"""Background monitoring loop"""
|
|
while self.running:
|
|
try:
|
|
self._update_health_file()
|
|
time.sleep(self.check_interval)
|
|
except Exception as e:
|
|
print(f"Monitor error: {e}")
|
|
time.sleep(5)
|
|
|
|
def _update_health_file(self):
|
|
"""Update the health status file"""
|
|
harness = get_harness()
|
|
|
|
try:
|
|
health_result = harness.execute("health_check")
|
|
system_result = harness.execute("system_info")
|
|
|
|
status = {
|
|
"timestamp": datetime.now().isoformat(),
|
|
"health": json.loads(health_result),
|
|
"system": json.loads(system_result)
|
|
}
|
|
|
|
self.health_file.write_text(json.dumps(status, indent=2))
|
|
self.last_health = status
|
|
|
|
except Exception as e:
|
|
print(f"Failed to update health file: {e}")
|
|
|
|
|
|
def main():
|
|
"""Run the health daemon"""
|
|
import signal
|
|
|
|
daemon = HealthDaemon()
|
|
|
|
def signal_handler(sig, frame):
|
|
print("\nShutting down...")
|
|
daemon.stop()
|
|
sys.exit(0)
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
|
|
daemon.start()
|
|
|
|
# Keep main thread alive
|
|
try:
|
|
while True:
|
|
time.sleep(1)
|
|
except KeyboardInterrupt:
|
|
daemon.stop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|