#!/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('