diff --git a/ecoos_daemon/ts/daemon/index.ts b/ecoos_daemon/ts/daemon/index.ts index 93e0498..dc4e337 100644 --- a/ecoos_daemon/ts/daemon/index.ts +++ b/ecoos_daemon/ts/daemon/index.ts @@ -149,6 +149,7 @@ export class EcoDaemon { async getDisplays(): Promise { if (this.swayStatus.state !== 'running') { + this.log(`[displays] Sway not running (state: ${this.swayStatus.state}), skipping display query`); return []; } const uid = await this.getUserUid(); diff --git a/ecoos_daemon/ts/daemon/process-manager.ts b/ecoos_daemon/ts/daemon/process-manager.ts index 729d71c..a7d3b1a 100644 --- a/ecoos_daemon/ts/daemon/process-manager.ts +++ b/ecoos_daemon/ts/daemon/process-manager.ts @@ -28,11 +28,39 @@ export class ProcessManager { private user: string; private swayProcess: Deno.ChildProcess | null = null; private browserProcess: Deno.ChildProcess | null = null; + private swaySocket: string | null = null; constructor(user: string) { this.user = user; } + /** + * Find the Sway IPC socket path in the runtime directory + * Sway creates sockets like: sway-ipc.$UID.$PID.sock + */ + async findSwaySocket(runtimeDir: string): Promise { + try { + for await (const entry of Deno.readDir(runtimeDir)) { + if (entry.name.startsWith('sway-ipc.') && entry.name.endsWith('.sock')) { + const socketPath = `${runtimeDir}/${entry.name}`; + console.log(`[sway] Found IPC socket: ${socketPath}`); + return socketPath; + } + } + } catch (error) { + console.error(`[sway] Error finding socket: ${error}`); + } + return null; + } + + getSwaySocket(): string | null { + return this.swaySocket; + } + + setSwaySocket(socket: string | null): void { + this.swaySocket = socket; + } + /** * Generate Sway configuration content for kiosk mode */ @@ -146,9 +174,19 @@ for_window [app_id="chromium-browser"] fullscreen enable * Run a swaymsg command to control Sway */ async swaymsg(config: { runtimeDir: string; waylandDisplay: string }, command: string): Promise { + // Find socket if not already found + if (!this.swaySocket) { + this.swaySocket = await this.findSwaySocket(config.runtimeDir); + } + + if (!this.swaySocket) { + console.error('[swaymsg] No Sway IPC socket found'); + return false; + } + const env: Record = { XDG_RUNTIME_DIR: config.runtimeDir, - WAYLAND_DISPLAY: config.waylandDisplay, + SWAYSOCK: this.swaySocket, }; const envString = Object.entries(env) @@ -292,6 +330,7 @@ for_window [app_id="chromium-browser"] fullscreen enable // Process may already be dead } this.swayProcess = null; + this.swaySocket = null; // Reset socket so we find new one on restart } } @@ -311,9 +350,19 @@ for_window [app_id="chromium-browser"] fullscreen enable * Get connected displays via swaymsg */ async getDisplays(config: { runtimeDir: string; waylandDisplay: string }): Promise { + // Find socket if not already found + if (!this.swaySocket) { + this.swaySocket = await this.findSwaySocket(config.runtimeDir); + } + + if (!this.swaySocket) { + console.error('[displays] No Sway IPC socket found'); + return []; + } + const env: Record = { XDG_RUNTIME_DIR: config.runtimeDir, - WAYLAND_DISPLAY: config.waylandDisplay, + SWAYSOCK: this.swaySocket, }; const envString = Object.entries(env) @@ -329,7 +378,8 @@ for_window [app_id="chromium-browser"] fullscreen enable try { const result = await cmd.output(); if (!result.success) { - console.error('[displays] Failed to get outputs'); + const stderr = new TextDecoder().decode(result.stderr); + console.error(`[displays] Failed to get outputs: ${stderr}`); return []; } @@ -427,6 +477,7 @@ for_window [app_id="chromium-browser"] fullscreen enable console.log(`[${name}] Process exited with code ${status.code}`); if (name === 'sway' && this.swayProcess === process) { this.swayProcess = null; + this.swaySocket = null; // Reset socket so we find new one on restart } else if (name === 'chromium' && this.browserProcess === process) { this.browserProcess = null; } diff --git a/ecoos_daemon/ts/version.ts b/ecoos_daemon/ts/version.ts index 0b28a75..2391cca 100644 --- a/ecoos_daemon/ts/version.ts +++ b/ecoos_daemon/ts/version.ts @@ -1 +1 @@ -export const VERSION = "0.3.9"; +export const VERSION = "0.4.2"; diff --git a/isobuild/config/includes.chroot/opt/eco/bin/eco-daemon b/isobuild/config/includes.chroot/opt/eco/bin/eco-daemon index 64d4bd1..dba7ed2 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/run-test.sh b/isotest/run-test.sh index d3c478f..36bc04f 100755 --- a/isotest/run-test.sh +++ b/isotest/run-test.sh @@ -48,7 +48,7 @@ else echo "KVM not available, using software emulation (slower)" fi -# Start QEMU with VirtIO-GPU (VirGL OpenGL acceleration) and serial console +# Start QEMU with multiple displays (3 total) for multi-monitor testing > "$SERIAL_LOG" # Clear old log qemu-system-x86_64 \ $KVM_OPTS \ @@ -57,7 +57,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 \ + -device virtio-gpu-pci,id=video0 \ + -device virtio-gpu-pci,id=video1 \ + -device virtio-gpu-pci,id=video2 \ -display none \ -spice port=5930,disable-ticketing=on \ -serial unix:"$SERIAL_SOCK",server,nowait \ diff --git a/isotest/screenshot.sh b/isotest/screenshot.sh index 8372e89..9cdefda 100755 --- a/isotest/screenshot.sh +++ b/isotest/screenshot.sh @@ -15,35 +15,57 @@ if [ ! -S "$MONITOR_SOCK" ]; then fi mkdir -p "$SCREENSHOT_DIR" -PPM_FILE="$SCREENSHOT_DIR/ecoos-$TIMESTAMP.ppm" -PNG_FILE="$SCREENSHOT_DIR/ecoos-$TIMESTAMP.png" -LATEST_FILE="$SCREENSHOT_DIR/latest.png" -echo "Taking screenshot..." -echo "screendump $PPM_FILE" | socat - UNIX-CONNECT:"$MONITOR_SOCK" -sleep 1 +# Screenshot all displays (video0, video1, video2) +DISPLAYS="video0 video1 video2" +SCREENSHOTS=() -# Check if PPM was created -if [ ! -f "$PPM_FILE" ]; then - echo "ERROR: Screenshot failed" - exit 1 -fi +echo "Taking screenshots of all displays..." -# Convert to PNG if imagemagick is available -if command -v convert &> /dev/null; then - convert "$PPM_FILE" "$PNG_FILE" - rm "$PPM_FILE" +for DISPLAY in $DISPLAYS; do + PPM_FILE="$SCREENSHOT_DIR/ecoos-$TIMESTAMP-$DISPLAY.ppm" + PNG_FILE="$SCREENSHOT_DIR/ecoos-$TIMESTAMP-$DISPLAY.png" - # Copy to latest.png - cp "$PNG_FILE" "$LATEST_FILE" + # Take screenshot of this display + echo "screendump $PPM_FILE $DISPLAY" | socat - UNIX-CONNECT:"$MONITOR_SOCK" 2>/dev/null + sleep 0.5 - echo "Screenshot saved: $PNG_FILE" + if [ -f "$PPM_FILE" ]; then + # Convert to PNG if imagemagick is available + if command -v convert &> /dev/null; then + convert "$PPM_FILE" "$PNG_FILE" + rm "$PPM_FILE" + SCREENSHOTS+=("$PNG_FILE") + echo " $DISPLAY: $PNG_FILE" + else + SCREENSHOTS+=("$PPM_FILE") + echo " $DISPLAY: $PPM_FILE" + fi + else + echo " $DISPLAY: (no output or not connected)" + fi +done + +# Create a combined image if we have multiple screenshots and imagemagick +if command -v convert &> /dev/null && [ ${#SCREENSHOTS[@]} -gt 1 ]; then + COMBINED_FILE="$SCREENSHOT_DIR/ecoos-$TIMESTAMP-combined.png" + LATEST_FILE="$SCREENSHOT_DIR/latest.png" + + # Combine images horizontally + convert "${SCREENSHOTS[@]}" +append "$COMBINED_FILE" + cp "$COMBINED_FILE" "$LATEST_FILE" + + echo "" + echo "Combined screenshot: $COMBINED_FILE" + echo "Also saved as: $LATEST_FILE" +elif [ ${#SCREENSHOTS[@]} -eq 1 ]; then + LATEST_FILE="$SCREENSHOT_DIR/latest.png" + cp "${SCREENSHOTS[0]}" "$LATEST_FILE" + echo "" echo "Also saved as: $LATEST_FILE" -else - echo "Screenshot saved: $PPM_FILE" - echo "(Install imagemagick to auto-convert to PNG)" fi -# Keep only last 20 screenshots (excluding latest.png) +# Keep only last 20 screenshot sets (excluding latest.png) cd "$SCREENSHOT_DIR" -ls -t ecoos-*.png 2>/dev/null | tail -n +21 | xargs -r rm -f +ls -t ecoos-*-combined.png 2>/dev/null | tail -n +21 | xargs -r rm -f +ls -t ecoos-*-video*.png 2>/dev/null | tail -n +61 | xargs -r rm -f diff --git a/package.json b/package.json index f4d8592..437b45e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ecobridge/eco-os", - "version": "0.4.1", + "version": "0.4.2", "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",