diff --git a/ecoos_daemon/ts/daemon/index.ts b/ecoos_daemon/ts/daemon/index.ts index dc4e337..105acd0 100644 --- a/ecoos_daemon/ts/daemon/index.ts +++ b/ecoos_daemon/ts/daemon/index.ts @@ -305,12 +305,13 @@ export class EcoDaemon { private async startSwayWithMode(mode: 'drm' | 'headless'): Promise { const uid = await this.getUserUid(); - - // Ensure XDG_RUNTIME_DIR exists + const gid = await this.getUserGid(); const runtimeDir = `/run/user/${uid}`; - await runCommand('mkdir', ['-p', runtimeDir]); - await runCommand('chown', [`${this.config.user}:${this.config.user}`, runtimeDir]); - await runCommand('chmod', ['700', runtimeDir]); + + // Ensure XDG_RUNTIME_DIR exists as a proper tmpfs mount + // 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') { this.log('Starting Sway with DRM backend (hardware rendering)'); @@ -374,6 +375,56 @@ export class EcoDaemon { return parseInt(result.stdout.trim(), 10); } + private async getUserGid(): Promise { + 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 { + // 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 { (async () => { try { diff --git a/ecoos_daemon/ts/version.ts b/ecoos_daemon/ts/version.ts index 430296b..952a2d0 100644 --- a/ecoos_daemon/ts/version.ts +++ b/ecoos_daemon/ts/version.ts @@ -1 +1 @@ -export const VERSION = "0.4.12"; +export const VERSION = "0.4.14"; diff --git a/isobuild/config/includes.chroot/opt/eco/bin/eco-daemon b/isobuild/config/includes.chroot/opt/eco/bin/eco-daemon index eb46dba..95c45fb 100755 Binary files a/isobuild/config/includes.chroot/opt/eco/bin/eco-daemon and b/isobuild/config/includes.chroot/opt/eco/bin/eco-daemon differ diff --git a/isotest/enable-displays.py b/isotest/enable-displays.py index 262e05d..c7cbe64 100755 --- a/isotest/enable-displays.py +++ b/isotest/enable-displays.py @@ -64,11 +64,11 @@ class SpiceDisplayEnabler: print(f"Setting display {i}: {self.width}x{self.height} at ({x}, {y})") try: - # Enable the display - self.main_channel.set_display_enabled(i, True) + # Enable the display using update_display_enabled (not set_display_enabled) + self.main_channel.update_display_enabled(i, True, False) - # Set display geometry (id, x, y, width, height) - self.main_channel.set_display(i, x, y, self.width, self.height) + # Set display geometry (id, x, y, width, height) using update_display + self.main_channel.update_display(i, x, y, self.width, self.height, False) except Exception as e: print(f" Error setting display {i}: {e}") diff --git a/isotest/run-test.sh b/isotest/run-test.sh index 7cbfb61..7f1b320 100755 --- a/isotest/run-test.sh +++ b/isotest/run-test.sh @@ -116,6 +116,14 @@ if ! command -v remote-viewer &> /dev/null; then exit 0 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 if [ -z "$DISPLAY" ]; then # Try to find an active X display @@ -175,18 +183,30 @@ if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then VIEWER_PID=$! echo "remote-viewer running headlessly (PID: $VIEWER_PID)" else - echo "Launching remote-viewer (DISPLAY=$DISPLAY, WAYLAND_DISPLAY=$WAYLAND_DISPLAY)..." - remote-viewer spice://localhost:5930 & + echo "Launching remote-viewer with fullscreen for multi-display (DISPLAY=$DISPLAY, WAYLAND_DISPLAY=$WAYLAND_DISPLAY)..." + remote-viewer --full-screen spice://localhost:5930 & VIEWER_PID=$! fi echo "" echo "=== Press Ctrl-C to stop ===" 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 " - View > Displays > Enable Display 2/3 for multi-monitor" echo " - pnpm run test:screenshot - Take screenshot" echo " - http://localhost:3006 - Management UI" +echo " - socat - UNIX-CONNECT:.nogit/vm/serial.sock - Serial console (login: ecouser/ecouser)" echo "" # Wait for either process to exit diff --git a/isotest/virt-viewer-settings b/isotest/virt-viewer-settings index 6790846..d01db0c 100644 --- a/isotest/virt-viewer-settings +++ b/isotest/virt-viewer-settings @@ -2,4 +2,4 @@ share-clipboard=true [fallback] -monitor-mapping=1:0;2:1;3:2 +monitor-mapping=1:1;2:2;3:3 diff --git a/package.json b/package.json index e2daf5b..46f4b6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ecobridge/eco-os", - "version": "0.4.12", + "version": "0.4.14", "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",