fix(multi-display): fix runtime directory race condition and SPICE display enabling
- Fix tmpfs race condition in daemon by mounting runtime directory explicitly before starting Sway, preventing sockets from being hidden when systemd-logind mounts over them later - Fix enable-displays.py to use correct SpiceClientGLib API methods (update_display_enabled/update_display instead of set_display_enabled/set_display) - Fix virt-viewer monitor-mapping to use 1-indexed client monitors - Add virt-viewer config setup and automatic display enabling to test script - Multi-display now works correctly with 3 QXL devices
This commit is contained in:
@@ -305,12 +305,13 @@ export class EcoDaemon {
|
|||||||
|
|
||||||
private async startSwayWithMode(mode: 'drm' | 'headless'): Promise<void> {
|
private async startSwayWithMode(mode: 'drm' | 'headless'): Promise<void> {
|
||||||
const uid = await this.getUserUid();
|
const uid = await this.getUserUid();
|
||||||
|
const gid = await this.getUserGid();
|
||||||
// Ensure XDG_RUNTIME_DIR exists
|
|
||||||
const runtimeDir = `/run/user/${uid}`;
|
const runtimeDir = `/run/user/${uid}`;
|
||||||
await runCommand('mkdir', ['-p', runtimeDir]);
|
|
||||||
await runCommand('chown', [`${this.config.user}:${this.config.user}`, runtimeDir]);
|
// Ensure XDG_RUNTIME_DIR exists as a proper tmpfs mount
|
||||||
await runCommand('chmod', ['700', runtimeDir]);
|
// This is critical - if Sway creates sockets before the tmpfs is mounted,
|
||||||
|
// they become hidden when systemd-logind mounts the tmpfs later
|
||||||
|
await this.ensureRuntimeDirTmpfs(runtimeDir, uid, gid);
|
||||||
|
|
||||||
if (mode === 'drm') {
|
if (mode === 'drm') {
|
||||||
this.log('Starting Sway with DRM backend (hardware rendering)');
|
this.log('Starting Sway with DRM backend (hardware rendering)');
|
||||||
@@ -374,6 +375,56 @@ export class EcoDaemon {
|
|||||||
return parseInt(result.stdout.trim(), 10);
|
return parseInt(result.stdout.trim(), 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getUserGid(): Promise<number> {
|
||||||
|
const result = await runCommand('id', ['-g', this.config.user]);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error('Failed to get user GID: ' + result.stderr);
|
||||||
|
}
|
||||||
|
return parseInt(result.stdout.trim(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the user runtime directory exists as a proper tmpfs mount.
|
||||||
|
* This prevents race conditions where Sway creates sockets before
|
||||||
|
* systemd-logind mounts the tmpfs, causing sockets to be hidden.
|
||||||
|
*/
|
||||||
|
private async ensureRuntimeDirTmpfs(runtimeDir: string, uid: number, gid: number): Promise<void> {
|
||||||
|
// Check if runtime dir is already a tmpfs mount
|
||||||
|
const mountCheck = await runCommand('findmnt', ['-n', '-o', 'FSTYPE', runtimeDir]);
|
||||||
|
if (mountCheck.success && mountCheck.stdout.trim() === 'tmpfs') {
|
||||||
|
this.log(`Runtime directory ${runtimeDir} is already a tmpfs mount`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the directory if it doesn't exist
|
||||||
|
await runCommand('mkdir', ['-p', runtimeDir]);
|
||||||
|
|
||||||
|
// Mount a tmpfs if not already mounted
|
||||||
|
this.log(`Mounting tmpfs on ${runtimeDir}`);
|
||||||
|
const mountResult = await runCommand('mount', [
|
||||||
|
'-t', 'tmpfs',
|
||||||
|
'-o', `mode=700,uid=${uid},gid=${gid},size=100M`,
|
||||||
|
'tmpfs',
|
||||||
|
runtimeDir
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!mountResult.success) {
|
||||||
|
// If mount fails, maybe it's already mounted by systemd-logind
|
||||||
|
// Double-check and continue if it's now a tmpfs
|
||||||
|
const recheckMount = await runCommand('findmnt', ['-n', '-o', 'FSTYPE', runtimeDir]);
|
||||||
|
if (recheckMount.success && recheckMount.stdout.trim() === 'tmpfs') {
|
||||||
|
this.log(`Runtime directory ${runtimeDir} was mounted by another process`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.log(`Warning: Failed to mount tmpfs on ${runtimeDir}: ${mountResult.stderr}`);
|
||||||
|
// Fall back to just ensuring the directory exists with correct permissions
|
||||||
|
await runCommand('chown', [`${uid}:${gid}`, runtimeDir]);
|
||||||
|
await runCommand('chmod', ['700', runtimeDir]);
|
||||||
|
} else {
|
||||||
|
this.log(`Successfully mounted tmpfs on ${runtimeDir}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private startJournalReader(): void {
|
private startJournalReader(): void {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const VERSION = "0.4.12";
|
export const VERSION = "0.4.14";
|
||||||
|
|||||||
Binary file not shown.
@@ -64,11 +64,11 @@ class SpiceDisplayEnabler:
|
|||||||
print(f"Setting display {i}: {self.width}x{self.height} at ({x}, {y})")
|
print(f"Setting display {i}: {self.width}x{self.height} at ({x}, {y})")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Enable the display
|
# Enable the display using update_display_enabled (not set_display_enabled)
|
||||||
self.main_channel.set_display_enabled(i, True)
|
self.main_channel.update_display_enabled(i, True, False)
|
||||||
|
|
||||||
# Set display geometry (id, x, y, width, height)
|
# Set display geometry (id, x, y, width, height) using update_display
|
||||||
self.main_channel.set_display(i, x, y, self.width, self.height)
|
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}")
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,14 @@ if ! command -v remote-viewer &> /dev/null; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Set up virt-viewer settings for multi-display
|
||||||
|
VIRT_VIEWER_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/virt-viewer"
|
||||||
|
mkdir -p "$VIRT_VIEWER_CONFIG_DIR"
|
||||||
|
if [ -f "$SCRIPT_DIR/virt-viewer-settings" ]; then
|
||||||
|
cp "$SCRIPT_DIR/virt-viewer-settings" "$VIRT_VIEWER_CONFIG_DIR/settings"
|
||||||
|
echo "Configured virt-viewer for 3 displays"
|
||||||
|
fi
|
||||||
|
|
||||||
# Detect DISPLAY if not set
|
# Detect DISPLAY if not set
|
||||||
if [ -z "$DISPLAY" ]; then
|
if [ -z "$DISPLAY" ]; then
|
||||||
# Try to find an active X display
|
# Try to find an active X display
|
||||||
@@ -175,18 +183,30 @@ if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then
|
|||||||
VIEWER_PID=$!
|
VIEWER_PID=$!
|
||||||
echo "remote-viewer running headlessly (PID: $VIEWER_PID)"
|
echo "remote-viewer running headlessly (PID: $VIEWER_PID)"
|
||||||
else
|
else
|
||||||
echo "Launching remote-viewer (DISPLAY=$DISPLAY, WAYLAND_DISPLAY=$WAYLAND_DISPLAY)..."
|
echo "Launching remote-viewer with fullscreen for multi-display (DISPLAY=$DISPLAY, WAYLAND_DISPLAY=$WAYLAND_DISPLAY)..."
|
||||||
remote-viewer spice://localhost:5930 &
|
remote-viewer --full-screen spice://localhost:5930 &
|
||||||
VIEWER_PID=$!
|
VIEWER_PID=$!
|
||||||
fi
|
fi
|
||||||
echo ""
|
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
|
||||||
|
echo "Waiting for eco-vdagent to be ready (10s)..."
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Enable all 3 displays via SPICE protocol
|
||||||
|
if [ -f "$SCRIPT_DIR/enable-displays.py" ]; then
|
||||||
|
echo "Enabling all 3 displays via SPICE protocol..."
|
||||||
|
python3 "$SCRIPT_DIR/enable-displays.py" "spice://localhost:5930" 3 2>&1 || true
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Tips:"
|
echo "Tips:"
|
||||||
echo " - View > Displays > Enable Display 2/3 for multi-monitor"
|
|
||||||
echo " - pnpm run test:screenshot - Take screenshot"
|
echo " - pnpm run test:screenshot - Take screenshot"
|
||||||
echo " - http://localhost:3006 - Management UI"
|
echo " - http://localhost:3006 - Management UI"
|
||||||
|
echo " - socat - UNIX-CONNECT:.nogit/vm/serial.sock - Serial console (login: ecouser/ecouser)"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Wait for either process to exit
|
# Wait for either process to exit
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
share-clipboard=true
|
share-clipboard=true
|
||||||
|
|
||||||
[fallback]
|
[fallback]
|
||||||
monitor-mapping=1:0;2:1;3:2
|
monitor-mapping=1:1;2:2;3:3
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ecobridge/eco-os",
|
"name": "@ecobridge/eco-os",
|
||||||
"version": "0.4.12",
|
"version": "0.4.14",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"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",
|
"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",
|
||||||
|
|||||||
Reference in New Issue
Block a user