diff --git a/apps/desktop/electron/bootstrap-platform.test.ts b/apps/desktop/electron/bootstrap-platform.test.ts index 3c736e70173c..7184462b3128 100644 --- a/apps/desktop/electron/bootstrap-platform.test.ts +++ b/apps/desktop/electron/bootstrap-platform.test.ts @@ -1,10 +1,12 @@ import assert from 'node:assert/strict' +import fs from 'node:fs' -import { test } from 'vitest' +import { test, vi } from 'vitest' import { bundledRuntimeImportCheck, detectRemoteDisplay, + isVirtualizedGpuEnvironment, isWindowsBinaryPathInWsl, isWslEnvironment, resolveLinuxPasswordStore @@ -18,6 +20,30 @@ test('isWslEnvironment detects WSL2 env vars on linux', () => { assert.equal(isWslEnvironment({ WSL_DISTRO_NAME: 'Ubuntu' }, 'darwin'), false) }) +test('isVirtualizedGpuEnvironment detects QEMU via injected DMI vendor', () => { + assert.match(String(isVirtualizedGpuEnvironment('linux', 'QEMU', '')), /QEMU detected/) + assert.equal(isVirtualizedGpuEnvironment('linux', 'LENOVO', ''), null) + assert.equal(isVirtualizedGpuEnvironment('darwin', 'QEMU', ''), null) +}) + +test('isVirtualizedGpuEnvironment detects Apple Virtualization.framework via injected product name', () => { + assert.match( + String(isVirtualizedGpuEnvironment('linux', 'Apple Inc.', 'Apple Virtualization Generic Platform')), + /Apple Virtualization\.framework detected/ + ) + assert.equal(isVirtualizedGpuEnvironment('linux', 'Apple Inc.', 'MacBookPro18,3'), null) +}) + +test('isVirtualizedGpuEnvironment returns null when the sysfs read fails', () => { + const spy = vi.spyOn(fs, 'readFileSync').mockImplementation(() => { + throw new Error('ENOENT') + }) + + assert.equal(isVirtualizedGpuEnvironment('linux'), null) + + spy.mockRestore() +}) + test('isWindowsBinaryPathInWsl blocks Windows binary types on WSL', () => { assert.equal(isWindowsBinaryPathInWsl('/mnt/c/Tools/hermes.exe', { isWsl: true }), true) assert.equal(isWindowsBinaryPathInWsl('/mnt/c/Tools/hermes.cmd', { isWsl: true }), true) @@ -35,8 +61,16 @@ test('bundledRuntimeImportCheck selects platform-specific import checks', () => test('detectRemoteDisplay keeps GPU on for local sessions', () => { // Plain local X11, Wayland, native Windows, native macOS — no remote signal. - assert.equal(detectRemoteDisplay({ env: { DISPLAY: ':0' }, platform: 'linux' }), null) - assert.equal(detectRemoteDisplay({ env: { WAYLAND_DISPLAY: 'wayland-0' }, platform: 'linux' }), null) + // sysVendor/productName are explicitly injected as empty so this test is + // isolated from whatever real hardware/VM it happens to run on. + assert.equal( + detectRemoteDisplay({ env: { DISPLAY: ':0' }, platform: 'linux', sysVendor: '', productName: '' }), + null + ) + assert.equal( + detectRemoteDisplay({ env: { WAYLAND_DISPLAY: 'wayland-0' }, platform: 'linux', sysVendor: '', productName: '' }), + null + ) assert.equal(detectRemoteDisplay({ env: { SESSIONNAME: 'Console' }, platform: 'win32' }), null) assert.equal(detectRemoteDisplay({ env: {}, platform: 'darwin' }), null) }) @@ -44,9 +78,23 @@ test('detectRemoteDisplay keeps GPU on for local sessions', () => { test('detectRemoteDisplay does not treat WSLg as remote', () => { // WSLg renders locally via vGPU and doesn't show the flicker, so a WSL // session with a local DISPLAY keeps hardware acceleration on. - assert.equal(detectRemoteDisplay({ env: { WSL_DISTRO_NAME: 'Ubuntu', DISPLAY: ':0' }, platform: 'linux' }), null) + // sysVendor/productName injected as empty to isolate from real host hardware. assert.equal( - detectRemoteDisplay({ env: { WSL_INTEROP: '/run/WSL/1_interop', DISPLAY: ':0' }, platform: 'linux' }), + detectRemoteDisplay({ + env: { WSL_DISTRO_NAME: 'Ubuntu', DISPLAY: ':0' }, + platform: 'linux', + sysVendor: '', + productName: '' + }), + null + ) + assert.equal( + detectRemoteDisplay({ + env: { WSL_INTEROP: '/run/WSL/1_interop', DISPLAY: ':0' }, + platform: 'linux', + sysVendor: '', + productName: '' + }), null ) }) @@ -63,7 +111,10 @@ test('detectRemoteDisplay flags SSH sessions on any platform', () => { test('detectRemoteDisplay flags forwarded X11 displays but not local ones', () => { assert.match(String(detectRemoteDisplay({ env: { DISPLAY: 'localhost:10.0' }, platform: 'linux' })), /x11-forwarding/) assert.match(String(detectRemoteDisplay({ env: { DISPLAY: '192.168.1.5:0' }, platform: 'linux' })), /x11-forwarding/) - assert.equal(detectRemoteDisplay({ env: { DISPLAY: ':1' }, platform: 'linux' }), null) + assert.equal( + detectRemoteDisplay({ env: { DISPLAY: ':1' }, platform: 'linux', sysVendor: '', productName: '' }), + null + ) }) test('detectRemoteDisplay flags RDP sessions', () => { @@ -86,6 +137,60 @@ test('detectRemoteDisplay honors the HERMES_DESKTOP_DISABLE_GPU override both wa ) }) +test('detectRemoteDisplay flags virtualized GPU environments via injected sysVendor', () => { + assert.match( + String(detectRemoteDisplay({ env: {}, platform: 'linux', sysVendor: 'QEMU', productName: '' })), + /virtualized-gpu \(QEMU detected/ + ) + assert.equal( + detectRemoteDisplay({ env: {}, platform: 'linux', sysVendor: 'LENOVO', productName: '' }), + null + ) +}) + +test('detectRemoteDisplay flags Apple Virtualization.framework (UTM on Apple Silicon) via injected productName', () => { + assert.match( + String( + detectRemoteDisplay({ + env: {}, + platform: 'linux', + sysVendor: 'Apple Inc.', + productName: 'Apple Virtualization Generic Platform' + }) + ), + /virtualized-gpu \(Apple Virtualization\.framework detected/ + ) + assert.equal( + detectRemoteDisplay({ + env: {}, + platform: 'linux', + sysVendor: 'Apple Inc.', + productName: 'MacBookPro18,3' + }), + null + ) +}) + +test('detectRemoteDisplay honors HERMES_DESKTOP_DISABLE_GPU override over virtualized GPU', () => { + assert.equal( + detectRemoteDisplay({ + env: { HERMES_DESKTOP_DISABLE_GPU: 'false' }, + platform: 'linux', + sysVendor: 'QEMU', + productName: '' + }), + null + ) + assert.equal( + detectRemoteDisplay({ + env: { HERMES_DESKTOP_DISABLE_GPU: 'false' }, + platform: 'linux', + sysVendor: 'Apple Inc.', + productName: 'Apple Virtualization Generic Platform' + }), + null + ) +}) test('resolveLinuxPasswordStore applies known backends on linux', () => { for (const store of ['gnome-libsecret', 'kwallet', 'kwallet5', 'kwallet6', 'basic']) { assert.deepEqual(resolveLinuxPasswordStore({ env: { HERMES_DESKTOP_PASSWORD_STORE: store }, platform: 'linux' }), { diff --git a/apps/desktop/electron/bootstrap-platform.ts b/apps/desktop/electron/bootstrap-platform.ts index 34ba0e76b7a2..91e1885b351b 100644 --- a/apps/desktop/electron/bootstrap-platform.ts +++ b/apps/desktop/electron/bootstrap-platform.ts @@ -18,6 +18,38 @@ function isWslEnvironment(env = process.env, platform = process.platform, kernel } } +function readDmiFieldSafe(field) { + try { + return fs.readFileSync(`/sys/class/dmi/id/${field}`, 'utf8').trim() + } catch { + return null + } +} + +function isVirtualizedGpuEnvironment(platform = process.platform, sysVendor = null, productName = null) { + if (platform !== 'linux') { + return null + } + + const vendor = sysVendor ?? readDmiFieldSafe('sys_vendor') ?? '' + + if (/qemu/i.test(vendor)) { + return `QEMU detected via DMI sys_vendor (${vendor})` + } + + // UTM on Apple Silicon defaults to Apple's Virtualization.framework backend + // instead of QEMU emulation — sys_vendor there just reflects the host + // ("Apple Inc."), so it's indistinguishable from real Mac hardware. The + // guest's product_name is the reliable signal for this backend instead. + const product = productName ?? readDmiFieldSafe('product_name') ?? '' + + if (/apple virtualization/i.test(product)) { + return `Apple Virtualization.framework detected via DMI product_name (${product})` + } + + return null +} + function isWindowsBinaryPathInWsl( filePath, options: { isWsl?: boolean; env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform } = {} @@ -60,7 +92,9 @@ const GPU_OVERRIDE_OFF = new Set(['0', 'false', 'no', 'off']) * * Pure + dependency-free so it can be unit-tested and called before app ready. */ -function detectRemoteDisplay(options: { env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform } = {}) { +function detectRemoteDisplay( + options: { env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform; sysVendor?: string; productName?: string } = {} +) { const env = options.env ?? process.env const platform = options.platform ?? process.platform @@ -88,10 +122,15 @@ function detectRemoteDisplay(options: { env?: NodeJS.ProcessEnv; platform?: Node // NB: WSLg deliberately isn't treated as remote — it reports // GPU-accelerated vGPU surfaces locally and doesn't show the flicker. const display = String(env.DISPLAY || '') - if (display.includes(':') && display.split(':')[0]) { return `x11-forwarding (DISPLAY=${display})` } + + const virtualizedGpuReason = isVirtualizedGpuEnvironment(platform, options.sysVendor, options.productName) + + if (virtualizedGpuReason) { + return `virtualized-gpu (${virtualizedGpuReason})` + } } if (platform === 'win32') { @@ -144,7 +183,8 @@ function resolveLinuxPasswordStore(options: { env?: NodeJS.ProcessEnv; platform? export { bundledRuntimeImportCheck, detectRemoteDisplay, + isVirtualizedGpuEnvironment, isWindowsBinaryPathInWsl, isWslEnvironment, resolveLinuxPasswordStore -} +} \ No newline at end of file