Abuse-Schutz fertig
Dieser Commit ist enthalten in:
@ -20,7 +20,14 @@
|
|||||||
"Bash(python tests:*)",
|
"Bash(python tests:*)",
|
||||||
"Bash(python tests/test_generator_tab_factory.py)",
|
"Bash(python tests/test_generator_tab_factory.py)",
|
||||||
"Bash(git push:*)",
|
"Bash(git push:*)",
|
||||||
"Bash(git remote set-url:*)"
|
"Bash(git remote set-url:*)",
|
||||||
|
"WebSearch",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Bash(mount:*)",
|
||||||
|
"Read(//mnt/a/**)",
|
||||||
|
"Read(//mnt/c/Users/Administrator/AppData/Local/Programs/Python/Python310/**)",
|
||||||
|
"Read(//mnt/c/Users/Administrator/**)",
|
||||||
|
"Bash(/mnt/c/Users/Administrator/AppData/Local/Programs/Python/Python310/python.exe -c \"\nimport sys\nsys.dont_write_bytecode = True # Verhindere .pyc Erstellung\nimport utils.email_handler as eh\nhandler = eh.EmailHandler()\npw = handler.config[''imap_pass'']\nprint(f''Windows Python lädt Passwort: {pw[:4]}...{pw[-4:]}'')\nif ''GZsg'' in pw:\n print(''✅ NEUES Passwort wird geladen!'')\nelse:\n print(''❌ ALTES Passwort wird noch geladen!'')\n\")"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"defaultMode": "acceptEdits",
|
"defaultMode": "acceptEdits",
|
||||||
|
|||||||
@ -558,8 +558,8 @@ if registration_successful:
|
|||||||
## 🛡️ Feature 5: Software Missbrauch Schutz
|
## 🛡️ Feature 5: Software Missbrauch Schutz
|
||||||
|
|
||||||
**Priorität:** Hoch
|
**Priorität:** Hoch
|
||||||
**Status:** Geplant
|
**Status:** ✅ Abgeschlossen (2025-11-10)
|
||||||
**Geschätzter Aufwand:** 2-3 Tage
|
**Tatsächlicher Aufwand:** 2 Tage
|
||||||
|
|
||||||
### Beschreibung
|
### Beschreibung
|
||||||
Schutzmaßnahmen gegen versehentlichen oder absichtlichen Missbrauch der Software durch parallele Prozesse, zu viele Browser-Instanzen oder wiederholte Fehlversuche.
|
Schutzmaßnahmen gegen versehentlichen oder absichtlichen Missbrauch der Software durch parallele Prozesse, zu viele Browser-Instanzen oder wiederholte Fehlversuche.
|
||||||
|
|||||||
@ -22,6 +22,9 @@ class PlaywrightManager:
|
|||||||
Verwaltet Browser-Sitzungen mit Playwright, einschließlich Stealth-Modus und Proxy-Einstellungen.
|
Verwaltet Browser-Sitzungen mit Playwright, einschließlich Stealth-Modus und Proxy-Einstellungen.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Klassen-Variable: Zählt aktive Browser-Instanzen (Feature 5: Browser-Instanz Schutz)
|
||||||
|
_active_count = 0
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
headless: bool = False,
|
headless: bool = False,
|
||||||
proxy: Optional[Dict[str, str]] = None,
|
proxy: Optional[Dict[str, str]] = None,
|
||||||
@ -131,10 +134,20 @@ class PlaywrightManager:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Page: Die Browser-Seite
|
Page: Die Browser-Seite
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: Wenn bereits eine Browser-Instanz aktiv ist
|
||||||
"""
|
"""
|
||||||
if self.page is not None:
|
if self.page is not None:
|
||||||
return self.page
|
return self.page
|
||||||
|
|
||||||
|
# Feature 5: Browser-Instanz Schutz - Nur eine Instanz gleichzeitig
|
||||||
|
if PlaywrightManager._active_count >= 1:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Browser bereits aktiv. Nur eine Browser-Instanz gleichzeitig erlaubt. "
|
||||||
|
"Beenden Sie den aktuellen Prozess."
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.playwright = sync_playwright().start()
|
self.playwright = sync_playwright().start()
|
||||||
|
|
||||||
@ -244,6 +257,10 @@ class PlaywrightManager:
|
|||||||
# Event-Listener für Konsolen-Logs
|
# Event-Listener für Konsolen-Logs
|
||||||
self.page.on("console", lambda msg: logger.debug(f"BROWSER CONSOLE: {msg.text}"))
|
self.page.on("console", lambda msg: logger.debug(f"BROWSER CONSOLE: {msg.text}"))
|
||||||
|
|
||||||
|
# Feature 5: Browser-Instanz Counter erhöhen
|
||||||
|
PlaywrightManager._active_count += 1
|
||||||
|
logger.info(f"Browser gestartet (aktive Instanzen: {PlaywrightManager._active_count})")
|
||||||
|
|
||||||
return self.page
|
return self.page
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -994,6 +1011,11 @@ class PlaywrightManager:
|
|||||||
logger.error(f"Force stop fehlgeschlagen: {e2}")
|
logger.error(f"Force stop fehlgeschlagen: {e2}")
|
||||||
self.playwright = None
|
self.playwright = None
|
||||||
|
|
||||||
|
# Feature 5: Browser-Instanz Counter dekrementieren
|
||||||
|
if PlaywrightManager._active_count > 0:
|
||||||
|
PlaywrightManager._active_count -= 1
|
||||||
|
logger.info(f"Browser geschlossen (aktive Instanzen: {PlaywrightManager._active_count})")
|
||||||
|
|
||||||
logger.info("Browser-Sitzung erfolgreich geschlossen")
|
logger.info("Browser-Sitzung erfolgreich geschlossen")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -2,5 +2,5 @@
|
|||||||
"imap_server": "imap.ionos.de",
|
"imap_server": "imap.ionos.de",
|
||||||
"imap_port": 993,
|
"imap_port": 993,
|
||||||
"imap_user": "info@z5m7q9dk3ah2v1plx6ju.com",
|
"imap_user": "info@z5m7q9dk3ah2v1plx6ju.com",
|
||||||
"imap_pass": "cz&ie.O9$!:!tYY@"
|
"imap_pass": "GZsg9:66@a@M%etP"
|
||||||
}
|
}
|
||||||
@ -112,6 +112,20 @@ class BasePlatformController(QObject):
|
|||||||
Args:
|
Args:
|
||||||
params: Parameter für die Account-Erstellung
|
params: Parameter für die Account-Erstellung
|
||||||
"""
|
"""
|
||||||
|
# Feature 5: Process Guard - Prüfe ob Prozess starten darf
|
||||||
|
from utils.process_guard import get_guard
|
||||||
|
guard = get_guard()
|
||||||
|
|
||||||
|
can_start, error_msg = guard.can_start("Account-Erstellung", self.platform_name)
|
||||||
|
if not can_start:
|
||||||
|
# Zeige Fehlermeldung
|
||||||
|
generator_tab = self.get_generator_tab()
|
||||||
|
generator_tab.show_error(error_msg)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Guard Lock setzen
|
||||||
|
guard.start("Account-Erstellung", self.platform_name)
|
||||||
|
|
||||||
self.logger.info(f"Account-Erstellung für {self.platform_name} gestartet")
|
self.logger.info(f"Account-Erstellung für {self.platform_name} gestartet")
|
||||||
# In Unterklassen implementieren
|
# In Unterklassen implementieren
|
||||||
|
|
||||||
|
|||||||
@ -48,6 +48,9 @@ class BaseAccountCreationWorkerThread(QThread):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Gemeinsame Logik für Account-Erstellung - IDENTISCH zum Original"""
|
"""Gemeinsame Logik für Account-Erstellung - IDENTISCH zum Original"""
|
||||||
|
# Feature 5: Tracke Erfolg für Process Guard
|
||||||
|
success = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.update_signal.emit("Status: Initialisierung...")
|
self.update_signal.emit("Status: Initialisierung...")
|
||||||
self.log_signal.emit(f"{self.platform_name}-Account-Erstellung gestartet...")
|
self.log_signal.emit(f"{self.platform_name}-Account-Erstellung gestartet...")
|
||||||
@ -185,6 +188,9 @@ class BaseAccountCreationWorkerThread(QThread):
|
|||||||
if save_result is not None:
|
if save_result is not None:
|
||||||
result["save_result"] = save_result
|
result["save_result"] = save_result
|
||||||
|
|
||||||
|
# Feature 5: Markiere als erfolgreich für Process Guard
|
||||||
|
success = True
|
||||||
|
|
||||||
self.finished_signal.emit(result)
|
self.finished_signal.emit(result)
|
||||||
else:
|
else:
|
||||||
error_msg = result.get("error", "Unbekannter Fehler")
|
error_msg = result.get("error", "Unbekannter Fehler")
|
||||||
@ -202,6 +208,12 @@ class BaseAccountCreationWorkerThread(QThread):
|
|||||||
self.error_signal.emit(interpreted_error)
|
self.error_signal.emit(interpreted_error)
|
||||||
self.progress_signal.emit(0) # Reset progress on error
|
self.progress_signal.emit(0) # Reset progress on error
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Feature 5: Process Guard freigeben
|
||||||
|
from utils.process_guard import get_guard
|
||||||
|
guard = get_guard()
|
||||||
|
guard.end(success)
|
||||||
|
|
||||||
def _interpret_error(self, error_message: str) -> str:
|
def _interpret_error(self, error_message: str) -> str:
|
||||||
"""Interpretiert Fehler mit Fuzzy-Matching"""
|
"""Interpretiert Fehler mit Fuzzy-Matching"""
|
||||||
error_lower = error_message.lower()
|
error_lower = error_message.lower()
|
||||||
|
|||||||
@ -51,6 +51,21 @@ class SessionController(QObject):
|
|||||||
platform = account_data.get("platform", "")
|
platform = account_data.get("platform", "")
|
||||||
username = account_data.get("username", "")
|
username = account_data.get("username", "")
|
||||||
logger.info(f"Ein-Klick-Login für Account {username} (ID: {account_id}) auf {platform}")
|
logger.info(f"Ein-Klick-Login für Account {username} (ID: {account_id}) auf {platform}")
|
||||||
|
|
||||||
|
# Feature 5: Process Guard - Prüfe ob Login starten darf
|
||||||
|
from utils.process_guard import get_guard
|
||||||
|
guard = get_guard()
|
||||||
|
|
||||||
|
can_start, error_msg = guard.can_start("Account-Login", platform)
|
||||||
|
if not can_start:
|
||||||
|
logger.warning(f"Login blockiert durch Guard: {error_msg}")
|
||||||
|
self.login_failed.emit(account_id, error_msg)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Guard Lock setzen
|
||||||
|
guard.start("Account-Login", platform)
|
||||||
|
logger.info(f"Guard locked für Login: {platform}")
|
||||||
|
|
||||||
self.login_started.emit(account_id)
|
self.login_started.emit(account_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -103,19 +118,39 @@ class SessionController(QObject):
|
|||||||
else:
|
else:
|
||||||
error_msg = f"Account mit ID {account_id} nicht gefunden"
|
error_msg = f"Account mit ID {account_id} nicht gefunden"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
|
# Feature 5: Guard freigeben da Worker nicht gestartet wird
|
||||||
|
guard.end(success=False)
|
||||||
self.login_failed.emit(account_id, error_msg)
|
self.login_failed.emit(account_id, error_msg)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Fehler beim Ein-Klick-Login: {e}")
|
logger.error(f"Fehler beim Ein-Klick-Login: {e}")
|
||||||
|
# Feature 5: Guard freigeben bei Fehler vor Worker-Start
|
||||||
|
guard.end(success=False)
|
||||||
self.login_failed.emit(account_id, str(e))
|
self.login_failed.emit(account_id, str(e))
|
||||||
|
|
||||||
def _cancel_login(self, account_id: str):
|
def _cancel_login(self, account_id: str):
|
||||||
"""Bricht den Login-Prozess ab"""
|
"""Bricht den Login-Prozess ab"""
|
||||||
logger.info(f"Login für Account {account_id} wurde abgebrochen")
|
logger.info(f"Login für Account {account_id} wurde abgebrochen")
|
||||||
|
|
||||||
|
# Feature 5: Guard freigeben bei Cancel
|
||||||
|
# HINWEIS: Worker Thread gibt Guard in seinem finally-Block frei
|
||||||
|
# Nur freigeben wenn Worker noch nicht gestartet (Guard locked aber kein Worker)
|
||||||
|
from utils.process_guard import get_guard
|
||||||
|
guard = get_guard()
|
||||||
|
if guard.is_locked() and not (hasattr(self, 'login_worker') and self.login_worker and self.login_worker.isRunning()):
|
||||||
|
logger.warning("Guard war locked aber Worker nicht aktiv - gebe Guard frei")
|
||||||
|
guard.end(success=False)
|
||||||
|
|
||||||
if hasattr(self, 'login_dialog') and self.login_dialog:
|
if hasattr(self, 'login_dialog') and self.login_dialog:
|
||||||
self.login_dialog.close()
|
self.login_dialog.close()
|
||||||
self.login_dialog = None
|
self.login_dialog = None
|
||||||
# TODO: Login-Worker stoppen falls vorhanden
|
|
||||||
|
# Login-Worker stoppen falls vorhanden
|
||||||
|
if hasattr(self, 'login_worker') and self.login_worker:
|
||||||
|
if self.login_worker.isRunning():
|
||||||
|
logger.info("Stoppe laufenden Login-Worker")
|
||||||
|
self.login_worker.terminate()
|
||||||
|
self.login_worker.wait(2000) # Warte max 2 Sekunden
|
||||||
|
|
||||||
def create_and_save_account(self, platform: str, account_data: Dict[str, Any]):
|
def create_and_save_account(self, platform: str, account_data: Dict[str, Any]):
|
||||||
"""
|
"""
|
||||||
@ -220,6 +255,9 @@ class SessionController(QObject):
|
|||||||
self.automation = automation # Verwende bereitgestellte Automation oder erstelle neue
|
self.automation = automation # Verwende bereitgestellte Automation oder erstelle neue
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
# Feature 5: Tracke Erfolg für Process Guard
|
||||||
|
success = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Verwende bereitgestellte Automation oder erstelle neue
|
# Verwende bereitgestellte Automation oder erstelle neue
|
||||||
if not self.automation:
|
if not self.automation:
|
||||||
@ -298,6 +336,8 @@ class SessionController(QObject):
|
|||||||
if result['success']:
|
if result['success']:
|
||||||
# Session-Speicherung komplett entfernt - nur Login-Erfolg melden
|
# Session-Speicherung komplett entfernt - nur Login-Erfolg melden
|
||||||
logger.info(f"Login erfolgreich für Account {self.account_id} - Session-Speicherung deaktiviert")
|
logger.info(f"Login erfolgreich für Account {self.account_id} - Session-Speicherung deaktiviert")
|
||||||
|
# Feature 5: Markiere als erfolgreich für Process Guard
|
||||||
|
success = True
|
||||||
self.login_completed.emit(self.account_id, result)
|
self.login_completed.emit(self.account_id, result)
|
||||||
else:
|
else:
|
||||||
self.login_failed.emit(self.account_id, result.get('error', 'Login fehlgeschlagen'))
|
self.login_failed.emit(self.account_id, result.get('error', 'Login fehlgeschlagen'))
|
||||||
@ -306,6 +346,13 @@ class SessionController(QObject):
|
|||||||
logger.error(f"Fehler beim normalen Login: {e}")
|
logger.error(f"Fehler beim normalen Login: {e}")
|
||||||
self.login_failed.emit(self.account_id, str(e))
|
self.login_failed.emit(self.account_id, str(e))
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Feature 5: Process Guard freigeben
|
||||||
|
from utils.process_guard import get_guard
|
||||||
|
guard = get_guard()
|
||||||
|
guard.end(success)
|
||||||
|
logger.info(f"Guard freigegeben nach Login (success={success})")
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
"""Browser NICHT schließen - User soll Kontrolle behalten"""
|
"""Browser NICHT schließen - User soll Kontrolle behalten"""
|
||||||
logger.info(f"Browser für Account {self.account_id} bleibt offen (User-Kontrolle)")
|
logger.info(f"Browser für Account {self.account_id} bleibt offen (User-Kontrolle)")
|
||||||
|
|||||||
Binäre Datei nicht angezeigt.
5
main.py
5
main.py
@ -30,6 +30,11 @@ def main():
|
|||||||
logger.info("Anwendung wird gestartet...")
|
logger.info("Anwendung wird gestartet...")
|
||||||
print("\n=== AccountForger wird gestartet ===")
|
print("\n=== AccountForger wird gestartet ===")
|
||||||
|
|
||||||
|
# Feature 5: Process Guard initialisieren
|
||||||
|
from utils.process_guard import get_guard
|
||||||
|
get_guard().reset()
|
||||||
|
logger.info("Process Guard initialisiert")
|
||||||
|
|
||||||
# QApplication erstellen
|
# QApplication erstellen
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
|||||||
@ -106,7 +106,7 @@ class EmailHandler:
|
|||||||
"imap_server": "imap.ionos.de",
|
"imap_server": "imap.ionos.de",
|
||||||
"imap_port": 993,
|
"imap_port": 993,
|
||||||
"imap_user": "info@z5m7q9dk3ah2v1plx6ju.com",
|
"imap_user": "info@z5m7q9dk3ah2v1plx6ju.com",
|
||||||
"imap_pass": "cz&ie.O9$!:!tYY@"
|
"imap_pass": "GZsg9:66@a@M%etP"
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -226,6 +226,9 @@ class EmailHandler:
|
|||||||
try:
|
try:
|
||||||
logger.info(f"Teste Verbindung zu {self.config['imap_server']}:{self.config['imap_port']}")
|
logger.info(f"Teste Verbindung zu {self.config['imap_server']}:{self.config['imap_port']}")
|
||||||
|
|
||||||
|
# DEBUG: Zeige geladene Config
|
||||||
|
logger.info(f"IMAP Config geladen: Server={self.config['imap_server']}, Port={self.config['imap_port']}, User={self.config['imap_user']}, Pass={self.config['imap_pass'][:4]}...{self.config['imap_pass'][-4:]}")
|
||||||
|
|
||||||
# SSL-Verbindung zum IMAP-Server herstellen
|
# SSL-Verbindung zum IMAP-Server herstellen
|
||||||
mail = imaplib.IMAP4_SSL(self.config["imap_server"], self.config["imap_port"])
|
mail = imaplib.IMAP4_SSL(self.config["imap_server"], self.config["imap_port"])
|
||||||
|
|
||||||
@ -292,6 +295,10 @@ class EmailHandler:
|
|||||||
List[Dict[str, Any]]: Liste der gefundenen E-Mails
|
List[Dict[str, Any]]: Liste der gefundenen E-Mails
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# DEBUG: Zeige Login-Daten SOFORT (ohne komplettes Passwort)
|
||||||
|
print(f"[EMAIL-DEBUG] IMAP Login-Versuch: Server={self.config['imap_server']}, Port={self.config['imap_port']}, User={self.config['imap_user']}, Pass={self.config['imap_pass'][:4]}...{self.config['imap_pass'][-4:]}")
|
||||||
|
logger.info(f"IMAP Login-Versuch: Server={self.config['imap_server']}, Port={self.config['imap_port']}, User={self.config['imap_user']}, Pass={self.config['imap_pass'][:4]}...{self.config['imap_pass'][-4:]}")
|
||||||
|
|
||||||
# Verbindung zum IMAP-Server herstellen
|
# Verbindung zum IMAP-Server herstellen
|
||||||
mail = imaplib.IMAP4_SSL(self.config["imap_server"], self.config["imap_port"])
|
mail = imaplib.IMAP4_SSL(self.config["imap_server"], self.config["imap_port"])
|
||||||
|
|
||||||
|
|||||||
266
utils/process_guard.py
Normale Datei
266
utils/process_guard.py
Normale Datei
@ -0,0 +1,266 @@
|
|||||||
|
"""
|
||||||
|
Process Guard - Schützt vor parallelen Prozessen und Fehler-Spam.
|
||||||
|
|
||||||
|
Dieser Guard verhindert:
|
||||||
|
- Parallele Prozesse (nur ein Vorgang gleichzeitig)
|
||||||
|
- Zu viele Fehlversuche (Zwangspause nach 3 Fehlern)
|
||||||
|
- Mehrere Browser-Instanzen gleichzeitig
|
||||||
|
|
||||||
|
Clean Code & YAGNI: Nur das Nötigste, keine Über-Engineering.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessGuard:
|
||||||
|
"""
|
||||||
|
Einfacher Guard für Prozess-Locks und Fehler-Tracking.
|
||||||
|
|
||||||
|
Verantwortlichkeiten:
|
||||||
|
- Process Lock Management (nur ein Prozess gleichzeitig)
|
||||||
|
- Fehler-Tracking (Zwangspause nach 3 Fehlern)
|
||||||
|
- Persistierung der Pause-Zeit über Neustarts
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Konfiguration
|
||||||
|
MAX_FAILURES = 3
|
||||||
|
PAUSE_DURATION_HOURS = 1
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialisiert den Process Guard."""
|
||||||
|
# Process Lock
|
||||||
|
self._is_locked = False
|
||||||
|
self._current_process = None
|
||||||
|
self._current_platform = None
|
||||||
|
|
||||||
|
# Error Tracking
|
||||||
|
self._failure_count = 0
|
||||||
|
self._pause_until = None
|
||||||
|
|
||||||
|
# Config File
|
||||||
|
self._config_file = Path("config/.process_guard")
|
||||||
|
|
||||||
|
def can_start(self, process_type: str, platform: str) -> Tuple[bool, Optional[str]]:
|
||||||
|
"""
|
||||||
|
Prüft ob ein Prozess gestartet werden darf.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
process_type: Art des Prozesses (z.B. "Account-Erstellung", "Login")
|
||||||
|
platform: Plattform (z.B. "Instagram", "Facebook")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(erlaubt: bool, fehler_nachricht: Optional[str])
|
||||||
|
- (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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
def start(self, process_type: str, platform: str):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
logger.info(f"Process locked: {process_type} ({platform})")
|
||||||
|
|
||||||
|
def end(self, success: bool):
|
||||||
|
"""
|
||||||
|
Beendet einen Prozess (gibt den Lock frei).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
success: War der Prozess erfolgreich?
|
||||||
|
"""
|
||||||
|
# Lock freigeben
|
||||||
|
process_info = f"{self._current_process} ({self._current_platform})"
|
||||||
|
self._is_locked = False
|
||||||
|
self._current_process = None
|
||||||
|
self._current_platform = None
|
||||||
|
|
||||||
|
# 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}")
|
||||||
|
|
||||||
|
if self._failure_count >= self.MAX_FAILURES:
|
||||||
|
self._activate_pause()
|
||||||
|
|
||||||
|
logger.info(f"Process unlocked: {process_info} (success={success})")
|
||||||
|
|
||||||
|
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._load_pause_state()
|
||||||
|
|
||||||
|
if self._is_paused():
|
||||||
|
remaining = self._get_pause_remaining_minutes()
|
||||||
|
logger.warning(f"Zwangspause aktiv: noch {remaining} Minuten")
|
||||||
|
|
||||||
|
logger.info("Process Guard initialisiert")
|
||||||
|
|
||||||
|
def is_locked(self) -> bool:
|
||||||
|
"""
|
||||||
|
Gibt zurück ob aktuell ein Prozess läuft.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True wenn ein Prozess aktiv ist
|
||||||
|
"""
|
||||||
|
return self._is_locked
|
||||||
|
|
||||||
|
def is_paused(self) -> bool:
|
||||||
|
"""
|
||||||
|
Gibt zurück ob Zwangspause aktiv ist.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True wenn Pause aktiv ist
|
||||||
|
"""
|
||||||
|
return self._is_paused()
|
||||||
|
|
||||||
|
def get_status_message(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Gibt Status-Nachricht zurück wenn blockiert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None wenn nicht blockiert, sonst Nachricht
|
||||||
|
"""
|
||||||
|
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"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Private Methoden
|
||||||
|
|
||||||
|
def _is_paused(self) -> bool:
|
||||||
|
"""Prüft ob Zwangspause aktiv ist."""
|
||||||
|
if not self._pause_until:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Prüfe ob Pause abgelaufen
|
||||||
|
if datetime.now() >= self._pause_until:
|
||||||
|
logger.info("Zwangspause ist abgelaufen")
|
||||||
|
self._pause_until = None
|
||||||
|
self._failure_count = 0
|
||||||
|
self._save_pause_state()
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _activate_pause(self):
|
||||||
|
"""Aktiviert Zwangspause."""
|
||||||
|
self._pause_until = datetime.now() + timedelta(hours=self.PAUSE_DURATION_HOURS)
|
||||||
|
self._save_pause_state()
|
||||||
|
|
||||||
|
pause_until_str = self._pause_until.strftime('%H:%M')
|
||||||
|
logger.error(
|
||||||
|
f"⏸ ZWANGSPAUSE AKTIVIERT bis {pause_until_str} "
|
||||||
|
f"nach {self.MAX_FAILURES} Fehlschlägen"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_pause_remaining_minutes(self) -> int:
|
||||||
|
"""Gibt verbleibende Minuten der Pause zurück."""
|
||||||
|
if not self._pause_until:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
remaining_seconds = (self._pause_until - datetime.now()).total_seconds()
|
||||||
|
remaining_minutes = max(0, int(remaining_seconds / 60))
|
||||||
|
return remaining_minutes
|
||||||
|
|
||||||
|
def _save_pause_state(self):
|
||||||
|
"""Speichert Pause-State in Datei (nur wenn nötig)."""
|
||||||
|
try:
|
||||||
|
# Erstelle config-Verzeichnis falls nicht vorhanden
|
||||||
|
self._config_file.parent.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'pause_until': self._pause_until.isoformat() if self._pause_until else None,
|
||||||
|
'failure_count': self._failure_count,
|
||||||
|
'last_update': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
self._config_file.write_text(json.dumps(data, indent=2))
|
||||||
|
logger.debug(f"Pause-State gespeichert: {data}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Speichern des Pause-State: {e}")
|
||||||
|
|
||||||
|
def _load_pause_state(self):
|
||||||
|
"""Lädt Pause-State aus Datei."""
|
||||||
|
if not self._config_file.exists():
|
||||||
|
logger.debug("Keine gespeicherte Pause-State gefunden")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(self._config_file.read_text())
|
||||||
|
|
||||||
|
# Lade Pause-Zeit
|
||||||
|
if data.get('pause_until'):
|
||||||
|
self._pause_until = datetime.fromisoformat(data['pause_until'])
|
||||||
|
self._failure_count = data.get('failure_count', 0)
|
||||||
|
logger.info(f"Pause-State geladen: Pause bis {self._pause_until}, Failures: {self._failure_count}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Laden des Pause-State: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# Globale Instanz (YAGNI: Kein komplexes Singleton-Pattern nötig)
|
||||||
|
_guard_instance = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_guard() -> ProcessGuard:
|
||||||
|
"""
|
||||||
|
Gibt die globale ProcessGuard-Instanz zurück.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ProcessGuard: Die globale Guard-Instanz
|
||||||
|
"""
|
||||||
|
global _guard_instance
|
||||||
|
if _guard_instance is None:
|
||||||
|
_guard_instance = ProcessGuard()
|
||||||
|
return _guard_instance
|
||||||
@ -11,7 +11,7 @@ from PyQt5.QtWidgets import (
|
|||||||
QCheckBox, QComboBox, QPushButton, QProgressBar,
|
QCheckBox, QComboBox, QPushButton, QProgressBar,
|
||||||
QMessageBox, QSizePolicy, QSpacerItem
|
QMessageBox, QSizePolicy, QSpacerItem
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt, pyqtSignal
|
from PyQt5.QtCore import Qt, pyqtSignal, QTimer
|
||||||
|
|
||||||
logger = logging.getLogger("generator_tab")
|
logger = logging.getLogger("generator_tab")
|
||||||
|
|
||||||
@ -34,6 +34,14 @@ class GeneratorTab(QWidget):
|
|||||||
self.language_manager.language_changed.connect(self.update_texts)
|
self.language_manager.language_changed.connect(self.update_texts)
|
||||||
self.update_texts()
|
self.update_texts()
|
||||||
|
|
||||||
|
# Feature 5: Timer für periodische Guard-Status-Prüfung
|
||||||
|
self.guard_check_timer = QTimer()
|
||||||
|
self.guard_check_timer.timeout.connect(self.update_button_states)
|
||||||
|
self.guard_check_timer.start(2000) # Alle 2 Sekunden prüfen
|
||||||
|
|
||||||
|
# Feature 5: Initiale Button-Status-Prüfung nach UI-Initialisierung
|
||||||
|
self.update_button_states()
|
||||||
|
|
||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
"""Initialisiert die Benutzeroberfläche."""
|
"""Initialisiert die Benutzeroberfläche."""
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
@ -114,6 +122,15 @@ class GeneratorTab(QWidget):
|
|||||||
|
|
||||||
def on_start_clicked(self):
|
def on_start_clicked(self):
|
||||||
"""Wird aufgerufen, wenn der Start-Button geklickt wird."""
|
"""Wird aufgerufen, wenn der Start-Button geklickt wird."""
|
||||||
|
# Feature 5: Prüfe Guard-Status vor Start
|
||||||
|
from utils.process_guard import get_guard
|
||||||
|
guard = get_guard()
|
||||||
|
|
||||||
|
can_start, error_msg = guard.can_start("Account-Erstellung", "Unknown")
|
||||||
|
if not can_start:
|
||||||
|
self.show_error(error_msg)
|
||||||
|
return
|
||||||
|
|
||||||
# Parameter sammeln
|
# Parameter sammeln
|
||||||
params = self.get_params()
|
params = self.get_params()
|
||||||
|
|
||||||
@ -237,8 +254,14 @@ class GeneratorTab(QWidget):
|
|||||||
|
|
||||||
def set_running(self, running: bool):
|
def set_running(self, running: bool):
|
||||||
"""Setzt den Status auf 'Wird ausgeführt' oder 'Bereit'."""
|
"""Setzt den Status auf 'Wird ausgeführt' oder 'Bereit'."""
|
||||||
self.start_button.setEnabled(not running)
|
# Feature 5: Prüfe Guard-Status bevor Buttons aktiviert werden
|
||||||
self.stop_button.setEnabled(running)
|
if not running:
|
||||||
|
# Prozess ist beendet, prüfe ob Guard erlaubt
|
||||||
|
self.update_button_states()
|
||||||
|
else:
|
||||||
|
# Prozess läuft
|
||||||
|
self.start_button.setEnabled(False)
|
||||||
|
self.stop_button.setEnabled(True)
|
||||||
|
|
||||||
if running:
|
if running:
|
||||||
self.set_status("Läuft...")
|
self.set_status("Läuft...")
|
||||||
@ -246,6 +269,38 @@ class GeneratorTab(QWidget):
|
|||||||
self.progress_bar.setValue(0)
|
self.progress_bar.setValue(0)
|
||||||
self.progress_bar.hide()
|
self.progress_bar.hide()
|
||||||
|
|
||||||
|
def update_button_states(self):
|
||||||
|
"""
|
||||||
|
Feature 5: Aktualisiert Button-Status basierend auf Process Guard.
|
||||||
|
Deaktiviert Buttons wenn Guard locked ist oder Pause aktiv ist.
|
||||||
|
"""
|
||||||
|
from utils.process_guard import get_guard
|
||||||
|
guard = get_guard()
|
||||||
|
|
||||||
|
# Prüfe ob blockiert
|
||||||
|
is_locked = guard.is_locked()
|
||||||
|
is_paused = guard.is_paused()
|
||||||
|
is_blocked = is_locked or is_paused
|
||||||
|
|
||||||
|
# Buttons entsprechend setzen
|
||||||
|
self.start_button.setEnabled(not is_blocked)
|
||||||
|
self.stop_button.setEnabled(False) # Stop nur wenn running
|
||||||
|
|
||||||
|
# Tooltip setzen
|
||||||
|
if is_blocked:
|
||||||
|
status_msg = guard.get_status_message()
|
||||||
|
self.start_button.setToolTip(f"⚠ {status_msg}")
|
||||||
|
else:
|
||||||
|
# Reset Tooltip
|
||||||
|
if self.language_manager:
|
||||||
|
tooltip = self.language_manager.get_text(
|
||||||
|
"buttons.create",
|
||||||
|
"Account erstellen"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
tooltip = "Account erstellen"
|
||||||
|
self.start_button.setToolTip(tooltip)
|
||||||
|
|
||||||
def set_progress(self, value: int):
|
def set_progress(self, value: int):
|
||||||
"""Setzt den Fortschritt der Fortschrittsanzeige."""
|
"""Setzt den Fortschritt der Fortschrittsanzeige."""
|
||||||
if value > 0:
|
if value > 0:
|
||||||
|
|||||||
@ -35,6 +35,11 @@ class AccountCard(QFrame):
|
|||||||
self.password_copy_timer = QTimer()
|
self.password_copy_timer = QTimer()
|
||||||
self.password_copy_timer.timeout.connect(self._restore_password_copy_icon)
|
self.password_copy_timer.timeout.connect(self._restore_password_copy_icon)
|
||||||
|
|
||||||
|
# Feature 5: Timer für periodische Guard-Status-Prüfung
|
||||||
|
self.guard_check_timer = QTimer()
|
||||||
|
self.guard_check_timer.timeout.connect(self.update_login_button_state)
|
||||||
|
self.guard_check_timer.start(2000) # Alle 2 Sekunden prüfen
|
||||||
|
|
||||||
# Original Icons speichern
|
# Original Icons speichern
|
||||||
self.copy_icon = None
|
self.copy_icon = None
|
||||||
self.check_icon = None
|
self.check_icon = None
|
||||||
@ -45,10 +50,60 @@ class AccountCard(QFrame):
|
|||||||
self.language_manager.language_changed.connect(self.update_texts)
|
self.language_manager.language_changed.connect(self.update_texts)
|
||||||
self.update_texts()
|
self.update_texts()
|
||||||
|
|
||||||
|
# Feature 5: Initiale Button-Status-Prüfung
|
||||||
|
self.update_login_button_state()
|
||||||
|
|
||||||
def _on_login_clicked(self):
|
def _on_login_clicked(self):
|
||||||
"""Handler für Login-Button"""
|
"""Handler für Login-Button"""
|
||||||
|
# Feature 5: Prüfe Guard-Status vor Login
|
||||||
|
from utils.process_guard import get_guard
|
||||||
|
guard = get_guard()
|
||||||
|
|
||||||
|
platform = self.account_data.get("platform", "Unknown")
|
||||||
|
can_start, error_msg = guard.can_start("Account-Login", platform)
|
||||||
|
|
||||||
|
if not can_start:
|
||||||
|
# Zeige benutzerfreundliche Fehlermeldung
|
||||||
|
from views.widgets.modern_message_box import show_error
|
||||||
|
show_error(self, "Login blockiert", error_msg)
|
||||||
|
return
|
||||||
|
|
||||||
self.login_requested.emit(self.account_data)
|
self.login_requested.emit(self.account_data)
|
||||||
|
|
||||||
|
def _on_export_clicked(self):
|
||||||
|
"""Handler für Export-Button"""
|
||||||
|
# Feature 5: Prüfe Guard-Status vor Export
|
||||||
|
from utils.process_guard import get_guard
|
||||||
|
guard = get_guard()
|
||||||
|
|
||||||
|
platform = self.account_data.get("platform", "Unknown")
|
||||||
|
can_start, error_msg = guard.can_start("Profil-Export", platform)
|
||||||
|
|
||||||
|
if not can_start:
|
||||||
|
# Zeige benutzerfreundliche Fehlermeldung
|
||||||
|
from views.widgets.modern_message_box import show_error
|
||||||
|
show_error(self, "Export blockiert", error_msg)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.export_requested.emit(self.account_data)
|
||||||
|
|
||||||
|
def _on_delete_clicked(self):
|
||||||
|
"""Handler für Delete-Button"""
|
||||||
|
# Feature 5: Prüfe Guard-Status vor Löschen
|
||||||
|
from utils.process_guard import get_guard
|
||||||
|
guard = get_guard()
|
||||||
|
|
||||||
|
platform = self.account_data.get("platform", "Unknown")
|
||||||
|
can_start, error_msg = guard.can_start("Account-Löschen", platform)
|
||||||
|
|
||||||
|
if not can_start:
|
||||||
|
# Zeige benutzerfreundliche Fehlermeldung
|
||||||
|
from views.widgets.modern_message_box import show_error
|
||||||
|
show_error(self, "Löschen blockiert", error_msg)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.delete_requested.emit(self.account_data)
|
||||||
|
|
||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
"""Initialisiert die UI nach Styleguide"""
|
"""Initialisiert die UI nach Styleguide"""
|
||||||
self.setObjectName("accountCard")
|
self.setObjectName("accountCard")
|
||||||
@ -191,7 +246,7 @@ class AccountCard(QFrame):
|
|||||||
self.export_btn = QPushButton("Profil\nexportieren")
|
self.export_btn = QPushButton("Profil\nexportieren")
|
||||||
self.export_btn.setCursor(Qt.PointingHandCursor)
|
self.export_btn.setCursor(Qt.PointingHandCursor)
|
||||||
self.export_btn.setObjectName("account_export_btn") # For QSS targeting
|
self.export_btn.setObjectName("account_export_btn") # For QSS targeting
|
||||||
self.export_btn.clicked.connect(lambda: self.export_requested.emit(self.account_data))
|
self.export_btn.clicked.connect(self._on_export_clicked)
|
||||||
actions_layout.addWidget(self.export_btn)
|
actions_layout.addWidget(self.export_btn)
|
||||||
|
|
||||||
actions_layout.addStretch()
|
actions_layout.addStretch()
|
||||||
@ -200,7 +255,7 @@ class AccountCard(QFrame):
|
|||||||
self.delete_btn = QPushButton("Löschen")
|
self.delete_btn = QPushButton("Löschen")
|
||||||
self.delete_btn.setCursor(Qt.PointingHandCursor)
|
self.delete_btn.setCursor(Qt.PointingHandCursor)
|
||||||
self.delete_btn.setObjectName("account_delete_btn") # For QSS targeting
|
self.delete_btn.setObjectName("account_delete_btn") # For QSS targeting
|
||||||
self.delete_btn.clicked.connect(lambda: self.delete_requested.emit(self.account_data))
|
self.delete_btn.clicked.connect(self._on_delete_clicked)
|
||||||
actions_layout.addWidget(self.delete_btn)
|
actions_layout.addWidget(self.delete_btn)
|
||||||
|
|
||||||
layout.addLayout(actions_layout)
|
layout.addLayout(actions_layout)
|
||||||
@ -240,6 +295,36 @@ class AccountCard(QFrame):
|
|||||||
self.email_copy_timer.stop()
|
self.email_copy_timer.stop()
|
||||||
self.email_copy_timer.start(2000)
|
self.email_copy_timer.start(2000)
|
||||||
|
|
||||||
|
def update_login_button_state(self):
|
||||||
|
"""
|
||||||
|
Feature 5: Aktualisiert ALLE Button-Status basierend auf Process Guard.
|
||||||
|
Deaktiviert Login, Export und Delete wenn Guard locked ist oder Pause aktiv ist.
|
||||||
|
"""
|
||||||
|
from utils.process_guard import get_guard
|
||||||
|
guard = get_guard()
|
||||||
|
|
||||||
|
# Prüfe ob blockiert
|
||||||
|
is_locked = guard.is_locked()
|
||||||
|
is_paused = guard.is_paused()
|
||||||
|
is_blocked = is_locked or is_paused
|
||||||
|
|
||||||
|
# Alle Buttons entsprechend setzen
|
||||||
|
self.login_btn.setEnabled(not is_blocked)
|
||||||
|
self.export_btn.setEnabled(not is_blocked)
|
||||||
|
self.delete_btn.setEnabled(not is_blocked)
|
||||||
|
|
||||||
|
# Tooltips setzen
|
||||||
|
if is_blocked:
|
||||||
|
status_msg = guard.get_status_message()
|
||||||
|
self.login_btn.setToolTip(f"⚠ {status_msg}")
|
||||||
|
self.export_btn.setToolTip(f"⚠ {status_msg}")
|
||||||
|
self.delete_btn.setToolTip(f"⚠ {status_msg}")
|
||||||
|
else:
|
||||||
|
# Reset Tooltips
|
||||||
|
self.login_btn.setToolTip("Login durchführen")
|
||||||
|
self.export_btn.setToolTip("Profil exportieren")
|
||||||
|
self.delete_btn.setToolTip("Account löschen")
|
||||||
|
|
||||||
def update_texts(self):
|
def update_texts(self):
|
||||||
"""Aktualisiert die Texte gemäß der aktuellen Sprache"""
|
"""Aktualisiert die Texte gemäß der aktuellen Sprache"""
|
||||||
if not self.language_manager:
|
if not self.language_manager:
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren