commit 4ed4d69b21e179d5794d26975a4787f04548fa41 Author: stephan Date: Sun Jun 29 07:39:33 2025 +0200 first commit diff --git a/.env b/.env new file mode 100644 index 0000000..474256b --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +TARGET_USER="stephan" +TARGET_IP="192.168.13.178" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e28018 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# Python virtual environment +venv/ + +# Environment variables diff --git a/__pycache__/main.cpython-310.pyc b/__pycache__/main.cpython-310.pyc new file mode 100644 index 0000000..a61d826 Binary files /dev/null and b/__pycache__/main.cpython-310.pyc differ diff --git a/__pycache__/netinf.cpython-310.pyc b/__pycache__/netinf.cpython-310.pyc new file mode 100644 index 0000000..6780000 Binary files /dev/null and b/__pycache__/netinf.cpython-310.pyc differ diff --git a/__pycache__/proginfo.cpython-310.pyc b/__pycache__/proginfo.cpython-310.pyc new file mode 100644 index 0000000..96922ed Binary files /dev/null and b/__pycache__/proginfo.cpython-310.pyc differ diff --git a/__pycache__/sysinfo.cpython-310.pyc b/__pycache__/sysinfo.cpython-310.pyc new file mode 100644 index 0000000..891ac8b Binary files /dev/null and b/__pycache__/sysinfo.cpython-310.pyc differ diff --git a/__pycache__/wilcon.cpython-310.pyc b/__pycache__/wilcon.cpython-310.pyc new file mode 100644 index 0000000..f16845d Binary files /dev/null and b/__pycache__/wilcon.cpython-310.pyc differ diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..a3431a3 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,171 @@ +#!/bin/bash + +# Deployment script for Wilcon Client Dashboard + +# --- Configuration --- +AGENT_USER="agent_user" +SERVICE_NAME="wilcon-agent" +BACKEND_PORT="8080" +BACKEND_SCRIPT="wilcon.py" +FRONTEND_FILE="index.html" + +# --- Functions --- +usage() { + echo "Usage: $0 [target_path]" + echo " : Username on the target device (e.g., 'ubuntu', 'pi')." + echo " : IP address or hostname of the target device." + echo " [target_path]: Optional. Full path on the target device where the project will be deployed. " + echo " Defaults to '/opt/wilcon_agent'." + exit 1 +} + +setup_ssh_key() { + echo "--- Setting up SSH Key for passwordless access ---" + # Check if ssh-keygen exists + if ! command -v ssh-keygen &>/dev/null; then + echo "Error: ssh-keygen not found. Please install OpenSSH client utilities." + exit 1 + fi + + # Check if ssh-copy-id exists + if ! command -v ssh-copy-id &>/dev/null; then + echo "Error: ssh-copy-id not found. Please install OpenSSH client utilities (often in 'openssh-client' or 'ssh-client' package)." + exit 1 + fi + + # Check if SSH key pair exists + if [ ! -f "$HOME/.ssh/id_rsa.pub" ]; then + echo " SSH key pair not found. Generating new SSH key..." + ssh-keygen -t rsa -b 4096 -f "$HOME/.ssh/id_rsa" -N "" # -N "" for no passphrase + if [ $? -ne 0 ]; then + echo "Error: Failed to generate SSH key. Aborting." + exit 1 + fi + echo " Local SSH key generated." + else + echo " Local SSH key pair already exists." + fi + + # Check if the key is already authorized on the target device, and copy it if not. + if ssh -q -o BatchMode=yes -o ConnectTimeout=5 "$TARGET_USER@$TARGET_IP" exit 2>/dev/null; then + echo " SSH key is already authorized on the target device. Skipping copy." + else + echo " Copying public key to $TARGET_USER@$TARGET_IP... (You may be prompted for the password for '$TARGET_USER' on '$TARGET_IP')" + ssh-copy-id "$TARGET_USER@$TARGET_IP" + if [ $? -ne 0 ]; then + echo "Error: Failed to copy SSH key. Please ensure SSH is enabled and the password is correct." + exit 1 + fi + echo " SSH key copied successfully." + fi + echo "" +} + +check_target_online() { + echo "Checking if target device $TARGET_IP is online..." + # Ping the target IP 3 times with a 1-second timeout per ping + if ping -c 3 -W 1 "$TARGET_IP" &>/dev/null; then + echo " Target device is online." + else + echo "Error: Target device $TARGET_IP appears to be offline or unreachable." + echo "Please ensure the device is powered on, connected to the network, and accessible via SSH." + exit 1 + fi + echo "" +} + +# --- Main Script --- + +# Check arguments +if [ -z "$1" ] || [ -z "$2" ]; then + usage +fi + +TARGET_USER="$1" +TARGET_IP="$2" +DEFAULT_TARGET_PATH="/opt/wilcon_agent" +TARGET_PATH="${3:-$DEFAULT_TARGET_PATH}" + +echo "--- Starting Deployment ---" +echo "Target User: $TARGET_USER" +echo "Target IP: $TARGET_IP" +echo "Target Path: $TARGET_PATH" +echo "" + +# Set up SSH key for passwordless access +setup_ssh_key + +# Check if the target device is online before proceeding +check_target_online + +# 1. Generate requirements.txt on the local machine +echo "1. Generating requirements.txt..." +if [ -d "venv" ]; then + source venv/bin/activate + pip freeze >requirements.txt + deactivate +else + echo "Warning: 'venv' not found. Assuming dependencies are globally installed or already in requirements.txt." + pip freeze >requirements.txt 2>/dev/null || echo "Could not generate requirements.txt. Ensure pip is installed." +fi +if [ ! -f "requirements.txt" ]; then + echo "Error: requirements.txt could not be created. Aborting." + exit 1 +fi +echo " requirements.txt generated." +echo "" + +# 2. Prepare target directory on the remote host +echo "2. Preparing target directory on $TARGET_IP..." +echo " This may ask for the sudo password for '$TARGET_USER' to create/access '$TARGET_PATH'." +REMOTE_DIR_PREP_COMMAND=" + set -e + echo 'Remote: Attempting to create directory $TARGET_PATH...' + sudo mkdir -p '$TARGET_PATH' + echo 'Remote: Attempting to set ownership for $TARGET_PATH...' + sudo chown -R $TARGET_USER:\$(id -gn $TARGET_USER) '$TARGET_PATH' + echo 'Remote: Directory preparation commands completed.' +" +if ! ssh -t "$TARGET_USER@$TARGET_IP" "$REMOTE_DIR_PREP_COMMAND"; then + echo "Error: Failed to create or set permissions for '$TARGET_PATH' on the target device." + echo "Please ensure user '$TARGET_USER' has sudo privileges and the path is correct." + exit 1 +fi +echo " Target directory prepared successfully." +echo "" + +# 3. Transfer project files to the target device +echo "3. Transferring project files to $TARGET_USER@$TARGET_IP:$TARGET_PATH..." +# Using tar to pipe files over SSH, which is more robust than 'scp -r .' +tar cf - --exclude=venv . | ssh "$TARGET_USER@$TARGET_IP" " + set -e + echo 'Remote: Navigating to $TARGET_PATH...' + cd '$TARGET_PATH' + echo 'Remote: Starting file extraction...' + tar xf - + echo 'Remote: File extraction completed.' +" +if [ $? -ne 0 ]; then + echo "Error: File transfer failed. Check SSH connectivity, permissions, and if 'tar' is installed on both systems." + exit 1 +fi +echo " Files transferred successfully." +echo "" + +# 4. Make installation script executable on the target device +echo "4. Making installation script executable on target..." +ssh "$TARGET_USER@$TARGET_IP" "chmod +x '$TARGET_PATH/install_service.sh'" +if [ $? -ne 0 ]; then + echo "Error: Failed to make install_service.sh executable." + exit 1 +fi +echo " Installation script is now executable." +echo "" + +echo "--- Deployment Complete! ---" +echo "Project files have been successfully deployed to $TARGET_IP:$TARGET_PATH" +echo "" +echo "Next Step: Log in to the target device and run the installation script:" +echo " ssh $TARGET_USER@$TARGET_IP" +echo " cd $TARGET_PATH" +echo " sudo ./install_service.sh" diff --git a/hardware.py b/hardware.py new file mode 100644 index 0000000..28953bd --- /dev/null +++ b/hardware.py @@ -0,0 +1,179 @@ +#sysinfo.py +#Version : 0.1.0 +import os +import sys +import subprocess +import cpuinfo +import json +import psutil +import platform +import distro +import socket +import logging +import argparse +from datetime import datetime + +try: + import GPUtil + gpu_available = True +except ImportError: + gpu_available = False + +# Farben für Debug-Logging +class LogColors: + INFO = "\033[94m" + DEBUG = "\033[92m" + WARNING = "\033[93m" + ERROR = "\033[91m" + RESET = "\033[0m" + +# Logging einrichten +LOG_FILE = os.path.expanduser("~/hardware_info.log") +logging.basicConfig( + filename=LOG_FILE, + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s" +) + +def log_message(level, message): + """Loggt Nachrichten mit Farben für die CLI, falls Debug aktiv ist.""" + if level == "info": + logging.info(message) + if args.debug: + print(f"{LogColors.INFO}[INFO]{LogColors.RESET} {message}") + elif level == "debug": + logging.debug(message) + if args.debug: + print(f"{LogColors.DEBUG}[DEBUG]{LogColors.RESET} {message}") + elif level == "warning": + logging.warning(message) + if args.debug: + print(f"{LogColors.WARNING}[WARNING]{LogColors.RESET} {message}") + elif level == "error": + logging.error(message) + if args.debug: + print(f"{LogColors.ERROR}[ERROR]{LogColors.RESET} {message}") + +def get_hardware_info(): + """Sammelt die relevanten Hardware- und Systeminformationen.""" + hardware_info = {} + + # Betriebssystem + log_message("info", "Sammle Betriebssystem-Daten...") + try: + os_info = { + "name": platform.system(), + "distribution": distro.name(pretty=True) + } + log_message("info", f"OS: {os_info['name']} - {os_info['distribution']}") + except Exception as e: + os_info = {} + log_message("warning", f"Konnte Betriebssystem-Daten nicht ermitteln: {str(e)}") + hardware_info["os"] = os_info + + # CPU + log_message("info", "Sammle CPU-Daten...") + try: + cpu_info = { + "model": cpuinfo.get_cpu_info().get('brand_raw', 'Unbekannt'), + "cores": psutil.cpu_count(logical=False), + "threads": psutil.cpu_count(logical=True), + "usage": psutil.cpu_percent(interval=1) + } + log_message("info", f"CPU: {cpu_info['model']} - {cpu_info['cores']} Cores, {cpu_info['threads']} Threads - {cpu_info['usage']}% Auslastung") + except Exception as e: + cpu_info = {} + log_message("warning", f"Konnte CPU-Daten nicht ermitteln: {str(e)}") + hardware_info["cpu"] = cpu_info + + # RAM + log_message("info", "Sammle RAM-Daten...") + try: + ram_info = { + "total": round(psutil.virtual_memory().total / (1024**3), 2), + "used": round(psutil.virtual_memory().used / (1024**3), 2), + "available": round(psutil.virtual_memory().available / (1024**3), 2) + } + log_message("info", f"RAM: {ram_info['total']}GB gesamt, {ram_info['used']}GB genutzt, {ram_info['available']}GB verfügbar") + except Exception as e: + ram_info = {} + log_message("warning", f"Konnte RAM-Daten nicht ermitteln: {str(e)}") + hardware_info["ram"] = ram_info + + # Festplatte + log_message("info", "Sammle Festplattendaten...") + try: + disk_info = { + "total": round(psutil.disk_usage('/').total / (1024**3), 2), + "used": round(psutil.disk_usage('/').used / (1024**3), 2), + "free": round(psutil.disk_usage('/').free / (1024**3), 2) + } + log_message("info", f"Disk: {disk_info['total']}GB gesamt, {disk_info['used']}GB genutzt, {disk_info['free']}GB frei") + except Exception as e: + disk_info = {} + log_message("warning", f"Konnte Festplatten-Daten nicht ermitteln: {str(e)}") + hardware_info["disk"] = disk_info + + log_message("info", "Sammle Netzwerkinformationen...") + try: + network_info = { + "hostname": socket.gethostname(), + "ip_address": socket.gethostbyname(socket.gethostname()) + } + log_message("info", f"Netzwerk: Hostname {network_info['hostname']}, IP {network_info['ip_address']}") + except Exception as e: + network_info = {} + log_message("warning", f"Konnte Netzwerkinformationen nicht ermitteln: {str(e)}") + hardware_info["network"] = network_info + + log_message("info", "Sammle Benutzerinformationen...") + try: + user_info = { + "logged_in": [user.name for user in psutil.users()], + "active_user": os.getlogin() + } + log_message("info", f"Benutzer: {', '.join(user_info['logged_in'])}, Aktiver User: {user_info['active_user']}") + except Exception as e: + user_info = {} + log_message("warning", f"Konnte Benutzerinformationen nicht ermitteln: {str(e)}") + hardware_info["users"] = user_info + + log_message("info", "Sammle dedizierte GPU-Daten...") + gpu_info = [] + try: + if gpu_available: + gpus = GPUtil.getGPUs() + for gpu in gpus: + gpu_info.append({ + "name": gpu.name, + "memory_total": f"{gpu.memoryTotal}MB", + "memory_used": f"{gpu.memoryUsed}MB", + "memory_free": f"{gpu.memoryFree}MB", + "load": f"{gpu.load * 100:.1f}%" + }) + log_message("info", f"Gefundene GPUs: {len(gpus)}") + else: + log_message("warning", "GPUtil nicht installiert oder keine dedizierte GPU vorhanden.") + except Exception as e: + log_message("warning", f"Konnte GPU-Daten nicht ermitteln: {str(e)}") + hardware_info["gpu"] = gpu_info + + log_message("info", "Hardware-Informationen erfolgreich erfasst.") + return hardware_info + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Ermittelt Hardware- und Systeminformationen.") + parser.add_argument("--debug", action="store_true", help="Aktiviert Debug-Logging auf der CLI.") + parser.add_argument("--json", type=str, help="Speichert die Ausgabe in einer JSON-Datei.") + args = parser.parse_args() + + # Hardware-Informationen abrufen + hardware_info = get_hardware_info() + + if args.json: + try: + with open(args.json, "w") as json_file: + json.dump(hardware_info, json_file, indent=4) + log_message("info", f"Hardware-Infos in JSON gespeichert: {args.json}") + except Exception as e: + log_message("error", f"Fehler beim Speichern der JSON-Datei: {str(e)}") diff --git a/index.html b/index.html new file mode 100644 index 0000000..ad6ba07 --- /dev/null +++ b/index.html @@ -0,0 +1,975 @@ + + + + + + Wilcon Client Dashboard + + + + +
+

Wilcon Client Dashboard

+
Lade Systemdaten...
+ + +
+

System & Hardware Informationen

+
+

Hostname:

+

+ Betriebssystem: + ( + ) +

+

+ Architektur: + +

+

+ CPU: ( + Cores, Threads, + ) +

+

+ RAM: GB / + GB (%) +

+

+ Disk: GB / + GB (%) +

+

Bootzeit:

+
+

Benutzerkonten

+
    +
    +
    +

    GPU Informationen

    +
    +
    +
    +
    + +
    +

    Netzwerk Informationen

    +
    +

    Hostname:

    +

    + IP-Adresse: +

    +

    + Standard-Gateway: + +

    +

    + DNS-Server: +

    +
    + Netzwerkschnittstellen +
    +
    +

    Aktive Verbindungen (Top 10):

    +
    + + + + + + + + + + + + + + + + + + + + +
    Typ + Lokale IP + + Lokaler Port + + Entfernte IP + + Entfernter Port + + Status +
    + + + + + + + + + + + +
    +
    +
    +
    + +
    + +

    Prozess Informationen ()

    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + +
    PIDNameUser + CPU (%) + + RAM (%) + + Status + + Laufzeit (s) +
    + + + + + + + +
    +
    +
    +
    + + +
    + + + + diff --git a/install_service.sh b/install_service.sh new file mode 100644 index 0000000..ba0124a --- /dev/null +++ b/install_service.sh @@ -0,0 +1,134 @@ +#!/bin/bash +# install_service.sh +# This script must be run with sudo on the target machine. +# It installs the Wilcon Agent as a systemd service. + +set -e # Exit immediately if a command exits with a non-zero status. + +echo "--- Wilcon Agent Service Installer ---" + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + echo "Please run this script with sudo: sudo ./install_service.sh" + exit 1 +fi + +# --- Configuration --- +AGENT_USER="agent_user" +SERVICE_NAME="wilcon-agent" +BACKEND_SCRIPT="wilcon.py" +FRONTEND_FILE="index.html" + +# Determine the script's own directory to find project files +# This makes the script location-independent. +INSTALL_PATH=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) +echo "Installation path detected: $INSTALL_PATH" + +# Determine the primary IP address of the machine for configuration +# This is needed for CORS and the frontend API URL. +TARGET_IP=$(hostname -I | awk '{print $1}') +if [ -z "$TARGET_IP" ]; then + echo "Error: Could not determine the IP address of this machine." + exit 1 +fi +echo "Machine IP address detected: $TARGET_IP" +BACKEND_PORT="8080" # This should match the port in wilcon.py + +LOG_DIR="/var/log/wilcon-agent" + +# --- Installation Steps --- + +# 1. Create log directory +echo " Creating log directory at $LOG_DIR..." +mkdir -p "$LOG_DIR" + +# 2. Create a non-login system user to run the service +if ! id -u "$AGENT_USER" >/dev/null 2>&1; then + echo " Creating system user '$AGENT_USER'..." + useradd -r -s /bin/false "$AGENT_USER" +else + echo " System user '$AGENT_USER' already exists." +fi + +# Set ownership of the log directory now that the user is guaranteed to exist +chown "${AGENT_USER}:${AGENT_USER}" "$LOG_DIR" + +# 3. Check for Python and install if necessary +echo " Checking for Python 3.10..." +# On Debian/Ubuntu, python3-venv is a separate package needed for creating virtual environments. +# We check for both the python command and the venv package. +if ! command -v python3.10 &>/dev/null || ! dpkg -s python3.10-venv &>/dev/null; then + echo " Python 3.10 and/or python3.10-venv not found. Attempting to install..." + apt update && apt install -y python3.10 python3.10-venv + if [ $? -ne 0 ]; then + echo " Error: Failed to install Python 3.10 and/or python3.10-venv. Please install them manually." + exit 1 + fi +fi + +# 4. Create virtual environment and install dependencies +echo " Creating Python virtual environment in $INSTALL_PATH/venv..." +echo " Ensuring clean state for virtual environment..." +rm -rf "$INSTALL_PATH/venv" +python3.10 -m venv "$INSTALL_PATH/venv" + +echo " Installing Python dependencies..." +# Activate venv and install +"$INSTALL_PATH/venv/bin/pip" install -r "$INSTALL_PATH/requirements.txt" +if [ $? -ne 0 ]; then + echo " Error: Failed to install Python dependencies. Check requirements.txt." + exit 1 +fi + +# 5. Configure application files +echo " Configuring frontend API URL in $FRONTEND_FILE..." +sed -i "s|const API_BASE_URL = \".*\";|const API_BASE_URL = \"http://${TARGET_IP}:${BACKEND_PORT}\";|" "$INSTALL_PATH/$FRONTEND_FILE" + +echo " Modifying $BACKEND_SCRIPT for network access..." +# This sed command inserts the new origin after the line containing "http://127.0.0.1:8080"," +sed -i "/\"http:\/\/127.0.0.1:8080\",/a \ \ \ \ \"http://${TARGET_IP}:${BACKEND_PORT}\"," "$INSTALL_PATH/$BACKEND_SCRIPT" + +# 6. Set final ownership for the application directory +echo " Setting final ownership for '$AGENT_USER'..." +chown -R "${AGENT_USER}:${AGENT_USER}" "$INSTALL_PATH" + +# 7. Create systemd service file +echo " Creating systemd service file..." +SERVICE_FILE_PATH="/etc/systemd/system/${SERVICE_NAME}.service" +tee "$SERVICE_FILE_PATH" >/dev/null < http://$TARGET_IP:$BACKEND_PORT/$FRONTEND_FILE" +echo "" +echo "To manage the service, you can use these commands:" +echo " sudo systemctl status $SERVICE_NAME" +echo " sudo systemctl stop $SERVICE_NAME" +echo " sudo systemctl start $SERVICE_NAME" +echo " sudo journalctl -u $SERVICE_NAME -f (to view logs)" diff --git a/main.py b/main.py new file mode 100644 index 0000000..2993e78 --- /dev/null +++ b/main.py @@ -0,0 +1,78 @@ +# main.py + +from fastapi import FastAPI +import psutil +import platform +import socket +from datetime import datetime +from fastapi.middleware.cors import CORSMiddleware + +# Eine FastAPI-Anwendung erstellen +app = FastAPI( + title="Client Agent API", + description="Ein einfacher Agent, der Systeminformationen bereitstellt.", + version="0.1.0" +) + +# Hier beginnt die CORS-Konfiguration +origins = [ + "http://localhost:5500", # Der Origin, von dem dein Frontend geladen wird + "http://127.0.0.1:5500", # Oft wird 127.0.0.1 anstelle von localhost verwendet + # Füge hier weitere Origins hinzu, falls dein Frontend von woanders geladen wird + # z.B. "http://localhost:8000" wenn du es mal direkt über uvicorn ausliefern würdest +] +app.add_middleware( + CORSMiddleware, + allow_origins=origins, # Erlaubt Anfragen von den oben definierten Origins + allow_credentials=True, # Erlaubt das Senden von Cookies (falls später benötigt) + allow_methods=["*"], # Erlaubt alle HTTP-Methoden (GET, POST, PUT, DELETE, etc.) + allow_headers=["*"], # Erlaubt alle Header in der Anfrage +) +# Hier endet die CORS-Konfiguration + +# Endpunkt für grundlegende Systeminformationen +@app.get("/api/system-info") +async def get_system_info(): + """ + Gibt grundlegende Systeminformationen des Client-Rechners zurück. + """ + boot_time_timestamp = psutil.boot_time() + boot_time_readable = datetime.fromtimestamp(boot_time_timestamp).strftime("%Y-%m-%d %H:%M:%S") + + # CPU-Auslastung (für einen kurzen Zeitraum) + cpu_percent = psutil.cpu_percent(interval=1) # Wartet 1 Sekunde für genaue Messung + + # RAM-Nutzung + ram = psutil.virtual_memory() + + # Disk-Nutzung (für das Root-Verzeichnis oder das primäre Laufwerk) + # Unter Windows könnte es 'C:\' sein, unter Linux/macOS '/' + disk_usage = psutil.disk_usage('/') + + return { + "hostname": socket.gethostname(), + "os_name": platform.system(), + "os_version": platform.release(), + "architecture": platform.machine(), + "python_version": platform.python_version(), + "boot_time": boot_time_readable, + "cpu_percent": f"{cpu_percent}%", + "ram_total_gb": round(ram.total / (1024**3), 2), + "ram_used_gb": round(ram.used / (1024**3), 2), + "ram_percent": f"{ram.percent}%", + "disk_total_gb": round(disk_usage.total / (1024**3), 2), + "disk_used_gb": round(disk_usage.used / (1024**3), 2), + "disk_percent": f"{disk_usage.percent}%" + } + +# Optional: Endpunkt für CORS, falls du das Frontend auf einer anderen URL testest +# Normalerweise nicht nötig, wenn Frontend und Backend vom selben Host und Port kommen +# from fastapi.middleware.cors import CORSMiddleware +# origins = ["http://localhost:8000", "http://127.0.0.1:8000"] # Beispiel-Origins, anpassen +# app.add_middleware( +# CORSMiddleware, +# allow_origins=origins, +# allow_credentials=True, +# allow_methods=["*"], +# allow_headers=["*"], +# ) \ No newline at end of file diff --git a/netinf.py b/netinf.py new file mode 100644 index 0000000..3697923 --- /dev/null +++ b/netinf.py @@ -0,0 +1,150 @@ +import os +import socket +import psutil +import json +import logging +import argparse + +# Globaler Debug-Status +_debug_mode = False + +def set_debug_mode(enabled: bool): + """Setzt den globalen Debug-Modus für das Modul.""" + global _debug_mode + _debug_mode = enabled + +# Farben für CLI-Ausgabe +class LogColors: + INFO = "\033[94m" + DEBUG = "\033[92m" + WARNING = "\033[93m" + ERROR = "\033[91m" + RESET = "\033[0m" + +def setup_logging(): + """Initialisiert das Logging""" + log_file = os.path.expanduser("~/netinf.log") + logging.basicConfig( + filename=log_file, + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s" + ) + +def log_message(level, message, debug_mode=False): + """Loggt Nachrichten mit Farben für die CLI.""" + if level == "info": + logging.info(message) + if debug_mode: + print(f"{LogColors.INFO}[INFO]{LogColors.RESET} {message}") + elif level == "warning": + logging.warning(message) + if debug_mode: + print(f"{LogColors.WARNING}[WARNING]{LogColors.RESET} {message}") + elif level == "error": + logging.error(message) + if debug_mode: + print(f"{LogColors.ERROR}[ERROR]{LogColors.RESET} {message}") + +def get_network_info(debug=None): + """Erfasst Netzwerkinformationen und gibt sie formatiert auf der CLI aus.""" + # Nutze den globalen Debug-Modus, wenn nicht explizit angegeben + is_debug = _debug_mode if debug is None else debug + + log_message("info", "Sammle Netzwerkinformationen...", is_debug) + network_info = {} + try: + hostname = socket.gethostname() + ip_address = socket.gethostbyname(hostname) + network_info["hostname"] = hostname + network_info["ip_address"] = ip_address + log_message("info", f"Hostname: {hostname}, IP-Adresse: {ip_address}", is_debug) + + print(f"{LogColors.INFO}Hostname:{LogColors.RESET} {hostname}") + print(f"{LogColors.INFO}IP-Adresse:{LogColors.RESET} {ip_address}") + + print(f"{LogColors.INFO}Netzwerkschnittstellen:{LogColors.RESET}") + interfaces = {} + for interface, addrs in psutil.net_if_addrs().items(): + print(f" {interface}:") + iface_info = {} + for addr in addrs: + if addr.family == socket.AF_INET: + print(f" IPv4: {addr.address}") + iface_info["ipv4"] = addr.address + elif addr.family == socket.AF_INET6: + print(f" IPv6: {addr.address}") + iface_info["ipv6"] = addr.address + elif addr.family == psutil.AF_LINK: + print(f" MAC: {addr.address}") + iface_info["mac"] = addr.address + interfaces[interface] = iface_info + network_info["interfaces"] = interfaces + + # Standard-Gateway abrufen + try: + gateway_info = os.popen("ip route | grep default").read().split()[2] + except Exception: + gateway_info = "Nicht verfügbar" + network_info["gateway"] = gateway_info + print(f"{LogColors.INFO}Standard-Gateway:{LogColors.RESET} {gateway_info}") + + # DNS-Server abrufen + dns_servers = [] + try: + dns_output = os.popen("resolvectl status | grep 'DNS Servers' | awk '{print $3}'").read().split() + if not dns_output: + dns_output = os.popen("nmcli dev show | grep 'IP4.DNS' | awk '{print $2}'").read().split() + if not dns_output: + with open("/etc/resolv.conf", "r") as f: + for line in f: + if line.startswith("nameserver"): + dns_servers.append(line.split()[1]) + else: + dns_servers = dns_output + except Exception: + dns_servers.append("Nicht verfügbar") + network_info["dns_servers"] = dns_servers + print(f"{LogColors.INFO}DNS-Server:{LogColors.RESET} {', '.join(dns_servers)}") + + connections = psutil.net_connections(kind='inet') + print(f"{LogColors.INFO}Erfasste Verbindungen ({len(connections)}):{LogColors.RESET}") + connection_list = [] + for conn in connections[:10]: # Begrenze die Anzahl der ausgegebenen Verbindungen + connection_info = { + "local_ip": conn.laddr.ip if conn.laddr else "", + "local_port": conn.laddr.port if conn.laddr else "", + "remote_ip": conn.raddr.ip if conn.raddr else "", + "remote_port": conn.raddr.port if conn.raddr else "", + "status": conn.status, + "type": "TCP" if conn.type == socket.SOCK_STREAM else "UDP" + } + connection_list.append(connection_info) + print( + f" {connection_info['type']} " + f"{connection_info['local_ip']}:{connection_info['local_port']} -> " + f"{connection_info['remote_ip']}:{connection_info['remote_port']} " + f"({connection_info['status']})" + ) + network_info["connections"] = connection_list + + except Exception as e: + log_message("error", f"Fehler beim Erfassen der Netzwerkinformationen: {e}", is_debug) + + return network_info + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Ermittelt Netzwerkinformationen.") + parser.add_argument("--debug", action="store_true", help="Aktiviert Debug-Logging auf der CLI.") + parser.add_argument("--json", type=str, help="Speichert die Ausgabe in einer JSON-Datei.") + args = parser.parse_args() + + setup_logging() + net_info = get_network_info(debug=args.debug) + + if args.json: + try: + with open(args.json, "w") as json_file: + json.dump(net_info, json_file, indent=4) + log_message("info", f"Netzwerk-Infos in JSON gespeichert: {args.json}", args.debug) + except Exception as e: + log_message("error", f"Fehler beim Speichern der JSON-Datei: {e}", args.debug) \ No newline at end of file diff --git a/proginfo.py b/proginfo.py new file mode 100644 index 0000000..bea5949 --- /dev/null +++ b/proginfo.py @@ -0,0 +1,109 @@ +import psutil +import time +import json +import logging +import argparse +import os +from collections import defaultdict +from datetime import datetime + +# Globaler Debug-Status +_debug_mode = False + +def set_debug_mode(enabled: bool): + """Setzt den globalen Debug-Modus für das Modul.""" + global _debug_mode + _debug_mode = enabled + +# Farben für CLI-Ausgabe +class LogColors: + INFO = "\033[94m" + DEBUG = "\033[92m" + WARNING = "\033[93m" + ERROR = "\033[91m" + RESET = "\033[0m" + +# Logging einrichten +LOG_FILE = os.path.expanduser("~/process_monitor.log") +logging.basicConfig( + filename=LOG_FILE, + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s" +) + +def log_message(level, message, debug_mode=False): + """Loggt Nachrichten mit Farben für die CLI.""" + if level == "info": + logging.info(message) + if debug_mode: + print(f"{LogColors.INFO}[INFO]{LogColors.RESET} {message}") + elif level == "debug": + logging.debug(message) + if debug_mode: + print(f"{LogColors.DEBUG}[DEBUG]{LogColors.RESET} {message}") + elif level == "warning": + logging.warning(message) + if debug_mode: + print(f"{LogColors.WARNING}[WARNING]{LogColors.RESET} {message}") + elif level == "error": + logging.error(message) + if debug_mode: + print(f"{LogColors.ERROR}[ERROR]{LogColors.RESET} {message}") + +def get_process_info(debug=None): + """Erfasst alle laufenden Prozesse mit CPU-, Speicher-, Benutzerinfos und Startzeit.""" + is_debug = _debug_mode if debug is None else debug + log_message("info", "Sammle Prozessinformationen...", debug_mode=is_debug) + process_info = [] + current_time = time.time() + + for proc in psutil.process_iter(['pid', 'name', 'username', 'cpu_percent', 'memory_percent', 'status', 'create_time']): + try: + start_time = datetime.fromtimestamp(proc.info['create_time']).strftime('%Y-%m-%d %H:%M:%S') + runtime_seconds = int(current_time - proc.info['create_time']) + + process_info.append({ + "pid": proc.info['pid'], + "name": proc.info['name'], + "user": proc.info['username'], + "cpu": proc.info['cpu_percent'], + "memory": proc.info['memory_percent'], + "status": proc.info['status'], + "start_time": start_time, + "runtime_seconds": runtime_seconds + }) + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + continue + + return sorted(process_info, key=lambda x: (x['name'], x['user'], -x['cpu'], -x['memory'])) + +def display_processes(process_info, debug=False): + """Formatiert und gibt die Prozessinformationen aus.""" + log_message("info", f"Gefundene Prozesse: {len(process_info)}", debug_mode=debug) + print(f"{LogColors.INFO}Laufende Prozesse:{LogColors.RESET}") + + for proc in process_info: + print(f" {LogColors.DEBUG}{proc['name']} (PID: {proc['pid']}, User: {proc['user']}){LogColors.RESET}") + print(f" CPU: {proc['cpu']}% | RAM: {proc['memory']}% | Status: {proc['status']}") + print(f" Startzeit: {proc['start_time']} | Laufzeit: {proc['runtime_seconds']} Sekunden") + +def save_process_info(process_info, filename, debug=False): + """Speichert die Prozessinformationen in einer JSON-Datei.""" + try: + with open(filename, "w") as json_file: + json.dump(process_info, json_file, indent=4) + log_message("info", f"Prozessdaten gespeichert: {filename}", debug_mode=debug) + except Exception as e: + log_message("error", f"Fehler beim Speichern der JSON-Datei: {str(e)}", debug_mode=debug) + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Überwachung laufender Prozesse mit CPU-, RAM-Nutzung und Startzeit.") + parser.add_argument("--debug", action="store_true", help="Aktiviert Debug-Logging auf der CLI.") + parser.add_argument("--json", type=str, help="Speichert die Ausgabe in einer JSON-Datei.") + args = parser.parse_args() + + process_data = get_process_info(debug=args.debug) + display_processes(process_data, debug=args.debug) + + if args.json: + save_process_info(process_data, args.json, debug=args.debug) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e85892b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,42 @@ +annotated-types==0.7.0 +anyio==4.9.0 +certifi==2025.6.15 +click==8.2.1 +dnspython==2.7.0 +email_validator==2.2.0 +exceptiongroup==1.3.0 +fastapi==0.115.13 +fastapi-cli==0.0.7 +h11==0.16.0 +httpcore==1.0.9 +httptools==0.6.4 +httpx==0.28.1 +idna==3.10 +itsdangerous==2.2.0 +Jinja2==3.1.6 +markdown-it-py==3.0.0 +MarkupSafe==3.0.2 +mdurl==0.1.2 +orjson==3.10.18 +psutil==7.0.0 +pydantic==2.11.7 +pydantic-extra-types==2.10.5 +pydantic-settings==2.9.1 +pydantic_core==2.33.2 +Pygments==2.19.1 +python-dotenv==1.1.0 +python-multipart==0.0.20 +PyYAML==6.0.2 +rich==14.0.0 +rich-toolkit==0.14.7 +shellingham==1.5.4 +sniffio==1.3.1 +starlette==0.46.2 +typer==0.16.0 +typing-inspection==0.4.1 +typing_extensions==4.14.0 +ujson==5.10.0 +uvicorn==0.34.3 +uvloop==0.21.0 +watchfiles==1.1.0 +websockets==15.0.1 diff --git a/safe_kiddo_user_control.sh b/safe_kiddo_user_control.sh new file mode 100755 index 0000000..3881033 --- /dev/null +++ b/safe_kiddo_user_control.sh @@ -0,0 +1,120 @@ +#!/bin/bash +# safe_kiddo_user_control.sh +# Deaktiviert/Aktiviert Userkonten mit optionalem Countdown, Sound und Shutdown nur bei aktivem Login +# Ausführen mit root-Rechten! + +USER_TO_MANAGE="$1" +ACTION="$2" +COUNTDOWN_MODE="$3" # optional: "countdown" +SOUND_MODE="$4" # optional: "sound" +COUNTDOWN_TIME="$5" # optional: Sekundenanzahl (default 60) + +if [[ -z "$COUNTDOWN_TIME" ]]; then + COUNTDOWN_TIME=60 +fi + +if [[ -z "$USER_TO_MANAGE" || -z "$ACTION" ]]; then + echo "Nutzung: $0 [countdown] [sound] [countdown_time_in_seconds]" + exit 1 +fi + +# Prüfe, ob notify-send verfügbar ist +if ! command -v notify-send &> /dev/null; then + echo "[WARNUNG] notify-send nicht gefunden. Bitte installiere libnotify-bin." + NOTIFY_AVAILABLE=false +else + NOTIFY_AVAILABLE=true +fi + +# Prüfe, ob ein Sound-Tool verfügbar ist +if command -v paplay &> /dev/null; then + SOUND_PLAYER="paplay" + SOUND_FILE="/usr/share/sounds/freedesktop/stereo/dialog-warning.oga" +elif command -v aplay &> /dev/null; + SOUND_PLAYER="aplay" + SOUND_FILE="/usr/share/sounds/alsa/Front_Center.wav" +elif command -v canberra-gtk-play &> /dev/null; then + SOUND_PLAYER="canberra-gtk-play" + SOUND_FILE="dialog-warning" +else + SOUND_PLAYER="" +fi + +function send_notify() { + local message="$1" + if [[ "$NOTIFY_AVAILABLE" == true ]]; then + sudo -u "$USER_TO_MANAGE" DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u $USER_TO_MANAGE)/bus notify-send "Safe Kiddo" "$message" + else + echo "[INFO] Benachrichtigung übersprungen: $message" + fi +} + +function is_user_logged_in() { + who | grep -q "^$USER_TO_MANAGE " +} + +function play_beep() { + if [[ "$SOUND_MODE" == "sound" ]]; then + if [[ "$SOUND_PLAYER" == "paplay" ]]; then + paplay "$SOUND_FILE" & + elif [[ "$SOUND_PLAYER" == "aplay" ]]; then + aplay "$SOUND_FILE" & + elif [[ "$SOUND_PLAYER" == "canberra-gtk-play" ]]; then + canberra-gtk-play -i "$SOUND_FILE" -d "SafeKiddo" & + else + echo -ne '\007' + fi + fi +} + +case "$ACTION" in + disable) + echo "[*] Deaktiviere Benutzer $USER_TO_MANAGE..." + sudo usermod -L "$USER_TO_MANAGE" + + USER_WAS_LOGGED_IN=false + + if is_user_logged_in; then + USER_WAS_LOGGED_IN=true + echo "[*] Benutzer ist eingeloggt." + + if [[ "$COUNTDOWN_MODE" == "countdown" ]]; then + echo "[*] Starte dramatischen Countdown über $COUNTDOWN_TIME Sekunden..." + for ((i=COUNTDOWN_TIME; i>0; i--)); do + send_notify "ACHTUNG! Shutdown in $i Sekunden!" + play_beep + sleep 1 + done + else + echo "[*] Schicke Warnung (ohne Countdown)..." + send_notify "Shutdown in $COUNTDOWN_TIME Sekunden! Speicher dein Spiel!" + echo "[*] Warten für $COUNTDOWN_TIME Sekunden..." + sleep "$COUNTDOWN_TIME" + fi + else + echo "[*] Benutzer ist NICHT eingeloggt. Countdown und Shutdown werden übersprungen." + fi + + echo "[*] Benutzer abmelden (falls noch eingeloggt)..." + sudo pkill -KILL -u "$USER_TO_MANAGE" || true # || true, damit es keinen Fehler gibt, wenn der User nicht gefunden wird + + if [[ "$USER_WAS_LOGGED_IN" == true ]]; then + echo "[*] Rechner wird jetzt heruntergefahren..." + sudo shutdown now + else + echo "[*] Kein aktiver Login – kein Shutdown." + fi + ;; + + enable) + echo "[*] Aktiviere Benutzer $USER_TO_MANAGE..." + sudo usermod -U "$USER_TO_MANAGE" + echo "[*] Benutzer $USER_TO_MANAGE kann sich wieder einloggen." + ;; + + *) + echo "Ungültige Aktion: $ACTION" + echo "Erlaubt sind: disable oder enable" + exit 1 + ;; +esac \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..ea25615 --- /dev/null +++ b/style.css @@ -0,0 +1,85 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 20px; + background-color: #f4f7f6; + color: #333; + display: flex; + justify-content: center; + align-items: flex-start; + min-height: 100vh; +} + +.container { + background-color: #ffffff; + padding: 30px; + border-radius: 8px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + max-width: 800px; + width: 100%; + text-align: center; +} + +h1 { + color: #2c3e50; + margin-bottom: 25px; +} + +h2 { + color: #34495e; + margin-top: 25px; + margin-bottom: 15px; + border-bottom: 1px solid #eee; + padding-bottom: 5px; +} + +.info-card { + background-color: #ecf0f1; + border-left: 5px solid #3498db; + padding: 15px 20px; + margin-bottom: 20px; + border-radius: 5px; + text-align: left; +} + +.info-card p { + margin: 8px 0; + line-height: 1.6; +} + +.info-card strong { + color: #2c3e50; + display: inline-block; + width: 150px; /* Für bessere Ausrichtung */ +} + +.message { + padding: 10px; + margin-bottom: 20px; + border-radius: 5px; + font-weight: bold; + color: #555; + background-color: #e9ecef; +} + +.message.error { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +button { + background-color: #28a745; + color: white; + padding: 10px 20px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 16px; + margin-top: 20px; + transition: background-color 0.3s ease; +} + +button:hover { + background-color: #218838; +} diff --git a/sysinfo.py b/sysinfo.py new file mode 100644 index 0000000..c322ddc --- /dev/null +++ b/sysinfo.py @@ -0,0 +1,375 @@ +# sysinfo.py (angepasst ohne cpuinfo und distro) +# Version : 0.1.2 (aktualisiert) + +import os +import sys +import subprocess +import json +import psutil +import platform +import socket +import logging +import argparse +from datetime import datetime + +# Nur versuchen zu importieren, wenn vorhanden +try: + import GPUtil + gpu_available = True +except ImportError: + gpu_available = False + +# Versuche cpuinfo zu importieren +try: + import cpuinfo + cpuinfo_available = True +except ImportError: + cpuinfo_available = False + +# Versuche distro zu importieren +try: + import distro + distro_available = True +except ImportError: + distro_available = False + + +# Globaler Debug-Status +_debug_mode_global = False + +def set_debug_mode(mode: bool): + global _debug_mode_global + _debug_mode_global = mode + +# Farben für Debug-Logging +class LogColors: + INFO = "\033[94m" + DEBUG = "\033[92m" + WARNING = "\033[93m" + ERROR = "\033[91m" + RESET = "\033[0m" + +# Logging einrichten +LOG_FILE = os.path.expanduser("~/hardware_info.log") +if not logging.getLogger().handlers: # Verhindert doppelte Handler bei --reload + logging.basicConfig( + filename=LOG_FILE, + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s" + ) + +def log_message(level, message, debug_mode=False): # args.debug wird hier nicht direkt übergeben + """Loggt Nachrichten mit Farben für die CLI, falls Debug aktiv ist.""" + if level == "info": + logging.info(message) + if debug_mode: # Verwendet den lokalen debug_mode Parameter + print(f"{LogColors.INFO}[INFO]{LogColors.RESET} {message}") + elif level == "debug": + logging.debug(message) + if debug_mode: + print(f"{LogColors.DEBUG}[DEBUG]{LogColors.RESET} {message}") + elif level == "warning": + logging.warning(message) + if debug_mode: + print(f"{LogColors.WARNING}[WARNING]{LogColors.RESET} {message}") + elif level == "error": + logging.error(message) + if debug_mode: + print(f"{LogColors.ERROR}[ERROR]{LogColors.RESET} {message}") + +def _get_last_login_linux(username, debug_mode=False): + """ + Ermittelt den letzten Login-Zeitpunkt für einen Benutzer unter Linux via 'lastlog'. + Gibt einen formatierten String, "Nie", "N/A" oder "Unbekannt" zurück. + """ + try: + # Erzwinge C-Locale für eine konsistente, nicht-lokalisierte Ausgabe + env = os.environ.copy() + env['LC_ALL'] = 'C' + # Führe 'lastlog' für den spezifischen Benutzer aus + result = subprocess.run( + ['lastlog', '-u', username], + capture_output=True, + text=True, + check=False, # Nicht abbrechen, wenn der Befehl fehlschlägt + env=env + ) + + # Wenn der Befehl fehlschlägt oder keine Ausgabe hat, abbrechen + if result.returncode != 0 or not result.stdout: + return "N/A" + + lines = result.stdout.strip().split('\n') + # Erwarte Header + Benutzerzeile + if len(lines) < 2: + return "N/A" + + header = lines[0] + user_line = lines[1] + + if "**Never logged in**" in user_line: + return "Nie" + + # Finde die Startposition der "Latest"-Spalte im Header + latest_pos = header.find("Latest") + if latest_pos == -1: + return "Unbekannt (Formatfehler)" + + # Extrahiere den Zeitstempel-String + last_login_str = user_line[latest_pos:].strip() + + # Versuche, den String in ein sauberes Datumsformat zu parsen + try: + # Beispiel: "Tue May 21 10:00:15 +0200 2024" + parts = last_login_str.split() + # Baue einen String, den datetime.strptime parsen kann + date_str_to_parse = f"{parts[5]} {parts[1]} {parts[2]} {parts[3]}" # "2024 May 21 10:00:15" + dt_object = datetime.strptime(date_str_to_parse, '%Y %b %d %H:%M:%S') + return dt_object.strftime('%Y-%m-%d %H:%M:%S') + except (ValueError, IndexError): + # Wenn das Parsen fehlschlägt, gib den Originalstring zurück + return last_login_str + + except FileNotFoundError: + log_message("warning", "Befehl 'lastlog' nicht gefunden. Kann letzte Logins nicht ermitteln.", debug_mode) + return "N/A" + except Exception as e: + log_message("debug", f"Fehler beim Ermitteln des letzten Logins für {username}: {e}", debug_mode) + return "N/A" + +def get_user_accounts(debug_mode=False): + """ + Ermittelt alle lokalen Benutzerkonten und ihren Anmeldestatus, + inklusive Login-Zeit oder letztem Login. + """ + log_message("info", "Sammle Benutzerkonten-Informationen...", debug_mode) + all_users = set() + + # Aktive Benutzer und deren Login-Zeiten erfassen + active_users_info = {user.name: user.started for user in psutil.users()} + active_users = set(active_users_info.keys()) + + system_type = platform.system() + + if system_type == "Linux": + try: + with open("/etc/passwd", "r") as f: + for line in f: + if not line.startswith(('#', '\n')): # Ignoriere Kommentare und leere Zeilen + parts = line.split(':') + if len(parts) > 0: + username = parts[0] + # Ignoriere Systembenutzer, die kein Login-Shell haben + if not (username.startswith("_") or username.startswith("nobody") or username.startswith("systemd") or username.endswith("bus")): + if "/bin/false" not in line and "/sbin/nologin" not in line: + all_users.add(username) + except Exception as e: + log_message("warning", f"Konnte /etc/passwd nicht lesen: {str(e)}", debug_mode) + # Fallback, falls /etc/passwd nicht zugreifbar ist + all_users.update(active_users) + elif system_type == "Windows": + # Unter Windows ist es komplexer, alle lokalen Benutzer ohne 'net user' zu bekommen. + # Für MVP nehmen wir hier die aktiven Benutzer + ggf. 'DefaultUser' als Platzhalter + # Später könnte man subprocess.run(["net", "user"]) parsen. + all_users.update(active_users) + if "DefaultUser" not in all_users: # Ein häufiger Windows-Platzhalter + all_users.add("DefaultUser") + log_message("warning", "Detaillierte Benutzerkonten-Erfassung für Windows nicht vollständig implementiert.", debug_mode) + elif system_type == "Darwin": # macOS + # Unter macOS wäre 'dscl . -list /Users' der Weg, aber auch komplex zu parsen. + all_users.update(active_users) + log_message("warning", "Detaillierte Benutzerkonten-Erfassung für macOS nicht vollständig implementiert.", debug_mode) + else: + all_users.update(active_users) + log_message("warning", f"Benutzerkonten-Erfassung für unbekanntes OS '{system_type}' nicht vollständig implementiert.", debug_mode) + + user_statuses = [] + for user in sorted(list(all_users)): + is_logged_in = user in active_users + login_time = None + last_login = None + + if is_logged_in: + login_timestamp = active_users_info.get(user) + if login_timestamp: + login_time = datetime.fromtimestamp(login_timestamp).strftime('%Y-%m-%d %H:%M:%S') + else: + # Letzten Login für inaktive Benutzer ermitteln + if system_type == "Linux": + last_login = _get_last_login_linux(user, debug_mode) + else: + # Platzhalter für andere Betriebssysteme + last_login = "N/A" + + user_statuses.append({ + "name": user, + "is_logged_in": is_logged_in, + "login_time": login_time, + "last_login": last_login + }) + + log_message("info", f"Gefundene Benutzerkonten: {len(user_statuses)}", debug_mode) + return user_statuses + + +def get_hardware_info(): + """Sammelt die relevanten Hardware- und Systeminformationen.""" + hardware_info = {} + + # Betriebssystem + log_message("info", "Sammle Betriebssystem-Daten...", _debug_mode_global) + try: + os_name = platform.system() + os_version = platform.release() + distribution = "Unbekannt" + if distro_available: + distribution = distro.name(pretty=True) + elif os_name == "Linux": + try: + # Fallback für Linux-Distribution, falls 'distro' nicht verfügbar ist + distribution = subprocess.check_output(["lsb_release", "-d", "-s"], text=True).strip() + except (subprocess.CalledProcessError, FileNotFoundError): + try: + with open('/etc/redhat-release', 'r') as f: + distribution = f.readline().strip() + except FileNotFoundError: + distribution = platform.platform() + + os_info = { + "name": os_name, + "version": os_version, + "distribution": distribution + } + log_message("info", f"OS: {os_info['name']} - {os_info['distribution']} ({os_info['version']})", _debug_mode_global) + except Exception as e: + os_info = {"name": "Fehler", "version": "Fehler", "distribution": "Fehler"} + log_message("warning", f"Konnte Betriebssystem-Daten nicht ermitteln: {str(e)}", _debug_mode_global) + hardware_info["os"] = os_info + + # CPU + log_message("info", "Sammle CPU-Daten...", _debug_mode_global) + try: + cpu_model = "Unbekannt" + if cpuinfo_available: + cpu_model = cpuinfo.get_cpu_info().get('brand_raw', 'Unbekannt') + else: + # Fallback für CPU-Modell ohne cpuinfo + if platform.system() == "Windows": + cpu_model = platform.processor() + elif platform.system() == "Linux": + try: + with open('/proc/cpuinfo') as f: + for line in f: + if 'model name' in line: + cpu_model = line.split(':')[1].strip() + break + except Exception: + cpu_model = platform.processor() # Fallback, wenn /proc/cpuinfo nicht gelesen werden kann + elif platform.system() == "Darwin": # macOS + cpu_model = subprocess.check_output(["sysctl", "-n", "machdep.cpu.brand_string"], text=True).strip() + + + cpu_info = { + "model": cpu_model, + "cores": psutil.cpu_count(logical=False), + "threads": psutil.cpu_count(logical=True), + "usage": psutil.cpu_percent(interval=0.1) # Kurzer Interval für schnelle Antwort + } + log_message("info", f"CPU: {cpu_info['model']} - {cpu_info['cores']} Cores, {cpu_info['threads']} Threads - {cpu_info['usage']}% Auslastung", _debug_mode_global) + except Exception as e: + cpu_info = {"model": "Fehler", "cores": 0, "threads": 0, "usage": 0.0} + log_message("warning", f"Konnte CPU-Daten nicht ermitteln: {str(e)}", _debug_mode_global) + hardware_info["cpu"] = cpu_info + + # RAM + log_message("info", "Sammle RAM-Daten...", _debug_mode_global) + try: + ram = psutil.virtual_memory() + ram_info = { + "total": round(ram.total / (1024**3), 2), + "used": round(ram.used / (1024**3), 2), + "available": round(ram.available / (1024**3), 2), + "percent": ram.percent + } + log_message("info", f"RAM: {ram_info['total']}GB gesamt, {ram_info['used']}GB genutzt, {ram_info['available']}GB verfügbar ({ram_info['percent']}%)", _debug_mode_global) + except Exception as e: + ram_info = {"total": 0.0, "used": 0.0, "available": 0.0, "percent": 0.0} + log_message("warning", f"Konnte RAM-Daten nicht ermitteln: {str(e)}", _debug_mode_global) + hardware_info["ram"] = ram_info + + # Festplatte + log_message("info", "Sammle Festplattendaten...", _debug_mode_global) + try: + # Annahme: Root-Partition für Linux/macOS, C:\ für Windows + path = '/' + if platform.system() == "Windows": + path = 'C:\\' + disk_usage = psutil.disk_usage(path) + + disk_info = { + "total": round(disk_usage.total / (1024**3), 2), + "used": round(disk_usage.used / (1024**3), 2), + "free": round(disk_usage.free / (1024**3), 2), + "percent": disk_usage.percent + } + log_message("info", f"Disk ({path}): {disk_info['total']}GB gesamt, {disk_info['used']}GB genutzt, {disk_info['free']}GB frei ({disk_info['percent']}%)", _debug_mode_global) + except Exception as e: + disk_info = {"total": 0.0, "used": 0.0, "free": 0.0, "percent": 0.0} + log_message("warning", f"Konnte Festplatten-Daten nicht ermitteln: {str(e)}", _debug_mode_global) + hardware_info["disk"] = disk_info + + log_message("info", "Sammle Netzwerkinformationen (Basis)...", _debug_mode_global) + try: + network_info = { + "hostname": socket.gethostname(), + "ip_address": socket.gethostbyname(socket.gethostname()) # Gibt nur die erste gefundene IP zurück + } + log_message("info", f"Netzwerk (Basis): Hostname {network_info['hostname']}, IP {network_info['ip_address']}", _debug_mode_global) + except Exception as e: + network_info = {"hostname": "Fehler", "ip_address": "Fehler"} + log_message("warning", f"Konnte Basis-Netzwerkinformationen nicht ermitteln: {str(e)}", _debug_mode_global) + hardware_info["network_basic"] = network_info # Um Konflikt mit netinf zu vermeiden, umbenannt + + # Hinzufügen der detaillierten Benutzerinformationen + hardware_info["user_accounts"] = get_user_accounts(_debug_mode_global) + + log_message("info", "Sammle dedizierte GPU-Daten...", _debug_mode_global) + gpu_info = [] + try: + if gpu_available: + gpus = GPUtil.getGPUs() + for gpu in gpus: + gpu_info.append({ + "name": gpu.name, + "memory_total_mb": gpu.memoryTotal, + "memory_used_mb": gpu.memoryUsed, + "memory_free_mb": gpu.memoryFree, + "load_percent": round(gpu.load * 100, 1) + }) + log_message("info", f"Gefundene GPUs: {len(gpus)}", _debug_mode_global) + else: + log_message("warning", "GPUtil nicht installiert oder keine dedizierte GPU vorhanden.", _debug_mode_global) + except Exception as e: + log_message("warning", f"Konnte GPU-Daten nicht ermitteln: {str(e)}", _debug_mode_global) + hardware_info["gpu"] = gpu_info + + log_message("info", "Hardware-Informationen erfolgreich erfasst.", _debug_mode_global) + return hardware_info + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Ermittelt Hardware- und Systeminformationen.") + parser.add_argument("--debug", action="store_true", help="Aktiviert Debug-Logging auf der CLI.") + parser.add_argument("--json", type=str, help="Speichert die Ausgabe in einer JSON-Datei.") + args = parser.parse_args() + + set_debug_mode(args.debug) + + hardware_info = get_hardware_info() + + if args.json: + try: + with open(args.json, "w") as json_file: + json.dump(hardware_info, json_file, indent=4) + log_message("info", f"Hardware-Infos in JSON gespeichert: {args.json}", args.debug) + except Exception as e: + log_message("error", f"Fehler beim Speichern der JSON-Datei: {str(e)}", args.debug) \ No newline at end of file diff --git a/user_control_agent.py b/user_control_agent.py new file mode 100644 index 0000000..69fb823 --- /dev/null +++ b/user_control_agent.py @@ -0,0 +1,275 @@ +# user_control_agent.py +import os +import sys +import subprocess +import time +import argparse +import logging +import psutil # Für is_user_logged_in und Prozessmanagement + +# Farben für CLI-Ausgabe (optional, aber nützlich für Debugging) +class LogColors: + INFO = "\033[94m" + DEBUG = "\033[92m" + WARNING = "\033[93m" + ERROR = "\033[91m" + RESET = "\033[0m" + +# Logging einrichten +LOG_FILE = os.path.expanduser("~/safe_kiddo_user_control.log") +logging.basicConfig( + filename=LOG_FILE, + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s" +) + +def log_message(level, message, debug_mode=False): + """Loggt Nachrichten und gibt sie optional farbig auf der CLI aus.""" + if level == "info": + logging.info(message) + if debug_mode: + print(f"{LogColors.INFO}[INFO]{LogColors.RESET} {message}") + elif level == "warning": + logging.warning(message) + if debug_mode: + print(f"{LogColors.WARNING}[WARNING]{LogColors.RESET} {message}") + elif level == "error": + logging.error(message) + if debug_mode: + print(f"{LogColors.ERROR}[ERROR]{LogColors.RESET} {message}") + elif level == "debug": # Hinzugefügt für detaillierteres Debugging + logging.debug(message) + if debug_mode: + print(f"{LogColors.DEBUG}[DEBUG]{LogColors.RESET} {message}") + +# --- Hilfsfunktionen zur Prüfung der Tool-Verfügbarkeit --- + +def check_command_available(cmd): + """Prüft, ob ein Systembefehl im PATH verfügbar ist.""" + return subprocess.run(['which', cmd], capture_output=True, text=True).returncode == 0 + +NOTIFY_SEND_AVAILABLE = False +SOUND_PLAYER = None +SOUND_FILE = None + +def setup_tool_availability(debug_mode=False): + """Richtet die globalen Variablen für die Tool-Verfügbarkeit ein.""" + global NOTIFY_SEND_AVAILABLE, SOUND_PLAYER, SOUND_FILE + + # Prüfe notify-send + if check_command_available('notify-send'): + NOTIFY_SEND_AVAILABLE = True + log_message("info", "notify-send gefunden.", debug_mode) + else: + log_message("warning", "notify-send nicht gefunden. Desktop-Benachrichtigungen sind deaktiviert. Bitte libnotify-bin installieren.", debug_mode) + NOTIFY_SEND_AVAILABLE = False + + # Prüfe Sound-Tools + if check_command_available('paplay'): + SOUND_PLAYER = "paplay" + SOUND_FILE = "/usr/share/sounds/freedesktop/stereo/dialog-warning.oga" + log_message("info", f"Sound-Player gefunden: {SOUND_PLAYER}", debug_mode) + elif check_command_available('aplay'): + SOUND_PLAYER = "aplay" + SOUND_FILE = "/usr/share/sounds/alsa/Front_Center.wav" + log_message("info", f"Sound-Player gefunden: {SOUND_PLAYER}", debug_mode) + elif check_command_available('canberra-gtk-play'): + SOUND_PLAYER = "canberra-gtk-play" + SOUND_FILE = "dialog-warning" # canberra-gtk-play nutzt Namen, keine Pfade + log_message("info", f"Sound-Player gefunden: {SOUND_PLAYER}", debug_mode) + else: + log_message("warning", "Kein passender Sound-Player gefunden. Sounds sind deaktiviert.", debug_mode) + SOUND_PLAYER = None + +# --- Funktionen für Aktionen --- + +def send_notify(username, message, debug_mode=False): + """Sendet eine Desktop-Benachrichtigung an den angegebenen Benutzer.""" + if not NOTIFY_SEND_AVAILABLE: + log_message("info", f"Benachrichtigung übersprungen (notify-send nicht verfügbar): {message}", debug_mode) + return + + try: + user_id = os.getuid() # Get the UID of the current user running the script + if os.name == 'posix': # Linux/macOS + # Try to get the session bus address from /run/user//bus + # This is tricky as we need the target user's UID, not the current script's UID + # If the script runs as root, os.getuid() will return 0. + # We need to get the actual user's UID for the DBUS_SESSION_BUS_ADDRESS. + # This is complex and might require 'loginctl show-user ' or parsing 'ps -eo user,args' + # For simplicity, we'll try to guess the user's UID or rely on environment if run as that user + + # For now, let's assume the user's UID is known or can be found. + # A more robust way is to get it from 'id -u ' + try: + target_uid = subprocess.check_output(['id', '-u', username], text=True).strip() + except subprocess.CalledProcessError: + log_message("error", f"Konnte UID für Benutzer '{username}' nicht ermitteln.", debug_mode) + return + + dbus_address_path = f"/run/user/{target_uid}/bus" + + # Check if the D-Bus session bus address file exists and is accessible + if not os.path.exists(dbus_address_path): + log_message("warning", f"D-Bus Session Bus Datei für Benutzer {username} nicht gefunden: {dbus_address_path}. Benachrichtigung könnte fehlschlagen.", debug_mode) + # Attempt to find it via environment of user's processes (more complex) + # For now, we proceed, but this is a common point of failure. + + # We need to ensure the command is run in the context of the user's display/session + # This typically requires running as the user with their DISPLAY/DBUS_SESSION_BUS_ADDRESS set + # which is hard when the Python script itself is run as root. + # The shell script uses 'sudo -u "$USER_TO_MANAGE" DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u $USER_TO_MANAGE)/bus notify-send' + # We mimic this: + + cmd = ['sudo', '-u', username, 'DISPLAY=:0', f'DBUS_SESSION_BUS_ADDRESS=unix:path={dbus_address_path}', 'notify-send', "Safe Kiddo", message] + log_message("debug", f"notify-send Befehl: {' '.join(cmd)}", debug_mode) + + result = subprocess.run(cmd, capture_output=True, text=True, check=False) + if result.returncode != 0: + log_message("error", f"Fehler beim Senden der Benachrichtigung an {username}: {result.stderr}", debug_mode) + else: + log_message("info", f"Benachrichtigung an {username} gesendet: '{message}'", debug_mode) + + elif os.name == 'nt': # Windows - using win10toast or similar + log_message("warning", "Desktop-Benachrichtigungen für Windows noch nicht implementiert.", debug_mode) + # You would use a library like 'win10toast' here: + # from win10toast import ToastNotifier + # ToastNotifier().show_toast("Safe Kiddo", message, duration=5) + else: + log_message("warning", f"Desktop-Benachrichtigungen für OS '{os.name}' nicht unterstützt.", debug_mode) + + except Exception as e: + log_message("error", f"Unerwarteter Fehler beim Senden der Benachrichtigung: {e}", debug_mode) + + +def play_beep_sound(debug_mode=False): + """Spielt einen Warnton ab, wenn ein Sound-Player verfügbar ist.""" + if not SOUND_PLAYER: + log_message("info", "Sound übersprungen (kein Player verfügbar).", debug_mode) + return + + try: + if SOUND_PLAYER == "canberra-gtk-play": + cmd = [SOUND_PLAYER, '-i', SOUND_FILE, '-d', "SafeKiddo"] + else: + cmd = [SOUND_PLAYER, SOUND_FILE] + + log_message("debug", f"Sound-Befehl: {' '.join(cmd)}", debug_mode) + + # subprocess.Popen, damit es im Hintergrund läuft und das Skript nicht blockiert + subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + log_message("info", "Sound abgespielt.", debug_mode) + except Exception as e: + log_message("error", f"Fehler beim Abspielen des Sounds: {e}", debug_mode) + + +def is_user_logged_in(username, debug_mode=False): + """Prüft, ob der angegebene Benutzer aktuell eingeloggt ist.""" + log_message("info", f"Prüfe, ob Benutzer '{username}' eingeloggt ist...", debug_mode) + for user in psutil.users(): + if user.name == username: + log_message("info", f"Benutzer '{username}' ist eingeloggt.", debug_mode) + return True + log_message("info", f"Benutzer '{username}' ist NICHT eingeloggt.", debug_mode) + return False + +def disable_user(username, countdown_mode, sound_mode, countdown_time, debug_mode=False): + """Deaktiviert ein Benutzerkonto und führt bei aktivem Login Countdown und Shutdown durch.""" + log_message("info", f"Deaktiviere Benutzer '{username}'...", debug_mode) + try: + # Benutzerkonto sperren + # usermod -L ist ein Linux-Befehl. Für Windows/macOS wären andere Befehle nötig. + # Hier muss das Skript mit Root-Rechten ausgeführt werden (z.B. sudo python3 user_control_agent.py) + result = subprocess.run(['sudo', 'usermod', '-L', username], capture_output=True, text=True, check=True) + log_message("info", f"Benutzer '{username}' Konto gesperrt: {result.stdout.strip()}", debug_mode) + except subprocess.CalledProcessError as e: + log_message("error", f"Fehler beim Sperren des Benutzers '{username}': {e.stderr.strip()}", debug_mode) + return False # Abbruch bei Fehler + + user_was_logged_in = is_user_logged_in(username, debug_mode) + + if user_was_logged_in: + log_message("info", f"Benutzer '{username}' ist eingeloggt. Starte Countdown/Shutdown-Prozess...", debug_mode) + + if countdown_mode: + log_message("info", f"Starte dramatischen Countdown über {countdown_time} Sekunden...", debug_mode) + for i in range(countdown_time, 0, -1): + send_notify(username, f"ACHTUNG! Shutdown in {i} Sekunden!", debug_mode) + if sound_mode: + play_beep_sound(debug_mode) + time.sleep(1) + else: + log_message("info", "Sende Warnung (ohne Countdown)...", debug_mode) + send_notify(username, f"Shutdown in {countdown_time} Sekunden! Speicher deine Arbeit/Spiel!", debug_mode) + log_message("info", f"Warten für {countdown_time} Sekunden...", debug_mode) + time.sleep(countdown_time) + + log_message("info", f"Benutzer '{username}' abmelden (falls noch eingeloggt)...", debug_mode) + try: + # Beende alle Prozesse des Benutzers. psutil ist hier robuster. + for proc in psutil.process_iter(['pid', 'name', 'username']): + if proc.info['username'] == username: + try: + proc.kill() + log_message("debug", f"Prozess {proc.info['name']} (PID: {proc.info['pid']}) beendet.", debug_mode) + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + log_message("debug", f"Konnte Prozess {proc.info['name']} (PID: {proc.info['pid']}) nicht beenden (bereits beendet/Zugriff verweigert).", debug_mode) + + # Alternative (weniger robust, da shell-Befehl): subprocess.run(['sudo', 'pkill', '-KILL', '-u', username], check=False) + log_message("info", f"Alle Prozesse von Benutzer '{username}' beendet.", debug_mode) + except Exception as e: + log_message("error", f"Fehler beim Beenden der Prozesse für Benutzer '{username}': {e}", debug_mode) + + log_message("info", "Rechner wird jetzt heruntergefahren...", debug_mode) + try: + # Shutdown-Befehl - erfordert ebenfalls Root-Rechte + subprocess.run(['sudo', 'shutdown', 'now'], check=True) + log_message("info", "Shutdown-Befehl gesendet.", debug_mode) + except subprocess.CalledProcessError as e: + log_message("error", f"Fehler beim Senden des Shutdown-Befehls: {e.stderr.strip()}", debug_mode) + return False # Fehler beim Shutdown + + else: + log_message("info", f"Benutzer '{username}' ist NICHT eingeloggt. Countdown und Shutdown werden übersprungen.", debug_mode) + log_message("info", f"Benutzer '{username}' wurde deaktiviert.", debug_mode) + + return True # Aktion erfolgreich (unabhängig vom Shutdown) + + +def enable_user(username, debug_mode=False): + """Aktiviert ein Benutzerkonto.""" + log_message("info", f"Aktiviere Benutzer '{username}'...", debug_mode) + try: + result = subprocess.run(['sudo', 'usermod', '-U', username], capture_output=True, text=True, check=True) + log_message("info", f"Benutzer '{username}' Konto entsperrt: {result.stdout.strip()}", debug_mode) + return True + except subprocess.CalledProcessError as e: + log_message("error", f"Fehler beim Entsperren des Benutzers '{username}': {e.stderr.strip()}", debug_mode) + return False + +# --- Haupt-Logik bei direktem Ausführen (CLI) --- +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Deaktiviert/Aktiviert Benutzerkonten mit optionalem Countdown und Shutdown.") + parser.add_argument("username", help="Der Benutzername, der verwaltet werden soll.") + parser.add_argument("action", choices=["disable", "enable"], help="Die Aktion: 'disable' oder 'enable'.") + parser.add_argument("--countdown", action="store_true", help="Führt einen Countdown mit Benachrichtigungen vor dem Shutdown durch.") + parser.add_argument("--sound", action="store_true", help="Spielt einen Warnton während des Countdowns ab.") + parser.add_argument("--time", type=int, default=60, help="Dauer des Countdowns in Sekunden (Standard: 60).") + parser.add_argument("--debug", action="store_true", help="Aktiviert detailliertes Debug-Logging auf der CLI.") + + args = parser.parse_args() + + # Debug-Modus für Log-Nachrichten setzen + set_debug_mode(args.debug) + + # Verfügbarkeit von System-Tools prüfen + setup_tool_availability(args.debug) + + if args.action == "disable": + log_message("info", f"Beginne 'disable' Aktion für Benutzer '{args.username}'.", args.debug) + disable_user(args.username, args.countdown, args.sound, args.time, args.debug) + elif args.action == "enable": + log_message("info", f"Beginne 'enable' Aktion für Benutzer '{args.username}'.", args.debug) + enable_user(args.username, args.debug) + + log_message("info", f"Aktion für Benutzer '{args.username}' abgeschlossen.", args.debug) \ No newline at end of file diff --git a/wilcon.py b/wilcon.py new file mode 100644 index 0000000..6642f06 --- /dev/null +++ b/wilcon.py @@ -0,0 +1,55 @@ +# wilcon.py +# version 0.3.1 (aktualisiert) + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from sysinfo import get_hardware_info, set_debug_mode as set_sysinfo_debug_mode +from netinf import get_network_info, set_debug_mode as set_netinf_debug_mode +from proginfo import get_process_info, set_debug_mode as set_proginfo_debug_mode + +app = FastAPI( + title="Wilcon Client Agent", + description="API für Client-System-, Netzwerk- und Prozessinformationen.", + version="0.3.1" +) + +# CORS-Konfiguration für wilcon.py (auf Port 8080) +origins = [ + "http://localhost:5500", # Für Live Server in VS Code + "http://127.0.0.1:5500", # Für Live Server in VS Code + "http://localhost:8080", # Wenn du später das Frontend direkt über Uvicorn ausliefern willst + "http://127.0.0.1:8080", + # Füge hier die IP-Adresse oder den Hostnamen deines Client-Rechners hinzu, + # wenn du von einem anderen Gerät im Netzwerk zugreifen möchtest, z.B. "http://192.168.1.100:5500" +] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Optional: Setze den Debug-Modus für die importierten Module. +# Dies wird die farbige CLI-Ausgabe in dem Terminal aktivieren, in dem uvicorn läuft. +# Nützlich für die Entwicklung. +# set_sysinfo_debug_mode(True) +# set_netinf_debug_mode(True) +# set_proginfo_debug_mode(True) + + +@app.get("/sysinfo") +async def get_sysinfo_endpoint(): # Umbenannt, um Konflikt mit importierter Funktion zu vermeiden + """API-Endpunkt für Hardware- und Systeminformationen""" + return get_hardware_info() + +@app.get("/netinfo") +async def get_netinfo_endpoint(): # Umbenannt + """API-Endpunkt für Netzwerkinformationen""" + return get_network_info() + +@app.get("/procinfo") +async def get_procinfo_endpoint(): # Umbenannt + """API-Endpunkt für Prozessinformationen""" + return get_process_info() \ No newline at end of file