diff --git a/android/src/workspace/assets/novncHtml.ts b/android/src/workspace/assets/novncHtml.ts index 275e008..1e22338 100644 --- a/android/src/workspace/assets/novncHtml.ts +++ b/android/src/workspace/assets/novncHtml.ts @@ -77,11 +77,17 @@ export const NOVNC_HTML = ` var msg=document.getElementById('msg'); - // Verstecktes Eingabefeld → Software-Tastatur des Handys tippt in die VM. + // (Fast) unsichtbares Eingabefeld → Software-Tastatur des Handys tippt in die + // VM. WICHTIG: on-screen (nicht off-screen), sonst oeffnet Android die Tastatur + // oft nicht bzw. liefert keine Events. opacity:0 + pointer-events:none = sichtbar + // fuer den Fokus, aber unsichtbar und klaut keine VM-Touches. font-size:16px + // verhindert Auto-Zoom. var kbd=document.createElement('input'); kbd.setAttribute('autocomplete','off'); kbd.setAttribute('autocorrect','off'); kbd.setAttribute('autocapitalize','off'); kbd.spellcheck=false; - kbd.style.cssText='position:absolute;left:-1000px;top:0;width:1px;height:1px;opacity:0;'; + kbd.style.cssText='position:fixed;bottom:0;left:0;width:100%;height:1px;opacity:0;' + +'border:0;padding:0;margin:0;background:transparent;color:transparent;' + +'caret-color:transparent;font-size:16px;pointer-events:none;'; document.body.appendChild(kbd); var SPECIAL={Enter:0xff0d,Backspace:0xff08,Tab:0xff09,Escape:0xff1b,Delete:0xffff, ArrowLeft:0xff51,ArrowUp:0xff52,ArrowRight:0xff53,ArrowDown:0xff54,Home:0xff50,End:0xff57}; @@ -101,16 +107,27 @@ export const NOVNC_HTML = ` // Tasten aus dem versteckten Feld an die VM schicken. function tap(keysym, code){ try{ rfb.sendKey(keysym, code||null, true); rfb.sendKey(keysym, code||null, false); }catch(_){} } + // Sondertasten (feuern zuverlaessig als keydown). kbd.addEventListener('keydown', function(e){ if(SPECIAL[e.key]!==undefined){ tap(SPECIAL[e.key], e.code); e.preventDefault(); } }); + // Druckbare Zeichen + Editieren: beforeinput ist auf Android robuster als + // ein input-Diff. preventDefault haelt das Feld leer → kein Doppel-Senden. + kbd.addEventListener('beforeinput', function(e){ + var t=e.inputType||''; + if(t==='insertText' && e.data){ for(var i=0;i=0 || t.indexOf('insertParagraph')>=0){ tap(0xff0d); e.preventDefault(); } + else if(t.indexOf('deleteContentBackward')>=0){ tap(0xff08); e.preventDefault(); } + kbd.value=''; + }); + // Fallback, falls beforeinput nicht unterstuetzt wird. kbd.addEventListener('input', function(){ - var v=kbd.value; for(var i=0;i str: return "" -def _ssh_aria_vm(*args: str, timeout: int = 25): +def _ssh_host(*cmd: str, timeout: int = 25): import subprocess - cmd = ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=8", - _ARIA_VM_HOST, "aria-vm", *[str(a) for a in args]] + full = ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=8", + _ARIA_VM_HOST, *[str(c) for c in cmd]] try: - r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) + r = subprocess.run(full, capture_output=True, text=True, timeout=timeout) return r.returncode, r.stdout or "", r.stderr or "" except Exception as exc: return 1, "", str(exc) +def _ssh_aria_vm(*args: str, timeout: int = 25): + return _ssh_host("aria-vm", *args, timeout=timeout) + + def _vm_running_names() -> set: rc, out, _err = _ssh_aria_vm("list", timeout=15) names = set() @@ -1074,11 +1078,13 @@ def project_vm_stop(project_id: str, name: str): @app.post("/projects/{project_id}/vms/{name}/screenshot") def project_vm_screenshot(project_id: str, name: str): - """Macht einen Screenshot der laufenden VM (aria-vm screenshot → PNG in - /shared/uploads) und liefert ihn als Base64 zurueck. So sieht Stefan den - VM-Bildschirm auch ohne Live-VNC (genau wie ARIA es beim Testen macht).""" + """Macht einen Screenshot der laufenden VM und liefert ihn als Base64. + + aria-vm schreibt das PNG ins VM-Verzeichnis (dem aria-User gehoerend — nicht + ins /root-Shared-Volume, wo der aria-User keinen Zugriff hat). Der Brain holt + die Datei danach per SSH (base64) — funktioniert unabhaengig von Volume- + Rechten. Zusaetzlich wird das PNG ins Projekt kopiert (Dateien-Panel).""" import base64 - import time as _t rc, out, err = _ssh_aria_vm("screenshot", name, timeout=30) if rc != 0: raise HTTPException(status_code=400, detail=f"Screenshot fehlgeschlagen: {(err or out).strip()[:200]}") @@ -1088,28 +1094,28 @@ def project_vm_screenshot(project_id: str, name: str): path = line.split("=", 1)[1].strip() if not path: raise HTTPException(status_code=500, detail=f"Kein Screenshot-Pfad: {out.strip()[:200]}") - local = os.path.join("/shared/uploads", os.path.basename(path)) - for _ in range(10): # kurze Bind-Mount-Latenz abfangen - if os.path.isfile(local): - break - _t.sleep(0.2) - if not os.path.isfile(local): - raise HTTPException(status_code=500, detail=f"Screenshot-Datei nicht gefunden: {local}") - with open(local, "rb") as f: - data = f.read() - # Auch ins Projekt kopieren → taucht im Dateien-Panel auf. + # PNG per SSH als Base64 holen (kein Shared-Volume noetig). + rc2, b64, err2 = _ssh_host("base64", "-w0", path, timeout=20) + if rc2 != 0 or not b64.strip(): + raise HTTPException(status_code=500, detail=f"Screenshot konnte nicht gelesen werden: {(err2 or 'leer').strip()[:200]}") + b64 = b64.strip() + try: + data = base64.b64decode(b64) + except Exception as exc: + raise HTTPException(status_code=500, detail=f"Base64 ungueltig: {exc}") + fname = os.path.basename(path) + # Ins Projekt kopieren → taucht im Dateien-Panel auf. proj_rel = "" try: shots_dir = os.path.join(_project_dir(project_id), "screenshots") os.makedirs(shots_dir, exist_ok=True) - with open(os.path.join(shots_dir, os.path.basename(local)), "wb") as f: + with open(os.path.join(shots_dir, fname), "wb") as f: f.write(data) - proj_rel = "screenshots/" + os.path.basename(local) + proj_rel = "screenshots/" + fname except Exception: proj_rel = "" - return {"ok": True, "name": name, "filename": os.path.basename(local), - "projectPath": proj_rel, - "base64": base64.b64encode(data).decode("ascii")} + return {"ok": True, "name": name, "filename": fname, + "projectPath": proj_rel, "base64": b64} @app.get("/conversation/stats") diff --git a/host-provisioning/aria-vm b/host-provisioning/aria-vm index de136c1..ac9a8c2 100755 --- a/host-provisioning/aria-vm +++ b/host-provisioning/aria-vm @@ -147,16 +147,20 @@ cmd_screenshot() { local d; d="$(vm_dir "${name}")" vm_running "${name}" || die "VM '${name}' laeuft nicht" command -v socat >/dev/null || die "socat fehlt (qemu-setup.sh?)" - mkdir -p "${SHOT_DIR}" + # Standard: ins VM-Verzeichnis schreiben (dem aria-User gehoerend) — NICHT + # nach /root/... (da kommt der aria-User nicht hin). Der Brain holt das PNG + # danach per SSH (base64). Ueberschreibbar via ARIA_VM_SHOT_DIR. + local out_dir="${ARIA_VM_SHOT_DIR:-${d}}" + mkdir -p "${out_dir}" local ts; ts="$(date +%s)" local ppm="${d}/shot-${ts}.ppm" printf 'screendump %s\n' "${ppm}" | socat - "unix-connect:${d}/monitor.sock" >/dev/null sleep 0.3 - local out="${SHOT_DIR}/${name}-${ts}.png" + local out="${out_dir}/${name}-${ts}.png" if command -v convert >/dev/null; then convert "${ppm}" "${out}" && rm -f "${ppm}" else - out="${SHOT_DIR}/${name}-${ts}.ppm"; mv "${ppm}" "${out}" + out="${out_dir}/${name}-${ts}.ppm"; mv "${ppm}" "${out}" fi echo "screenshot=${out}" }