feat(host-agent): cross-platform — Linux, macOS UND Windows
Eine Codebasis fuer alle drei Desktop-OS. Der Agent waehlt je OS automatisch: - exec: bash -lc (Linux/macOS) bzw. PowerShell (Windows) - Root/Admin: sudo (Unix) bzw. 'als Administrator starten' (Windows, kein sudo) - Screenshot: grim/scrot (Linux) · screencapture (macOS) · PowerShell/System. Drawing (Windows) - Root-Check: _is_admin() (os.geteuid Unix / IsUserAnAdmin Windows) statt hartem os.geteuid (crashte auf Windows) - info: user aus USER|USERNAME; getloadavg bleibt guarded Build: build.sh (Linux/Docker), build-native.sh (Linux/macOS), build-native.bat (Windows). PyInstaller cross-kompiliert nicht -> je OS bauen. README-Plattform- Tabelle ergaenzt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+25
-4
@@ -21,14 +21,35 @@ Geräte in einem LAN; der Host-Agent steuert *den Rechner, auf dem er läuft*.
|
|||||||
ARIA nutzt diese über die Brain-Tools `host_list` / `host_exec` / `host_read` /
|
ARIA nutzt diese über die Brain-Tools `host_list` / `host_exec` / `host_read` /
|
||||||
`host_write` / `host_info` / `host_screenshot`.
|
`host_write` / `host_info` / `host_screenshot`.
|
||||||
|
|
||||||
## Bauen (portable Binary)
|
## Plattformen
|
||||||
|
|
||||||
|
Eine Codebasis, läuft auf **Linux, macOS und Windows** (der Agent wählt Shell,
|
||||||
|
Screenshot-Methode und Root/Admin-Check je OS automatisch):
|
||||||
|
|
||||||
|
| | exec | Root/Admin | Screenshot |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Linux** | `bash -lc` | sudo (`SUDO_PASSWORD`/`SUDO_NOPASSWD`) / root | grim (Wayland) · scrot/maim (X11) |
|
||||||
|
| **macOS** | `bash -lc` | sudo (wie Linux) | `screencapture` (Bordmittel) |
|
||||||
|
| **Windows** | PowerShell | Agent **als Administrator** starten (kein sudo) | PowerShell/System.Drawing (Bordmittel) |
|
||||||
|
|
||||||
|
PyInstaller kann **nicht cross-kompilieren** — jede Binary wird auf ihrem OS gebaut.
|
||||||
|
|
||||||
|
## Bauen
|
||||||
|
|
||||||
|
**Linux (portabel, empfohlen)** — Docker-Container mit altem glibc:
|
||||||
```bash
|
```bash
|
||||||
./build.sh # braucht Docker; erzeugt dist/aria-host-agent (~15 MB)
|
./build.sh # -> dist/aria-host-agent (~15 MB, läuft auf vielen Distros)
|
||||||
```
|
```
|
||||||
|
|
||||||
Gebaut wird in einem bullseye-Container (altes glibc), damit die Binary auf
|
**Linux/macOS ohne Docker** — PyInstaller direkt (linkt gegen lokales glibc/OS):
|
||||||
möglichst vielen Distributionen läuft.
|
```bash
|
||||||
|
./build-native.sh # -> dist/aria-host-agent
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows** — PyInstaller (Python 3 im PATH nötig):
|
||||||
|
```bat
|
||||||
|
build-native.bat REM -> dist\aria-host-agent.exe
|
||||||
|
```
|
||||||
|
|
||||||
### Docker scheitert? (Live-ISO / overlayfs-Root)
|
### Docker scheitert? (Live-ISO / overlayfs-Root)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
@echo off
|
||||||
|
REM ARIA Host-Agent — Windows-Build mit PyInstaller (kein Docker).
|
||||||
|
REM Voraussetzung: Python 3 installiert und im PATH (python.org, "Add to PATH").
|
||||||
|
REM
|
||||||
|
REM build-native.bat
|
||||||
|
REM
|
||||||
|
REM Ergebnis: dist\aria-host-agent.exe (Onefile). Danach .env danebenlegen
|
||||||
|
REM (siehe .env.example) und starten. Fuer Admin-Rechte die .exe per Rechtsklick
|
||||||
|
REM "Als Administrator ausfuehren".
|
||||||
|
setlocal
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
python -m venv .buildenv || goto :err
|
||||||
|
call .buildenv\Scripts\activate.bat
|
||||||
|
python -m pip install --quiet --upgrade pip
|
||||||
|
python -m pip install --quiet pyinstaller -r requirements.txt || goto :err
|
||||||
|
pyinstaller --onefile --name aria-host-agent --collect-all psutil host_agent.py || goto :err
|
||||||
|
call deactivate
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Fertig: dist\aria-host-agent.exe
|
||||||
|
echo .env danebenlegen (siehe .env.example), dann starten (ggf. als Administrator).
|
||||||
|
goto :eof
|
||||||
|
|
||||||
|
:err
|
||||||
|
echo.
|
||||||
|
echo FEHLER beim Bauen. Ist Python 3 installiert und im PATH? (python --version)
|
||||||
|
exit /b 1
|
||||||
+49
-10
@@ -31,11 +31,28 @@ import re
|
|||||||
import shutil
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import websockets
|
import websockets
|
||||||
|
|
||||||
|
# ─── Plattform-Weichen (Linux / macOS / Windows) ────────────────────
|
||||||
|
IS_WINDOWS = os.name == "nt"
|
||||||
|
IS_MAC = sys.platform == "darwin"
|
||||||
|
|
||||||
|
|
||||||
|
def _is_admin() -> bool:
|
||||||
|
"""root (Unix) bzw. Administrator (Windows)."""
|
||||||
|
try:
|
||||||
|
return os.geteuid() == 0 # Unix (Linux/macOS)
|
||||||
|
except AttributeError:
|
||||||
|
try:
|
||||||
|
import ctypes
|
||||||
|
return ctypes.windll.shell32.IsUserAnAdmin() != 0 # Windows
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format="%(asctime)s [host-agent] %(levelname)s %(message)s",
|
format="%(asctime)s [host-agent] %(levelname)s %(message)s",
|
||||||
@@ -180,8 +197,18 @@ def _window_text(text: str, params: dict) -> dict:
|
|||||||
# ─── Aktionen ───────────────────────────────────────────────────────
|
# ─── Aktionen ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _wrap_sudo(cmd: str, use_sudo: bool):
|
def _wrap_sudo(cmd: str, use_sudo: bool):
|
||||||
"""Gibt (argv, stdin_data) oder (None, fehlertext) wenn sudo nicht moeglich."""
|
"""Baut die Argv (OS-abhaengige Shell) + optional Root/Admin-Rechte.
|
||||||
if not use_sudo or os.geteuid() == 0:
|
Gibt (argv, stdin_data) oder (None, fehlertext)."""
|
||||||
|
if IS_WINDOWS:
|
||||||
|
# PowerShell; kein sudo. Fuer Admin-Rechte muss der Agent SELBST als
|
||||||
|
# Administrator laufen (UAC) — dann hat 'sudo:true' bereits volle Rechte.
|
||||||
|
if use_sudo and not _is_admin():
|
||||||
|
return None, ("Windows kennt kein sudo. Starte den Agent als "
|
||||||
|
"Administrator ('Als Administrator ausfuehren'), dann "
|
||||||
|
"laufen Kommandos mit vollen Rechten.")
|
||||||
|
return ["powershell", "-NoProfile", "-NonInteractive", "-Command", cmd], None
|
||||||
|
# Unix: Linux + macOS (bash vorhanden; macOS-sudo verhaelt sich wie Linux)
|
||||||
|
if not use_sudo or _is_admin():
|
||||||
return ["bash", "-lc", cmd], None
|
return ["bash", "-lc", cmd], None
|
||||||
if SUDO_PASSWORD:
|
if SUDO_PASSWORD:
|
||||||
return ["sudo", "-S", "-p", "", "bash", "-lc", cmd], SUDO_PASSWORD + "\n"
|
return ["sudo", "-S", "-p", "", "bash", "-lc", cmd], SUDO_PASSWORD + "\n"
|
||||||
@@ -266,7 +293,8 @@ def _do_info(params: dict) -> dict:
|
|||||||
"host": HOST_NAME, "hostname": socket.gethostname(),
|
"host": HOST_NAME, "hostname": socket.gethostname(),
|
||||||
"os": platform.platform(), "kernel": platform.release(),
|
"os": platform.platform(), "kernel": platform.release(),
|
||||||
"arch": platform.machine(), "python": platform.python_version(),
|
"arch": platform.machine(), "python": platform.python_version(),
|
||||||
"user": os.environ.get("USER") or "", "is_root": os.geteuid() == 0,
|
"user": os.environ.get("USER") or os.environ.get("USERNAME") or "",
|
||||||
|
"is_root": _is_admin(),
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
import psutil
|
import psutil
|
||||||
@@ -306,13 +334,24 @@ def _do_info(params: dict) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def _do_screenshot(params: dict) -> dict:
|
def _do_screenshot(params: dict) -> dict:
|
||||||
"""Bildschirmfoto via System-Tool (Wayland: grim; X11: scrot/maim/import/
|
"""Bildschirmfoto — OS-abhaengig. Windows: PowerShell/System.Drawing;
|
||||||
gnome-screenshot). Braucht eine aktive grafische Session (DISPLAY/WAYLAND)."""
|
macOS: screencapture; Linux: grim (Wayland) / scrot/maim/import (X11).
|
||||||
|
Braucht eine aktive grafische Session."""
|
||||||
import tempfile
|
import tempfile
|
||||||
tmp = os.path.join(tempfile.gettempdir(), f"aria_shot_{int(time.time())}.png")
|
tmp = os.path.join(tempfile.gettempdir(), f"aria_shot_{int(time.time())}.png")
|
||||||
wayland = bool(os.environ.get("WAYLAND_DISPLAY"))
|
|
||||||
candidates = []
|
candidates = []
|
||||||
if wayland and shutil.which("grim"):
|
if IS_WINDOWS:
|
||||||
|
ps = ("Add-Type -AssemblyName System.Windows.Forms,System.Drawing;"
|
||||||
|
"$b=[System.Windows.Forms.SystemInformation]::VirtualScreen;"
|
||||||
|
"$bmp=New-Object System.Drawing.Bitmap $b.Width,$b.Height;"
|
||||||
|
"$g=[System.Drawing.Graphics]::FromImage($bmp);"
|
||||||
|
"$g.CopyFromScreen($b.Location,[System.Drawing.Point]::Empty,$b.Size);"
|
||||||
|
f"$bmp.Save('{tmp}');$g.Dispose();$bmp.Dispose()")
|
||||||
|
candidates.append(["powershell", "-NoProfile", "-NonInteractive", "-Command", ps])
|
||||||
|
elif IS_MAC:
|
||||||
|
candidates.append(["screencapture", "-x", tmp]) # -x = ohne Ton
|
||||||
|
else:
|
||||||
|
if os.environ.get("WAYLAND_DISPLAY") and shutil.which("grim"):
|
||||||
candidates.append(["grim", tmp])
|
candidates.append(["grim", tmp])
|
||||||
for tool, argv in (("scrot", ["scrot", "-o", tmp]),
|
for tool, argv in (("scrot", ["scrot", "-o", tmp]),
|
||||||
("maim", ["maim", tmp]),
|
("maim", ["maim", tmp]),
|
||||||
@@ -322,8 +361,8 @@ def _do_screenshot(params: dict) -> dict:
|
|||||||
candidates.append(argv)
|
candidates.append(argv)
|
||||||
if not candidates:
|
if not candidates:
|
||||||
return {"ok": False, "error":
|
return {"ok": False, "error":
|
||||||
"Kein Screenshot-Tool gefunden. Installiere grim (Wayland) oder "
|
"Kein Screenshot-Tool gefunden. Linux: grim (Wayland) oder "
|
||||||
"scrot/maim (X11)."}
|
"scrot/maim (X11) installieren. (Windows/macOS nutzen Bordmittel.)"}
|
||||||
last_err = ""
|
last_err = ""
|
||||||
for argv in candidates:
|
for argv in candidates:
|
||||||
try:
|
try:
|
||||||
@@ -466,7 +505,7 @@ class HostAgent:
|
|||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
logger.info("ARIA Host-Agent startet — id=%s name=%s control=%s root=%s",
|
logger.info("ARIA Host-Agent startet — id=%s name=%s control=%s root=%s",
|
||||||
HOST_ID, HOST_NAME, CONTROL_ENABLED, os.geteuid() == 0)
|
HOST_ID, HOST_NAME, CONTROL_ENABLED, _is_admin())
|
||||||
try:
|
try:
|
||||||
asyncio.run(HostAgent().run())
|
asyncio.run(HostAgent().run())
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
Reference in New Issue
Block a user