/** * desktop — Desktop-/VNC-Anbindung fuer Code-Projekte. * * Zwei Aufgaben: * 1. Verfuegbarkeit: `check_desktop` triggert die Bridge, `desktop_status` * meldet zurueck ob eine QEMU-VNC laeuft (und ggf. eine direkte URL). * 2. VNC-Tunnel: der noVNC-Client in der App-WebView spricht kein eigenes * WebSocket, sondern schickt RFB-Bytes als `vnc_input` (Base64) ueber RVS; * die Bridge oeffnet die TCP-Verbindung zu QEMU (host:5901) und streamt die * Antwort als `vnc_data` zurueck. Base64-in-JSON wie audio_pcm. * * Eine Session = ein Desktop; wir nutzen die Projekt-ID als Session-Key (leer = * 'main'). Muster wie services/rvs.ts (Singleton mit Listener-Listen). */ import rvs, { RVSMessage } from './rvs'; export interface DesktopStatus { available: boolean; session: string; /** optionale direkte noVNC-URL (falls Host direkt erreichbar) */ url?: string; message?: string; } type StatusSub = (s: DesktopStatus) => void; type VncDataSub = (b64: string) => void; const DEFAULT_VNC_PORT = 5901; const sessionOf = (projectId: string) => projectId || 'main'; class DesktopService { private status: DesktopStatus = { available: false, session: '' }; private statusSubs: StatusSub[] = []; private vncDataSubs: VncDataSub[] = []; private currentSession = ''; constructor() { rvs.onMessage((m) => this.onMessage(m)); } private onMessage(m: RVSMessage): void { const p = (m.payload || {}) as any; if (m.type === 'desktop_status') { this.status = { available: !!p.available, session: p.session || '', url: typeof p.url === 'string' ? p.url : undefined, message: p.message, }; const s = this.status; this.statusSubs.forEach((cb) => cb(s)); } else if (m.type === 'vnc_data') { if (this.currentSession && p.session && p.session !== this.currentSession) return; const b64 = typeof p.b64 === 'string' ? p.b64 : ''; if (b64) this.vncDataSubs.forEach((cb) => cb(b64)); } } getStatus(): DesktopStatus { return this.status; } subscribeStatus(cb: StatusSub): () => void { this.statusSubs.push(cb); cb(this.status); return () => { this.statusSubs = this.statusSubs.filter((s) => s !== cb); }; } /** Bridge fragen, ob fuer dieses Projekt ein QEMU-Desktop laeuft. */ requestCheck(projectId: string, port: number = DEFAULT_VNC_PORT): void { rvs.send('check_desktop', { projectId: projectId || '', session: sessionOf(projectId), port }); } /** VNC-Tunnel oeffnen — Bridge verbindet TCP zu QEMU. */ openVnc(projectId: string, port: number = DEFAULT_VNC_PORT): string { const session = sessionOf(projectId); this.currentSession = session; rvs.send('vnc_open', { session, port }); return session; } closeVnc(): void { if (this.currentSession) rvs.send('vnc_close', { session: this.currentSession }); this.currentSession = ''; } /** RFB-Bytes (Base64) aus der noVNC-WebView an die Bridge weiterreichen. */ sendInput(b64: string): void { if (!this.currentSession) return; rvs.send('vnc_input', { session: this.currentSession, b64 }); } /** Listener fuer eingehende RFB-Bytes (Base64) — die noVNC-WebView. */ onVncData(cb: VncDataSub): () => void { this.vncDataSubs.push(cb); return () => { this.vncDataSubs = this.vncDataSubs.filter((s) => s !== cb); }; } } const desktop = new DesktopService(); export default desktop;