first commit
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# Python virtual environment
|
||||
venv/
|
||||
|
||||
# Environment variables
|
||||
BIN
__pycache__/main.cpython-310.pyc
Normal file
BIN
__pycache__/main.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/netinf.cpython-310.pyc
Normal file
BIN
__pycache__/netinf.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/proginfo.cpython-310.pyc
Normal file
BIN
__pycache__/proginfo.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/sysinfo.cpython-310.pyc
Normal file
BIN
__pycache__/sysinfo.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/wilcon.cpython-310.pyc
Normal file
BIN
__pycache__/wilcon.cpython-310.pyc
Normal file
Binary file not shown.
171
deploy.sh
Executable file
171
deploy.sh
Executable file
@ -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_user> <target_ip> [target_path]"
|
||||
echo " <target_user>: Username on the target device (e.g., 'ubuntu', 'pi')."
|
||||
echo " <target_ip>: 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"
|
||||
179
hardware.py
Normal file
179
hardware.py
Normal file
@ -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)}")
|
||||
975
index.html
Normal file
975
index.html
Normal file
@ -0,0 +1,975 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Wilcon Client Dashboard</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<style>
|
||||
/* Zusätzliches Styling für Prozesse und Tabellen */
|
||||
.process-table-container {
|
||||
max-height: 400px; /* Max Höhe für scrollbare Prozessliste */
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.process-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.process-table th,
|
||||
.process-table td {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
text-align: left;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.process-table th {
|
||||
background-color: #f2f2f2;
|
||||
font-weight: bold;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1; /* Damit der Header beim Scrollen oben bleibt */
|
||||
}
|
||||
|
||||
.process-table tr:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.process-table td.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.info-card.network-card p,
|
||||
.info-card.network-card ul,
|
||||
.info-card.network-card li {
|
||||
text-align: left;
|
||||
margin: 5px 0;
|
||||
}
|
||||
.info-card.network-card ul {
|
||||
list-style-type: none;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.info-card.network-card ul ul {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
/* Styling für Benutzerliste */
|
||||
#user-accounts-list li {
|
||||
padding: 2px 0;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
/* Styling für Filter und sortierbare Spalten */
|
||||
.table-controls {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
#process-filter {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.process-table th.sortable {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.process-table th.sortable:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
.process-table th.sortable span {
|
||||
font-size: 0.8em;
|
||||
padding-left: 5px;
|
||||
}
|
||||
/* Styling for Network Connections Table */
|
||||
.connection-table-container {
|
||||
max-height: 250px; /* Max height for scrollable connection list */
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.connection-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.connection-table th,
|
||||
.connection-table td {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
text-align: left;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.connection-table th {
|
||||
background-color: #f2f2f2;
|
||||
font-weight: bold;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.connection-table tr:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.filter-row th {
|
||||
padding: 4px 8px;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
.column-filter {
|
||||
width: 100%;
|
||||
padding: 6px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
/* Styling for Interface Details */
|
||||
.interface-details {
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
background-color: #fdfdfd;
|
||||
}
|
||||
.interface-details summary {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.interface-details summary:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
.interface-details ul {
|
||||
padding: 10px 20px 10px 40px; /* Adjust padding for nested ul */
|
||||
margin: 0;
|
||||
}
|
||||
.interface-details ul li {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.subsection-details {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.subsection-details > summary {
|
||||
font-weight: bold;
|
||||
font-size: 1.17em; /* Standard h3 size */
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
background-color: #f7f7f7;
|
||||
border-radius: 4px;
|
||||
list-style: revert; /* Ensure triangle is visible */
|
||||
}
|
||||
.subsection-details > summary:hover {
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
.subsection-details > div {
|
||||
padding: 10px 0 0 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Wilcon Client Dashboard</h1>
|
||||
<div id="loading" class="message">Lade Systemdaten...</div>
|
||||
<div id="error" class="message error" style="display: none"></div>
|
||||
|
||||
<details class="info-section" open>
|
||||
<summary><h2>System & Hardware Informationen</h2></summary>
|
||||
<div id="sysInfoCard" class="info-card">
|
||||
<p><strong>Hostname:</strong> <span id="sysinfo-hostname"></span></p>
|
||||
<p>
|
||||
<strong>Betriebssystem:</strong>
|
||||
<span id="sysinfo-os-name"></span> (<span
|
||||
id="sysinfo-os-distribution"
|
||||
></span>
|
||||
<span id="sysinfo-os-version"></span>)
|
||||
</p>
|
||||
<p>
|
||||
<strong>Architektur:</strong>
|
||||
<span id="sysinfo-architecture"></span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>CPU:</strong> <span id="sysinfo-cpu-model"></span> (<span
|
||||
id="sysinfo-cpu-cores"
|
||||
></span>
|
||||
Cores, <span id="sysinfo-cpu-threads"></span> Threads,
|
||||
<span id="sysinfo-cpu-usage"></span>)
|
||||
</p>
|
||||
<p>
|
||||
<strong>RAM:</strong> <span id="sysinfo-ram-used"></span>GB /
|
||||
<span id="sysinfo-ram-total"></span>GB (<span
|
||||
id="sysinfo-ram-percent"
|
||||
></span
|
||||
>%)
|
||||
</p>
|
||||
<p>
|
||||
<strong>Disk:</strong> <span id="sysinfo-disk-used"></span>GB /
|
||||
<span id="sysinfo-disk-total"></span>GB (<span
|
||||
id="sysinfo-disk-percent"
|
||||
></span
|
||||
>%)
|
||||
</p>
|
||||
<p><strong>Bootzeit:</strong> <span id="sysinfo-boot-time"></span></p>
|
||||
<div id="sysinfo-user-accounts-section">
|
||||
<h3>Benutzerkonten</h3>
|
||||
<ul
|
||||
id="user-accounts-list"
|
||||
style="list-style-type: none; padding-left: 0"
|
||||
></ul>
|
||||
</div>
|
||||
<div id="sysinfo-gpu-info">
|
||||
<h3>GPU Informationen</h3>
|
||||
<div id="gpu-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="info-section">
|
||||
<summary><h2>Netzwerk Informationen</h2></summary>
|
||||
<div id="netInfoCard" class="info-card network-card">
|
||||
<p><strong>Hostname:</strong> <span id="netinfo-hostname"></span></p>
|
||||
<p>
|
||||
<strong>IP-Adresse:</strong> <span id="netinfo-ip-address"></span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Standard-Gateway:</strong>
|
||||
<span id="netinfo-gateway"></span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>DNS-Server:</strong> <span id="netinfo-dns-servers"></span>
|
||||
</p>
|
||||
<details class="subsection-details">
|
||||
<summary>Netzwerkschnittstellen</summary>
|
||||
<div id="netinfo-interfaces-container"></div>
|
||||
</details>
|
||||
<h3>Aktive Verbindungen (Top 10):</h3>
|
||||
<div class="connection-table-container">
|
||||
<table class="connection-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sortable" data-sort="type">Typ<span></span></th>
|
||||
<th class="sortable" data-sort="local_ip">
|
||||
Lokale IP<span></span>
|
||||
</th>
|
||||
<th class="sortable" data-sort="local_port">
|
||||
Lokaler Port<span></span>
|
||||
</th>
|
||||
<th class="sortable" data-sort="remote_ip">
|
||||
Entfernte IP<span></span>
|
||||
</th>
|
||||
<th class="sortable" data-sort="remote_port">
|
||||
Entfernter Port<span></span>
|
||||
</th>
|
||||
<th class="sortable" data-sort="status">
|
||||
Status<span></span>
|
||||
</th>
|
||||
</tr>
|
||||
<tr class="filter-row">
|
||||
<th>
|
||||
<select class="column-filter" data-filter-key="type">
|
||||
<option value="">Alle</option>
|
||||
</select>
|
||||
</th>
|
||||
<th>
|
||||
<input
|
||||
type="text"
|
||||
class="column-filter"
|
||||
data-filter-key="local_ip"
|
||||
placeholder="Filter..."
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
<input
|
||||
type="text"
|
||||
class="column-filter"
|
||||
data-filter-key="local_port"
|
||||
placeholder="Filter..."
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
<input
|
||||
type="text"
|
||||
class="column-filter"
|
||||
data-filter-key="remote_ip"
|
||||
placeholder="Filter..."
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
<input
|
||||
type="text"
|
||||
class="column-filter"
|
||||
data-filter-key="remote_port"
|
||||
placeholder="Filter..."
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
<select class="column-filter" data-filter-key="status">
|
||||
<option value="">Alle</option>
|
||||
</select>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="netinfo-connections-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="info-section">
|
||||
<summary>
|
||||
<h2>Prozess Informationen (<span id="procinfo-count"></span>)</h2>
|
||||
</summary>
|
||||
<div id="procInfoCard" class="info-card">
|
||||
<div class="process-table-container">
|
||||
<table class="process-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sort="pid" class="sortable">PID<span></span></th>
|
||||
<th data-sort="name" class="sortable">Name<span></span></th>
|
||||
<th data-sort="user" class="sortable">User<span></span></th>
|
||||
<th data-sort="cpu" class="text-right sortable">
|
||||
CPU (%)<span></span>
|
||||
</th>
|
||||
<th data-sort="memory" class="text-right sortable">
|
||||
RAM (%)<span></span>
|
||||
</th>
|
||||
<th data-sort="status" class="sortable">
|
||||
Status<span></span>
|
||||
</th>
|
||||
<th data-sort="runtime_seconds" class="sortable">
|
||||
Laufzeit (s)<span></span>
|
||||
</th>
|
||||
</tr>
|
||||
<tr class="filter-row">
|
||||
<th>
|
||||
<input
|
||||
type="text"
|
||||
class="column-filter"
|
||||
data-filter-key="pid"
|
||||
placeholder="Filter..."
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
<input
|
||||
type="text"
|
||||
class="column-filter"
|
||||
data-filter-key="name"
|
||||
placeholder="Filter..."
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
<select class="column-filter" data-filter-key="user">
|
||||
<option value="">Alle Benutzer</option>
|
||||
</select>
|
||||
</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th>
|
||||
<select class="column-filter" data-filter-key="status">
|
||||
<option value="">Alle Status</option>
|
||||
</select>
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="procinfo-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<button id="refreshButton">Alle Daten aktualisieren</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE_URL = "http://localhost:8080";
|
||||
|
||||
let allProcesses = []; // Um die volle Prozessliste zu speichern
|
||||
let loggedInUsers = []; // Um angemeldete Benutzer zu speichern
|
||||
let currentSort = { key: "cpu", direction: "desc" }; // Standard-Sortierung
|
||||
let allConnections = []; // Um die volle Verbindungsliste zu speichern
|
||||
let currentConnectionSort = { key: "status", direction: "asc" }; // Standard-Sortierung für Verbindungen
|
||||
|
||||
const loadingMessage = document.getElementById("loading");
|
||||
const errorMessage = document.getElementById("error");
|
||||
const refreshButton = document.getElementById("refreshButton");
|
||||
|
||||
function displayMessage(type, message = "") {
|
||||
loadingMessage.style.display = "none";
|
||||
errorMessage.style.display = "none";
|
||||
if (type === "loading") {
|
||||
loadingMessage.textContent = message || "Lade Systemdaten...";
|
||||
loadingMessage.style.display = "block";
|
||||
} else if (type === "error") {
|
||||
errorMessage.textContent =
|
||||
message || "Ein unbekannter Fehler ist aufgetreten.";
|
||||
errorMessage.style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
function hideAllCards() {
|
||||
// document.getElementById('sysInfoCard').style.display = 'none'; // ENTFERNEN ODER AUSKOMMENTIEREN
|
||||
// document.getElementById('netInfoCard').style.display = 'none'; // ENTFERNEN ODER AUSKOMMENTIEREN
|
||||
// document.getElementById('procInfoCard').style.display = 'none'; // ENTFERNEN ODER AUSKOMMENTIEREN
|
||||
}
|
||||
|
||||
async function fetchSysInfo() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/sysinfo`);
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
`HTTP-Fehler beim Abrufen der Systeminfos! Status: ${response.status}`
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
// SYSTEM & HARDWARE INFORMATIONEN AKTUALISIEREN
|
||||
document.getElementById("sysinfo-hostname").textContent =
|
||||
data.network_basic.hostname;
|
||||
document.getElementById("sysinfo-os-name").textContent = data.os.name;
|
||||
document.getElementById("sysinfo-os-distribution").textContent =
|
||||
data.os.distribution;
|
||||
document.getElementById("sysinfo-os-version").textContent =
|
||||
data.os.version;
|
||||
// 'platform.machine()' ist ein Fallback für Architektur, falls es nicht in data.os enthalten ist
|
||||
document.getElementById("sysinfo-architecture").textContent =
|
||||
data.os.architecture || "N/A";
|
||||
|
||||
document.getElementById("sysinfo-cpu-model").textContent =
|
||||
data.cpu.model;
|
||||
document.getElementById("sysinfo-cpu-cores").textContent =
|
||||
data.cpu.cores;
|
||||
document.getElementById("sysinfo-cpu-threads").textContent =
|
||||
data.cpu.threads;
|
||||
document.getElementById(
|
||||
"sysinfo-cpu-usage"
|
||||
).textContent = `${data.cpu.usage}%`;
|
||||
|
||||
document.getElementById("sysinfo-ram-total").textContent =
|
||||
data.ram.total;
|
||||
document.getElementById("sysinfo-ram-used").textContent =
|
||||
data.ram.used;
|
||||
document.getElementById("sysinfo-ram-percent").textContent =
|
||||
data.ram.percent;
|
||||
|
||||
document.getElementById("sysinfo-disk-total").textContent =
|
||||
data.disk.total;
|
||||
document.getElementById("sysinfo-disk-used").textContent =
|
||||
data.disk.used;
|
||||
document.getElementById("sysinfo-disk-percent").textContent =
|
||||
data.disk.percent;
|
||||
|
||||
document.getElementById("sysinfo-boot-time").textContent =
|
||||
data.os.boot_time || "N/A";
|
||||
|
||||
// BENUTZERKONTEN aktualisieren
|
||||
const userAccountsList =
|
||||
document.getElementById("user-accounts-list");
|
||||
|
||||
loggedInUsers = data.user_accounts
|
||||
.filter((u) => u.is_logged_in)
|
||||
.map((u) => u.name);
|
||||
|
||||
userAccountsList.innerHTML = "";
|
||||
if (data.user_accounts && data.user_accounts.length > 0) {
|
||||
// Füge diesen Sortier-Code HIER ein:
|
||||
data.user_accounts.sort((a, b) => {
|
||||
// Angemeldete Benutzer (true) kommen vor nicht angemeldeten (false)
|
||||
if (a.is_logged_in === b.is_logged_in) {
|
||||
// Wenn gleicher Status, alphabetisch nach Name sortieren
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
return b.is_logged_in - a.is_logged_in; // true - false = 1, false - true = -1
|
||||
});
|
||||
data.user_accounts.forEach((user) => {
|
||||
const li = document.createElement("li");
|
||||
|
||||
let statusText = "";
|
||||
if (user.is_logged_in) {
|
||||
statusText = `Angemeldet seit: ${
|
||||
user.login_time || "Unbekannt"
|
||||
}`;
|
||||
} else {
|
||||
statusText = `Letzter Login: ${user.last_login || "Unbekannt"}`;
|
||||
}
|
||||
li.textContent = `${user.name} (${statusText})`;
|
||||
|
||||
li.style.color = user.is_logged_in ? "#28a745" : "#6c757d";
|
||||
li.style.fontWeight = user.is_logged_in ? "bold" : "normal";
|
||||
userAccountsList.appendChild(li);
|
||||
});
|
||||
document.getElementById(
|
||||
"sysinfo-user-accounts-section"
|
||||
).style.display = "block";
|
||||
} else {
|
||||
userAccountsList.innerHTML =
|
||||
"<li>Keine Benutzerkonten gefunden.</li>";
|
||||
document.getElementById(
|
||||
"sysinfo-user-accounts-section"
|
||||
).style.display = "block";
|
||||
}
|
||||
|
||||
// GPU Informationen aktualisieren
|
||||
const gpuListDiv = document.getElementById("gpu-list");
|
||||
gpuListDiv.innerHTML = "";
|
||||
if (data.gpu && data.gpu.length > 0) {
|
||||
data.gpu.forEach((gpu) => {
|
||||
const p = document.createElement("p");
|
||||
p.innerHTML = `<strong>${gpu.name}:</strong> ${gpu.memory_used_mb}MB / ${gpu.memory_total_mb}MB genutzt (${gpu.load_percent}%)`;
|
||||
gpuListDiv.appendChild(p);
|
||||
});
|
||||
document.getElementById("sysinfo-gpu-info").style.display = "block";
|
||||
} else {
|
||||
document.getElementById("sysinfo-gpu-info").style.display = "none";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Abrufen der Systeminformationen:", error);
|
||||
displayMessage(
|
||||
"error",
|
||||
`Fehler beim Laden der Systeminfos: ${error.message}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchNetInfo() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/netinfo`);
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
`HTTP-Fehler beim Abrufen der Netzwerkinfos! Status: ${response.status}`
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
// NETZWERK INFORMATIONEN AKTUALISIEREN
|
||||
document.getElementById("netinfo-hostname").textContent =
|
||||
data.hostname;
|
||||
document.getElementById("netinfo-ip-address").textContent =
|
||||
data.ip_address;
|
||||
document.getElementById("netinfo-gateway").textContent =
|
||||
data.gateway || "N/A";
|
||||
document.getElementById("netinfo-dns-servers").textContent =
|
||||
data.dns_servers.join(", ") || "N/A";
|
||||
|
||||
// Netzwerkschnittstellen
|
||||
const interfacesContainer = document.getElementById(
|
||||
"netinfo-interfaces-container"
|
||||
);
|
||||
interfacesContainer.innerHTML = "";
|
||||
for (const ifaceName in data.interfaces) {
|
||||
const iface = data.interfaces[ifaceName];
|
||||
const details = document.createElement("details");
|
||||
details.classList.add("interface-details"); // Add a class for potential styling
|
||||
const summary = document.createElement("summary");
|
||||
summary.textContent = ifaceName;
|
||||
details.appendChild(summary);
|
||||
|
||||
const ul = document.createElement("ul");
|
||||
ul.style.listStyleType = "none"; // Remove bullet points
|
||||
ul.style.paddingLeft = "20px"; // Indent
|
||||
if (iface.ipv4) {
|
||||
ul.innerHTML += `<li>IPv4: ${iface.ipv4}</li>`;
|
||||
}
|
||||
if (iface.ipv6) {
|
||||
ul.innerHTML += `<li>IPv6: ${iface.ipv6}</li>`;
|
||||
}
|
||||
if (iface.mac) {
|
||||
ul.innerHTML += `<li>MAC: ${iface.mac}</li>`;
|
||||
}
|
||||
details.appendChild(ul);
|
||||
interfacesContainer.appendChild(details);
|
||||
}
|
||||
|
||||
// Verbindungen speichern und Rendering anstoßen
|
||||
allConnections = data.connections || [];
|
||||
populateConnectionFilterDropdowns(allConnections);
|
||||
sortAndRenderConnections();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Fehler beim Abrufen der Netzwerkinformationen:",
|
||||
error
|
||||
);
|
||||
displayMessage(
|
||||
"error",
|
||||
`Fehler beim Laden der Netzwerkinfos: ${error.message}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchProcInfo() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/procinfo`);
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
`HTTP-Fehler beim Abrufen der Prozessinfos! Status: ${response.status}`
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
allProcesses = data; // Volle Liste speichern
|
||||
populateFilterDropdowns(allProcesses);
|
||||
sortAndRenderProcesses(); // Initial sortieren und rendern, nachdem die Dropdowns gefüllt sind
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Abrufen der Prozessinformationen:", error);
|
||||
displayMessage(
|
||||
"error",
|
||||
`Fehler beim Laden der Prozessinfos: ${error.message}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function populateConnectionFilterDropdowns(connections) {
|
||||
const types = [
|
||||
...new Set(connections.map((c) => c.type).filter(Boolean)),
|
||||
].sort();
|
||||
const statuses = [
|
||||
...new Set(connections.map((c) => c.status).filter(Boolean)),
|
||||
].sort();
|
||||
|
||||
const typeSelect = document.querySelector(
|
||||
'.connection-table select[data-filter-key="type"]'
|
||||
);
|
||||
const statusSelect = document.querySelector(
|
||||
'.connection-table select[data-filter-key="status"]'
|
||||
);
|
||||
|
||||
// Bewahre den aktuell ausgewählten Wert
|
||||
const selectedType = typeSelect.value;
|
||||
const selectedStatus = statusSelect.value;
|
||||
|
||||
typeSelect.innerHTML = '<option value="">Alle</option>';
|
||||
statusSelect.innerHTML = '<option value="">Alle</option>';
|
||||
|
||||
types.forEach((type) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = type;
|
||||
option.textContent = type;
|
||||
typeSelect.appendChild(option);
|
||||
});
|
||||
|
||||
statuses.forEach((status) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = status;
|
||||
option.textContent = status;
|
||||
statusSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function populateFilterDropdowns(processes) {
|
||||
const users = [
|
||||
...new Set(processes.map((p) => p.user).filter(Boolean)),
|
||||
].sort();
|
||||
const statuses = [
|
||||
...new Set(processes.map((p) => p.status).filter(Boolean)),
|
||||
].sort();
|
||||
|
||||
const userSelect = document.querySelector(
|
||||
'select[data-filter-key="user"]'
|
||||
);
|
||||
const statusSelect = document.querySelector(
|
||||
'select[data-filter-key="status"]'
|
||||
);
|
||||
|
||||
// Bewahre den aktuell ausgewählten Wert
|
||||
const selectedUser = userSelect.value;
|
||||
const selectedStatus = statusSelect.value;
|
||||
|
||||
userSelect.innerHTML = '<option value="">Alle Benutzer</option>';
|
||||
statusSelect.innerHTML = '<option value="">Alle Status</option>';
|
||||
|
||||
users.forEach((user) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = user;
|
||||
option.textContent = user;
|
||||
userSelect.appendChild(option);
|
||||
});
|
||||
|
||||
statuses.forEach((status) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = status;
|
||||
option.textContent = status;
|
||||
statusSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function renderConnectionTable(connections) {
|
||||
const connectionsListBody = document.getElementById(
|
||||
"netinfo-connections-list"
|
||||
);
|
||||
connectionsListBody.innerHTML = "";
|
||||
|
||||
if (connections.length > 0) {
|
||||
connections.forEach((conn) => {
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `
|
||||
<td>${conn.type}</td>
|
||||
<td>${conn.local_ip || "N/A"}</td>
|
||||
<td>${conn.local_port || "N/A"}</td>
|
||||
<td>${conn.remote_ip || "N/A"}</td>
|
||||
<td>${conn.remote_port || "N/A"}</td>
|
||||
<td>${conn.status}</td>
|
||||
`;
|
||||
connectionsListBody.appendChild(row);
|
||||
});
|
||||
} else {
|
||||
const row = document.createElement("tr");
|
||||
const hasActiveFilter = Array.from(
|
||||
document.querySelectorAll(".connection-table .column-filter")
|
||||
).some((el) => el.value);
|
||||
const message = hasActiveFilter
|
||||
? "Keine Verbindungen entsprechen dem Filter."
|
||||
: "Keine aktiven Verbindungen gefunden.";
|
||||
row.innerHTML = `<td colspan="6">${message}</td>`;
|
||||
connectionsListBody.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
function renderProcessTable(processes) {
|
||||
document.getElementById("procinfo-count").textContent =
|
||||
processes.length;
|
||||
const procListBody = document.getElementById("procinfo-list");
|
||||
procListBody.innerHTML = ""; // Vorherige Einträge löschen
|
||||
|
||||
if (processes.length > 0) {
|
||||
processes.forEach((proc) => {
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `
|
||||
<td>${proc.pid}</td>
|
||||
<td>${proc.name}</td>
|
||||
<td>${proc.user || "N/A"}</td>
|
||||
<td class="text-right">${proc.cpu.toFixed(1)}</td>
|
||||
<td class="text-right">${proc.memory.toFixed(1)}</td>
|
||||
<td>${proc.status}</td>
|
||||
<td>${proc.runtime_seconds}</td>
|
||||
`;
|
||||
procListBody.appendChild(row);
|
||||
});
|
||||
} else {
|
||||
const row = document.createElement("tr");
|
||||
const hasActiveFilter = Array.from(
|
||||
document.querySelectorAll(".column-filter")
|
||||
).some((el) => el.value);
|
||||
const message = hasActiveFilter
|
||||
? "Keine Prozesse entsprechen dem Filter."
|
||||
: "Keine Prozessinformationen verfügbar.";
|
||||
row.innerHTML = `<td colspan="7">${message}</td>`;
|
||||
procListBody.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
function sortAndRenderProcesses() {
|
||||
const { key, direction } = currentSort;
|
||||
|
||||
allProcesses.sort((a, b) => {
|
||||
const valA = a[key];
|
||||
const valB = b[key];
|
||||
|
||||
let comparison = 0;
|
||||
if (typeof valA === "string") {
|
||||
comparison = (valA || "").localeCompare(valB || "");
|
||||
} else {
|
||||
comparison = (valA || 0) - (valB || 0);
|
||||
}
|
||||
|
||||
return direction === "asc" ? comparison : -comparison;
|
||||
});
|
||||
|
||||
updateSortIndicators(".process-table", currentSort);
|
||||
applyFiltersAndRender();
|
||||
}
|
||||
|
||||
function sortAndRenderConnections() {
|
||||
const { key, direction } = currentConnectionSort;
|
||||
|
||||
allConnections.sort((a, b) => {
|
||||
const valA = a[key];
|
||||
const valB = b[key];
|
||||
|
||||
let comparison = 0;
|
||||
if (typeof valA === "string") {
|
||||
comparison = (valA || "").localeCompare(valB || "");
|
||||
} else {
|
||||
comparison = (valA || 0) - (valB || 0);
|
||||
}
|
||||
|
||||
return direction === "asc" ? comparison : -comparison;
|
||||
});
|
||||
|
||||
updateSortIndicators(".connection-table", currentConnectionSort);
|
||||
applyConnectionFiltersAndRender();
|
||||
}
|
||||
|
||||
function applyFiltersAndRender() {
|
||||
const filterValues = {};
|
||||
document
|
||||
.querySelectorAll(".process-table .column-filter")
|
||||
.forEach((input) => {
|
||||
if (input.value) {
|
||||
filterValues[input.dataset.filterKey] = input.value.toLowerCase();
|
||||
}
|
||||
});
|
||||
|
||||
const filteredProcesses = allProcesses.filter((proc) => {
|
||||
return Object.keys(filterValues).every((key) => {
|
||||
const procValue = proc[key];
|
||||
const filterValue = filterValues[key];
|
||||
return (
|
||||
procValue != null &&
|
||||
String(procValue).toLowerCase().includes(filterValue)
|
||||
);
|
||||
});
|
||||
});
|
||||
renderProcessTable(filteredProcesses);
|
||||
}
|
||||
|
||||
function applyConnectionFiltersAndRender() {
|
||||
const filterValues = {};
|
||||
document
|
||||
.querySelectorAll(".connection-table .column-filter")
|
||||
.forEach((input) => {
|
||||
if (input.value) {
|
||||
filterValues[input.dataset.filterKey] = input.value.toLowerCase();
|
||||
}
|
||||
});
|
||||
|
||||
const filteredConnections = allConnections.filter((conn) => {
|
||||
return Object.keys(filterValues).every((key) => {
|
||||
const connValue = conn[key];
|
||||
const filterValue = filterValues[key];
|
||||
return (
|
||||
connValue != null &&
|
||||
String(connValue).toLowerCase().includes(filterValue)
|
||||
);
|
||||
});
|
||||
});
|
||||
renderConnectionTable(filteredConnections);
|
||||
}
|
||||
|
||||
function updateSortIndicators(tableSelector, sortState) {
|
||||
document
|
||||
.querySelectorAll(`${tableSelector} th.sortable`)
|
||||
.forEach((th) => {
|
||||
const span = th.querySelector("span");
|
||||
if (th.dataset.sort === sortState.key) {
|
||||
span.textContent = sortState.direction === "asc" ? "▲" : "▼";
|
||||
} else {
|
||||
span.textContent = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Hauptfunktion zum Laden aller Daten
|
||||
async function loadAllData() {
|
||||
displayMessage("loading", "Lade alle Systemdaten...");
|
||||
hideAllCards(); // Alle Karten ausblenden, bevor neue Daten geladen werden
|
||||
errorMessage.style.display = "none"; // Fehler beim Neuladen verstecken
|
||||
|
||||
// Führe alle drei Fetch-Operationen parallel aus
|
||||
await Promise.all([fetchSysInfo(), fetchNetInfo(), fetchProcInfo()])
|
||||
.then(() => {
|
||||
// Setze den Standardfilter, nachdem alle Daten geladen und die Dropdowns gefüllt sind
|
||||
if (loggedInUsers.length > 0) {
|
||||
const userFilterSelect = document.querySelector(
|
||||
'select[data-filter-key="user"]'
|
||||
);
|
||||
const userExistsAsOption = Array.from(
|
||||
userFilterSelect.options
|
||||
).some((opt) => opt.value === loggedInUsers[0]);
|
||||
if (userExistsAsOption) {
|
||||
userFilterSelect.value = loggedInUsers[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Führe die initiale Sortierung und das Rendern durch, was den Standardfilter berücksichtigt
|
||||
sortAndRenderProcesses();
|
||||
|
||||
displayMessage(); // Lade-Nachricht ausblenden, wenn alles fertig
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"Ein oder mehrere Ladevorgänge sind fehlgeschlagen:",
|
||||
error
|
||||
);
|
||||
displayMessage(
|
||||
"error",
|
||||
`Nicht alle Daten konnten geladen werden: ${
|
||||
error.message
|
||||
}. Stelle sicher, dass der Agent auf Port ${
|
||||
API_BASE_URL.split(":")[2]
|
||||
} läuft.`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Daten beim Laden der Seite abrufen
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
loadAllData();
|
||||
|
||||
// Listener für Prozess-Filter
|
||||
document
|
||||
.querySelectorAll(".process-table .column-filter")
|
||||
.forEach((input) => {
|
||||
input.addEventListener("input", applyFiltersAndRender);
|
||||
});
|
||||
|
||||
// Listener für Verbindungs-Filter
|
||||
document
|
||||
.querySelectorAll(".connection-table .column-filter")
|
||||
.forEach((input) => {
|
||||
input.addEventListener("input", applyConnectionFiltersAndRender);
|
||||
});
|
||||
|
||||
// Listener für die sortierbaren Spaltenüberschriften
|
||||
document
|
||||
.querySelectorAll(".process-table th.sortable")
|
||||
.forEach((th) => {
|
||||
th.addEventListener("click", () => {
|
||||
const sortKey = th.dataset.sort;
|
||||
if (currentSort.key === sortKey) {
|
||||
currentSort.direction =
|
||||
currentSort.direction === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
currentSort.key = sortKey;
|
||||
currentSort.direction = ["name", "user", "status"].includes(
|
||||
sortKey
|
||||
)
|
||||
? "asc"
|
||||
: "desc";
|
||||
}
|
||||
sortAndRenderProcesses();
|
||||
});
|
||||
});
|
||||
|
||||
document
|
||||
.querySelectorAll(".connection-table th.sortable")
|
||||
.forEach((th) => {
|
||||
th.addEventListener("click", () => {
|
||||
const sortKey = th.dataset.sort;
|
||||
if (currentConnectionSort.key === sortKey) {
|
||||
currentConnectionSort.direction =
|
||||
currentConnectionSort.direction === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
currentConnectionSort.key = sortKey;
|
||||
currentConnectionSort.direction = [
|
||||
"local_port",
|
||||
"remote_port",
|
||||
].includes(sortKey)
|
||||
? "asc"
|
||||
: "desc";
|
||||
}
|
||||
sortAndRenderConnections();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Daten beim Klick auf den Button aktualisieren
|
||||
refreshButton.addEventListener("click", loadAllData);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
134
install_service.sh
Normal file
134
install_service.sh
Normal file
@ -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 <<EOF_SERVICE
|
||||
[Unit]
|
||||
Description=Wilcon Client Dashboard Agent
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=${AGENT_USER}
|
||||
Group=${AGENT_USER}
|
||||
WorkingDirectory=${INSTALL_PATH}
|
||||
ExecStart=${INSTALL_PATH}/venv/bin/uvicorn wilcon:app --host 0.0.0.0 --port 8080
|
||||
Restart=always
|
||||
Environment="LOG_DIR=${LOG_DIR}"
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF_SERVICE
|
||||
|
||||
# 8. Reload systemd, enable and start the service
|
||||
echo " Reloading systemd, enabling and starting service..."
|
||||
systemctl daemon-reload
|
||||
systemctl enable "$SERVICE_NAME.service"
|
||||
systemctl restart "$SERVICE_NAME.service"
|
||||
echo " Service status:"
|
||||
systemctl status "$SERVICE_NAME.service" --no-pager
|
||||
|
||||
echo ""
|
||||
echo "--- Installation Complete! ---"
|
||||
echo "The Wilcon Client Dashboard is now running as a service."
|
||||
echo "You can access the dashboard from any browser on your network at:"
|
||||
echo " => 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)"
|
||||
78
main.py
Normal file
78
main.py
Normal file
@ -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=["*"],
|
||||
# )
|
||||
150
netinf.py
Normal file
150
netinf.py
Normal file
@ -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)
|
||||
109
proginfo.py
Normal file
109
proginfo.py
Normal file
@ -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)
|
||||
42
requirements.txt
Normal file
42
requirements.txt
Normal file
@ -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
|
||||
120
safe_kiddo_user_control.sh
Executable file
120
safe_kiddo_user_control.sh
Executable file
@ -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 <username> <disable|enable> [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
|
||||
85
style.css
Normal file
85
style.css
Normal file
@ -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;
|
||||
}
|
||||
375
sysinfo.py
Normal file
375
sysinfo.py
Normal file
@ -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)
|
||||
275
user_control_agent.py
Normal file
275
user_control_agent.py
Normal file
@ -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/<UID>/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 <username>' 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 <username>'
|
||||
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)
|
||||
55
wilcon.py
Normal file
55
wilcon.py
Normal file
@ -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()
|
||||
Reference in New Issue
Block a user