first commit

This commit is contained in:
stephan
2025-06-29 07:39:33 +02:00
commit 4ed4d69b21
20 changed files with 2754 additions and 0 deletions

2
.env Normal file
View File

@ -0,0 +1,2 @@
TARGET_USER="stephan"
TARGET_IP="192.168.13.178"

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Python virtual environment
venv/
# Environment variables

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

171
deploy.sh Executable file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()