@@ -1,11 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Enable multiple displays on a SPICE VM by sending monitor configuration.
|
Enable multiple displays on a SPICE VM by sending monitor configuration.
|
||||||
Uses SpiceMainChannel.set_display() to configure displays directly.
|
Retries until the SPICE agent in the guest is connected.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
gi.require_version('SpiceClientGLib', '2.0')
|
gi.require_version('SpiceClientGLib', '2.0')
|
||||||
from gi.repository import SpiceClientGLib, GLib
|
from gi.repository import SpiceClientGLib, GLib
|
||||||
@@ -15,114 +16,150 @@ CHANNEL_MAIN = 1
|
|||||||
CHANNEL_DISPLAY = 2
|
CHANNEL_DISPLAY = 2
|
||||||
|
|
||||||
class SpiceDisplayEnabler:
|
class SpiceDisplayEnabler:
|
||||||
def __init__(self, uri, num_displays=3, width=1920, height=1080):
|
def __init__(self, uri, num_displays=3, width=1920, height=1080, timeout=60):
|
||||||
self.uri = uri
|
self.uri = uri
|
||||||
self.num_displays = num_displays
|
self.num_displays = num_displays
|
||||||
self.width = width
|
self.width = width
|
||||||
self.height = height
|
self.height = height
|
||||||
|
self.timeout = timeout
|
||||||
self.session = None
|
self.session = None
|
||||||
self.main_channel = None
|
self.main_channel = None
|
||||||
self.display_channels = []
|
self.display_channels = []
|
||||||
self.loop = GLib.MainLoop()
|
self.loop = GLib.MainLoop()
|
||||||
self.configured = False
|
self.configured = False
|
||||||
|
self.agent_connected = False
|
||||||
|
self.config_sent = False
|
||||||
|
|
||||||
def on_channel_new(self, session, channel):
|
def on_channel_new(self, session, channel):
|
||||||
"""Handle new channel creation"""
|
"""Handle new channel creation"""
|
||||||
channel_type = channel.get_property('channel-type')
|
channel_type = channel.get_property('channel-type')
|
||||||
channel_id = channel.get_property('channel-id')
|
channel_id = channel.get_property('channel-id')
|
||||||
print(f"New channel: type={channel_type}, id={channel_id}")
|
|
||||||
|
|
||||||
if channel_type == CHANNEL_MAIN:
|
if channel_type == CHANNEL_MAIN:
|
||||||
self.main_channel = channel
|
self.main_channel = channel
|
||||||
channel.connect_after('channel-event', self.on_channel_event)
|
channel.connect_after('channel-event', self.on_channel_event)
|
||||||
|
# Check agent status periodically
|
||||||
|
GLib.timeout_add(500, self.check_agent_and_configure)
|
||||||
elif channel_type == CHANNEL_DISPLAY:
|
elif channel_type == CHANNEL_DISPLAY:
|
||||||
self.display_channels.append((channel_id, channel))
|
self.display_channels.append((channel_id, channel))
|
||||||
print(f" Display channel {channel_id} added")
|
|
||||||
|
|
||||||
def on_channel_event(self, channel, event):
|
def on_channel_event(self, channel, event):
|
||||||
"""Handle channel events"""
|
"""Handle channel events"""
|
||||||
print(f"Channel event: {event}")
|
|
||||||
if event == SpiceClientGLib.ChannelEvent.OPENED:
|
if event == SpiceClientGLib.ChannelEvent.OPENED:
|
||||||
print("Main channel opened, configuring displays...")
|
# Start checking for agent
|
||||||
GLib.timeout_add(2000, self.configure_monitors)
|
GLib.timeout_add(100, self.check_agent_and_configure)
|
||||||
|
|
||||||
|
def check_agent_and_configure(self):
|
||||||
|
"""Check if agent is connected and configure if ready"""
|
||||||
|
if self.config_sent:
|
||||||
|
return False # Stop checking
|
||||||
|
|
||||||
|
if not self.main_channel:
|
||||||
|
return True # Keep checking
|
||||||
|
|
||||||
|
self.agent_connected = self.main_channel.get_property('agent-connected')
|
||||||
|
|
||||||
|
if self.agent_connected and not self.config_sent:
|
||||||
|
print(f"Agent connected! Configuring {self.num_displays} displays...")
|
||||||
|
self.configure_monitors()
|
||||||
|
return False # Stop checking
|
||||||
|
|
||||||
|
return True # Keep checking
|
||||||
|
|
||||||
def configure_monitors(self):
|
def configure_monitors(self):
|
||||||
"""Configure multiple monitors via SPICE protocol"""
|
"""Configure multiple monitors via SPICE protocol"""
|
||||||
print(f"\n=== Configuring {self.num_displays} displays ===")
|
if self.config_sent:
|
||||||
print(f"Display channels available: {len(self.display_channels)}")
|
return
|
||||||
|
|
||||||
if not self.main_channel:
|
if not self.main_channel:
|
||||||
print("No main channel!")
|
print("No main channel!")
|
||||||
GLib.timeout_add(1000, self.quit)
|
return
|
||||||
return False
|
|
||||||
|
|
||||||
# Enable and configure each display
|
# Enable and configure each display
|
||||||
for i in range(self.num_displays):
|
for i in range(self.num_displays):
|
||||||
x = i * self.width # Position displays side by side
|
x = i * self.width # Position displays side by side
|
||||||
y = 0
|
y = 0
|
||||||
|
|
||||||
print(f"Setting display {i}: {self.width}x{self.height} at ({x}, {y})")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Enable the display using update_display_enabled (not set_display_enabled)
|
|
||||||
self.main_channel.update_display_enabled(i, True, False)
|
self.main_channel.update_display_enabled(i, True, False)
|
||||||
|
|
||||||
# Set display geometry (id, x, y, width, height) using update_display
|
|
||||||
self.main_channel.update_display(i, x, y, self.width, self.height, False)
|
self.main_channel.update_display(i, x, y, self.width, self.height, False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" Error setting display {i}: {e}")
|
print(f" Error setting display {i}: {e}")
|
||||||
|
|
||||||
# Send the configuration immediately
|
# Send the configuration
|
||||||
print("\nSending monitor config to guest...")
|
|
||||||
try:
|
try:
|
||||||
self.main_channel.send_monitor_config()
|
self.main_channel.send_monitor_config()
|
||||||
|
self.config_sent = True
|
||||||
self.configured = True
|
self.configured = True
|
||||||
print("Monitor config sent!")
|
print(f"Configured {self.num_displays} displays at {self.width}x{self.height}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error sending config: {e}")
|
print(f"Error sending config: {e}")
|
||||||
|
|
||||||
# Wait a bit then check agent status and quit
|
# Quit after a short delay
|
||||||
GLib.timeout_add(3000, self.check_and_quit)
|
GLib.timeout_add(1000, self.quit)
|
||||||
return False
|
|
||||||
|
|
||||||
def check_and_quit(self):
|
|
||||||
"""Check final status and quit"""
|
|
||||||
if self.main_channel:
|
|
||||||
agent_connected = self.main_channel.get_property('agent-connected')
|
|
||||||
print(f"\nAgent connected: {agent_connected}")
|
|
||||||
self.quit()
|
|
||||||
return False
|
|
||||||
|
|
||||||
def quit(self):
|
def quit(self):
|
||||||
self.loop.quit()
|
self.loop.quit()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def on_timeout(self):
|
||||||
|
"""Handle overall timeout"""
|
||||||
|
if not self.configured:
|
||||||
|
print(f"Timeout after {self.timeout}s - agent not connected")
|
||||||
|
self.quit()
|
||||||
|
return False
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
print(f"Connecting to {self.uri}...")
|
print(f"Connecting to {self.uri}...")
|
||||||
print(f"Target: {self.num_displays} displays at {self.width}x{self.height}")
|
print(f"Waiting up to {self.timeout}s for agent...")
|
||||||
|
|
||||||
self.session = SpiceClientGLib.Session()
|
self.session = SpiceClientGLib.Session()
|
||||||
self.session.set_property('uri', self.uri)
|
self.session.set_property('uri', self.uri)
|
||||||
self.session.connect_after('channel-new', self.on_channel_new)
|
self.session.connect_after('channel-new', self.on_channel_new)
|
||||||
|
|
||||||
if not self.session.connect():
|
if not self.session.connect():
|
||||||
print("Failed to connect")
|
print("Failed to connect to SPICE server")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Fallback timeout to configure monitors
|
# Set overall timeout
|
||||||
GLib.timeout_add(5000, self.configure_monitors)
|
GLib.timeout_add(self.timeout * 1000, self.on_timeout)
|
||||||
GLib.timeout_add(15000, self.quit)
|
|
||||||
self.loop.run()
|
self.loop.run()
|
||||||
|
|
||||||
print(f"\n=== Result ===")
|
if self.configured:
|
||||||
print(f"Configured: {self.configured}")
|
print(f"Success: {self.num_displays} displays enabled")
|
||||||
print(f"Display channels: {len(self.display_channels)}")
|
else:
|
||||||
|
print("Failed: Could not enable displays")
|
||||||
|
|
||||||
return self.configured
|
return self.configured
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
uri = sys.argv[1] if len(sys.argv) > 1 else 'spice://localhost:5930'
|
|
||||||
num_displays = int(sys.argv[2]) if len(sys.argv) > 2 else 3
|
|
||||||
|
|
||||||
enabler = SpiceDisplayEnabler(uri, num_displays)
|
def main():
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser(description='Enable SPICE VM displays')
|
||||||
|
parser.add_argument('uri', nargs='?', default='spice://localhost:5930',
|
||||||
|
help='SPICE URI (default: spice://localhost:5930)')
|
||||||
|
parser.add_argument('num_displays', nargs='?', type=int, default=3,
|
||||||
|
help='Number of displays to enable (default: 3)')
|
||||||
|
parser.add_argument('--timeout', '-t', type=int, default=60,
|
||||||
|
help='Timeout in seconds (default: 60)')
|
||||||
|
parser.add_argument('--width', '-W', type=int, default=1920,
|
||||||
|
help='Display width (default: 1920)')
|
||||||
|
parser.add_argument('--height', '-H', type=int, default=1080,
|
||||||
|
help='Display height (default: 1080)')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
enabler = SpiceDisplayEnabler(
|
||||||
|
args.uri,
|
||||||
|
args.num_displays,
|
||||||
|
args.width,
|
||||||
|
args.height,
|
||||||
|
args.timeout
|
||||||
|
)
|
||||||
success = enabler.run()
|
success = enabler.run()
|
||||||
sys.exit(0 if success else 1)
|
sys.exit(0 if success else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|||||||
@@ -2,6 +2,17 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
AUTO_MODE=false
|
||||||
|
for arg in "$@"; do
|
||||||
|
case $arg in
|
||||||
|
--auto)
|
||||||
|
AUTO_MODE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
PROJECT_ROOT="$SCRIPT_DIR/.."
|
PROJECT_ROOT="$SCRIPT_DIR/.."
|
||||||
VM_DIR="$PROJECT_ROOT/.nogit/vm"
|
VM_DIR="$PROJECT_ROOT/.nogit/vm"
|
||||||
ISO_PATH="$PROJECT_ROOT/.nogit/iso/ecoos.iso"
|
ISO_PATH="$PROJECT_ROOT/.nogit/iso/ecoos.iso"
|
||||||
@@ -192,15 +203,12 @@ echo ""
|
|||||||
echo "=== Press Ctrl-C to stop ==="
|
echo "=== Press Ctrl-C to stop ==="
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Wait for eco-vdagent to be ready in the guest, then enable all 3 displays
|
# Enable all 3 displays via SPICE protocol (waits for agent automatically)
|
||||||
echo "Waiting for eco-vdagent to be ready (10s)..."
|
# Using 300s timeout since ISO boot can take several minutes
|
||||||
sleep 10
|
|
||||||
|
|
||||||
# Enable all 3 displays via SPICE protocol
|
|
||||||
if [ -f "$SCRIPT_DIR/enable-displays.py" ]; then
|
if [ -f "$SCRIPT_DIR/enable-displays.py" ]; then
|
||||||
echo "Enabling all 3 displays via SPICE protocol..."
|
echo "Enabling displays (waiting for SPICE agent, up to 5 minutes)..."
|
||||||
python3 "$SCRIPT_DIR/enable-displays.py" "spice://localhost:5930" 3 2>&1 || true
|
python3 "$SCRIPT_DIR/enable-displays.py" --timeout 300 2>&1 &
|
||||||
echo ""
|
ENABLE_PID=$!
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Tips:"
|
echo "Tips:"
|
||||||
@@ -209,5 +217,39 @@ echo " - http://localhost:3006 - Management UI"
|
|||||||
echo " - socat - UNIX-CONNECT:.nogit/vm/serial.sock - Serial console (login: ecouser/ecouser)"
|
echo " - socat - UNIX-CONNECT:.nogit/vm/serial.sock - Serial console (login: ecouser/ecouser)"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Wait for either process to exit
|
if [ "$AUTO_MODE" = true ]; then
|
||||||
wait $QEMU_PID 2>/dev/null || true
|
echo "=== Auto mode: waiting for display setup ==="
|
||||||
|
|
||||||
|
# Wait for enable-displays.py to complete
|
||||||
|
if [ -n "$ENABLE_PID" ]; then
|
||||||
|
wait $ENABLE_PID
|
||||||
|
ENABLE_EXIT=$?
|
||||||
|
if [ $ENABLE_EXIT -ne 0 ]; then
|
||||||
|
echo "FAIL: Could not enable displays (exit code: $ENABLE_EXIT)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Take screenshot
|
||||||
|
echo "Taking screenshot..."
|
||||||
|
"$SCRIPT_DIR/screenshot.sh"
|
||||||
|
|
||||||
|
# Verify screenshot dimensions (should be 5760x1080 for 3 displays)
|
||||||
|
SCREENSHOT="$PROJECT_ROOT/.nogit/screenshots/latest.png"
|
||||||
|
if [ -f "$SCREENSHOT" ]; then
|
||||||
|
WIDTH=$(identify -format "%w" "$SCREENSHOT" 2>/dev/null || echo "0")
|
||||||
|
if [ "$WIDTH" -ge 5760 ]; then
|
||||||
|
echo "SUCCESS: Multi-display test passed (width: ${WIDTH}px)"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "FAIL: Screenshot width is ${WIDTH}px, expected >= 5760px"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "FAIL: Screenshot not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Interactive mode - wait for QEMU to exit
|
||||||
|
wait $QEMU_PID 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user