Release 0.9.0
This commit is contained in:
673
bot/resources/GUIDELINES_DEPLOYMENT.md
Normal file
673
bot/resources/GUIDELINES_DEPLOYMENT.md
Normal file
@@ -0,0 +1,673 @@
|
||||
# chrani-bot-tng Deployment-Anleitung
|
||||
|
||||
Umfassende Anleitung zur Installation, Konfiguration und zum Betrieb von chrani-bot-tng mit modernem Python, gunicorn und nginx.
|
||||
|
||||
## Inhaltsverzeichnis
|
||||
|
||||
1. [Systemanforderungen](#systemanforderungen)
|
||||
2. [Schnellstart - Lokales Testen](#schnellstart---lokales-testen)
|
||||
3. [Produktions-Deployment mit gunicorn](#produktions-deployment-mit-gunicorn)
|
||||
4. [Deployment mit nginx (Reverse Proxy)](#deployment-mit-nginx-reverse-proxy)
|
||||
5. [Systemd-Service einrichten](#systemd-service-einrichten)
|
||||
6. [Fehlerbehebung](#fehlerbehebung)
|
||||
7. [Wartung und Updates](#wartung-und-updates)
|
||||
|
||||
---
|
||||
|
||||
## Systemanforderungen
|
||||
|
||||
### Unterstützte Systeme
|
||||
- **Linux**: Ubuntu 20.04+, Debian 10+, CentOS 8+, Fedora, Arch Linux
|
||||
- **Python**: 3.8 oder höher (empfohlen: Python 3.10+)
|
||||
- **Weitere Software**:
|
||||
- Git
|
||||
- pip (Python Package Manager)
|
||||
- virtualenv oder venv
|
||||
- Optional: nginx (für Reverse Proxy)
|
||||
- Optional: systemd (für Service-Management)
|
||||
|
||||
### Mindestanforderungen Hardware
|
||||
- **CPU**: 1 Core
|
||||
- **RAM**: 512 MB (1 GB empfohlen)
|
||||
- **Festplatte**: 500 MB freier Speicher
|
||||
|
||||
---
|
||||
|
||||
## Schnellstart - Lokales Testen
|
||||
|
||||
Diese Anleitung zeigt Ihnen, wie Sie chrani-bot-tng lokal auf Ihrem Computer zum Testen ausführen können.
|
||||
|
||||
### Schritt 1: Repository klonen
|
||||
|
||||
```bash
|
||||
cd ~
|
||||
git clone https://github.com/your-username/chrani-bot-tng.git
|
||||
cd chrani-bot-tng
|
||||
```
|
||||
|
||||
### Schritt 2: Python Virtual Environment erstellen
|
||||
|
||||
Ein Virtual Environment isoliert die Python-Abhängigkeiten des Projekts von Ihrem System.
|
||||
|
||||
```bash
|
||||
# Virtual Environment erstellen
|
||||
python3 -m venv venv
|
||||
|
||||
# Virtual Environment aktivieren
|
||||
source venv/bin/activate
|
||||
|
||||
# Ihr Prompt sollte jetzt mit (venv) beginnen
|
||||
```
|
||||
|
||||
### Schritt 3: Abhängigkeiten installieren
|
||||
|
||||
```bash
|
||||
# pip aktualisieren
|
||||
pip install --upgrade pip
|
||||
|
||||
# Projektabhängigkeiten installieren
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Schritt 4: Konfiguration anpassen
|
||||
|
||||
Die Bot-Konfiguration befindet sich in JSON-Dateien im `bot/options/` Verzeichnis.
|
||||
|
||||
```bash
|
||||
# Verzeichnis prüfen
|
||||
ls -la bot/options/
|
||||
|
||||
# Beispiel: Webserver-Konfiguration bearbeiten
|
||||
nano bot/options/module_webserver.json
|
||||
```
|
||||
|
||||
**Wichtige Einstellungen** in `module_webserver.json`:
|
||||
```json
|
||||
{
|
||||
"host": "0.0.0.0",
|
||||
"port": 5000,
|
||||
"Flask_secret_key": "ÄNDERN-SIE-DIES-IN-PRODUKTION"
|
||||
}
|
||||
```
|
||||
|
||||
**Telnet-Konfiguration** (für Verbindung zum 7 Days to Die Server):
|
||||
```bash
|
||||
nano bot/options/module_telnet.json
|
||||
```
|
||||
|
||||
Passen Sie die Telnet-Verbindungsdetails an Ihren Gameserver an.
|
||||
|
||||
### Schritt 5: Bot starten (Entwicklungsmodus)
|
||||
|
||||
Es gibt zwei Möglichkeiten, den Bot zu starten:
|
||||
|
||||
#### Methode A: Standalone-Modus (Flask Development Server)
|
||||
|
||||
```bash
|
||||
# Mit dem originalen Entry-Point
|
||||
python3 app.py
|
||||
```
|
||||
|
||||
Der Bot sollte starten und ausgeben:
|
||||
```
|
||||
modules started: ['module_dom', 'module_storage', 'module_telnet', ...]
|
||||
```
|
||||
|
||||
Der Webserver läuft auf: `http://localhost:5000`
|
||||
|
||||
#### Methode B: Mit gunicorn (empfohlen für Tests)
|
||||
|
||||
```bash
|
||||
# Mit gunicorn starten (wie in Produktion)
|
||||
gunicorn -c gunicorn.conf.py wsgi:application
|
||||
```
|
||||
|
||||
Vorteile:
|
||||
- Näher an der Produktionsumgebung
|
||||
- Bessere Performance
|
||||
- WebSocket-Support optimiert
|
||||
|
||||
### Schritt 6: Im Browser testen
|
||||
|
||||
Öffnen Sie Ihren Browser und navigieren Sie zu:
|
||||
|
||||
```
|
||||
http://localhost:5000
|
||||
```
|
||||
|
||||
Sie sollten die LCARS-Style Weboberfläche sehen. Klicken Sie auf "use your steam-account to log in" um sich zu authentifizieren.
|
||||
|
||||
### Schritt 7: Bot beenden
|
||||
|
||||
```bash
|
||||
# STRG+C drücken um den Bot zu stoppen
|
||||
|
||||
# Virtual Environment deaktivieren
|
||||
deactivate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Produktions-Deployment mit gunicorn
|
||||
|
||||
Für den Produktionsbetrieb ist gunicorn deutlich besser geeignet als der Flask Development Server.
|
||||
|
||||
### Warum gunicorn?
|
||||
|
||||
- **Performance**: Optimiert für hohe Last und viele gleichzeitige Verbindungen
|
||||
- **Stabilität**: Automatisches Neustart bei Fehlern
|
||||
- **WebSocket-Support**: Mit gevent-websocket-Worker für Socket.IO optimiert
|
||||
- **Production-Ready**: Bewährt in vielen großen Projekten
|
||||
|
||||
### Schritt 1: Konfiguration überprüfen
|
||||
|
||||
Die Datei `gunicorn.conf.py` enthält die gunicorn-Konfiguration:
|
||||
|
||||
```bash
|
||||
nano gunicorn.conf.py
|
||||
```
|
||||
|
||||
**Wichtige Einstellungen**:
|
||||
|
||||
```python
|
||||
# Server Socket - wo gunicorn lauscht
|
||||
bind = "0.0.0.0:5000" # Alle Interfaces, Port 5000
|
||||
|
||||
# Worker - WICHTIG: Für WebSocket nur 1 Worker!
|
||||
workers = 1
|
||||
worker_class = "geventwebsocket.gunicorn.workers.GeventWebSocketWorker" # für WebSocket-Upgrades
|
||||
worker_connections = 1000
|
||||
|
||||
# Timeouts
|
||||
timeout = 120
|
||||
keepalive = 5
|
||||
```
|
||||
|
||||
Hinweis: In `wsgi.py` exportiert `wsgi:application` die Flask-App (`webserver_module.app`), WebSockets laufen über den gevent-websocket-Worker.
|
||||
|
||||
### Schritt 2: Bot mit gunicorn starten
|
||||
|
||||
```bash
|
||||
# Virtual Environment aktivieren
|
||||
cd ~/chrani-bot-tng
|
||||
source venv/bin/activate
|
||||
|
||||
# Mit gunicorn starten
|
||||
gunicorn -c gunicorn.conf.py wsgi:application
|
||||
```
|
||||
|
||||
Sie sollten folgende Ausgabe sehen:
|
||||
|
||||
```
|
||||
============================================================
|
||||
chrani-bot-tng is starting...
|
||||
============================================================
|
||||
Worker spawned (pid: 12345)
|
||||
Worker initialized (pid: 12345)
|
||||
webserver: Running under WSGI server mode
|
||||
modules started: [...]
|
||||
============================================================
|
||||
chrani-bot-tng is ready to accept connections
|
||||
Listening on: 0.0.0.0:5000
|
||||
============================================================
|
||||
```
|
||||
|
||||
### Schritt 3: Verbindung testen
|
||||
|
||||
```bash
|
||||
# In einem neuen Terminal:
|
||||
curl http://localhost:5000
|
||||
|
||||
# Oder im Browser:
|
||||
# http://your-server-ip:5000
|
||||
```
|
||||
|
||||
### Schritt 4: Bot im Hintergrund laufen lassen
|
||||
|
||||
```bash
|
||||
# Mit nohup (einfache Methode)
|
||||
nohup gunicorn -c gunicorn.conf.py wsgi:application > gunicorn.log 2>&1 &
|
||||
|
||||
# Prozess-ID wird angezeigt
|
||||
echo $! > gunicorn.pid
|
||||
|
||||
# Logs anschauen
|
||||
tail -f gunicorn.log
|
||||
|
||||
# Bot stoppen
|
||||
kill $(cat gunicorn.pid)
|
||||
```
|
||||
|
||||
**Besser**: Verwenden Sie systemd (siehe Abschnitt unten)
|
||||
|
||||
---
|
||||
|
||||
## Deployment mit nginx (Reverse Proxy)
|
||||
|
||||
nginx als Reverse Proxy bietet zusätzliche Vorteile:
|
||||
|
||||
- **SSL/TLS-Terminierung** (HTTPS-Verschlüsselung)
|
||||
- **Load Balancing** (bei mehreren Instanzen)
|
||||
- **Static File Serving** (schnelleres Ausliefern von CSS/JS)
|
||||
- **DDoS-Schutz** und Rate Limiting
|
||||
- **Besseres Logging**
|
||||
|
||||
### Schritt 1: nginx installieren
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt update
|
||||
sudo apt install nginx
|
||||
|
||||
# CentOS/RHEL
|
||||
sudo yum install nginx
|
||||
|
||||
# Fedora
|
||||
sudo dnf install nginx
|
||||
```
|
||||
|
||||
### Schritt 2: nginx-Konfiguration erstellen
|
||||
|
||||
```bash
|
||||
# Beispielkonfiguration kopieren
|
||||
sudo cp nginx.conf.example /etc/nginx/sites-available/chrani-bot-tng
|
||||
|
||||
# Konfiguration bearbeiten
|
||||
sudo nano /etc/nginx/sites-available/chrani-bot-tng
|
||||
```
|
||||
|
||||
**Wichtige Anpassungen**:
|
||||
|
||||
1. **Server-Name ändern**:
|
||||
```nginx
|
||||
server_name your-domain.com www.your-domain.com;
|
||||
```
|
||||
|
||||
2. **SSL-Zertifikate** (für HTTPS):
|
||||
```nginx
|
||||
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
|
||||
```
|
||||
|
||||
3. **Upstream-Server prüfen**:
|
||||
```nginx
|
||||
upstream chrani_bot_app {
|
||||
server 127.0.0.1:5000 fail_timeout=0;
|
||||
}
|
||||
```
|
||||
|
||||
### Schritt 3: Konfiguration aktivieren
|
||||
|
||||
```bash
|
||||
# Symlink erstellen
|
||||
sudo ln -s /etc/nginx/sites-available/chrani-bot-tng /etc/nginx/sites-enabled/
|
||||
|
||||
# Standard-Site deaktivieren (optional)
|
||||
sudo rm /etc/nginx/sites-enabled/default
|
||||
|
||||
# Konfiguration testen
|
||||
sudo nginx -t
|
||||
|
||||
# Bei erfolgreicher Prüfung:
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### Schritt 4: Firewall konfigurieren
|
||||
|
||||
```bash
|
||||
# ufw (Ubuntu/Debian)
|
||||
sudo ufw allow 'Nginx Full'
|
||||
sudo ufw enable
|
||||
|
||||
# firewalld (CentOS/RHEL/Fedora)
|
||||
sudo firewall-cmd --permanent --add-service=http
|
||||
sudo firewall-cmd --permanent --add-service=https
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
### Schritt 5: SSL-Zertifikat mit Let's Encrypt (optional)
|
||||
|
||||
```bash
|
||||
# Certbot installieren
|
||||
sudo apt install certbot python3-certbot-nginx # Ubuntu/Debian
|
||||
# sudo yum install certbot python3-certbot-nginx # CentOS
|
||||
|
||||
# Zertifikat erstellen (interaktiv)
|
||||
sudo certbot --nginx -d your-domain.com -d www.your-domain.com
|
||||
|
||||
# Automatische Erneuerung testen
|
||||
sudo certbot renew --dry-run
|
||||
```
|
||||
|
||||
### Schritt 6: Zugriff testen
|
||||
|
||||
```bash
|
||||
# HTTP (sollte zu HTTPS umleiten)
|
||||
curl -I http://your-domain.com
|
||||
|
||||
# HTTPS
|
||||
curl -I https://your-domain.com
|
||||
```
|
||||
|
||||
Im Browser: `https://your-domain.com`
|
||||
|
||||
---
|
||||
|
||||
## Systemd-Service einrichten
|
||||
|
||||
Mit systemd läuft chrani-bot-tng als System-Service, startet automatisch beim Booten und wird bei Fehlern neu gestartet.
|
||||
|
||||
### Schritt 1: Service-Datei anpassen
|
||||
|
||||
```bash
|
||||
# Service-Datei bearbeiten
|
||||
nano chrani-bot-tng.service
|
||||
```
|
||||
|
||||
**Wichtige Anpassungen**:
|
||||
|
||||
```ini
|
||||
# Benutzer und Gruppe ändern
|
||||
User=your-username
|
||||
Group=your-username
|
||||
|
||||
# Pfade anpassen
|
||||
WorkingDirectory=/home/your-username/chrani-bot-tng
|
||||
Environment="PATH=/home/your-username/chrani-bot-tng/venv/bin"
|
||||
ExecStart=/home/your-username/chrani-bot-tng/venv/bin/gunicorn -c gunicorn.conf.py wsgi:application
|
||||
```
|
||||
|
||||
### Schritt 2: Service installieren
|
||||
|
||||
```bash
|
||||
# Service-Datei nach systemd kopieren
|
||||
sudo cp chrani-bot-tng.service /etc/systemd/system/
|
||||
|
||||
# systemd neu laden
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# Service aktivieren (Autostart beim Booten)
|
||||
sudo systemctl enable chrani-bot-tng
|
||||
|
||||
# Service starten
|
||||
sudo systemctl start chrani-bot-tng
|
||||
```
|
||||
|
||||
### Schritt 3: Service-Status prüfen
|
||||
|
||||
```bash
|
||||
# Status anzeigen
|
||||
sudo systemctl status chrani-bot-tng
|
||||
|
||||
# Logs anzeigen
|
||||
sudo journalctl -u chrani-bot-tng -f
|
||||
|
||||
# Logs der letzten Stunde
|
||||
sudo journalctl -u chrani-bot-tng --since "1 hour ago"
|
||||
```
|
||||
|
||||
### Schritt 4: Service-Befehle
|
||||
|
||||
```bash
|
||||
# Service stoppen
|
||||
sudo systemctl stop chrani-bot-tng
|
||||
|
||||
# Service neu starten
|
||||
sudo systemctl restart chrani-bot-tng
|
||||
|
||||
# Service neu laden (bei Konfigurationsänderungen)
|
||||
sudo systemctl reload chrani-bot-tng
|
||||
|
||||
# Autostart deaktivieren
|
||||
sudo systemctl disable chrani-bot-tng
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
### Problem: "Module not found" Fehler
|
||||
|
||||
**Symptom**: ImportError beim Starten
|
||||
|
||||
**Lösung**:
|
||||
```bash
|
||||
# Virtual Environment aktiviert?
|
||||
source venv/bin/activate
|
||||
|
||||
# Dependencies neu installieren
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Python-Version prüfen (mindestens 3.8)
|
||||
python3 --version
|
||||
```
|
||||
|
||||
### Problem: Port bereits in Benutzung
|
||||
|
||||
**Symptom**: "Address already in use"
|
||||
|
||||
**Lösung**:
|
||||
```bash
|
||||
# Prozess auf Port 5000 finden
|
||||
sudo lsof -i :5000
|
||||
|
||||
# Oder mit netstat
|
||||
sudo netstat -tulpn | grep :5000
|
||||
|
||||
# Prozess beenden
|
||||
sudo kill -9 <PID>
|
||||
|
||||
# Oder anderen Port in gunicorn.conf.py verwenden
|
||||
```
|
||||
|
||||
### Problem: WebSocket-Verbindung schlägt fehl
|
||||
|
||||
**Symptom**: "WebSocket connection failed" im Browser
|
||||
|
||||
**Lösung**:
|
||||
1. Prüfen Sie, dass nur 1 Worker in `gunicorn.conf.py` konfiguriert ist:
|
||||
```python
|
||||
workers = 1
|
||||
worker_class = "gevent"
|
||||
```
|
||||
|
||||
2. nginx-Konfiguration prüfen (WebSocket-Headers):
|
||||
```nginx
|
||||
location /socket.io/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
```
|
||||
|
||||
### Problem: 502 Bad Gateway (nginx)
|
||||
|
||||
**Symptom**: nginx zeigt "502 Bad Gateway"
|
||||
|
||||
**Lösung**:
|
||||
```bash
|
||||
# Ist gunicorn gestartet?
|
||||
sudo systemctl status chrani-bot-tng
|
||||
|
||||
# Logs prüfen
|
||||
sudo journalctl -u chrani-bot-tng -n 50
|
||||
|
||||
# nginx-Fehlerlog prüfen
|
||||
sudo tail -f /var/log/nginx/chrani-bot-tng-error.log
|
||||
|
||||
# Upstream in nginx.conf prüfen
|
||||
# Stimmt IP und Port mit gunicorn überein?
|
||||
```
|
||||
|
||||
### Problem: Hohe CPU-Last
|
||||
|
||||
**Symptom**: Server reagiert langsam, CPU bei 100%
|
||||
|
||||
**Lösung**:
|
||||
```bash
|
||||
# Prozesse prüfen
|
||||
top -u your-username
|
||||
|
||||
# gunicorn-Worker-Anzahl anpassen (aber max. 1 für WebSocket!)
|
||||
# Timeout erhöhen in gunicorn.conf.py
|
||||
timeout = 300
|
||||
|
||||
# Logs auf Endlosschleifen prüfen
|
||||
sudo journalctl -u chrani-bot-tng -f
|
||||
```
|
||||
|
||||
### Problem: Telnet-Verbindung zum Gameserver schlägt fehl
|
||||
|
||||
**Symptom**: Bot kann sich nicht mit dem 7 Days to Die Server verbinden
|
||||
|
||||
**Lösung**:
|
||||
1. Telnet-Einstellungen prüfen:
|
||||
```bash
|
||||
nano bot/options/module_telnet.json
|
||||
```
|
||||
|
||||
2. Verbindung manuell testen:
|
||||
```bash
|
||||
telnet <gameserver-ip> <telnet-port>
|
||||
# Passwort eingeben
|
||||
```
|
||||
|
||||
3. Firewall-Regeln prüfen
|
||||
4. Gameserver-Konfiguration prüfen (telnet aktiviert?)
|
||||
|
||||
---
|
||||
|
||||
## Wartung und Updates
|
||||
|
||||
### Code-Updates einspielen
|
||||
|
||||
```bash
|
||||
# Zum Projektverzeichnis
|
||||
cd ~/chrani-bot-tng
|
||||
|
||||
# Änderungen abrufen
|
||||
git fetch origin
|
||||
|
||||
# Aktuellen Branch prüfen
|
||||
git branch
|
||||
|
||||
# Updates herunterladen und anwenden
|
||||
git pull origin main # oder Ihr Branch-Name
|
||||
|
||||
# Dependencies aktualisieren
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt --upgrade
|
||||
|
||||
# Service neu starten
|
||||
sudo systemctl restart chrani-bot-tng
|
||||
```
|
||||
|
||||
### Backup erstellen
|
||||
|
||||
```bash
|
||||
# Gesamtes Projektverzeichnis sichern
|
||||
tar -czf chrani-bot-tng-backup-$(date +%Y%m%d).tar.gz ~/chrani-bot-tng
|
||||
|
||||
# Nur Konfiguration sichern
|
||||
tar -czf chrani-bot-config-backup-$(date +%Y%m%d).tar.gz ~/chrani-bot-tng/bot/options
|
||||
|
||||
# Backup an sicheren Ort verschieben
|
||||
mv chrani-bot-*.tar.gz /path/to/backup/location/
|
||||
```
|
||||
|
||||
### Logs rotieren
|
||||
|
||||
nginx rotiert Logs automatisch. Für gunicorn/systemd:
|
||||
|
||||
```bash
|
||||
# Log-Größe prüfen
|
||||
sudo journalctl --disk-usage
|
||||
|
||||
# Alte Logs löschen (älter als 7 Tage)
|
||||
sudo journalctl --vacuum-time=7d
|
||||
|
||||
# Oder nach Größe (maximal 500 MB behalten)
|
||||
sudo journalctl --vacuum-size=500M
|
||||
```
|
||||
|
||||
### Performance-Monitoring
|
||||
|
||||
```bash
|
||||
# Systemressourcen überwachen
|
||||
htop
|
||||
|
||||
# Nur chrani-bot-tng Prozesse
|
||||
htop -u your-username
|
||||
|
||||
# Netzwerk-Verbindungen prüfen
|
||||
sudo netstat -tulpn | grep gunicorn
|
||||
|
||||
# Echtzeit-Logs
|
||||
sudo journalctl -u chrani-bot-tng -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Zusammenfassung der Befehle
|
||||
|
||||
### Lokaler Test (Schnellstart)
|
||||
|
||||
```bash
|
||||
# Setup
|
||||
git clone <repository-url>
|
||||
cd chrani-bot-tng
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Starten
|
||||
gunicorn -c gunicorn.conf.py wsgi:application
|
||||
|
||||
# Stoppen: STRG+C
|
||||
```
|
||||
|
||||
### Produktions-Deployment
|
||||
|
||||
```bash
|
||||
# Einmalige Einrichtung
|
||||
sudo cp chrani-bot-tng.service /etc/systemd/system/
|
||||
sudo cp nginx.conf.example /etc/nginx/sites-available/chrani-bot-tng
|
||||
sudo ln -s /etc/nginx/sites-available/chrani-bot-tng /etc/nginx/sites-enabled/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable chrani-bot-tng
|
||||
sudo systemctl enable nginx
|
||||
|
||||
# Starten
|
||||
sudo systemctl start chrani-bot-tng
|
||||
sudo systemctl start nginx
|
||||
|
||||
# Status
|
||||
sudo systemctl status chrani-bot-tng
|
||||
sudo journalctl -u chrani-bot-tng -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Weitere Ressourcen
|
||||
|
||||
- **Gunicorn Dokumentation**: https://docs.gunicorn.org/
|
||||
- **nginx Dokumentation**: https://nginx.org/en/docs/
|
||||
- **Flask-SocketIO Dokumentation**: https://flask-socketio.readthedocs.io/
|
||||
- **systemd Dokumentation**: https://www.freedesktop.org/software/systemd/man/
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
Bei Problemen:
|
||||
|
||||
1. Prüfen Sie die Logs: `sudo journalctl -u chrani-bot-tng -n 100`
|
||||
2. Suchen Sie in den GitHub Issues
|
||||
3. Erstellen Sie ein neues Issue mit:
|
||||
- Fehlermeldung
|
||||
- Logs
|
||||
- Systeminfo (OS, Python-Version, etc.)
|
||||
|
||||
---
|
||||
|
||||
**Viel Erfolg mit chrani-bot-tng!** 🚀
|
||||
106
bot/resources/GUIDELINES_LOGGING.md
Normal file
106
bot/resources/GUIDELINES_LOGGING.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Logging System Analysis & Plan
|
||||
|
||||
## System Architecture Overview
|
||||
|
||||
```
|
||||
Browser (JavaScript)
|
||||
↕ Socket.io
|
||||
Python Backend (Flask + Socket.io)
|
||||
↕ Telnet
|
||||
7D2D Game Server
|
||||
```
|
||||
|
||||
## Current Logging State
|
||||
|
||||
### Python Backend
|
||||
- **Format:** Inconsistent mix of:
|
||||
- `print(f"[PREFIX] message")`
|
||||
- `print("{}: message".format(module))`
|
||||
- Plain `print("message")`
|
||||
- **No:**
|
||||
- Timestamps
|
||||
- Log levels (ERROR, WARN, INFO, DEBUG)
|
||||
- User context
|
||||
- Correlation IDs
|
||||
- Structured data
|
||||
|
||||
### JavaScript Frontend
|
||||
- **Issues:**
|
||||
- Ding/Dong logs every 10 seconds (spam)
|
||||
- Many debug messages that don't help troubleshooting
|
||||
- No structured logging
|
||||
- Success messages mixed with errors
|
||||
|
||||
## Event Flow Analysis
|
||||
|
||||
### 1. User Action Flow
|
||||
```
|
||||
User clicks checkbox (Browser)
|
||||
→ Widget event sent via Socket.io
|
||||
→ Python: webserver receives event
|
||||
→ Python: Action handler (e.g., dom_management/select)
|
||||
→ Python: DOM upsert
|
||||
→ Python: Callback triggers
|
||||
→ Python: Widget handler updates
|
||||
→ Socket.io sends update back
|
||||
→ Browser: DOM updated
|
||||
```
|
||||
|
||||
**Critical Logging Points:**
|
||||
- [ ] Action received (user, action, data)
|
||||
- [ ] Action validation failed
|
||||
- [ ] DOM operation (path, method, user)
|
||||
- [ ] Callback trigger (which handler, why)
|
||||
- [ ] Socket send (to whom, what type)
|
||||
|
||||
### 2. Tile Request Flow
|
||||
```
|
||||
Browser requests tile (HTTP GET)
|
||||
→ Python: webserver /map_tiles route
|
||||
→ Python: Auth check
|
||||
→ Python: Proxy to game server
|
||||
→ 7D2D: Returns tile OR error
|
||||
→ Python: Forward response OR error
|
||||
→ Browser: Displays tile OR 404
|
||||
```
|
||||
|
||||
**Critical Logging Points:**
|
||||
- [ ] Tile request (only on ERROR)
|
||||
- [ ] Auth failure
|
||||
- [ ] Game server error (status code, url)
|
||||
- [ ] Network timeout
|
||||
|
||||
### 3. Telnet Command Flow
|
||||
```
|
||||
Python: Action needs game data
|
||||
→ Python: Telnet send command
|
||||
→ 7D2D: Processes command
|
||||
→ 7D2D: Returns response
|
||||
→ Python: Parse response
|
||||
→ Python: Update DOM
|
||||
→ Python: Trigger callbacks
|
||||
```
|
||||
|
||||
## Log Format
|
||||
### Python Backend Format
|
||||
```
|
||||
[LEVEL] [TIMESTAMP] event_name | context_key=value context_key=value
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
[ERROR] [2025-01-19 12:34:56.123] tile_fetch_failed | user=steamid123 z=4 x=-2 y=1 status=404 url=http://...
|
||||
[WARN ] [2025-01-19 12:34:57.456] auth_missing_sid | user=steamid456 action=tile_request
|
||||
[INFO ] [2025-01-19 12:00:00.000] module_loaded | module=webserver version=1.0
|
||||
```
|
||||
|
||||
### JavaScript Frontend Format
|
||||
```
|
||||
[PREFIX] event_name | context
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
[SOCKET ERROR] event_processing_failed | data_type=widget_content error=Cannot read property 'id' of undefined
|
||||
[MAP ERROR ] shape_creation_failed | location_id=map_owner_loc1 shape=circle error=Invalid radius
|
||||
```
|
||||
211
bot/resources/GUIDELINES_MODULE_WIDGET_MENU.md
Normal file
211
bot/resources/GUIDELINES_MODULE_WIDGET_MENU.md
Normal file
@@ -0,0 +1,211 @@
|
||||
### Add a menu button and view to a module widget (How‑To)
|
||||
|
||||
This guide explains how to add a new button to a widget's pull‑out menu and display a corresponding screen ("view"). It follows the menu pattern used in the `locations` module and the example implemented in the `telnet` module.
|
||||
|
||||
The pattern consists of four parts:
|
||||
- A view registry in the widget Python file that defines available views and their labels.
|
||||
- A shared Jinja2 macro `construct_view_menu` to render the menu.
|
||||
- A small widget template `control_view_menu.html` that invokes the macro for the module.
|
||||
- A server action `toggle_<module>_widget_view` that switches the current view.
|
||||
|
||||
Below, `<module>` stands for your module name (e.g., `telnet`, `locations`), and `<view_id>` is the identifier of the view you are adding (e.g., `test`).
|
||||
|
||||
---
|
||||
|
||||
#### 1) Ensure the view menu macro is available for your module
|
||||
|
||||
Your module's Jinja2 macros file (e.g., `bot/modules/<module>/templates/jinja2_macros.html`) should contain the macro `construct_view_menu`. If your module doesn't have it yet, mirror the implementation from:
|
||||
- `bot/modules/locations/templates/jinja2_macros.html`
|
||||
|
||||
The macro expects these parameters:
|
||||
- `views`: dict of view entries (see step 2)
|
||||
- `current_view`: name of the active view
|
||||
- `module_name`: the module identifier (e.g., `telnet`)
|
||||
- `steamid`: the current user's steamid
|
||||
- `default_view`: the view to return to from an active state (usually `frontend`)
|
||||
|
||||
The macro renders toggle links that emit the standard socket event:
|
||||
```
|
||||
['widget_event', [module_name, ['toggle_' ~ module_name ~ '_widget_view', {'steamid': steamid, 'action': action}]]]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2) Define or extend the VIEW_REGISTRY in the widget Python file
|
||||
|
||||
In your widget file (e.g., `bot/modules/<module>/widgets/<widget_name>.py`) define a `VIEW_REGISTRY` mapping of view IDs to config objects. Each entry can have:
|
||||
- `label_active`: label when this view is currently active (typically `back`)
|
||||
- `label_inactive`: label when the view is not active (e.g., `options`, `test`)
|
||||
- `action`: the action string to be sent by the menu (e.g., `show_options`, `show_test`)
|
||||
- `include_in_menu`: whether to show it in the menu (set `False` for base `frontend`)
|
||||
|
||||
Example (excerpt with a new `test` view):
|
||||
```python
|
||||
VIEW_REGISTRY = {
|
||||
'frontend': {
|
||||
'label_active': 'back',
|
||||
'label_inactive': 'main',
|
||||
'action': 'show_frontend',
|
||||
'include_in_menu': False
|
||||
},
|
||||
'options': {
|
||||
'label_active': 'back',
|
||||
'label_inactive': 'options',
|
||||
'action': 'show_options',
|
||||
'include_in_menu': True
|
||||
},
|
||||
'test': {
|
||||
'label_active': 'back',
|
||||
'label_inactive': 'test',
|
||||
'action': 'show_test',
|
||||
'include_in_menu': True
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update `select_view` to route to your new view:
|
||||
```python
|
||||
def select_view(*args, **kwargs):
|
||||
module = args[0]
|
||||
dispatchers_steamid = kwargs.get('dispatchers_steamid', None)
|
||||
current_view = module.get_current_view(dispatchers_steamid)
|
||||
if current_view == 'options':
|
||||
options_view(module, dispatchers_steamid=dispatchers_steamid)
|
||||
elif current_view == 'test':
|
||||
test_view(module, dispatchers_steamid=dispatchers_steamid)
|
||||
else:
|
||||
frontend_view(module, dispatchers_steamid=dispatchers_steamid)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 3) Create the control_view_menu.html template for the widget
|
||||
|
||||
Create a template next to your widget's templates folder that renders the menu by importing the macro and passing the registry:
|
||||
|
||||
`bot/modules/<module>/templates/<widget_name>/control_view_menu.html`
|
||||
```jinja2
|
||||
{%- from 'jinja2_macros.html' import construct_view_menu with context -%}
|
||||
<div id="<widget_dom_id>_options_toggle" class="pull_out right">
|
||||
{{ construct_view_menu(
|
||||
views=views,
|
||||
current_view=current_view,
|
||||
module_name='<module>',
|
||||
steamid=steamid,
|
||||
default_view='frontend'
|
||||
)}}
|
||||
</div>
|
||||
```
|
||||
|
||||
Replace `<widget_dom_id>` with the widget DOM id (e.g., `telnet_table_widget_options_toggle`), and `<module>` with the module name literal (e.g., `telnet`).
|
||||
|
||||
---
|
||||
|
||||
#### 4) Render the menu in each view and add your new view function
|
||||
|
||||
In every view function (`frontend_view`, `options_view`, and your new `test_view`) render the menu and include it into the page as `options_toggle`:
|
||||
|
||||
```python
|
||||
template_view_menu = module.templates.get_template('<widget_name>/control_view_menu.html')
|
||||
current_view = module.get_current_view(dispatchers_steamid)
|
||||
options_toggle = module.template_render_hook(
|
||||
module,
|
||||
template=template_view_menu,
|
||||
views=VIEW_REGISTRY,
|
||||
current_view=current_view,
|
||||
steamid=dispatchers_steamid
|
||||
)
|
||||
|
||||
data_to_emit = module.template_render_hook(
|
||||
module,
|
||||
template=template_for_this_view,
|
||||
options_toggle=options_toggle,
|
||||
# ... other template params ...
|
||||
)
|
||||
```
|
||||
|
||||
Create the new view template (e.g., `view_test.html`) that uses the aside area to render the menu:
|
||||
```jinja2
|
||||
<header>
|
||||
<div>
|
||||
<span>Some Title</span>
|
||||
</div>
|
||||
</header>
|
||||
<aside>
|
||||
{{ options_toggle }}
|
||||
</aside>
|
||||
<main>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Test View</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><span>Test</span></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 5) Wire the server action to toggle views
|
||||
|
||||
Each module has an action named `toggle_<module>_widget_view` that updates the current view for a dispatcher. Extend it to support your new action string (`show_<view_id>`):
|
||||
|
||||
`bot/modules/<module>/actions/toggle_<module>_widget_view.py`
|
||||
```python
|
||||
def main_function(module, event_data, dispatchers_steamid):
|
||||
action = event_data[1].get('action', None)
|
||||
event_data[1]['action_identifier'] = action_name
|
||||
|
||||
if action == 'show_options':
|
||||
current_view = 'options'
|
||||
elif action == 'show_frontend':
|
||||
current_view = 'frontend'
|
||||
elif action == 'show_test':
|
||||
current_view = 'test'
|
||||
else:
|
||||
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
|
||||
return
|
||||
|
||||
module.set_current_view(dispatchers_steamid, {
|
||||
'current_view': current_view
|
||||
})
|
||||
module.callback_success(callback_success, module, event_data, dispatchers_steamid)
|
||||
```
|
||||
|
||||
Make sure the view ID you set here matches the view IDs used in `VIEW_REGISTRY` and `select_view`.
|
||||
|
||||
---
|
||||
|
||||
#### 6) Reference implementation to compare
|
||||
|
||||
Use these files as working examples:
|
||||
- Locations menu macro: `bot/modules/locations/templates/jinja2_macros.html`
|
||||
- Locations widget menu template: `bot/modules/locations/templates/manage_locations_widget/control_view_menu.html`
|
||||
- Telnet macros with menu: `bot/modules/telnet/templates/jinja2_macros.html`
|
||||
- Telnet menu template: `bot/modules/telnet/templates/telnet_log_widget/control_view_menu.html`
|
||||
- Telnet widget with added view: `bot/modules/telnet/widgets/telnet_log_widget.py`
|
||||
- Telnet action wiring: `bot/modules/telnet/actions/toggle_telnet_widget_view.py`
|
||||
- Telnet new view template: `bot/modules/telnet/templates/telnet_log_widget/view_test.html`
|
||||
|
||||
---
|
||||
|
||||
#### 7) Quick checklist
|
||||
|
||||
- [ ] `VIEW_REGISTRY` contains your `<view_id>` with a correct `action` (`show_<view_id>`) and `include_in_menu = True`.
|
||||
- [ ] `select_view` routes to `<view_id>` by calling `<view_id>_view`.
|
||||
- [ ] The new `<view_id>_view` renders `options_toggle` using `control_view_menu.html`.
|
||||
- [ ] The new view has a template and includes `{{ options_toggle }}` in the aside.
|
||||
- [ ] The module's `toggle_<module>_widget_view` action handles `show_<view_id>` and sets `current_view` accordingly.
|
||||
- [ ] Menu renders without errors and buttons switch between views (`back` returns to `frontend`).
|
||||
|
||||
---
|
||||
|
||||
#### Notes
|
||||
|
||||
- Keep naming consistent: action strings are `show_<view_id>`, event name is `toggle_<module>_widget_view`.
|
||||
- The base view `frontend` is usually not included in the menu (`include_in_menu: False`), but is the `default_view` used for the "back" behavior.
|
||||
- Follow the module's existing code style and avoid unrelated refactors when introducing new views/buttons.
|
||||
74
bot/resources/configs/.env.example
Normal file
74
bot/resources/configs/.env.example
Normal file
@@ -0,0 +1,74 @@
|
||||
# chrani-bot-tng Environment Configuration
|
||||
# Copy this file to .env and adjust the values for your setup
|
||||
|
||||
# ============================================================================
|
||||
# Application Settings
|
||||
# ============================================================================
|
||||
|
||||
# Set to 'true' when running under gunicorn or other WSGI servers
|
||||
RUNNING_UNDER_WSGI=true
|
||||
|
||||
# Flask secret key - CHANGE THIS to a random string in production!
|
||||
FLASK_SECRET_KEY=change-this-to-a-random-secret-key
|
||||
|
||||
# ============================================================================
|
||||
# Server Configuration
|
||||
# ============================================================================
|
||||
|
||||
# Host to bind to (use 0.0.0.0 for all interfaces, 127.0.0.1 for localhost only)
|
||||
HOST=0.0.0.0
|
||||
|
||||
# Port to run on
|
||||
PORT=5000
|
||||
|
||||
# ============================================================================
|
||||
# 7 Days to Die Server Settings
|
||||
# ============================================================================
|
||||
|
||||
# Telnet connection settings for the game server
|
||||
TELNET_HOST=localhost
|
||||
TELNET_PORT=8081
|
||||
TELNET_PASSWORD=your-telnet-password
|
||||
|
||||
# ============================================================================
|
||||
# Logging
|
||||
# ============================================================================
|
||||
|
||||
# Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# ============================================================================
|
||||
# Development Settings
|
||||
# ============================================================================
|
||||
|
||||
# Enable Flask debug mode (DO NOT use in production!)
|
||||
FLASK_DEBUG=false
|
||||
|
||||
# Enable SocketIO debug mode
|
||||
SOCKETIO_DEBUG=false
|
||||
|
||||
# Enable engineio logger
|
||||
ENGINEIO_LOGGER=false
|
||||
|
||||
# ============================================================================
|
||||
# Production Settings
|
||||
# ============================================================================
|
||||
|
||||
# Number of gunicorn workers (for WebSocket use 1 worker with gevent)
|
||||
GUNICORN_WORKERS=1
|
||||
|
||||
# Gunicorn worker class (use 'gevent' for WebSocket support)
|
||||
GUNICORN_WORKER_CLASS=gevent
|
||||
|
||||
# Maximum number of concurrent connections per worker
|
||||
GUNICORN_WORKER_CONNECTIONS=1000
|
||||
|
||||
# Request timeout in seconds
|
||||
GUNICORN_TIMEOUT=120
|
||||
|
||||
# ============================================================================
|
||||
# Database/Storage (if applicable)
|
||||
# ============================================================================
|
||||
|
||||
# Add any database connection strings or storage paths here
|
||||
# DATA_DIR=/var/lib/chrani-bot-tng
|
||||
51
bot/resources/configs/chrani-bot-tng.service
Normal file
51
bot/resources/configs/chrani-bot-tng.service
Normal file
@@ -0,0 +1,51 @@
|
||||
[Unit]
|
||||
Description=chrani-bot-tng - 7 Days to Die Server Management Bot
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
# The specific user that our service will run as
|
||||
# CHANGE THIS to your actual username
|
||||
User=your-username
|
||||
Group=your-username
|
||||
|
||||
# Set working directory
|
||||
WorkingDirectory=/path/to/chrani-bot-tng
|
||||
|
||||
# Environment variables
|
||||
Environment="PATH=/path/to/chrani-bot-tng/venv/bin"
|
||||
Environment="RUNNING_UNDER_WSGI=true"
|
||||
|
||||
# If you're using a .env file for configuration
|
||||
# EnvironmentFile=/path/to/chrani-bot-tng/.env
|
||||
|
||||
# The command to start the service
|
||||
ExecStart=/path/to/chrani-bot-tng/venv/bin/gunicorn -c gunicorn.conf.py wsgi:application
|
||||
|
||||
# Restart policy
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
# Performance and security settings
|
||||
RuntimeDirectory=chrani-bot-tng
|
||||
RuntimeDirectoryMode=755
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=chrani-bot-tng
|
||||
|
||||
# Security hardening (optional but recommended)
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
|
||||
# Resource limits (adjust as needed)
|
||||
LimitNOFILE=65536
|
||||
LimitNPROC=512
|
||||
|
||||
# Timeout for start/stop
|
||||
TimeoutStartSec=300
|
||||
TimeoutStopSec=300
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
146
bot/resources/configs/gunicorn.conf.py
Normal file
146
bot/resources/configs/gunicorn.conf.py
Normal file
@@ -0,0 +1,146 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Gunicorn Configuration File for chrani-bot-tng
|
||||
|
||||
This configuration is optimized for running the bot with Flask-SocketIO and gevent.
|
||||
"""
|
||||
import multiprocessing
|
||||
import os
|
||||
|
||||
# Server Socket
|
||||
bind = "0.0.0.0:5000"
|
||||
backlog = 2048
|
||||
|
||||
# Worker Processes
|
||||
# For WebSocket support with gevent, use only 1 worker
|
||||
# Multiple workers don't work well with socket.io state
|
||||
workers = 1
|
||||
worker_class = "gevent"
|
||||
worker_connections = 1000
|
||||
max_requests = 0 # Disable automatic worker restart
|
||||
max_requests_jitter = 0
|
||||
timeout = 120
|
||||
keepalive = 5
|
||||
|
||||
# Logging
|
||||
accesslog = "-" # Log to stdout
|
||||
errorlog = "-" # Log to stderr
|
||||
loglevel = "info"
|
||||
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
|
||||
|
||||
# Process Naming
|
||||
proc_name = "chrani-bot-tng"
|
||||
|
||||
# Server Mechanics
|
||||
daemon = False
|
||||
pidfile = None
|
||||
umask = 0
|
||||
user = None
|
||||
group = None
|
||||
tmp_upload_dir = None
|
||||
|
||||
# SSL (uncomment and configure if using HTTPS directly with gunicorn)
|
||||
# keyfile = "/path/to/keyfile"
|
||||
# certfile = "/path/to/certfile"
|
||||
# ca_certs = "/path/to/ca_certs"
|
||||
# cert_reqs = 0
|
||||
# ssl_version = 2
|
||||
# ciphers = None
|
||||
|
||||
# Server Hooks
|
||||
def on_starting(server):
|
||||
"""
|
||||
Called just before the master process is initialized.
|
||||
"""
|
||||
print("=" * 60)
|
||||
print("chrani-bot-tng is starting...")
|
||||
print("=" * 60)
|
||||
|
||||
def on_reload(server):
|
||||
"""
|
||||
Called to recycle workers during a reload via SIGHUP.
|
||||
"""
|
||||
print("Reloading workers...")
|
||||
|
||||
def when_ready(server):
|
||||
"""
|
||||
Called just after the server is started.
|
||||
"""
|
||||
print("=" * 60)
|
||||
print("chrani-bot-tng is ready to accept connections")
|
||||
print(f"Listening on: {bind}")
|
||||
print("=" * 60)
|
||||
|
||||
def pre_fork(server, worker):
|
||||
"""
|
||||
Called just before a worker is forked.
|
||||
"""
|
||||
pass
|
||||
|
||||
def post_fork(server, worker):
|
||||
"""
|
||||
Called just after a worker has been forked.
|
||||
"""
|
||||
print(f"Worker spawned (pid: {worker.pid})")
|
||||
|
||||
def post_worker_init(worker):
|
||||
"""
|
||||
Called just after a worker has initialized the application.
|
||||
"""
|
||||
print(f"Worker initialized (pid: {worker.pid})")
|
||||
|
||||
def worker_int(worker):
|
||||
"""
|
||||
Called just after a worker received the SIGINT or SIGQUIT signal.
|
||||
"""
|
||||
print(f"Worker received INT or QUIT signal (pid: {worker.pid})")
|
||||
|
||||
def worker_abort(worker):
|
||||
"""
|
||||
Called when a worker received the SIGABRT signal.
|
||||
"""
|
||||
print(f"Worker received SIGABRT signal (pid: {worker.pid})")
|
||||
|
||||
def pre_exec(server):
|
||||
"""
|
||||
Called just before a new master process is forked.
|
||||
"""
|
||||
print("Forking new master process...")
|
||||
|
||||
def pre_request(worker, req):
|
||||
"""
|
||||
Called just before a worker processes the request.
|
||||
"""
|
||||
worker.log.debug(f"{req.method} {req.path}")
|
||||
|
||||
def post_request(worker, req, environ, resp):
|
||||
"""
|
||||
Called after a worker processes the request.
|
||||
"""
|
||||
pass
|
||||
|
||||
def child_exit(server, worker):
|
||||
"""
|
||||
Called just after a worker has been exited.
|
||||
"""
|
||||
print(f"Worker exited (pid: {worker.pid})")
|
||||
|
||||
def worker_exit(server, worker):
|
||||
"""
|
||||
Called just after a worker has been exited.
|
||||
"""
|
||||
print(f"Worker process exiting (pid: {worker.pid})")
|
||||
|
||||
def nworkers_changed(server, new_value, old_value):
|
||||
"""
|
||||
Called just after num_workers has been changed.
|
||||
"""
|
||||
print(f"Number of workers changed from {old_value} to {new_value}")
|
||||
|
||||
def on_exit(server):
|
||||
"""
|
||||
Called just before exiting gunicorn.
|
||||
"""
|
||||
print("=" * 60)
|
||||
print("chrani-bot-tng is shutting down...")
|
||||
print("=" * 60)
|
||||
156
bot/resources/configs/nginx.conf.example
Normal file
156
bot/resources/configs/nginx.conf.example
Normal file
@@ -0,0 +1,156 @@
|
||||
# nginx configuration for chrani-bot-tng
|
||||
#
|
||||
# INSTALLATION:
|
||||
# 1. Copy this file to /etc/nginx/sites-available/chrani-bot-tng
|
||||
# 2. Update the server_name to match your domain
|
||||
# 3. Update the paths to SSL certificates if using HTTPS
|
||||
# 4. Create a symlink: sudo ln -s /etc/nginx/sites-available/chrani-bot-tng /etc/nginx/sites-enabled/
|
||||
# 5. Test config: sudo nginx -t
|
||||
# 6. Reload nginx: sudo systemctl reload nginx
|
||||
|
||||
# Upstream configuration for gunicorn
|
||||
upstream chrani_bot_app {
|
||||
# Use Unix socket for better performance (recommended)
|
||||
# Make sure this matches the bind setting in gunicorn.conf.py
|
||||
server 127.0.0.1:5000 fail_timeout=0;
|
||||
|
||||
# Alternative: Unix socket (requires changing gunicorn bind setting)
|
||||
# server unix:/var/run/chrani-bot-tng/gunicorn.sock fail_timeout=0;
|
||||
}
|
||||
|
||||
# HTTP Server (redirects to HTTPS)
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name your-domain.com www.your-domain.com;
|
||||
|
||||
# Redirect all HTTP traffic to HTTPS
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
# HTTPS Server
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name your-domain.com www.your-domain.com;
|
||||
|
||||
# SSL Configuration
|
||||
# Update these paths with your actual certificate paths
|
||||
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
|
||||
|
||||
# SSL Security Settings
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
|
||||
# Logging
|
||||
access_log /var/log/nginx/chrani-bot-tng-access.log;
|
||||
error_log /var/log/nginx/chrani-bot-tng-error.log;
|
||||
|
||||
# Max upload size (adjust as needed)
|
||||
client_max_body_size 4M;
|
||||
|
||||
# Security Headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
# Main location block - proxy to gunicorn
|
||||
location / {
|
||||
proxy_pass http://chrani_bot_app;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Timeouts
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
|
||||
# Disable buffering for better real-time response
|
||||
proxy_buffering off;
|
||||
proxy_redirect off;
|
||||
}
|
||||
|
||||
# WebSocket support for Socket.IO
|
||||
location /socket.io/ {
|
||||
proxy_pass http://chrani_bot_app/socket.io/;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
# WebSocket specific headers
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# Standard proxy headers
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Timeouts for WebSocket
|
||||
proxy_connect_timeout 7d;
|
||||
proxy_send_timeout 7d;
|
||||
proxy_read_timeout 7d;
|
||||
|
||||
# Disable buffering for WebSocket
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
# Static files (if you have any)
|
||||
location /static/ {
|
||||
alias /path/to/chrani-bot-tng/static/;
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# Favicon
|
||||
location = /favicon.ico {
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
# Robots.txt
|
||||
location = /robots.txt {
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
}
|
||||
|
||||
# Alternative: HTTP-only configuration (for local/dev use)
|
||||
# Uncomment this and comment out the HTTPS server above if not using SSL
|
||||
#
|
||||
# server {
|
||||
# listen 80;
|
||||
# listen [::]:80;
|
||||
# server_name your-domain.com www.your-domain.com;
|
||||
#
|
||||
# access_log /var/log/nginx/chrani-bot-tng-access.log;
|
||||
# error_log /var/log/nginx/chrani-bot-tng-error.log;
|
||||
#
|
||||
# client_max_body_size 4M;
|
||||
#
|
||||
# location / {
|
||||
# proxy_pass http://chrani_bot_app;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# proxy_buffering off;
|
||||
# proxy_redirect off;
|
||||
# }
|
||||
#
|
||||
# location /socket.io/ {
|
||||
# proxy_pass http://chrani_bot_app/socket.io/;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Upgrade $http_upgrade;
|
||||
# proxy_set_header Connection "upgrade";
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_buffering off;
|
||||
# }
|
||||
# }
|
||||
345
bot/resources/services/analyze_callback_usage.py
Executable file
345
bot/resources/services/analyze_callback_usage.py
Executable file
@@ -0,0 +1,345 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Callback Dict Usage Analyzer
|
||||
|
||||
Scans the codebase for all callback_dict usage:
|
||||
- dom.data.upsert() calls
|
||||
- dom.data.append() calls
|
||||
- dom.data.remove_key_by_path() calls
|
||||
- widget_meta["handlers"] registrations
|
||||
|
||||
Generates a complete inventory for Phase 0 of the refactoring plan.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Tuple
|
||||
|
||||
|
||||
class CallbackAnalyzer:
|
||||
def __init__(self, root_dir: str):
|
||||
self.root_dir = Path(root_dir)
|
||||
self.results = {
|
||||
"upsert_calls": [],
|
||||
"append_calls": [],
|
||||
"remove_calls": [],
|
||||
"handler_registrations": []
|
||||
}
|
||||
|
||||
def analyze(self):
|
||||
"""Run all analysis passes."""
|
||||
print("🔍 Analyzing callback_dict usage...")
|
||||
print(f"📁 Root directory: {self.root_dir}\n")
|
||||
|
||||
# Find all Python files in bot/modules
|
||||
modules_dir = self.root_dir / "bot" / "modules"
|
||||
if not modules_dir.exists():
|
||||
print(f"❌ Error: {modules_dir} does not exist!")
|
||||
return
|
||||
|
||||
python_files = list(modules_dir.rglob("*.py"))
|
||||
print(f"📄 Found {len(python_files)} Python files\n")
|
||||
|
||||
# Analyze each file
|
||||
for py_file in python_files:
|
||||
self._analyze_file(py_file)
|
||||
|
||||
# Print results
|
||||
self._print_results()
|
||||
|
||||
# Generate markdown report
|
||||
self._generate_report()
|
||||
|
||||
def _analyze_file(self, file_path: Path):
|
||||
"""Analyze a single Python file."""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
lines = content.split('\n')
|
||||
|
||||
relative_path = file_path.relative_to(self.root_dir)
|
||||
|
||||
# Find upsert calls
|
||||
self._find_upsert_calls(relative_path, content, lines)
|
||||
|
||||
# Find append calls
|
||||
self._find_append_calls(relative_path, content, lines)
|
||||
|
||||
# Find remove calls
|
||||
self._find_remove_calls(relative_path, content, lines)
|
||||
|
||||
# Find handler registrations
|
||||
self._find_handler_registrations(relative_path, content, lines)
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error analyzing {file_path}: {e}")
|
||||
|
||||
def _find_upsert_calls(self, file_path: Path, content: str, lines: List[str]):
|
||||
"""Find all dom.data.upsert() calls."""
|
||||
# Pattern: module.dom.data.upsert( or self.dom.data.upsert(
|
||||
pattern = r'(module|self)\.dom\.data\.upsert\('
|
||||
|
||||
for match in re.finditer(pattern, content):
|
||||
line_num = content[:match.start()].count('\n') + 1
|
||||
|
||||
# Try to extract callback levels
|
||||
min_level, max_level = self._extract_callback_levels(lines, line_num)
|
||||
|
||||
# Try to determine data depth
|
||||
depth = self._estimate_data_depth(lines, line_num)
|
||||
|
||||
self.results["upsert_calls"].append({
|
||||
"file": str(file_path),
|
||||
"line": line_num,
|
||||
"min_callback_level": min_level,
|
||||
"max_callback_level": max_level,
|
||||
"estimated_depth": depth
|
||||
})
|
||||
|
||||
def _find_append_calls(self, file_path: Path, content: str, lines: List[str]):
|
||||
"""Find all dom.data.append() calls."""
|
||||
pattern = r'(module|self)\.dom\.data\.append\('
|
||||
|
||||
for match in re.finditer(pattern, content):
|
||||
line_num = content[:match.start()].count('\n') + 1
|
||||
|
||||
self.results["append_calls"].append({
|
||||
"file": str(file_path),
|
||||
"line": line_num
|
||||
})
|
||||
|
||||
def _find_remove_calls(self, file_path: Path, content: str, lines: List[str]):
|
||||
"""Find all dom.data.remove_key_by_path() calls."""
|
||||
pattern = r'(module|self)\.dom\.data\.remove_key_by_path\('
|
||||
|
||||
for match in re.finditer(pattern, content):
|
||||
line_num = content[:match.start()].count('\n') + 1
|
||||
|
||||
self.results["remove_calls"].append({
|
||||
"file": str(file_path),
|
||||
"line": line_num
|
||||
})
|
||||
|
||||
def _find_handler_registrations(self, file_path: Path, content: str, lines: List[str]):
|
||||
"""Find all widget_meta handler registrations."""
|
||||
# Look for widget_meta = { ... "handlers": { ... } ... }
|
||||
|
||||
if 'widget_meta' not in content:
|
||||
return
|
||||
|
||||
if '"handlers"' not in content and "'handlers'" not in content:
|
||||
return
|
||||
|
||||
# Find line with handlers dict
|
||||
in_handlers = False
|
||||
handlers_start = None
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if '"handlers"' in line or "'handlers'" in line:
|
||||
in_handlers = True
|
||||
handlers_start = i + 1
|
||||
continue
|
||||
|
||||
if in_handlers:
|
||||
# Look for path patterns
|
||||
# Pattern: "path/pattern": handler_function,
|
||||
path_match = re.search(r'["\']([^"\']+)["\']:\s*(\w+)', line)
|
||||
|
||||
if path_match:
|
||||
path_pattern = path_match.group(1)
|
||||
handler_name = path_match.group(2)
|
||||
|
||||
# Calculate depth
|
||||
depth = path_pattern.count('/')
|
||||
|
||||
self.results["handler_registrations"].append({
|
||||
"file": str(file_path),
|
||||
"line": i + 1,
|
||||
"path_pattern": path_pattern,
|
||||
"handler_function": handler_name,
|
||||
"depth": depth
|
||||
})
|
||||
|
||||
# Check if we've left the handlers dict
|
||||
if '}' in line and ',' not in line:
|
||||
in_handlers = False
|
||||
|
||||
def _extract_callback_levels(self, lines: List[str], start_line: int) -> Tuple[str, str]:
|
||||
"""Try to extract min_callback_level and max_callback_level from upsert call."""
|
||||
min_level = "None"
|
||||
max_level = "None"
|
||||
|
||||
# Look at next ~10 lines for callback level params
|
||||
for i in range(start_line - 1, min(start_line + 10, len(lines))):
|
||||
line = lines[i]
|
||||
|
||||
if 'min_callback_level' in line:
|
||||
match = re.search(r'min_callback_level\s*=\s*(\d+|None)', line)
|
||||
if match:
|
||||
min_level = match.group(1)
|
||||
|
||||
if 'max_callback_level' in line:
|
||||
match = re.search(r'max_callback_level\s*=\s*(\d+|None)', line)
|
||||
if match:
|
||||
max_level = match.group(1)
|
||||
|
||||
# Stop at closing paren
|
||||
if ')' in line:
|
||||
break
|
||||
|
||||
return min_level, max_level
|
||||
|
||||
def _estimate_data_depth(self, lines: List[str], start_line: int) -> int:
|
||||
"""Estimate the depth of data being upserted by counting dict nesting."""
|
||||
depth = 0
|
||||
brace_count = 0
|
||||
|
||||
# Look backwards for opening brace
|
||||
for i in range(start_line - 1, max(0, start_line - 30), -1):
|
||||
line = lines[i]
|
||||
|
||||
if 'upsert({' in line or 'upsert( {' in line:
|
||||
# Count colons/keys in following lines until we reach reasonable depth
|
||||
for j in range(i, min(i + 50, len(lines))):
|
||||
check_line = lines[j]
|
||||
|
||||
# Count dictionary depth by tracking braces
|
||||
brace_count += check_line.count('{')
|
||||
brace_count -= check_line.count('}')
|
||||
|
||||
if brace_count < 0:
|
||||
break
|
||||
|
||||
# Rough estimate: each key-value pair increases depth
|
||||
if '":' in check_line or "\':" in check_line:
|
||||
depth += 1
|
||||
|
||||
break
|
||||
|
||||
# Depth is usually around 4-6 for locations/players
|
||||
return min(depth, 10) # Cap at reasonable number
|
||||
|
||||
def _print_results(self):
|
||||
"""Print analysis results to console."""
|
||||
print("\n" + "=" * 70)
|
||||
print("📊 ANALYSIS RESULTS")
|
||||
print("=" * 70 + "\n")
|
||||
|
||||
print(f"🔹 Upsert Calls: {len(self.results['upsert_calls'])}")
|
||||
print(f"🔹 Append Calls: {len(self.results['append_calls'])}")
|
||||
print(f"🔹 Remove Calls: {len(self.results['remove_calls'])}")
|
||||
print(f"🔹 Handler Registrations: {len(self.results['handler_registrations'])}")
|
||||
|
||||
print("\n" + "-" * 70)
|
||||
print("UPSERT CALLS BY CALLBACK LEVELS:")
|
||||
print("-" * 70)
|
||||
|
||||
# Group by callback levels
|
||||
level_groups = {}
|
||||
for call in self.results["upsert_calls"]:
|
||||
key = f"min={call['min_callback_level']}, max={call['max_callback_level']}"
|
||||
if key not in level_groups:
|
||||
level_groups[key] = []
|
||||
level_groups[key].append(call)
|
||||
|
||||
for levels, calls in sorted(level_groups.items()):
|
||||
print(f"\n{levels}: {len(calls)} calls")
|
||||
for call in calls:
|
||||
print(f" 📄 {call['file']}:{call['line']}")
|
||||
|
||||
print("\n" + "-" * 70)
|
||||
print("HANDLER REGISTRATIONS BY DEPTH:")
|
||||
print("-" * 70)
|
||||
|
||||
# Group by depth
|
||||
depth_groups = {}
|
||||
for handler in self.results["handler_registrations"]:
|
||||
depth = handler['depth']
|
||||
if depth not in depth_groups:
|
||||
depth_groups[depth] = []
|
||||
depth_groups[depth].append(handler)
|
||||
|
||||
for depth in sorted(depth_groups.keys()):
|
||||
handlers = depth_groups[depth]
|
||||
print(f"\nDepth {depth}: {len(handlers)} handlers")
|
||||
for h in handlers:
|
||||
print(f" 📄 {h['file']}:{h['line']}")
|
||||
print(f" Pattern: {h['path_pattern']}")
|
||||
print(f" Handler: {h['handler_function']}")
|
||||
|
||||
def _generate_report(self):
|
||||
"""Generate markdown report."""
|
||||
report_path = self.root_dir / "CALLBACK_DICT_INVENTORY.md"
|
||||
|
||||
with open(report_path, 'w', encoding='utf-8') as f:
|
||||
f.write("# Callback Dict Usage Inventory\n\n")
|
||||
f.write("Generated by analyze_callback_usage.py\n\n")
|
||||
|
||||
f.write("## Summary\n\n")
|
||||
f.write(f"- **Upsert Calls:** {len(self.results['upsert_calls'])}\n")
|
||||
f.write(f"- **Append Calls:** {len(self.results['append_calls'])}\n")
|
||||
f.write(f"- **Remove Calls:** {len(self.results['remove_calls'])}\n")
|
||||
f.write(f"- **Handler Registrations:** {len(self.results['handler_registrations'])}\n\n")
|
||||
|
||||
f.write("---\n\n")
|
||||
|
||||
f.write("## Upsert Calls\n\n")
|
||||
f.write("| File | Line | Min Level | Max Level | Est. Depth |\n")
|
||||
f.write("|------|------|-----------|-----------|------------|\n")
|
||||
for call in self.results["upsert_calls"]:
|
||||
f.write(f"| {call['file']} | {call['line']} | "
|
||||
f"{call['min_callback_level']} | {call['max_callback_level']} | "
|
||||
f"{call['estimated_depth']} |\n")
|
||||
|
||||
f.write("\n---\n\n")
|
||||
|
||||
f.write("## Append Calls\n\n")
|
||||
f.write("| File | Line |\n")
|
||||
f.write("|------|------|\n")
|
||||
for call in self.results["append_calls"]:
|
||||
f.write(f"| {call['file']} | {call['line']} |\n")
|
||||
|
||||
f.write("\n---\n\n")
|
||||
|
||||
f.write("## Remove Calls\n\n")
|
||||
f.write("| File | Line |\n")
|
||||
f.write("|------|------|\n")
|
||||
for call in self.results["remove_calls"]:
|
||||
f.write(f"| {call['file']} | {call['line']} |\n")
|
||||
|
||||
f.write("\n---\n\n")
|
||||
|
||||
f.write("## Handler Registrations\n\n")
|
||||
f.write("| File | Line | Depth | Path Pattern | Handler Function |\n")
|
||||
f.write("|------|------|-------|--------------|------------------|\n")
|
||||
for handler in self.results["handler_registrations"]:
|
||||
f.write(f"| {handler['file']} | {handler['line']} | "
|
||||
f"{handler['depth']} | `{handler['path_pattern']}` | "
|
||||
f"`{handler['handler_function']}` |\n")
|
||||
|
||||
f.write("\n---\n\n")
|
||||
f.write("## Next Steps\n\n")
|
||||
f.write("1. Review each upsert call - does it send complete or partial data?\n")
|
||||
f.write("2. Review each handler - does it expect complete or partial data?\n")
|
||||
f.write("3. Identify mismatches that will benefit from enrichment\n")
|
||||
f.write("4. Plan which files need updating in Phase 2 and Phase 3\n")
|
||||
|
||||
print(f"\n✅ Report generated: {report_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
root = sys.argv[1]
|
||||
else:
|
||||
# Assume we're in the scripts directory
|
||||
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
analyzer = CallbackAnalyzer(root)
|
||||
analyzer.analyze()
|
||||
|
||||
print("\n✨ Analysis complete!")
|
||||
print("📋 Review CALLBACK_DICT_INVENTORY.md for detailed results")
|
||||
print("📖 See CALLBACK_DICT_REFACTOR_PLAN.md for next steps\n")
|
||||
71
bot/resources/services/debug_telnet.py
Normal file
71
bot/resources/services/debug_telnet.py
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Debug script to see raw telnet responses from 7D2D server.
|
||||
This helps update the regex patterns in the action files.
|
||||
"""
|
||||
import telnetlib
|
||||
import json
|
||||
import time
|
||||
import re
|
||||
|
||||
# Load config
|
||||
with open('bot/options/module_telnet.json', 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
HOST = config['host']
|
||||
PORT = config['port']
|
||||
PASSWORD = config['password']
|
||||
|
||||
print(f"Connecting to {HOST}:{PORT}...")
|
||||
|
||||
# Connect
|
||||
tn = telnetlib.Telnet(HOST, PORT, timeout=5)
|
||||
|
||||
# Wait for password prompt
|
||||
response = tn.read_until(b"Please enter password:", timeout=3)
|
||||
print("Got password prompt")
|
||||
|
||||
# Send password
|
||||
tn.write(PASSWORD.encode('ascii') + b"\r\n")
|
||||
|
||||
# Wait for welcome message
|
||||
time.sleep(1)
|
||||
welcome = tn.read_very_eager().decode('utf-8')
|
||||
print("Connected!\n")
|
||||
|
||||
# Commands to test
|
||||
commands = [
|
||||
'admin list',
|
||||
'lp',
|
||||
'gettime',
|
||||
'getgamepref',
|
||||
'getgamestat',
|
||||
'listents'
|
||||
]
|
||||
|
||||
for cmd in commands:
|
||||
print(f"\n{'='*80}")
|
||||
print(f"COMMAND: {cmd}")
|
||||
print('='*80)
|
||||
|
||||
# Send command
|
||||
tn.write(cmd.encode('ascii') + b"\r\n")
|
||||
|
||||
# Wait a bit for response
|
||||
time.sleep(2)
|
||||
|
||||
# Read response
|
||||
response = tn.read_very_eager().decode('utf-8')
|
||||
|
||||
print("RAW RESPONSE:")
|
||||
print(repr(response)) # Show with escape characters
|
||||
print("\nFORMATTED:")
|
||||
print(response)
|
||||
print()
|
||||
|
||||
tn.write(b"exit\r\n")
|
||||
tn.close()
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("Done! Use these responses to update the regex patterns.")
|
||||
print("="*80)
|
||||
179
bot/resources/services/test_all_commands.py
Normal file
179
bot/resources/services/test_all_commands.py
Normal file
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comprehensive telnet command tester for 7D2D server
|
||||
Tests all bot commands and validates regex patterns
|
||||
"""
|
||||
import telnetlib
|
||||
import json
|
||||
import time
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
# Load config
|
||||
config_path = Path(__file__).parent / 'bot' / 'options' / 'module_telnet.json'
|
||||
if config_path.exists():
|
||||
with open(config_path, 'r') as f:
|
||||
config = json.load(f)
|
||||
else:
|
||||
# Manual config if file doesn't exist
|
||||
config = {
|
||||
'host': input('Server IP: '),
|
||||
'port': int(input('Server Port: ')),
|
||||
'password': input('Telnet Password: ')
|
||||
}
|
||||
|
||||
print(f"\n{'='*80}")
|
||||
print(f"Connecting to {config['host']}:{config['port']}")
|
||||
print(f"{'='*80}\n")
|
||||
|
||||
# Connect
|
||||
tn = telnetlib.Telnet(config['host'], config['port'], timeout=10)
|
||||
|
||||
# Wait for password prompt
|
||||
time.sleep(0.5)
|
||||
output = tn.read_very_eager().decode('ascii', errors='ignore')
|
||||
print('[CONNECTION] Established')
|
||||
|
||||
# Send password
|
||||
tn.write((config['password'] + '\n').encode('ascii'))
|
||||
time.sleep(0.5)
|
||||
output = tn.read_very_eager().decode('ascii', errors='ignore')
|
||||
print('[AUTH] Authenticated\n')
|
||||
|
||||
# Test commands with their expected regex patterns
|
||||
commands = [
|
||||
{
|
||||
'name': 'admin list',
|
||||
'command': 'admin list',
|
||||
'regex': r"Executing\scommand\s\'admin list\'\sby\sTelnet\sfrom\s(?P<called_by>.*?)\r?\n"
|
||||
r"(?P<raw_adminlist>(?:Defined User Permissions\:.*?(?=Defined Group Permissions|$)))",
|
||||
'wait': 1,
|
||||
'description': 'Get admin list'
|
||||
},
|
||||
{
|
||||
'name': 'lp (getplayers)',
|
||||
'command': 'lp',
|
||||
'regex': r"Executing\scommand\s\'lp\'\sby\sTelnet\sfrom\s"
|
||||
r"(?P<called_by>.*?)\r?\n"
|
||||
r"(?P<raw_playerdata>[\s\S]*?)"
|
||||
r"Total\sof\s(?P<player_count>\d{1,2})\sin\sthe\sgame",
|
||||
'wait': 1,
|
||||
'description': 'Get player list'
|
||||
},
|
||||
{
|
||||
'name': 'gettime',
|
||||
'command': 'gettime',
|
||||
'regex': r"Day\s(?P<day>\d{1,5}),\s(?P<hour>\d{1,2}):(?P<minute>\d{1,2})",
|
||||
'wait': 1,
|
||||
'description': 'Get game time'
|
||||
},
|
||||
{
|
||||
'name': 'listents (getentities)',
|
||||
'command': 'listents',
|
||||
'regex': r"Executing\scommand\s\'listents\'\sby\sTelnet\sfrom\s(?P<called_by>.*?)\r?\n"
|
||||
r"(?P<raw_entity_data>[\s\S]*?)"
|
||||
r"Total\sof\s(?P<entity_count>\d{1,3})\sin\sthe\sgame",
|
||||
'wait': 1,
|
||||
'description': 'Get entity list'
|
||||
},
|
||||
{
|
||||
'name': 'getgamepref',
|
||||
'command': 'getgamepref',
|
||||
'regex': r"Executing\scommand\s\'getgamepref\'\sby\sTelnet\sfrom\s(?P<called_by>.*?)\r?\n"
|
||||
r"(?P<raw_gameprefs>(?:GamePref\..*?\r?\n)+)",
|
||||
'wait': 2,
|
||||
'description': 'Get game preferences'
|
||||
},
|
||||
{
|
||||
'name': 'getgamestat',
|
||||
'command': 'getgamestat',
|
||||
'regex': r"Executing\scommand\s\'getgamestat\'\sby\sTelnet\sfrom\s(?P<called_by>.*?)\r?\n"
|
||||
r"(?P<raw_gamestats>(?:GameStat\..*?\r?\n)+)",
|
||||
'wait': 2,
|
||||
'description': 'Get game statistics'
|
||||
},
|
||||
{
|
||||
'name': 'version',
|
||||
'command': 'version',
|
||||
'regex': None, # Just check raw output
|
||||
'wait': 1,
|
||||
'description': 'Get server version'
|
||||
},
|
||||
{
|
||||
'name': 'help',
|
||||
'command': 'help',
|
||||
'regex': None, # Just check raw output
|
||||
'wait': 2,
|
||||
'description': 'Get available commands'
|
||||
}
|
||||
]
|
||||
|
||||
results = {
|
||||
'passed': [],
|
||||
'failed': [],
|
||||
'no_regex': []
|
||||
}
|
||||
|
||||
for test in commands:
|
||||
print(f"\n{'='*80}")
|
||||
print(f"TEST: {test['name']}")
|
||||
print(f"Description: {test['description']}")
|
||||
print(f"{'='*80}")
|
||||
|
||||
# Send command
|
||||
print(f"\n>>> Sending: {test['command']}")
|
||||
tn.write((test['command'] + '\n').encode('ascii'))
|
||||
|
||||
# Wait for response
|
||||
time.sleep(test['wait'])
|
||||
output = tn.read_very_eager().decode('ascii', errors='ignore')
|
||||
|
||||
# Show raw output
|
||||
print(f"\n--- RAW OUTPUT (repr) ---")
|
||||
print(repr(output))
|
||||
print(f"\n--- RAW OUTPUT (formatted) ---")
|
||||
print(output)
|
||||
|
||||
# Test regex if provided
|
||||
if test['regex']:
|
||||
print(f"\n--- REGEX TEST ---")
|
||||
print(f"Pattern: {test['regex'][:100]}...")
|
||||
|
||||
matches = list(re.finditer(test['regex'], output, re.MULTILINE | re.DOTALL))
|
||||
|
||||
if matches:
|
||||
print(f"✓ REGEX MATCHED! ({len(matches)} match(es))")
|
||||
for i, match in enumerate(matches, 1):
|
||||
print(f"\nMatch {i}:")
|
||||
for group_name, group_value in match.groupdict().items():
|
||||
value_preview = repr(group_value)[:100]
|
||||
print(f" {group_name}: {value_preview}")
|
||||
results['passed'].append(test['name'])
|
||||
else:
|
||||
print(f"✗ REGEX FAILED - NO MATCH!")
|
||||
results['failed'].append(test['name'])
|
||||
else:
|
||||
print(f"\n--- NO REGEX TEST (raw output only) ---")
|
||||
results['no_regex'].append(test['name'])
|
||||
|
||||
print(f"\n{'='*80}\n")
|
||||
time.sleep(0.5) # Small delay between commands
|
||||
|
||||
# Close connection
|
||||
tn.close()
|
||||
|
||||
# Summary
|
||||
print(f"\n\n{'='*80}")
|
||||
print("SUMMARY")
|
||||
print(f"{'='*80}")
|
||||
print(f"✓ Passed: {len(results['passed'])} - {', '.join(results['passed']) if results['passed'] else 'none'}")
|
||||
print(f"✗ Failed: {len(results['failed'])} - {', '.join(results['failed']) if results['failed'] else 'none'}")
|
||||
print(f"- No test: {len(results['no_regex'])} - {', '.join(results['no_regex']) if results['no_regex'] else 'none'}")
|
||||
print(f"{'='*80}\n")
|
||||
|
||||
if results['failed']:
|
||||
print("⚠️ SOME TESTS FAILED - Regex patterns need to be fixed!")
|
||||
exit(1)
|
||||
else:
|
||||
print("✓ All regex tests passed!")
|
||||
exit(0)
|
||||
Reference in New Issue
Block a user