diff --git a/ecoos_daemon/ts/version.ts b/ecoos_daemon/ts/version.ts index 86305df..0fdd7d6 100644 --- a/ecoos_daemon/ts/version.ts +++ b/ecoos_daemon/ts/version.ts @@ -1 +1 @@ -export const VERSION = "0.4.4"; +export const VERSION = "0.4.11"; diff --git a/ecoos_daemon/vdagent/eco-vdagent.py b/ecoos_daemon/vdagent/eco-vdagent.py new file mode 100644 index 0000000..f1ddda3 --- /dev/null +++ b/ecoos_daemon/vdagent/eco-vdagent.py @@ -0,0 +1,448 @@ +#!/usr/bin/env python3 +""" +EcoOS Wayland Display Agent (eco-vdagent) + +A Wayland-native replacement for spice-vdagent that uses swaymsg/wlr-output-management +instead of xrandr to configure displays. + +Listens on the SPICE virtio-serial port for VD_AGENT_MONITORS_CONFIG messages +and applies the configuration to Sway outputs. +""" + +import os +import sys +import struct +import subprocess +import json +import time +import signal +import logging +from pathlib import Path + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - eco-vdagent - %(levelname)s - %(message)s' +) +log = logging.getLogger('eco-vdagent') + +# SPICE VDAgent Protocol Constants +VD_AGENT_PROTOCOL = 1 + +# Message types +VD_AGENT_MOUSE_STATE = 1 +VD_AGENT_MONITORS_CONFIG = 2 +VD_AGENT_REPLY = 3 +VD_AGENT_CLIPBOARD = 4 +VD_AGENT_DISPLAY_CONFIG = 5 +VD_AGENT_ANNOUNCE_CAPABILITIES = 6 +VD_AGENT_CLIPBOARD_GRAB = 7 +VD_AGENT_CLIPBOARD_REQUEST = 8 +VD_AGENT_CLIPBOARD_RELEASE = 9 +VD_AGENT_FILE_XFER_START = 10 +VD_AGENT_FILE_XFER_STATUS = 11 +VD_AGENT_FILE_XFER_DATA = 12 +VD_AGENT_CLIENT_DISCONNECTED = 13 +VD_AGENT_MAX_CLIPBOARD = 14 +VD_AGENT_AUDIO_VOLUME_SYNC = 15 +VD_AGENT_GRAPHICS_DEVICE_INFO = 16 + +# Reply error codes +VD_AGENT_SUCCESS = 1 +VD_AGENT_ERROR = 2 + +# Capability bits +VD_AGENT_CAP_MOUSE_STATE = 0 +VD_AGENT_CAP_MONITORS_CONFIG = 1 +VD_AGENT_CAP_REPLY = 2 +VD_AGENT_CAP_CLIPBOARD = 3 +VD_AGENT_CAP_DISPLAY_CONFIG = 4 +VD_AGENT_CAP_CLIPBOARD_BY_DEMAND = 5 +VD_AGENT_CAP_CLIPBOARD_SELECTION = 6 +VD_AGENT_CAP_SPARSE_MONITORS_CONFIG = 7 +VD_AGENT_CAP_GUEST_LINEEND_LF = 8 +VD_AGENT_CAP_GUEST_LINEEND_CRLF = 9 +VD_AGENT_CAP_MAX_CLIPBOARD = 10 +VD_AGENT_CAP_AUDIO_VOLUME_SYNC = 11 +VD_AGENT_CAP_MONITORS_CONFIG_POSITION = 12 +VD_AGENT_CAP_FILE_XFER_DISABLED = 13 +VD_AGENT_CAP_FILE_XFER_DETAILED_ERRORS = 14 +VD_AGENT_CAP_GRAPHICS_DEVICE_INFO = 15 +VD_AGENT_CAP_CLIPBOARD_NO_RELEASE_ON_REGRAB = 16 +VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL = 17 + +# Virtio serial port path +VIRTIO_PORT = '/dev/virtio-ports/com.redhat.spice.0' + +# VDI Chunk header: port(4) + size(4) = 8 bytes +VDI_CHUNK_HEADER_SIZE = 8 +VDI_CHUNK_HEADER_FMT = ' len(data): + log.error(f"Truncated monitor config at index {i}") + break + + height, width, depth, x, y = struct.unpack( + MON_CONFIG_FMT, + data[offset:offset + MON_CONFIG_SIZE] + ) + + monitors.append({ + 'width': width, + 'height': height, + 'depth': depth, + 'x': x, + 'y': y + }) + log.info(f" Monitor {i}: {width}x{height}+{x}+{y} depth={depth}") + offset += MON_CONFIG_SIZE + + return monitors + + def send_reply(self, msg_type, error_code): + """Send VD_AGENT_REPLY message""" + # Reply data: type(4) + error(4) = 8 bytes + reply_data = struct.pack(' 0: + try: + os.write(self.port_fd, message) + return True + except OSError as e: + if e.errno == 11: # EAGAIN - resource temporarily unavailable + retries -= 1 + time.sleep(0.1) + continue + log.error(f"Failed to send message type {msg_type}: {e}") + return False + log.error(f"Failed to send message type {msg_type}: EAGAIN after retries") + return False + + def announce_capabilities(self): + """Send VD_AGENT_ANNOUNCE_CAPABILITIES to register with SPICE server""" + # Build capability bits - we support monitors config + caps = 0 + caps |= (1 << VD_AGENT_CAP_MONITORS_CONFIG) + caps |= (1 << VD_AGENT_CAP_REPLY) + caps |= (1 << VD_AGENT_CAP_SPARSE_MONITORS_CONFIG) + caps |= (1 << VD_AGENT_CAP_MONITORS_CONFIG_POSITION) + + # VDAgentAnnounceCapabilities: request(4) + caps(4) = 8 bytes + # request=1 means we want the server to send us its capabilities + announce_data = struct.pack(' len(data): + log.error(f"Truncated monitor config at index {i}") + break + + height, width, depth, x, y = struct.unpack( + MON_CONFIG_FMT, + data[offset:offset + MON_CONFIG_SIZE] + ) + + monitors.append({ + 'width': width, + 'height': height, + 'depth': depth, + 'x': x, + 'y': y + }) + log.info(f" Monitor {i}: {width}x{height}+{x}+{y} depth={depth}") + offset += MON_CONFIG_SIZE + + return monitors + + def send_reply(self, msg_type, error_code): + """Send VD_AGENT_REPLY message""" + # Reply data: type(4) + error(4) = 8 bytes + reply_data = struct.pack(' 0: + try: + os.write(self.port_fd, message) + return True + except OSError as e: + if e.errno == 11: # EAGAIN - resource temporarily unavailable + retries -= 1 + time.sleep(0.1) + continue + log.error(f"Failed to send message type {msg_type}: {e}") + return False + log.error(f"Failed to send message type {msg_type}: EAGAIN after retries") + return False + + def announce_capabilities(self): + """Send VD_AGENT_ANNOUNCE_CAPABILITIES to register with SPICE server""" + # Build capability bits - we support monitors config + caps = 0 + caps |= (1 << VD_AGENT_CAP_MONITORS_CONFIG) + caps |= (1 << VD_AGENT_CAP_REPLY) + caps |= (1 << VD_AGENT_CAP_SPARSE_MONITORS_CONFIG) + caps |= (1 << VD_AGENT_CAP_MONITORS_CONFIG_POSITION) + + # VDAgentAnnounceCapabilities: request(4) + caps(4) = 8 bytes + # request=1 means we want the server to send us its capabilities + announce_data = struct.pack(' 1 else 'spice://localhost:5930' + num_displays = int(sys.argv[2]) if len(sys.argv) > 2 else 3 + + enabler = SpiceDisplayEnabler(uri, num_displays) + success = enabler.run() + sys.exit(0 if success else 1) diff --git a/isotest/run-test.sh b/isotest/run-test.sh index 3098bb8..7cbfb61 100755 --- a/isotest/run-test.sh +++ b/isotest/run-test.sh @@ -55,6 +55,9 @@ cleanup() { if [ -n "$VIEWER_PID" ] && kill -0 "$VIEWER_PID" 2>/dev/null; then kill "$VIEWER_PID" 2>/dev/null || true fi + if [ -n "$TWM_PID" ] && kill -0 "$TWM_PID" 2>/dev/null; then + kill "$TWM_PID" 2>/dev/null || true + fi if [ -n "$XORG_PID" ] && kill -0 "$XORG_PID" 2>/dev/null; then kill "$XORG_PID" 2>/dev/null || true fi @@ -78,7 +81,9 @@ qemu-system-x86_64 \ -bios /usr/share/qemu/OVMF.fd \ -drive file="$ISO_PATH",media=cdrom \ -drive file="$DISK_PATH",format=qcow2,if=virtio \ - -device virtio-vga,max_outputs=3 \ + -device qxl-vga,id=video0,ram_size=67108864,vram_size=67108864,vgamem_mb=64 \ + -device qxl,id=video1,ram_size=67108864,vram_size=67108864,vgamem_mb=64 \ + -device qxl,id=video2,ram_size=67108864,vram_size=67108864,vgamem_mb=64 \ -display none \ -spice port=5930,disable-ticketing=on \ -device virtio-serial-pci \ @@ -163,10 +168,9 @@ if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then xrandr --output DUMMY1 --mode 1920x1080 --pos 1920x0 2>/dev/null || true xrandr --output DUMMY2 --mode 1920x1080 --pos 3840x0 2>/dev/null || true - echo "Headless X server started on :$XDISPLAY with 3 RandR monitors" - xrandr --listmonitors + echo "Headless X server started on :$XDISPLAY" - # Launch remote-viewer in fullscreen to use all monitors + # Launch remote-viewer in fullscreen to request all monitors remote-viewer --full-screen spice://localhost:5930 & VIEWER_PID=$! echo "remote-viewer running headlessly (PID: $VIEWER_PID)" diff --git a/isotest/virt-viewer-settings b/isotest/virt-viewer-settings new file mode 100644 index 0000000..6790846 --- /dev/null +++ b/isotest/virt-viewer-settings @@ -0,0 +1,5 @@ +[virt-viewer] +share-clipboard=true + +[fallback] +monitor-mapping=1:0;2:1;3:2 diff --git a/package.json b/package.json index e61a8bb..96aa707 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ecobridge/eco-os", - "version": "0.4.4", + "version": "0.4.11", "private": true, "scripts": { "build": "[ -z \"$CI\" ] && npm version patch --no-git-tag-version || true && node -e \"const v=require('./package.json').version; require('fs').writeFileSync('ecoos_daemon/ts/version.ts', 'export const VERSION = \\\"'+v+'\\\";\\n');\" && pnpm run daemon:bundle && cp ecoos_daemon/bundle/eco-daemon isobuild/config/includes.chroot/opt/eco/bin/ && mkdir -p .nogit/iso && docker build --no-cache -t ecoos-builder -f isobuild/Dockerfile . && docker run --privileged --name ecoos-build ecoos-builder && docker cp ecoos-build:/output/ecoos.iso .nogit/iso/ecoos.iso && docker rm ecoos-build",