Projektarbeit-MYP/backend/utils/queue_manager.py
2025-05-31 22:40:29 +02:00

426 lines
17 KiB
Python

"""
Queue Manager für die Verwaltung von Druckjobs in Warteschlangen.
Überwacht offline Drucker und aktiviert Jobs automatisch.
"""
import threading
import time
import logging
import subprocess
import os
import requests
import signal
import atexit
from datetime import datetime, timedelta
from typing import List, Dict, Optional, Tuple
from contextlib import contextmanager
from models import get_db_session, Job, Printer, User, Notification
from utils.logging_config import get_logger
# Windows-spezifische Imports
if os.name == 'nt':
try:
from utils.windows_fixes import get_windows_thread_manager
except ImportError:
get_windows_thread_manager = None
else:
get_windows_thread_manager = None
# Logger für Queue-Manager
queue_logger = get_logger("queue_manager")
def check_printer_status(ip_address: str, timeout: int = 5) -> Tuple[str, bool]:
"""
Vereinfachte Drucker-Status-Prüfung für den Queue Manager.
Args:
ip_address: IP-Adresse der Drucker-Steckdose
timeout: Timeout in Sekunden (Standard: 5)
Returns:
Tuple[str, bool]: (Status, Aktiv) - Status ist "online" oder "offline", Aktiv ist True/False
"""
if not ip_address or ip_address.strip() == "":
return "offline", False
try:
# Ping-Test um Erreichbarkeit zu prüfen
if os.name == 'nt': # Windows
cmd = ['ping', '-n', '1', '-w', str(timeout * 1000), ip_address.strip()]
else: # Unix/Linux/macOS
cmd = ['ping', '-c', '1', '-W', str(timeout), ip_address.strip()]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout + 1,
encoding='utf-8',
errors='replace'
)
# Wenn Ping erfolgreich ist, als online betrachten
if result.returncode == 0:
queue_logger.debug(f"✅ Drucker {ip_address} ist erreichbar (Ping erfolgreich)")
return "online", True
else:
queue_logger.debug(f"❌ Drucker {ip_address} nicht erreichbar (Ping fehlgeschlagen)")
return "offline", False
except subprocess.TimeoutExpired:
queue_logger.warning(f"⏱️ Ping-Timeout für Drucker {ip_address} nach {timeout} Sekunden")
return "offline", False
except Exception as e:
queue_logger.error(f"❌ Fehler beim Status-Check für Drucker {ip_address}: {str(e)}")
return "offline", False
class PrinterQueueManager:
"""
Verwaltet die Warteschlangen für offline Drucker und überwacht deren Status.
Verbesserte Version mit ordnungsgemäßem Thread-Management für Windows.
"""
def __init__(self):
self.is_running = False
self.monitor_thread = None
self.shutdown_event = threading.Event() # Sauberes Shutdown-Signal
self.check_interval = 120 # 2 Minuten zwischen Status-Checks
self.last_status_cache = {} # Cache für letzten bekannten Status
self.notification_cooldown = {} # Verhindert Spam-Benachrichtigungen
self._lock = threading.Lock() # Thread-Sicherheit
# Windows-spezifische Signal-Handler registrieren
if os.name == 'nt':
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
def _signal_handler(self, signum, frame):
"""Signal-Handler für ordnungsgemäßes Shutdown."""
queue_logger.warning(f"🛑 Signal {signum} empfangen - stoppe Queue Manager...")
self.stop()
def start(self):
"""Startet den Queue-Manager mit verbessertem Thread-Management."""
with self._lock:
if not self.is_running:
self.is_running = True
self.shutdown_event.clear()
self.monitor_thread = threading.Thread(target=self._monitor_loop, daemon=False)
self.monitor_thread.name = "PrinterQueueMonitor"
# Windows Thread-Manager verwenden falls verfügbar
if os.name == 'nt' and get_windows_thread_manager:
try:
thread_manager = get_windows_thread_manager()
thread_manager.register_thread(self.monitor_thread)
thread_manager.register_cleanup_function(self.stop)
queue_logger.debug("✅ Queue Manager bei Windows Thread-Manager registriert")
except Exception as e:
queue_logger.warning(f"⚠️ Windows Thread-Manager nicht verfügbar: {str(e)}")
self.monitor_thread.start()
queue_logger.info("✅ Printer Queue Manager erfolgreich gestartet")
def stop(self):
"""Stoppt den Queue-Manager ordnungsgemäß."""
with self._lock:
if self.is_running:
queue_logger.info("🔄 Beende Queue Manager...")
self.is_running = False
self.shutdown_event.set()
if self.monitor_thread and self.monitor_thread.is_alive():
queue_logger.debug("⏳ Warte auf Thread-Beendigung...")
self.monitor_thread.join(timeout=10)
if self.monitor_thread.is_alive():
queue_logger.warning("⚠️ Thread konnte nicht ordnungsgemäß beendet werden")
else:
queue_logger.info("✅ Monitor-Thread erfolgreich beendet")
self.monitor_thread = None
queue_logger.info("❌ Printer Queue Manager gestoppt")
def _monitor_loop(self):
"""Hauptschleife für die Überwachung der Drucker mit verbessertem Shutdown-Handling."""
queue_logger.info(f"🔄 Queue-Überwachung gestartet (Intervall: {self.check_interval} Sekunden)")
while self.is_running and not self.shutdown_event.is_set():
try:
self._check_waiting_jobs()
# Verwende Event.wait() statt time.sleep() für unterbrechbares Warten
if self.shutdown_event.wait(timeout=self.check_interval):
# Shutdown-Signal erhalten
queue_logger.info("🛑 Shutdown-Signal empfangen - beende Monitor-Loop")
break
except Exception as e:
queue_logger.error(f"❌ Fehler in Monitor-Schleife: {str(e)}")
# Kürzere Wartezeit bei Fehlern, aber auch unterbrechbar
if self.shutdown_event.wait(timeout=30):
break
queue_logger.info("🔚 Monitor-Loop beendet")
def _check_waiting_jobs(self):
"""Überprüft alle wartenden Jobs und aktiviert sie bei verfügbaren Druckern."""
if self.shutdown_event.is_set():
return
db_session = get_db_session()
try:
# Alle wartenden Jobs abrufen
waiting_jobs = db_session.query(Job).filter(
Job.status == "waiting_for_printer"
).all()
if not waiting_jobs:
return
queue_logger.info(f"🔍 Überprüfe {len(waiting_jobs)} wartende Jobs...")
activated_jobs = []
for job in waiting_jobs:
# Shutdown-Check zwischen Jobs
if self.shutdown_event.is_set():
break
# Drucker-Status prüfen
printer = db_session.get(Printer, job.printer_id)
if not printer:
continue
# Status-Check mit Cache-Optimierung
printer_key = f"printer_{printer.id}"
current_status = None
try:
if printer.plug_ip:
status, active = check_printer_status(printer.plug_ip, timeout=5)
current_status = "online" if (status == "online" and active) else "offline"
else:
current_status = "offline"
except Exception as e:
queue_logger.warning(f"⚠️ Status-Check für Drucker {printer.name} fehlgeschlagen: {str(e)}")
current_status = "offline"
# Prüfen, ob Drucker online geworden ist
last_status = self.last_status_cache.get(printer_key, "offline")
self.last_status_cache[printer_key] = current_status
if current_status == "online" and last_status == "offline":
# Drucker ist online geworden!
queue_logger.info(f"🟢 Drucker {printer.name} ist ONLINE geworden - aktiviere wartende Jobs")
# Job aktivieren
job.status = "scheduled"
printer.status = "available"
printer.active = True
printer.last_checked = datetime.now()
activated_jobs.append({
"job": job,
"printer": printer
})
elif current_status == "online":
# Drucker ist bereits online, Job kann aktiviert werden
job.status = "scheduled"
printer.status = "available"
printer.active = True
printer.last_checked = datetime.now()
activated_jobs.append({
"job": job,
"printer": printer
})
else:
# Drucker bleibt offline
printer.status = "offline"
printer.active = False
printer.last_checked = datetime.now()
# Speichere alle Änderungen
if activated_jobs:
db_session.commit()
queue_logger.info(f"{len(activated_jobs)} Jobs erfolgreich aktiviert")
# Benachrichtigungen versenden (nur wenn nicht im Shutdown)
if not self.shutdown_event.is_set():
for item in activated_jobs:
self._send_job_activation_notification(item["job"], item["printer"])
else:
# Auch offline-Status speichern
db_session.commit()
except Exception as e:
db_session.rollback()
queue_logger.error(f"❌ Fehler beim Überprüfen wartender Jobs: {str(e)}")
finally:
db_session.close()
def _send_job_activation_notification(self, job: Job, printer: Printer):
"""Sendet eine Benachrichtigung, wenn ein Job aktiviert wird."""
if self.shutdown_event.is_set():
return
try:
# Cooldown prüfen (keine Spam-Benachrichtigungen)
cooldown_key = f"job_{job.id}_activated"
now = datetime.now()
if cooldown_key in self.notification_cooldown:
last_notification = self.notification_cooldown[cooldown_key]
if (now - last_notification).total_seconds() < 300: # 5 Minuten Cooldown
return
self.notification_cooldown[cooldown_key] = now
# Benachrichtigung erstellen
db_session = get_db_session()
try:
user = db_session.get(User, job.user_id)
if not user:
return
notification = Notification(
user_id=user.id,
type="job_activated",
payload={
"job_id": job.id,
"job_name": job.name,
"printer_id": printer.id,
"printer_name": printer.name,
"start_time": job.start_at.isoformat() if job.start_at else None,
"message": f"🎉 Gute Nachrichten! Drucker '{printer.name}' ist online. Ihr Job '{job.name}' wurde aktiviert und startet bald."
}
)
db_session.add(notification)
db_session.commit()
queue_logger.info(f"📧 Benachrichtigung für User {user.name} gesendet: Job {job.name} aktiviert")
except Exception as e:
db_session.rollback()
queue_logger.error(f"❌ Fehler beim Erstellen der Benachrichtigung: {str(e)}")
finally:
db_session.close()
except Exception as e:
queue_logger.error(f"❌ Fehler beim Senden der Aktivierungs-Benachrichtigung: {str(e)}")
def get_queue_status(self) -> Dict:
"""Gibt den aktuellen Status der Warteschlangen zurück."""
db_session = get_db_session()
try:
# Wartende Jobs zählen
waiting_jobs = db_session.query(Job).filter(
Job.status == "waiting_for_printer"
).count()
# Offline Drucker mit wartenden Jobs
offline_printers_with_queue = db_session.query(Printer).join(Job).filter(
Printer.status == "offline",
Job.status == "waiting_for_printer"
).distinct().count()
# Online Drucker
online_printers = db_session.query(Printer).filter(
Printer.status == "available"
).count()
total_printers = db_session.query(Printer).count()
return {
"waiting_jobs": waiting_jobs,
"offline_printers_with_queue": offline_printers_with_queue,
"online_printers": online_printers,
"total_printers": total_printers,
"queue_manager_running": self.is_running,
"last_check": datetime.now().isoformat(),
"check_interval_seconds": self.check_interval
}
except Exception as e:
queue_logger.error(f"❌ Fehler beim Abrufen des Queue-Status: {str(e)}")
return {
"error": str(e),
"queue_manager_running": self.is_running
}
finally:
db_session.close()
def is_healthy(self) -> bool:
"""Prüft, ob der Queue Manager ordnungsgemäß läuft."""
return (self.is_running and
self.monitor_thread is not None and
self.monitor_thread.is_alive() and
not self.shutdown_event.is_set())
# Globale Instanz des Queue-Managers
_queue_manager_instance = None
_queue_manager_lock = threading.Lock()
def get_queue_manager() -> PrinterQueueManager:
"""Gibt die globale Instanz des Queue-Managers zurück."""
global _queue_manager_instance
with _queue_manager_lock:
if _queue_manager_instance is None:
_queue_manager_instance = PrinterQueueManager()
return _queue_manager_instance
def start_queue_manager():
"""Startet den globalen Queue-Manager."""
manager = get_queue_manager()
manager.start()
return manager
def stop_queue_manager():
"""Stoppt den globalen Queue-Manager definitiv und sicher."""
global _queue_manager_instance
with _queue_manager_lock:
if _queue_manager_instance:
try:
queue_logger.info("🔄 Stoppe Queue-Manager...")
# Shutdown-Event setzen
_queue_manager_instance.shutdown_event.set()
# Monitor-Thread beenden
if (_queue_manager_instance.monitor_thread and
_queue_manager_instance.monitor_thread.is_alive()):
queue_logger.info("⏳ Warte auf Monitor-Thread...")
_queue_manager_instance.monitor_thread.join(timeout=5.0)
# Falls Thread nicht beendet wurde, forciere Beendigung
if _queue_manager_instance.monitor_thread.is_alive():
queue_logger.warning("⚠️ Monitor-Thread reagiert nicht - forciere Beendigung")
# Thread als Daemon markieren für automatische Beendigung
_queue_manager_instance.monitor_thread.daemon = True
# Status auf gestoppt setzen
_queue_manager_instance.is_running = False
# Explizit stop() aufrufen
_queue_manager_instance.stop()
queue_logger.info("✅ Queue-Manager erfolgreich gestoppt")
except Exception as e:
queue_logger.error(f"❌ Fehler beim Stoppen des Queue-Managers: {str(e)}")
finally:
# Instanz definitiv auf None setzen
_queue_manager_instance = None
# Automatisches Cleanup bei Prozess-Ende registrieren
atexit.register(stop_queue_manager)