ToolBoxV2 — Service Manager & Start Profiles¶
Datei:
toolboxv2/utils/clis/service_manager.pyEntry-Point:tb --sm(Boot-Startup) ·tb services <cmd>(interaktiv) Decoupled von: ISAA Job-System (headless_runner.py,os_scheduler.py) — komplett unabhängig
1. Architektur-Überblick¶
tb --sm ──────────────────► run_service_manager_startup()
│
▼
tb services <cmd> ──────► cli_services() → ServiceManager
│
┌───────────────────────┤
▼ ▼
ServiceRegistry ServiceManager
(Singleton) (PID-File Tracking)
┌──────────────┐ ┌──────────────────┐
│ Built-in Svcs│ │ .info/pids/*.pid │
│ per category │ │ .info/services.json│
└──────────────┘ └──────────────────┘
Kernprinzip: Kein Daemon, kein OS-Service-API. Jeder Service ist ein detached Subprocess (tb <name>), getrackt via PID-File.
2. Datenstrukturen¶
ServiceDefinition (dataclass)¶
| Feld | Typ | Default | Bedeutung |
|---|---|---|---|
name |
str |
— | Eindeutiger Key |
description |
str |
— | Anzeige-Text |
category |
str |
— | core / infrastructure / extension |
module |
str |
— | Python-Modul-Pfad |
entry_point |
str |
— | Funktion im Modul |
is_async |
bool |
False |
Asyncio-Entrypoint |
runner_key |
Optional[str] |
None |
CLI-Argument-Key |
ServiceStartResult (dataclass)¶
| Feld | Typ | Bedeutung |
|---|---|---|
name |
str |
Service-Name |
success |
bool |
Start erfolgreich? |
pid |
Optional[int] |
Prozess-ID (bei Erfolg) |
error |
Optional[str] |
Fehlermeldung (bei Misserfolg) |
3. ServiceRegistry — Singleton¶
Registriert alle Built-in Services beim ersten Aufruf, danach identische Instanz.
Built-in Services¶
🔷 Core¶
| Name | Beschreibung |
|---|---|
custom |
Beliebiger TB-Custom-Command |
workers |
Worker-Orchestrierung (HTTP, WS, Broker) |
db |
MinIO Blob Storage Management (async) |
isaaK |
Discord & Telegram Kernel Agent (async) |
🔧 Infrastructure¶
| Name | Beschreibung |
|---|---|
broker |
Event-Management via ZMQ |
http_worker |
HTTP Worker Server |
ws_worker |
WebSocket Worker Server |
🔌 Extension¶
| Name | Beschreibung |
|---|---|
p2p |
P2P Chat, File Transfer, Voice |
mcp |
MCP Server für AI Agents |
gui |
Graphical User Interface |
llm-gateway |
OpenAI-kompatibler LLM-Proxy (llama.cpp) |
Registry API¶
registry.get("workers") # → ServiceDefinition | None
registry.get_all() # → Dict[str, ServiceDefinition]
registry.get_by_category("core") # → List[ServiceDefinition]
registry.list_names() # → List[str]
registry.register(ServiceDefinition(...)) # Custom Service hinzufügen
4. ServiceManager — Kern-Klasse¶
Dateipfade¶
tb_root_dir/.info/
pids/
workers.pid ← PID-File pro Service
db.pid
...
services.json ← Persistente Konfiguration
Konfiguration (services.json)¶
{
"services": {
"workers": {
"auto_start": true,
"auto_restart": false,
"args": ["--port", "8080"]
},
"mcp": {
"auto_start": true,
"auto_restart": false,
"args": []
}
}
}
Methoden-Referenz¶
Config¶
manager.load_config() # → Dict
manager.save_config(config: Dict) # speichert JSON
manager.configure_service(name, auto_start, auto_restart, args)
manager.get_auto_start_services() # → List[str]
manager.get_service_args(name) # → List[str]
PID / Laufzeitstatus¶
running, pid = manager.is_service_running(name)
# Prüft via:
# Windows → tasklist /FI "PID eq <pid>"
# Unix → os.kill(pid, 0)
# Stale PID-Files werden automatisch gelöscht
Start / Stop / Restart¶
result = manager.start_service(name, args=None, save_args=True)
# Subprocess: python -m toolboxv2 <name> [args...]
# Windows: CREATE_NO_WINDOW | DETACHED_PROCESS
# Unix: start_new_session=True, stdout/stderr=DEVNULL
# Wartet 0.5s und verifiziert, ob Prozess noch läuft
# args=None → gespeicherte Args aus Config
manager.stop_service(name, graceful=True)
# Unix: SIGTERM (graceful) / SIGKILL (force)
# Windows: taskkill [/F]
# Wartet bis 5s auf Shutdown, räumt PID-File auf
Status / Info¶
status = manager.get_all_status(include_registry=True)
# → Dict[name → {running, pid, auto_start, auto_restart, category, ...}]
# include_registry=True: ergänzt alle Registry-Services (auch unkonfigurierte)
# include_registry=False: nur explizit konfigurierte Services
info = manager.get_service_info(name)
# → Dict mit: running, pid, auto_start, auto_restart, category,
# description, module, entry_point, is_async, runner_key
# → None wenn Service komplett unbekannt
5. Boot-Startup (tb --sm)¶
# __main__.py
if "--sm" in sys.argv:
from toolboxv2.utils.clis.service_manager import run_service_manager_startup
if 'init' in sys.argv:
# Linux: setup_service_linux()
# Windows: asyncio.run(setup_service_windows())
sys.exit(run_service_manager_startup())
run_service_manager_startup():
1. Lädt alle auto_start=True Services aus der Config
2. Startet jeden mit start_service() (benutzt gespeicherte Args)
3. Gibt Exit-Code 0 (alle OK) oder 1 (mind. ein Fehler) zurück
6. CLI — tb services¶
tb services status [--name <n>] Laufzeit-Tabelle aller Services
tb services start [name] [args...] Start (ohne name: alle auto-start)
--auto-start Auto-start direkt beim Start aktivieren
tb services stop [name] [--force] Stop (ohne name: alle)
tb services restart [name] [args...] Restart (ohne name: alle laufenden)
--force
tb services config <name> Konfiguriere Service
--auto-start=true|false
--auto-restart=true|false
--args [ARG...] Standardargumente setzen
--clear-args Gespeicherte Args löschen
tb services list Konfigurierte Services (mit Flags)
tb services info <name> Detailview: PID, Modul, Entry-Point, Args
tb services registry Alle Built-in Services nach Kategorie
7. Decoupling: Service Manager vs. ISAA Job-System¶
Service Manager ISAA Job-System (isaa-spezifisch)
─────────────── ──────────────────────────────────
TB-interne Prozesse Geplante Agent-Aufgaben
PID-File Tracking jobs.json + JobDefinition
tb services start/stop JobManager, headless_runner.py
auto_start / auto_restart on_time / on_interval / on_cron / on_boot
via tb --sm via os_scheduler.py (schtasks/cron/launchd)
keine ISAA-Dependency braucht toolboxv2.mods.isaa.*
Start Profiles — Design-Spec¶
Idee: Nutzungsprofile (
work,development,freetime) die automatisch TB-Services starten UND externe Anwendungen an definierten Bildschirmpositionen öffnen. Integration: Vollständig inservice_manager.py, Profile wechselbar viatb services profile switch <name>.
8. Konzept¶
Ein Profil definiert:
- Welche TB-Services gestartet werden (ersetzt individuelle auto_start Flags)
- Welche externen Apps geöffnet werden (Browser, IDE, Terminal, etc.)
- Für jede App: Fensterposition (Monitor, x/y, Breite/Höhe) und Startup-URL/Argument
Vordefinierte Profile¶
| Profil | TB-Services | Apps |
|---|---|---|
work |
workers, db, mcp | Browser (SimpleCore), IDE, Slack |
development |
workers, db, broker, llm-gateway | VS Code, Browser (localhost), Terminal |
freetime |
gui | — |
minimal |
(keine) | — |
9. Datenstrukturen (Erweiterung)¶
@dataclass
class AppLaunchConfig:
"""Externe Anwendung mit Fensterposition"""
name: str
command: List[str] # z.B. ["code", "/path/to/project"]
args: List[str] = field(default_factory=list)
# Fensterpositionierung (optional)
window_x: Optional[int] = None
window_y: Optional[int] = None
window_width: Optional[int] = None
window_height: Optional[int] = None
monitor: int = 0 # 0 = primary
# Verhalten
wait_seconds: float = 0.5 # Wartezeit nach Start (für Fenster-Init)
platform: Optional[str] = None # None = alle, "windows"/"linux"/"darwin"
@dataclass
class StartProfile:
"""Nutzungsprofil mit Services + Apps"""
name: str
description: str
# TB-Services (überschreibt auto_start)
services: List[str] = field(default_factory=list)
service_args: Dict[str, List[str]] = field(default_factory=dict)
# Externe Apps
apps: List[AppLaunchConfig] = field(default_factory=list)
# Verhalten
stop_other_services: bool = True # Services nicht im Profil stoppen
is_default: bool = False
Konfig-Erweiterung services.json¶
{
"services": { ... },
"active_profile": "work",
"profiles": {
"work": {
"description": "Arbeitsumgebung",
"services": ["workers", "db", "mcp"],
"service_args": {
"workers": ["--env", "prod"]
},
"apps": [
{
"name": "Browser",
"command": ["firefox", "https://simplecore.app"],
"window_x": 0, "window_y": 0,
"window_width": 1280, "window_height": 1080,
"monitor": 0
},
{
"name": "IDE",
"command": ["code", "/srv/projects/ToolBoxV2"],
"window_x": 1280, "window_y": 0,
"window_width": 1280, "window_height": 1080,
"monitor": 0
}
],
"stop_other_services": true
},
"development": {
"description": "Entwicklungsumgebung",
"services": ["workers", "db", "broker", "llm-gateway"],
"apps": [
{
"name": "VS Code",
"command": ["code", "/srv/projects/ToolBoxV2"],
"window_x": 0, "window_y": 0,
"window_width": 1920, "window_height": 1080
},
{
"name": "Dev Browser",
"command": ["firefox", "http://localhost:8080"],
"monitor": 1
}
]
},
"freetime": {
"description": "Freizeit",
"services": ["gui"],
"apps": [],
"stop_other_services": true
}
}
}
10. ProfileManager — Implementierung (Sketch)¶
class ProfileManager:
"""Verwaltet Start-Profile, integriert in ServiceManager"""
def __init__(self, manager: ServiceManager):
self.manager = manager
# ── Profil-CRUD ───────────────────────────────────────────────────
def get_profile(self, name: str) -> Optional[StartProfile]:
config = self.manager.load_config()
raw = config.get("profiles", {}).get(name)
return StartProfile(**raw) if raw else None
def save_profile(self, profile: StartProfile) -> None:
config = self.manager.load_config()
config.setdefault("profiles", {})[profile.name] = asdict(profile)
self.manager.save_config(config)
def list_profiles(self) -> List[str]:
config = self.manager.load_config()
return list(config.get("profiles", {}).keys())
def get_active(self) -> Optional[str]:
return self.manager.load_config().get("active_profile")
# ── Switch ────────────────────────────────────────────────────────
def switch(self, name: str, launch_apps: bool = True) -> None:
profile = self.get_profile(name)
if not profile:
raise ValueError(f"Profile '{name}' not found")
# 1. Andere Services stoppen (wenn konfiguriert)
if profile.stop_other_services:
running = self.manager.get_all_status(include_registry=False)
for svc_name, info in running.items():
if info["running"] and svc_name not in profile.services:
self.manager.stop_service(svc_name)
# 2. Profil-Services starten
for svc_name in profile.services:
args = profile.service_args.get(svc_name)
self.manager.start_service(svc_name, args=args)
# 3. Apps öffnen + positionieren
if launch_apps:
for app in profile.apps:
self._launch_app(app)
# 4. Aktives Profil speichern
config = self.manager.load_config()
config["active_profile"] = name
self.manager.save_config(config)
# ── App-Launcher ──────────────────────────────────────────────────
def _launch_app(self, app: AppLaunchConfig) -> None:
"""Startet externe App und positioniert Fenster (platform-agnostic)"""
import subprocess, time
# Platform-Filter
if app.platform and app.platform != sys.platform:
return
cmd = app.command + app.args
subprocess.Popen(cmd, start_new_session=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
# Fensterpositionierung (nach kurzer Wartezeit)
if any(v is not None for v in [app.window_x, app.window_y,
app.window_width, app.window_height]):
time.sleep(app.wait_seconds)
self._position_window(app)
def _position_window(self, app: AppLaunchConfig) -> None:
"""Plattform-spezifische Fensterpositionierung"""
if IS_LINUX:
self._position_linux(app)
elif IS_WINDOWS:
self._position_windows(app)
elif IS_MACOS:
self._position_macos(app)
def _position_linux(self, app: AppLaunchConfig) -> None:
# wmctrl oder xdotool
# wmctrl -r :ACTIVE: -e 0,<x>,<y>,<w>,<h>
# Suche Fenster per Name: wmctrl -l | grep <app.name>
try:
subprocess.run([
"wmctrl", "-r", app.name, "-e",
f"0,{app.window_x},{app.window_y},{app.window_width},{app.window_height}"
], capture_output=True)
except FileNotFoundError:
pass # wmctrl nicht installiert, skip
def _position_windows(self, app: AppLaunchConfig) -> None:
# pywin32: win32gui.MoveWindow / win32gui.FindWindow
# Oder PowerShell: Add-Type -A System.Windows.Forms
pass
def _position_macos(self, app: AppLaunchConfig) -> None:
# AppleScript: tell application "X" to set bounds of window 1 to {x,y,w,h}
script = (
f'tell application "{app.name}" to '
f'set bounds of window 1 to '
f'{{{app.window_x}, {app.window_y}, '
f'{app.window_x + (app.window_width or 0)}, '
f'{app.window_y + (app.window_height or 0)}}}'
)
subprocess.run(["osascript", "-e", script], capture_output=True)
11. CLI-Erweiterung: tb services profile¶
tb services profile list Alle Profile anzeigen (mit aktivem Marker)
tb services profile switch <name> Profil wechseln (Stop/Start Services + Apps)
--no-apps Nur Services, keine Apps öffnen
tb services profile show <name> Profil-Details (Services + Apps)
tb services profile set-default <name> Standard-Profil für tb --sm
tb services profile create <name> Interaktiv neues Profil erstellen (guided)
tb services profile edit <name> Profil in $EDITOR öffnen (JSON)
tb services profile delete <name> Profil löschen
Integration mit tb --sm¶
# Boot: Standard-Profil aktivieren
tb --sm # → aktives/default Profil laden
tb --sm --profile work # → explizit Profil angeben
# __main__.py Erweiterung
if "--sm" in sys.argv:
profile_idx = sys.argv.index("--profile") if "--profile" in sys.argv else -1
profile_name = sys.argv[profile_idx + 1] if profile_idx >= 0 else None
from toolboxv2.utils.clis.service_manager import (
run_service_manager_startup, ServiceManager, ProfileManager
)
if profile_name:
manager = ServiceManager()
pm = ProfileManager(manager)
pm.switch(profile_name, launch_apps=True)
sys.exit(0)
else:
sys.exit(run_service_manager_startup())
12. Beispiel-Workflow¶
# Einmalig: Profile konfigurieren
tb services profile create work # geführter Assistent
# Oder direkt in JSON editieren
tb services profile edit work
# Profil testen
tb services profile switch work
# Als Boot-Standard setzen
tb services profile set-default work
# Dann bei Boot
tb --sm # → work-Profil startet automatisch
# Schnell wechseln während der Arbeit
tb services profile switch development
tb services profile switch freetime
# Aktuelles Profil prüfen
tb services profile list
# Output:
# ● work (aktiv) — workers, db, mcp
# development — workers, db, broker, llm-gateway
# freetime — gui
13. Test-Erweiterungen (unittest)¶
Neue Test-Klassen für test_cli_service_manager.py:
class TestStartProfile(unittest.TestCase):
"""Tests für StartProfile Dataclass"""
def test_profile_creation(self): ...
def test_profile_defaults(self): ...
class TestProfileManager(unittest.TestCase):
"""Tests für ProfileManager"""
def test_save_and_load_profile(self): ...
def test_list_profiles(self): ...
def test_get_active_profile(self): ...
def test_switch_stops_other_services(self): ... # mock stop_service
def test_switch_starts_profile_services(self): ...
def test_switch_sets_active_profile(self): ...
def test_switch_unknown_profile_raises(self): ...
class TestAppLaunchConfig(unittest.TestCase):
"""Tests für AppLaunchConfig"""
def test_platform_filter(self): ... # app.platform != sys.platform → skip
def test_no_window_position_skips_positioning(self): ...
14. Abhängigkeiten (Linux-Fensterpositionierung)¶
# Empfohlen: wmctrl (X11)
sudo apt install wmctrl
# Alternative: xdotool
sudo apt install xdotool
# Wayland (kein wmctrl): ydotool oder KDE-spezifisch
Für Windows: pywin32 (pip install pywin32) — optional, graceful skip wenn nicht vorhanden.
Generiert aus: service_manager.py, test_cli_service_manager.py, os_scheduler.py, headless_runner.py