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:
2026-01-10 08:23:50 +00:00
parent 7727fafeec
commit e02b5b7046
7 changed files with 86 additions and 15 deletions

View File

@@ -305,12 +305,13 @@ export class EcoDaemon {
private async startSwayWithMode(mode: 'drm' | 'headless'): Promise<void> {
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<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 {
(async () => {
try {