Update changes
Dieser Commit ist enthalten in:
@ -7,6 +7,12 @@ Dieser Guard verhindert:
|
||||
- Mehrere Browser-Instanzen gleichzeitig
|
||||
|
||||
Clean Code & YAGNI: Nur das Nötigste, keine Über-Engineering.
|
||||
|
||||
WICHTIG - Korrekte Verwendung:
|
||||
- start() → Prozess beginnt
|
||||
- end(success=True/False) → Prozess endet normal (zählt für Failure-Tracking)
|
||||
- release() → Prozess wird abgebrochen (zählt NICHT als Failure)
|
||||
- Alle Methoden sind idempotent (mehrfacher Aufruf ist sicher)
|
||||
"""
|
||||
|
||||
import json
|
||||
@ -14,6 +20,7 @@ import logging
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple
|
||||
import threading
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -26,6 +33,9 @@ class ProcessGuard:
|
||||
- Process Lock Management (nur ein Prozess gleichzeitig)
|
||||
- Fehler-Tracking (Zwangspause nach 3 Fehlern)
|
||||
- Persistierung der Pause-Zeit über Neustarts
|
||||
|
||||
Thread-Safety:
|
||||
- Alle öffentlichen Methoden sind thread-safe durch Lock
|
||||
"""
|
||||
|
||||
# Konfiguration
|
||||
@ -35,11 +45,15 @@ class ProcessGuard:
|
||||
|
||||
def __init__(self):
|
||||
"""Initialisiert den Process Guard."""
|
||||
# Thread-Safety Lock
|
||||
self._thread_lock = threading.Lock()
|
||||
|
||||
# Process Lock
|
||||
self._is_locked = False
|
||||
self._current_process = None
|
||||
self._current_platform = None
|
||||
self._lock_started_at = None # Timestamp für Auto-Timeout
|
||||
self._lock_id = None # Eindeutige ID für jeden Lock
|
||||
|
||||
# Error Tracking
|
||||
self._failure_count = 0
|
||||
@ -48,6 +62,9 @@ class ProcessGuard:
|
||||
# Config File
|
||||
self._config_file = Path("config/.process_guard")
|
||||
|
||||
# Counter für Lock-IDs
|
||||
self._lock_counter = 0
|
||||
|
||||
def can_start(self, process_type: str, platform: str) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Prüft ob ein Prozess gestartet werden darf.
|
||||
@ -61,98 +78,172 @@ class ProcessGuard:
|
||||
- (True, None) wenn erlaubt
|
||||
- (False, "Fehlermeldung") wenn blockiert
|
||||
"""
|
||||
# 1. Prüfe Zwangspause
|
||||
if self._is_paused():
|
||||
remaining_min = self._get_pause_remaining_minutes()
|
||||
error_msg = (
|
||||
f"⏸ Zwangspause aktiv\n\n"
|
||||
f"Nach 3 fehlgeschlagenen Versuchen ist eine Pause erforderlich.\n"
|
||||
f"Verbleibende Zeit: {remaining_min} Minuten\n\n"
|
||||
f"Empfehlung:\n"
|
||||
f"• Proxy-Einstellungen prüfen\n"
|
||||
f"• Internetverbindung prüfen\n"
|
||||
f"• Plattform-Status überprüfen"
|
||||
)
|
||||
return False, error_msg
|
||||
with self._thread_lock:
|
||||
# 1. Prüfe Zwangspause
|
||||
if self._is_paused():
|
||||
remaining_min = self._get_pause_remaining_minutes()
|
||||
error_msg = (
|
||||
f"⏸ Zwangspause aktiv\n\n"
|
||||
f"Nach 3 fehlgeschlagenen Versuchen ist eine Pause erforderlich.\n"
|
||||
f"Verbleibende Zeit: {remaining_min} Minuten\n\n"
|
||||
f"Empfehlung:\n"
|
||||
f"• Proxy-Einstellungen prüfen\n"
|
||||
f"• Internetverbindung prüfen\n"
|
||||
f"• Plattform-Status überprüfen"
|
||||
)
|
||||
return False, error_msg
|
||||
|
||||
# 2. Prüfe Process Lock
|
||||
if self._is_locked:
|
||||
error_msg = (
|
||||
f"⚠ Prozess läuft bereits\n\n"
|
||||
f"Aktuell aktiv: {self._current_process} ({self._current_platform})\n\n"
|
||||
f"Bitte warten Sie bis der aktuelle Vorgang abgeschlossen ist."
|
||||
)
|
||||
return False, error_msg
|
||||
# 2. Prüfe Process Lock (mit Auto-Timeout-Check)
|
||||
if self._is_locked_with_timeout_check():
|
||||
error_msg = (
|
||||
f"⚠ Prozess läuft bereits\n\n"
|
||||
f"Aktuell aktiv: {self._current_process} ({self._current_platform})\n\n"
|
||||
f"Bitte warten Sie bis der aktuelle Vorgang abgeschlossen ist."
|
||||
)
|
||||
return False, error_msg
|
||||
|
||||
return True, None
|
||||
return True, None
|
||||
|
||||
def start(self, process_type: str, platform: str):
|
||||
def start(self, process_type: str, platform: str) -> int:
|
||||
"""
|
||||
Startet einen Prozess (setzt den Lock).
|
||||
|
||||
Args:
|
||||
process_type: Art des Prozesses
|
||||
platform: Plattform
|
||||
"""
|
||||
self._is_locked = True
|
||||
self._current_process = process_type
|
||||
self._current_platform = platform
|
||||
self._lock_started_at = datetime.now() # Timestamp für Auto-Timeout
|
||||
logger.info(f"Process locked: {process_type} ({platform})")
|
||||
|
||||
def end(self, success: bool):
|
||||
Returns:
|
||||
int: Lock-ID für diesen Prozess (für spätere Freigabe)
|
||||
"""
|
||||
with self._thread_lock:
|
||||
self._lock_counter += 1
|
||||
self._lock_id = self._lock_counter
|
||||
self._is_locked = True
|
||||
self._current_process = process_type
|
||||
self._current_platform = platform
|
||||
self._lock_started_at = datetime.now()
|
||||
logger.info(f"Process locked [ID={self._lock_id}]: {process_type} ({platform})")
|
||||
return self._lock_id
|
||||
|
||||
def end(self, success: bool) -> bool:
|
||||
"""
|
||||
Beendet einen Prozess (gibt den Lock frei).
|
||||
|
||||
Diese Methode ist IDEMPOTENT - mehrfacher Aufruf ist sicher.
|
||||
Der Failure-Counter wird nur erhöht wenn der Lock aktiv war.
|
||||
|
||||
Args:
|
||||
success: War der Prozess erfolgreich?
|
||||
|
||||
Returns:
|
||||
bool: True wenn Lock freigegeben wurde, False wenn kein Lock aktiv war
|
||||
"""
|
||||
# Lock freigeben
|
||||
process_info = f"{self._current_process} ({self._current_platform})"
|
||||
self._is_locked = False
|
||||
self._current_process = None
|
||||
self._current_platform = None
|
||||
self._lock_started_at = None # Timestamp zurücksetzen
|
||||
with self._thread_lock:
|
||||
# IDEMPOTENZ: Prüfe ob Lock überhaupt aktiv ist
|
||||
if not self._is_locked:
|
||||
logger.debug("end() aufgerufen, aber kein Lock aktiv - ignoriere")
|
||||
return False
|
||||
|
||||
# Fehler-Tracking
|
||||
if success:
|
||||
if self._failure_count > 0:
|
||||
logger.info(f"Fehler-Counter zurückgesetzt nach Erfolg (war: {self._failure_count})")
|
||||
self._failure_count = 0
|
||||
self._save_pause_state()
|
||||
else:
|
||||
self._failure_count += 1
|
||||
logger.warning(f"Fehlschlag #{self._failure_count} bei {process_info}")
|
||||
# Lock-Info für Logging speichern
|
||||
process_info = f"{self._current_process} ({self._current_platform})"
|
||||
lock_id = self._lock_id
|
||||
|
||||
if self._failure_count >= self.MAX_FAILURES:
|
||||
self._activate_pause()
|
||||
# Lock freigeben
|
||||
self._is_locked = False
|
||||
self._current_process = None
|
||||
self._current_platform = None
|
||||
self._lock_started_at = None
|
||||
self._lock_id = None
|
||||
|
||||
logger.info(f"Process unlocked: {process_info} (success={success})")
|
||||
# Fehler-Tracking (nur wenn Lock aktiv war)
|
||||
if success:
|
||||
if self._failure_count > 0:
|
||||
logger.info(f"Fehler-Counter zurückgesetzt nach Erfolg (war: {self._failure_count})")
|
||||
self._failure_count = 0
|
||||
self._save_pause_state()
|
||||
else:
|
||||
self._failure_count += 1
|
||||
logger.warning(f"Fehlschlag #{self._failure_count} bei {process_info}")
|
||||
|
||||
if self._failure_count >= self.MAX_FAILURES:
|
||||
self._activate_pause()
|
||||
|
||||
logger.info(f"Process unlocked [ID={lock_id}]: {process_info} (success={success})")
|
||||
return True
|
||||
|
||||
def release(self) -> bool:
|
||||
"""
|
||||
Gibt den Lock frei OHNE den Failure-Counter zu beeinflussen.
|
||||
|
||||
Verwendung:
|
||||
- User-Abbruch (Cancel-Button)
|
||||
- Validierungsfehler VOR Prozessstart
|
||||
- Cleanup bei App-Schließung
|
||||
|
||||
Diese Methode ist IDEMPOTENT - mehrfacher Aufruf ist sicher.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Lock freigegeben wurde, False wenn kein Lock aktiv war
|
||||
"""
|
||||
with self._thread_lock:
|
||||
# IDEMPOTENZ: Prüfe ob Lock überhaupt aktiv ist
|
||||
if not self._is_locked:
|
||||
logger.debug("release() aufgerufen, aber kein Lock aktiv - ignoriere")
|
||||
return False
|
||||
|
||||
# Lock-Info für Logging speichern
|
||||
process_info = f"{self._current_process} ({self._current_platform})"
|
||||
lock_id = self._lock_id
|
||||
|
||||
# Lock freigeben (OHNE Failure-Tracking)
|
||||
self._is_locked = False
|
||||
self._current_process = None
|
||||
self._current_platform = None
|
||||
self._lock_started_at = None
|
||||
self._lock_id = None
|
||||
|
||||
logger.info(f"Process released [ID={lock_id}]: {process_info} (kein Failure gezählt)")
|
||||
return True
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Reset beim App-Start.
|
||||
Lädt Pause-State, resettet aber Lock (da Lock nicht über Neustarts persistiert).
|
||||
"""
|
||||
self._is_locked = False
|
||||
self._current_process = None
|
||||
self._current_platform = None
|
||||
self._lock_started_at = None # Timestamp zurücksetzen
|
||||
self._load_pause_state()
|
||||
with self._thread_lock:
|
||||
self._is_locked = False
|
||||
self._current_process = None
|
||||
self._current_platform = None
|
||||
self._lock_started_at = None
|
||||
self._lock_id = None
|
||||
self._load_pause_state()
|
||||
|
||||
if self._is_paused():
|
||||
remaining = self._get_pause_remaining_minutes()
|
||||
logger.warning(f"Zwangspause aktiv: noch {remaining} Minuten")
|
||||
if self._is_paused():
|
||||
remaining = self._get_pause_remaining_minutes()
|
||||
logger.warning(f"Zwangspause aktiv: noch {remaining} Minuten")
|
||||
|
||||
logger.info("Process Guard initialisiert")
|
||||
logger.info("Process Guard initialisiert")
|
||||
|
||||
def is_locked(self) -> bool:
|
||||
"""
|
||||
Gibt zurück ob aktuell ein Prozess läuft (mit Auto-Timeout-Check).
|
||||
|
||||
Thread-safe Methode.
|
||||
|
||||
Returns:
|
||||
True wenn ein Prozess aktiv ist
|
||||
"""
|
||||
with self._thread_lock:
|
||||
return self._is_locked_with_timeout_check()
|
||||
|
||||
def _is_locked_with_timeout_check(self) -> bool:
|
||||
"""
|
||||
Interne Methode: Prüft Lock-Status mit Auto-Timeout.
|
||||
MUSS innerhalb eines _thread_lock aufgerufen werden!
|
||||
|
||||
Returns:
|
||||
True wenn Lock aktiv ist
|
||||
"""
|
||||
if not self._is_locked:
|
||||
return False
|
||||
|
||||
@ -162,14 +253,15 @@ class ProcessGuard:
|
||||
|
||||
if elapsed_minutes > self.LOCK_TIMEOUT_MINUTES:
|
||||
logger.warning(
|
||||
f"⏰ AUTO-TIMEOUT: Lock nach {int(elapsed_minutes)} Minuten freigegeben. "
|
||||
f"⏰ AUTO-TIMEOUT: Lock [ID={self._lock_id}] nach {int(elapsed_minutes)} Minuten freigegeben. "
|
||||
f"Prozess: {self._current_process} ({self._current_platform})"
|
||||
)
|
||||
# Lock automatisch freigeben
|
||||
# Lock automatisch freigeben (OHNE Failure-Zählung - Timeout ist kein User-Fehler)
|
||||
self._is_locked = False
|
||||
self._current_process = None
|
||||
self._current_platform = None
|
||||
self._lock_started_at = None
|
||||
self._lock_id = None
|
||||
return False
|
||||
|
||||
return True
|
||||
@ -178,26 +270,44 @@ class ProcessGuard:
|
||||
"""
|
||||
Gibt zurück ob Zwangspause aktiv ist.
|
||||
|
||||
Thread-safe Methode.
|
||||
|
||||
Returns:
|
||||
True wenn Pause aktiv ist
|
||||
"""
|
||||
return self._is_paused()
|
||||
with self._thread_lock:
|
||||
return self._is_paused()
|
||||
|
||||
def get_status_message(self) -> Optional[str]:
|
||||
"""
|
||||
Gibt Status-Nachricht zurück wenn blockiert.
|
||||
|
||||
Thread-safe Methode.
|
||||
|
||||
Returns:
|
||||
None wenn nicht blockiert, sonst Nachricht
|
||||
"""
|
||||
if self._is_paused():
|
||||
remaining = self._get_pause_remaining_minutes()
|
||||
return f"Zwangspause aktiv (noch {remaining} Min)"
|
||||
with self._thread_lock:
|
||||
if self._is_paused():
|
||||
remaining = self._get_pause_remaining_minutes()
|
||||
return f"Zwangspause aktiv (noch {remaining} Min)"
|
||||
|
||||
if self._is_locked:
|
||||
return f"'{self._current_process}' läuft"
|
||||
if self._is_locked:
|
||||
return f"'{self._current_process}' läuft"
|
||||
|
||||
return None
|
||||
return None
|
||||
|
||||
def get_failure_count(self) -> int:
|
||||
"""
|
||||
Gibt den aktuellen Failure-Counter zurück.
|
||||
|
||||
Thread-safe Methode.
|
||||
|
||||
Returns:
|
||||
int: Anzahl der Fehlschläge seit letztem Erfolg
|
||||
"""
|
||||
with self._thread_lock:
|
||||
return self._failure_count
|
||||
|
||||
# Private Methoden
|
||||
|
||||
@ -273,18 +383,24 @@ class ProcessGuard:
|
||||
logger.error(f"Fehler beim Laden des Pause-State: {e}")
|
||||
|
||||
|
||||
# Globale Instanz (YAGNI: Kein komplexes Singleton-Pattern nötig)
|
||||
# Globale Instanz mit Thread-Safety
|
||||
_guard_instance = None
|
||||
_guard_instance_lock = threading.Lock()
|
||||
|
||||
|
||||
def get_guard() -> ProcessGuard:
|
||||
"""
|
||||
Gibt die globale ProcessGuard-Instanz zurück.
|
||||
|
||||
Thread-safe Singleton-Pattern.
|
||||
|
||||
Returns:
|
||||
ProcessGuard: Die globale Guard-Instanz
|
||||
"""
|
||||
global _guard_instance
|
||||
if _guard_instance is None:
|
||||
_guard_instance = ProcessGuard()
|
||||
with _guard_instance_lock:
|
||||
# Double-check locking
|
||||
if _guard_instance is None:
|
||||
_guard_instance = ProcessGuard()
|
||||
return _guard_instance
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren