diff --git a/aria-brain/agent.py b/aria-brain/agent.py index 8630b5b..8c1266f 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -1184,7 +1184,7 @@ META_TOOLS = [ "disk": {"type": "string", "description": "optional: qcow2-Pfad (Festplatte)."}, "floppy": {"type": "string", "description": "optional: Disketten-Image-Pfad (-fda)."}, "iso": {"type": "string", "description": "optional: Boot-ISO-Pfad."}, - "vnc_display": {"type": "integer", "description": "VNC-Display (Default 1 → Port 5901)."}, + "vnc_display": {"type": "integer", "description": "meist WEGLASSEN — das VNC-Display wird global eindeutig auto-vergeben (kein Port-Konflikt bei mehreren VMs). Nur setzen wenn Du ein bestimmtes willst."}, "mem": {"type": "integer", "description": "RAM in MB (Default 1024)."}, }, "required": ["name", "arch"], @@ -2846,7 +2846,7 @@ class Agent: (arguments.get("iso") or "").strip(), (arguments.get("floppy") or "").strip(), (arguments.get("disk") or "").strip(), - int(arguments.get("vnc_display") or 1), + int(arguments.get("vnc_display") or 0), int(arguments.get("mem") or 1024), ) except (ValueError, TypeError) as exc: diff --git a/aria-brain/main.py b/aria-brain/main.py index dbf5c10..3c863cb 100644 --- a/aria-brain/main.py +++ b/aria-brain/main.py @@ -1063,7 +1063,7 @@ def project_vm_boot(project_id: str, name: str): boot_args += ["--vnc-bind", gw] rc, out, err = _ssh_aria_vm(*boot_args, timeout=40) return {"ok": rc == 0, "name": name, "vnc_port": 5900 + int(vm.get("vnc_display", 1)), - "output": (out.strip() or err.strip())[:500]} + "vnc_bind": gw or "127.0.0.1", "output": (out.strip() or err.strip())[:500]} @app.post("/projects/{project_id}/vms/{name}/stop") diff --git a/aria-brain/project_vms.py b/aria-brain/project_vms.py index 67c8084..6eea94f 100644 --- a/aria-brain/project_vms.py +++ b/aria-brain/project_vms.py @@ -51,13 +51,30 @@ def get_vm(project_id: str, name: str) -> Optional[dict]: return None +def _used_displays(data: dict, exclude: object = None) -> set: + """Alle VNC-Displays, die ueber ALLE Projekte belegt sind (exclude = eine + VM-Dict-Instanz, die ignoriert wird — fuer Updates).""" + used = set() + for lst in data.values(): + for v in lst: + if v is exclude: + continue + try: + used.add(int(v.get("vnc_display", 1))) + except (TypeError, ValueError): + pass + return used + + def add_vm(project_id: str, name: str, arch: str, iso: str = "", floppy: str = "", disk: str = "", - vnc_display: int = 1, mem: int = 1024) -> dict: - """Registriert/aktualisiert eine VM. Medien (disk/floppy/iso) sind optional — + vnc_display: int = 0, mem: int = 1024) -> dict: + """Registriert/aktualisiert eine VM. Medien (disk/floppy/iso) optional — leer = aria-vm erkennt disk.qcow2/floppy.img/cdrom.iso im VM-Ordner selbst. - ARIA setzt hier, was sie zum Task passend gebaut hat (Festplatte fuer DOS- - Spiele, Diskette fuer OS-Dev, ISO fuer Installer).""" + + Das VNC-Display wird GLOBAL eindeutig vergeben (ueber alle Projekte), damit + mehrere laufende VMs nicht denselben Port doppelt binden. vnc_display<=0 oder + ein bereits belegtes Display → automatisch das naechste freie.""" if not NAME_RE.match(name or ""): raise ValueError(f"Ungueltiger VM-Name: {name!r} (nur a-z0-9_-, max 40)") if arch not in VALID_ARCH: @@ -65,13 +82,23 @@ def add_vm(project_id: str, name: str, arch: str, iso: str = "", data = _load() lst = data.setdefault(project_id or "", []) now = int(time.time()) + existing = next((v for v in lst if v.get("name") == name), None) + + used = _used_displays(data, exclude=existing) + req = int(vnc_display or 0) + if req <= 0 and existing: # Update ohne Display-Wunsch → behalten + req = int(existing.get("vnc_display", 0) or 0) + if req <= 0 or req in used: # frei/eindeutig machen + req = 1 + while req in used: + req += 1 + fields = {"arch": arch, "iso": iso, "floppy": floppy, "disk": disk, - "vnc_display": int(vnc_display), "mem": int(mem), "updated_at": now} - for v in lst: - if v.get("name") == name: - v.update(fields) - _save(data) - return v + "vnc_display": req, "mem": int(mem), "updated_at": now} + if existing: + existing.update(fields) + _save(data) + return existing vm = {"name": name, "created_at": now, **fields} lst.append(vm) _save(data) diff --git a/aria-brain/seed_rules.py b/aria-brain/seed_rules.py index 5bd2d73..f895e17 100644 --- a/aria-brain/seed_rules.py +++ b/aria-brain/seed_rules.py @@ -435,8 +435,10 @@ SEED_RULES: List[dict] = [ " - `aria-vm list` / `aria-vm stop ` / `aria-vm rm `.\n" "\n" "PFLICHT nach dem Bau/Boot: `vm_register(name, arch, disk?/floppy?/iso?, " - "vnc_display, mem)` im aktuellen Projekt aufrufen — mit den Medien, die Du " - "gebaut hast. ERST DANN erscheint die VM in Stefans Desktop-Panel (Cockpit), " + "mem?)` im aktuellen Projekt aufrufen — mit den Medien, die Du gebaut hast. " + "vnc_display WEGLASSEN — es wird global eindeutig auto-vergeben (kein " + "Port-Konflikt, wenn mehrere VMs laufen). ERST DANN erscheint die VM in " + "Stefans Desktop-Panel (Cockpit), " "wo er sie Starten/Stoppen/Verbinden kann. Ohne vm_register bleibt seine " "Liste leer, obwohl die VM laeuft. Der Startbefehl steht als Wert dahinter.\n" "\n"