feat: Remote-Execute für Linux/Bash – OS-basierte Tool-Descriptions und remote.sh Script

Remote-Verbindungen erkennen jetzt das OS des Clients und passen die Tool-Description
dynamisch an (PowerShell für Windows, Bash für Linux). Neues remote.sh Script für
Linux/macOS-Clients mit embedded Python WebSocket-Client.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.6 committed 2026-04-16 09:31:56 +02:00
1 parent 1035066a39
commit 53cee67bef
11 files changed
+381 -27

No files matched your search

+3 -2
View File
@@ -53,8 +53,9 @@ public static class ChatEndpoint
var maxIterations = settings.GetInt("MaxIterations", 100); var maxIterations = settings.GetInt("MaxIterations", 100);
var hasRemote = remoteMgr.GetBySessionId(sessionId) != null; var remoteConn = remoteMgr.GetBySessionId(sessionId);
var ctx = await chatService.PrepareContext(sessionId, userId, maxIterations, planMode: body.PlanMode, hasRemoteConnection: hasRemote); var hasRemote = remoteConn != null;
var ctx = await chatService.PrepareContext(sessionId, userId, maxIterations, planMode: body.PlanMode, hasRemoteConnection: hasRemote, remoteOs: remoteConn?.OsVersion);
if (ctx == null) if (ctx == null)
{ {
res.StatusCode = 400; res.StatusCode = 400;
+5 -5
View File
@@ -29,9 +29,9 @@ public static class RemoteEndpoint
app.MapPost("/api/remote/assign", AssignConnection); app.MapPost("/api/remote/assign", AssignConnection);
app.MapPost("/api/remote/unassign", UnassignConnection); app.MapPost("/api/remote/unassign", UnassignConnection);
// PowerShell-Script ausliefern // Scripts ausliefern
app.MapGet("/remote.ps1", (Delegate)ServeScript); app.MapGet("/remote.ps1", (Delegate)((HttpContext ctx) => ServeScript(ctx, "remote.ps1")));
app.MapGet("/remote", (Delegate)ServeScript); app.MapGet("/remote.sh", (Delegate)((HttpContext ctx) => ServeScript(ctx, "remote.sh")));
} }
private static async Task HandleWebSocket(HttpContext ctx, RemoteConnectionManager mgr) private static async Task HandleWebSocket(HttpContext ctx, RemoteConnectionManager mgr)
@@ -239,9 +239,9 @@ public static class RemoteEndpoint
return Results.Ok(new { ok = true }); return Results.Ok(new { ok = true });
} }
private static async Task<IResult> ServeScript(HttpContext ctx) private static async Task<IResult> ServeScript(HttpContext ctx, string filename)
{ {
var scriptPath = Path.Combine(AppPaths.WwwRoot, "remote.ps1"); var scriptPath = Path.Combine(AppPaths.WwwRoot, filename);
if (!File.Exists(scriptPath)) if (!File.Exists(scriptPath))
return Results.NotFound("Script nicht gefunden"); return Results.NotFound("Script nicht gefunden");
+1 -1
View File
@@ -25,7 +25,7 @@ public class AuthMiddleware
var isAutologin = path == "/api/auth/autologin" && ctx.Request.Method == "GET"; var isAutologin = path == "/api/auth/autologin" && ctx.Request.Method == "GET";
var isImageFile = path.StartsWith("/api/images/file/") && ctx.Request.Method == "GET"; var isImageFile = path.StartsWith("/api/images/file/") && ctx.Request.Method == "GET";
var isRemoteWs = path == "/api/remote"; var isRemoteWs = path == "/api/remote";
var isRemoteScript = path == "/remote.ps1"; var isRemoteScript = path == "/remote.ps1" || path == "/remote.sh";
var isInternal = path.StartsWith("/api/internal/"); var isInternal = path.StartsWith("/api/internal/");
if (!path.StartsWith("/api/") || path == "/api/auth/login" || isTelegramWebhook || isSharedGet || isAutologin || isImageFile || isRemoteWs || isRemoteScript || isInternal) if (!path.StartsWith("/api/") || path == "/api/auth/login" || isTelegramWebhook || isSharedGet || isAutologin || isImageFile || isRemoteWs || isRemoteScript || isInternal)
{ {
+7 -4
View File
@@ -64,8 +64,10 @@ public partial class AnthropicService
try try
{ {
// MCP-Config generieren (immer – für ask_user_choice, optional Memory-Tools + remote_execute) // MCP-Config generieren (immer – für ask_user_choice, optional Memory-Tools + remote_execute)
var hasRemote = remoteMgr != null && !string.IsNullOrEmpty(sessionId) && remoteMgr.GetBySessionId(sessionId) != null; var remoteConn = remoteMgr != null && !string.IsNullOrEmpty(sessionId) ? remoteMgr.GetBySessionId(sessionId) : null;
mcpConfigPath = GenerateMcpConfig(sessionId, agentId, hasRemote, unityMcpUrl, unityMcpToken); var hasRemote = remoteConn != null;
var remoteOs = remoteConn?.OsVersion ?? "";
mcpConfigPath = GenerateMcpConfig(sessionId, agentId, hasRemote, unityMcpUrl, unityMcpToken, remoteOs);
// Python-Verfügbarkeit prüfen und ggf. warnen // Python-Verfügbarkeit prüfen und ggf. warnen
if (FindPythonCommand() == null) if (FindPythonCommand() == null)
@@ -567,7 +569,7 @@ public partial class AnthropicService
/// Generiert eine temporäre MCP-Config-Datei für die Claude CLI. /// Generiert eine temporäre MCP-Config-Datei für die Claude CLI.
/// Enthält den MCP-Server für remote_execute. /// Enthält den MCP-Server für remote_execute.
/// </summary> /// </summary>
private string GenerateMcpConfig(string sessionId, string agentId = null, bool hasRemote = false, string unityMcpUrl = null, string unityMcpToken = null) private string GenerateMcpConfig(string sessionId, string agentId = null, bool hasRemote = false, string unityMcpUrl = null, string unityMcpToken = null, string remoteOs = null)
{ {
// Port aus Konfiguration oder Default // Port aus Konfiguration oder Default
var port = 19000; var port = 19000;
@@ -587,7 +589,8 @@ public partial class AnthropicService
["GOTT_SESSION_ID"] = sessionId ?? "", ["GOTT_SESSION_ID"] = sessionId ?? "",
["GOTT_AUTH_TOKEN"] = Endpoints.InternalEndpoint.InternalToken, ["GOTT_AUTH_TOKEN"] = Endpoints.InternalEndpoint.InternalToken,
["GOTT_AGENT_ID"] = agentId ?? "", ["GOTT_AGENT_ID"] = agentId ?? "",
["GOTT_HAS_REMOTE"] = hasRemote ? "true" : "false" ["GOTT_HAS_REMOTE"] = hasRemote ? "true" : "false",
["GOTT_REMOTE_OS"] = remoteOs ?? ""
}; };
var pythonCmd = FindPythonCommand(); var pythonCmd = FindPythonCommand();
+3 -1
View File
@@ -101,7 +101,8 @@ public partial class AnthropicService
if (systemPrompt != null && systemPrompt.Count > 0) if (systemPrompt != null && systemPrompt.Count > 0)
payload["system"] = systemPrompt; payload["system"] = systemPrompt;
var hasRemote = remoteMgr?.GetBySessionId(sessionId) != null; var remoteConn = remoteMgr?.GetBySessionId(sessionId);
var hasRemote = remoteConn != null;
AddToolsToPayload(payload, new ToolPayloadOptions AddToolsToPayload(payload, new ToolPayloadOptions
{ {
Workspace = workspace, Workspace = workspace,
@@ -113,6 +114,7 @@ public partial class AnthropicService
AllowUserChoice = allowUserChoice, AllowUserChoice = allowUserChoice,
PlanMode = planMode, PlanMode = planMode,
IncludeRemote = hasRemote, IncludeRemote = hasRemote,
RemoteOs = remoteConn?.OsVersion,
IncludeGitHubSearch = useGitHubSearch && _toolService.HasGitHubToken, IncludeGitHubSearch = useGitHubSearch && _toolService.HasGitHubToken,
IncludeSerperSearch = useSerperSearch && _toolService.HasSerperKey IncludeSerperSearch = useSerperSearch && _toolService.HasSerperKey
}); });
+2 -1
View File
@@ -31,6 +31,7 @@ public partial class AnthropicService
public bool AllowUserChoice { get; init; } public bool AllowUserChoice { get; init; }
public bool PlanMode { get; init; } public bool PlanMode { get; init; }
public bool IncludeRemote { get; init; } public bool IncludeRemote { get; init; }
public string RemoteOs { get; init; }
public bool IncludeGitHubSearch { get; init; } public bool IncludeGitHubSearch { get; init; }
public bool IncludeSerperSearch { get; init; } public bool IncludeSerperSearch { get; init; }
} }
@@ -337,7 +338,7 @@ public partial class AnthropicService
var allTools = _toolService.GetToolDefinitions(new ToolDefinitionOptions var allTools = _toolService.GetToolDefinitions(new ToolDefinitionOptions
{ {
IncludeFileTools = hasWorkspace, IncludeMemory = includeMemory, IncludeFileTools = hasWorkspace, IncludeMemory = includeMemory,
IncludeUserChoice = opts.AllowUserChoice, IncludeRemote = opts.IncludeRemote, IncludeUserChoice = opts.AllowUserChoice, IncludeRemote = opts.IncludeRemote, RemoteOs = opts.RemoteOs,
IncludeMemorySearch = includeMemorySearch, IncludeSendToSession = includeSendToSession, IncludeMemorySearch = includeMemorySearch, IncludeSendToSession = includeSendToSession,
IncludeGitHubSearch = opts.IncludeGitHubSearch, IncludeSerperSearch = opts.IncludeSerperSearch IncludeGitHubSearch = opts.IncludeGitHubSearch, IncludeSerperSearch = opts.IncludeSerperSearch
}); });
+2 -2
View File
@@ -51,7 +51,7 @@ public class ChatService
/// <summary> /// <summary>
/// Baut den vollständigen ChatContext auf: Agent, Model, SystemPrompt, Chain, Workspace etc. /// Baut den vollständigen ChatContext auf: Agent, Model, SystemPrompt, Chain, Workspace etc.
/// </summary> /// </summary>
public async Task<ChatContext> PrepareContext(string sessionId, string userId, int maxIterations = 100, bool planMode = false, bool hasRemoteConnection = false) public async Task<ChatContext> PrepareContext(string sessionId, string userId, int maxIterations = 100, bool planMode = false, bool hasRemoteConnection = false, string remoteOs = null)
{ {
var session = await _db.GetSession(sessionId); var session = await _db.GetSession(sessionId);
if (session == null || (userId != null && session.UserId != userId) || string.IsNullOrEmpty(session.AgentId)) if (session == null || (userId != null && session.UserId != userId) || string.IsNullOrEmpty(session.AgentId))
@@ -89,7 +89,7 @@ public class ChatService
? _toolService.GetToolDefinitions(new ToolDefinitionOptions ? _toolService.GetToolDefinitions(new ToolDefinitionOptions
{ {
IncludeFileTools = hasWorkspace, IncludeMemory = includeMemory, IncludeFileTools = hasWorkspace, IncludeMemory = includeMemory,
IncludeUserChoice = agent.AllowUserChoice, IncludeRemote = hasRemoteConnection, IncludeUserChoice = agent.AllowUserChoice, IncludeRemote = hasRemoteConnection, RemoteOs = remoteOs,
IncludeGitHubSearch = includeGitHubSearch, IncludeSerperSearch = includeSerperSearch IncludeGitHubSearch = includeGitHubSearch, IncludeSerperSearch = includeSerperSearch
}) })
: Array.Empty<object>(); : Array.Empty<object>();
+13 -4
View File
@@ -21,6 +21,7 @@ public class ToolDefinitionOptions
public bool IncludeMemory { get; set; } public bool IncludeMemory { get; set; }
public bool IncludeUserChoice { get; set; } = true; public bool IncludeUserChoice { get; set; } = true;
public bool IncludeRemote { get; set; } public bool IncludeRemote { get; set; }
public string RemoteOs { get; set; }
public bool IncludeMemorySearch { get; set; } public bool IncludeMemorySearch { get; set; }
public bool IncludeSendToSession { get; set; } public bool IncludeSendToSession { get; set; }
public bool IncludeGitHubSearch { get; set; } public bool IncludeGitHubSearch { get; set; }
@@ -106,7 +107,7 @@ public class ToolService
if (opts.IncludeMemorySearch) if (opts.IncludeMemorySearch)
allTools.Add(GetMemorySearchTool()); allTools.Add(GetMemorySearchTool());
if (opts.IncludeRemote) if (opts.IncludeRemote)
allTools.Add(GetRemoteExecuteTool()); allTools.Add(GetRemoteExecuteTool(opts.RemoteOs));
if (opts.IncludeSendToSession) if (opts.IncludeSendToSession)
allTools.Add(GetSendToSessionTool()); allTools.Add(GetSendToSessionTool());
if (opts.IncludeGitHubSearch) if (opts.IncludeGitHubSearch)
@@ -352,18 +353,26 @@ public class ToolService
}; };
} }
private object GetRemoteExecuteTool() private object GetRemoteExecuteTool(string remoteOs = null)
{ {
var isLinux = (remoteOs ?? "").Contains("Linux", StringComparison.OrdinalIgnoreCase);
var desc = isLinux
? "Führt einen Bash-Befehl auf der verbundenen Remote-Linux-Workstation aus. Gibt stdout und stderr zurück. Nutze dies für Diagnose und Administration des Remote-PCs (z.B. Netzwerk, Services, Logs, Pakete). Timeout: 180 Sekunden."
: "Führt einen PowerShell-Befehl auf der verbundenen Remote-Windows-Workstation aus. Gibt stdout und stderr zurück. Nutze dies für Diagnose und Administration des Remote-PCs (z.B. Druckerprobleme, Netzwerk, Services, Registry). Timeout: 180 Sekunden.";
var cmdDesc = isLinux
? "Bash-Befehl zum Ausführen auf dem Remote-PC"
: "PowerShell-Befehl zum Ausführen auf dem Remote-PC";
return new return new
{ {
name = "remote_execute", name = "remote_execute",
description = "Führt einen PowerShell-Befehl auf der verbundenen Remote-Windows-Workstation aus. Gibt stdout und stderr zurück. Nutze dies für Diagnose und Administration des Remote-PCs (z.B. Druckerprobleme, Netzwerk, Services, Registry). Timeout: 180 Sekunden.", description = desc,
input_schema = new input_schema = new
{ {
type = "object", type = "object",
properties = new Dictionary<string, object> properties = new Dictionary<string, object>
{ {
["command"] = new { type = "string", description = "PowerShell-Befehl zum Ausführen auf dem Remote-PC" } ["command"] = new { type = "string", description = cmdDesc }
}, },
required = new[] { "command" } required = new[] { "command" }
} }
+17 -5
View File
@@ -21,22 +21,34 @@ SESSION_ID = os.environ.get("GOTT_SESSION_ID", "")
AUTH_TOKEN = os.environ.get("GOTT_AUTH_TOKEN", "") AUTH_TOKEN = os.environ.get("GOTT_AUTH_TOKEN", "")
AGENT_ID = os.environ.get("GOTT_AGENT_ID", "") AGENT_ID = os.environ.get("GOTT_AGENT_ID", "")
HAS_REMOTE = os.environ.get("GOTT_HAS_REMOTE", "false") == "true" HAS_REMOTE = os.environ.get("GOTT_HAS_REMOTE", "false") == "true"
REMOTE_OS = os.environ.get("GOTT_REMOTE_OS", "").lower() # z.B. "windows", "linux"
TOOLS = [] TOOLS = []
# Remote-Execute Tool (nur wenn Remote-Verbindung vorhanden) # Remote-Execute Tool (nur wenn Remote-Verbindung vorhanden)
if HAS_REMOTE: if HAS_REMOTE:
TOOLS.append({ if "linux" in REMOTE_OS:
"name": "remote_execute", _desc = (
"description": ( "Führt einen Bash-Befehl auf der verbundenen Remote-Linux-Workstation aus. "
"Gibt stdout und stderr zurück. Nutze dies für Diagnose und Administration des Remote-PCs "
"(z.B. Netzwerk, Services, Logs, Pakete). Timeout: 180 Sekunden."
)
_cmd_desc = "Bash-Befehl zum Ausführen auf dem Remote-PC"
else:
_desc = (
"Führt einen PowerShell-Befehl auf der verbundenen Remote-Windows-Workstation aus. " "Führt einen PowerShell-Befehl auf der verbundenen Remote-Windows-Workstation aus. "
"Gibt stdout und stderr zurück. Nutze dies für Diagnose und Administration des Remote-PCs " "Gibt stdout und stderr zurück. Nutze dies für Diagnose und Administration des Remote-PCs "
"(z.B. Druckerprobleme, Netzwerk, Services, Registry). Timeout: 180 Sekunden." "(z.B. Druckerprobleme, Netzwerk, Services, Registry). Timeout: 180 Sekunden."
), )
_cmd_desc = "PowerShell-Befehl zum Ausführen auf dem Remote-PC"
TOOLS.append({
"name": "remote_execute",
"description": _desc,
"inputSchema": { "inputSchema": {
"type": "object", "type": "object",
"properties": { "properties": {
"command": {"type": "string", "description": "PowerShell-Befehl zum Ausführen auf dem Remote-PC"} "command": {"type": "string", "description": _cmd_desc}
}, },
"required": ["command"] "required": ["command"]
} }
+4 -2
View File
@@ -324,8 +324,10 @@ async function showRemoteModal(sessionId) {
var empty = document.createElement('div'); var empty = document.createElement('div');
empty.className = 'sp-loading'; empty.className = 'sp-loading';
empty.innerHTML = '<div>Powershell-Befehl unter Windows ausführen</div>' + empty.innerHTML = '<div>Windows (PowerShell)</div>' +
'<code>irm ' + esc(location.origin) + '/remote | iex</code>'; '<code>irm ' + esc(location.origin) + '/remote.ps1 | iex</code>' +
'<div style="margin-top:8px">Linux (Bash)</div>' +
'<code>curl -s ' + esc(location.origin) + '/remote.sh | bash</code>';
body.appendChild(empty); body.appendChild(empty);
if (!connections || connections.length === 0) { if (!connections || connections.length === 0) {
+324
View File
@@ -0,0 +1,324 @@
#!/usr/bin/env bash
# GOTT Remote Agent - Linux Bash Client
# Verbindet sich per WebSocket zum GOTT-Server und fuehrt Befehle aus.
#
# Verwendung:
# curl -s https://gott.example.com/remote.sh | bash
# oder:
# chmod +x remote.sh && ./remote.sh
# oder mit eigener URL:
# WS_URL="wss://example.com/api/remote" ./remote.sh
WS_URL="${WS_URL:-{{WS_URL}}}"
# Python finden
PYTHON=""
for cmd in python3 python py; do
if command -v "$cmd" &>/dev/null; then
PYTHON="$cmd"
break
fi
done
if [ -z "$PYTHON" ]; then
echo ""
echo " [FEHLER] Python nicht gefunden!"
echo " Bitte installiere Python 3: sudo apt install python3"
echo ""
exit 1
fi
# Python-Script inline ausfuehren
exec "$PYTHON" - "$WS_URL" << 'PYTHON_SCRIPT'
import sys
import os
import json
import subprocess
import signal
import socket
import ssl
import hashlib
import base64
import struct
import time
from urllib.parse import urlparse
WS_URL = sys.argv[1] if len(sys.argv) > 1 else "ws://localhost/api/remote"
CYAN = "\033[96m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RED = "\033[91m"
GRAY = "\033[90m"
WHITE = "\033[97m"
RESET = "\033[0m"
def cprint(msg, color=""):
print(f"{color}{msg}{RESET}", flush=True)
class WebSocketClient:
"""Minimaler WebSocket-Client (RFC 6455) ohne externe Abhaengigkeiten."""
def __init__(self, url):
self.url = url
self.sock = None
self.closed = False
def connect(self):
parsed = urlparse(self.url)
use_ssl = parsed.scheme in ("wss", "https")
host = parsed.hostname
port = parsed.port or (443 if use_ssl else 80)
path = parsed.path or "/"
if parsed.query:
path += "?" + parsed.query
self.sock = socket.create_connection((host, port), timeout=10)
if use_ssl:
ctx = ssl.create_default_context()
self.sock = ctx.wrap_socket(self.sock, server_hostname=host)
# WebSocket-Handshake
key = base64.b64encode(os.urandom(16)).decode()
handshake = (
f"GET {path} HTTP/1.1\r\n"
f"Host: {host}\r\n"
f"Upgrade: websocket\r\n"
f"Connection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\n"
f"Sec-WebSocket-Version: 13\r\n"
f"\r\n"
)
self.sock.sendall(handshake.encode())
# Response lesen
response = b""
while b"\r\n\r\n" not in response:
chunk = self.sock.recv(4096)
if not chunk:
raise ConnectionError("Handshake fehlgeschlagen")
response += chunk
status_line = response.split(b"\r\n")[0].decode()
if "101" not in status_line:
raise ConnectionError(f"WebSocket-Handshake fehlgeschlagen: {status_line}")
self.sock.settimeout(None)
def send(self, data):
if self.closed:
return
payload = data.encode("utf-8")
frame = bytearray()
frame.append(0x81) # FIN + Text
length = len(payload)
if length < 126:
frame.append(0x80 | length) # Mask-Bit gesetzt
elif length < 65536:
frame.append(0x80 | 126)
frame.extend(struct.pack(">H", length))
else:
frame.append(0x80 | 127)
frame.extend(struct.pack(">Q", length))
# Masking Key (Client muss maskieren)
mask = os.urandom(4)
frame.extend(mask)
masked = bytearray(b ^ mask[i % 4] for i, b in enumerate(payload))
frame.extend(masked)
self.sock.sendall(frame)
def recv(self):
if self.closed:
return None
def read_exact(n):
data = b""
while len(data) < n:
chunk = self.sock.recv(n - len(data))
if not chunk:
raise ConnectionError("Verbindung geschlossen")
data += chunk
return data
header = read_exact(2)
opcode = header[0] & 0x0F
is_masked = header[1] & 0x80
length = header[1] & 0x7F
if opcode == 0x8: # Close
self.closed = True
return None
if opcode == 0x9: # Ping
self.send_pong()
return self.recv()
if length == 126:
length = struct.unpack(">H", read_exact(2))[0]
elif length == 127:
length = struct.unpack(">Q", read_exact(8))[0]
if is_masked:
mask = read_exact(4)
data = read_exact(length)
data = bytearray(b ^ mask[i % 4] for i, b in enumerate(data))
else:
data = read_exact(length)
return data.decode("utf-8")
def send_pong(self):
if self.closed:
return
frame = bytearray([0x8A, 0x80]) # FIN + Pong, Mask-Bit, Length 0
frame.extend(os.urandom(4)) # Mask key
self.sock.sendall(frame)
def close(self):
if self.closed:
return
self.closed = True
try:
# Close-Frame senden
frame = bytearray([0x88, 0x82]) # FIN + Close, Mask-Bit, Length 2
mask = os.urandom(4)
frame.extend(mask)
# Status 1000 (Normal Closure) maskiert
code = struct.pack(">H", 1000)
frame.extend(bytearray(b ^ mask[i % 4] for i, b in enumerate(code)))
self.sock.sendall(frame)
except Exception:
pass
try:
self.sock.close()
except Exception:
pass
def main():
print()
cprint(" ============================", CYAN)
cprint(" GOTT Remote Agent", CYAN)
cprint(" ============================", CYAN)
print()
cprint(f" Verbinde zu: {WS_URL}", GRAY)
print()
ws = WebSocketClient(WS_URL)
running = True
def on_signal(sig, frame):
nonlocal running
running = False
signal.signal(signal.SIGINT, on_signal)
signal.signal(signal.SIGTERM, on_signal)
try:
ws.connect()
except Exception as e:
cprint(f" [FEHLER] Verbindung fehlgeschlagen: {e}", RED)
cprint(f" URL: {WS_URL}", GRAY)
return
cprint(" [OK] Verbunden!", GREEN)
# Hello-Message senden
hostname = os.uname().nodename if hasattr(os, "uname") else socket.gethostname()
username = os.environ.get("USER", os.environ.get("LOGNAME", "unknown"))
import platform
os_version = f"{platform.system()} {platform.release()}"
hello = json.dumps({
"type": "hello",
"hostname": hostname,
"username": username,
"os": os_version
})
ws.send(hello)
cprint(f" Host: {username}@{hostname} ({os_version})", GRAY)
print()
cprint(" Warte auf Befehle... (Strg+C zum Beenden)", YELLOW)
print()
while running and not ws.closed:
try:
raw = ws.recv()
except (ConnectionError, OSError):
cprint(" Verbindung verloren.", RED)
break
if raw is None:
cprint(" Server hat Verbindung geschlossen.", YELLOW)
break
try:
msg = json.loads(raw)
except json.JSONDecodeError:
continue
msg_type = msg.get("type", "")
if msg_type == "execute":
command = msg.get("command", "")
timestamp = time.strftime("%H:%M:%S")
print(f" {CYAN}[{timestamp}] Ausfuehren: {WHITE}{command}{RESET}", flush=True)
exit_code = 0
stdout = ""
stderr = ""
try:
result = subprocess.run(
["bash", "-c", command],
capture_output=True,
text=True,
timeout=300
)
stdout = result.stdout
stderr = result.stderr
exit_code = result.returncode
except subprocess.TimeoutExpired:
stderr = "Timeout nach 300 Sekunden"
exit_code = 124
except Exception as e:
stderr = str(e)
exit_code = 1
response = json.dumps({
"type": "result",
"exitCode": exit_code,
"output": stdout or "",
"error": stderr or ""
})
ws.send(response)
# Kurzausgabe lokal
if stdout:
preview = stdout[:200] + "..." if len(stdout) > 200 else stdout.rstrip()
cprint(f" -> {preview}", GRAY)
if stderr:
cprint(f" [stderr] {stderr.rstrip()}", RED)
color = GREEN if exit_code == 0 else RED
cprint(f" [Exit: {exit_code}]", color)
print()
elif msg_type == "ping":
ws.send('{"type":"pong"}')
elif msg_type == "pong":
pass
else:
cprint(f" Unbekannte Nachricht: {msg_type}", YELLOW)
ws.close()
print()
cprint(" Verbindung beendet.", YELLOW)
main()
PYTHON_SCRIPT