From 33ddab70b797c6bb6f14db0c8525e162ff674832 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 26 Oct 2026 02:53:57 -0700 Subject: [PATCH] feat(serve): keep macOS serve out of the Dock and opt in to the GPU on Linux (#33878) * feat(serve): keep macOS serve out of the Dock and opt in to the GPU on Linux On macOS, orca serve now runs as an accessory app with no Dock icon or Cmd-Tab entry, and gets them back only while it hosts a desktop window. On Linux, ORCA_SERVE_GPU=1 keeps hardware acceleration on for hosts that expose a GPU to the server. * fix(serve): restore the Dock only for real launches, after window services resolve Gate the restore on any background or E2E launch, since dock.show() activates the app, and run it just before the window is created so a services failure cannot leave a windowless Dock tile. Say in the docs what the GPU opt-in can't do on Orca's own Xvfb. --- docs/site/content/docs/remote-servers.mdx | 12 +++++ .../startup/ensure-virtual-display.test.ts | 17 +++++++ src/main/startup/ensure-virtual-display.ts | 4 ++ .../startup/main-process-ready-foundation.ts | 7 ++- src/main/startup/main-window-controller.ts | 10 ++++ .../foreground-activation-policy.test.ts | 39 ++++++++++++++- .../window/foreground-activation-policy.ts | 47 +++++++++++++++---- 7 files changed, 123 insertions(+), 13 deletions(-) diff --git a/docs/site/content/docs/remote-servers.mdx b/docs/site/content/docs/remote-servers.mdx index 7e7021ed967..d9be9701a25 100644 --- a/docs/site/content/docs/remote-servers.mdx +++ b/docs/site/content/docs/remote-servers.mdx @@ -151,6 +151,7 @@ orca-ide serve --pairing-address 100.64.1.20 The command: - starts the Orca runtime without opening the desktop window; +- on macOS, stays out of the Dock and the Cmd-Tab switcher until you open a desktop window from it; - runs in the foreground until you press `Ctrl-C`; - prints the bound endpoint and a runtime pairing URL; - uses `--pairing-address` only for the address clients should dial. @@ -186,6 +187,17 @@ orca-ide serve --pairing-address 100.64.1.20 - Orca reads them only when its terminal host process starts. That process keeps running when `orca serve` restarts, so terminals keep their old limits until it restarts too, for example after the server reboots. - They have no effect on macOS or Windows. +### Use the GPU on a Linux server + +On Linux, `orca serve` turns off hardware acceleration by default, because most headless hosts have no GPU and a failing GPU process can stop the server from starting. Browser panes then render in software. If the host gives the Orca process a GPU (for example a render node such as `/dev/dri/renderD128`), set `ORCA_SERVE_GPU=1` in the environment that starts `orca serve` (or in its service unit) to keep hardware acceleration on: + +```bash +export ORCA_SERVE_GPU=1 +orca-ide serve --pairing-address 100.64.1.20 +``` + +This only stops Orca from turning the GPU off. Chromium may still render in software on Orca's own Xvfb, which has no GPU of its own; open `chrome://gpu` in a browser pane to check, and point `DISPLAY` at a GPU-backed X server if it reports software rendering. If you installed the server with `orca serve service install`, add the variable to the service unit yourself. If the server stops starting or browser panes crash with it set, unset it to go back to software rendering. + ### Mobile from a headless server For the Orca mobile app, request a mobile-scoped QR code and link: diff --git a/src/main/startup/ensure-virtual-display.test.ts b/src/main/startup/ensure-virtual-display.test.ts index afbe10a75ad..b4ed9d23726 100644 --- a/src/main/startup/ensure-virtual-display.test.ts +++ b/src/main/startup/ensure-virtual-display.test.ts @@ -124,6 +124,23 @@ describe('ensureVirtualDisplayForHeadlessServe', () => { expect(appMock.commandLine.appendSwitch).toHaveBeenCalledWith('disable-gpu') }) + it('keeps hardware acceleration when ORCA_SERVE_GPU=1 opts in', async () => { + setPlatform('linux') + process.env.DISPLAY = ':0' + vi.stubEnv('ORCA_SERVE_GPU', '1') + mockLiveXDisplay() + const { ensureVirtualDisplayForHeadlessServe } = await import('./ensure-virtual-display') + + try { + expect(ensureVirtualDisplayForHeadlessServe({ isServeMode: true })).toBe(true) + } finally { + vi.unstubAllEnvs() + } + expect(appMock.disableHardwareAcceleration).not.toHaveBeenCalled() + expect(appMock.commandLine.appendSwitch).toHaveBeenCalledWith('disable-dev-shm-usage') + expect(appMock.commandLine.appendSwitch).not.toHaveBeenCalledWith('disable-gpu') + }) + it('reports unsupported when Xvfb cannot be launched', async () => { setPlatform('linux') statSyncMock.mockImplementation(() => { diff --git a/src/main/startup/ensure-virtual-display.ts b/src/main/startup/ensure-virtual-display.ts index fd3db4c47d7..2c1177b0893 100644 --- a/src/main/startup/ensure-virtual-display.ts +++ b/src/main/startup/ensure-virtual-display.ts @@ -27,6 +27,10 @@ let xvfbProcess: ChildProcess | null = null function configureHeadlessServeChromiumFlags(): void { // Small sandbox /dev/shm mounts can exhaust and crash Chromium utility services; use disk-backed temp. app.commandLine.appendSwitch('disable-dev-shm-usage') + // Opt-in for hosts that expose a GPU to this process (#22450). + if (process.env.ORCA_SERVE_GPU === '1') { + return + } // External displays may also be Xvfb; GPU forks can trap before serve readiness. app.disableHardwareAcceleration() app.commandLine.appendSwitch('disable-gpu') diff --git a/src/main/startup/main-process-ready-foundation.ts b/src/main/startup/main-process-ready-foundation.ts index 85720e14dca..379f3e41864 100644 --- a/src/main/startup/main-process-ready-foundation.ts +++ b/src/main/startup/main-process-ready-foundation.ts @@ -60,8 +60,11 @@ import { reportProfileStateSaveDelay } from './profile-state-save-delay' export async function initializeReadyFoundation(): Promise { logStartupMilestone('app-ready') - // Background automation must not claim the macOS Dock/menu bar. - applyBackgroundActivationPolicy({ warn: console.warn }) + // Background automation and a windowless serve must not claim the macOS Dock/menu bar. + applyBackgroundActivationPolicy({ + serveWithoutDesktopWindow: state.isServeMode, + warn: console.warn + }) installElectronProxyRequestGuard(session.defaultSession) app.on('login', (event, webContents, details, authInfo, callback) => { handleElectronProxyLogin( diff --git a/src/main/startup/main-window-controller.ts b/src/main/startup/main-window-controller.ts index 9a6a1877515..3c9c7df52f1 100644 --- a/src/main/startup/main-window-controller.ts +++ b/src/main/startup/main-window-controller.ts @@ -40,6 +40,10 @@ import { requireMainWindowServices } from './main-window-service-readiness' import { recordRendererLaunchFailureProbe } from '../window/renderer-launch-failure-probe' import { isResourceWindowVisible } from '../window/resource-view-admission' +import { + applyBackgroundActivationPolicy, + applyServeDesktopWindowActivationPolicy +} from '../window/foreground-activation-policy' const TRAY_CREATE_FALLBACK_MS = 12_000 const AGENT_STATE_CRASH_BREADCRUMB_MIN_INTERVAL_MS = 30_000 @@ -87,6 +91,9 @@ export function openMainWindow(options: { revealOnDidFinishLoad?: boolean } = {} noteWindowsInstallDirAclProbePending() } } + if (state.isServeMode) { + applyServeDesktopWindowActivationPolicy({ warn: console.warn }) + } const window = createMainWindow(store, { getIsQuitting: () => state.isQuitting, onQuitAborted: () => { @@ -220,6 +227,9 @@ export function openMainWindow(options: { revealOnDidFinishLoad?: boolean } = {} window.on('closed', () => { if (state.mainWindow === window) { state.mainWindow = null + if (state.isServeMode) { + applyBackgroundActivationPolicy({ serveWithoutDesktopWindow: true, warn: console.warn }) + } } clearExpectedRendererReload(rendererWebContentsId) state.automations?.setWebContents(null) diff --git a/src/main/window/foreground-activation-policy.test.ts b/src/main/window/foreground-activation-policy.test.ts index 91c71bbd28f..41b9fbfc612 100644 --- a/src/main/window/foreground-activation-policy.test.ts +++ b/src/main/window/foreground-activation-policy.test.ts @@ -5,6 +5,7 @@ vi.mock('electron', () => ({ app: { dock: { hide: vi.fn() }, setActivationPolicy const { applyBackgroundActivationPolicy, + applyServeDesktopWindowActivationPolicy, isBackgroundLaunch, isOrcadSidecarLaunch, isWindowlessLaunch, @@ -104,7 +105,7 @@ describe('showWindowWithoutStealingFocus', () => { describe('applyBackgroundActivationPolicy', () => { function makeApp() { return { - dock: { hide: vi.fn(() => {}) }, + dock: { hide: vi.fn(() => {}), show: vi.fn(async () => {}) }, setActivationPolicy: vi.fn((_policy: 'accessory' | 'prohibited' | 'regular') => {}) } } @@ -139,6 +140,42 @@ describe('applyBackgroundActivationPolicy', () => { expect(user.setActivationPolicy).not.toHaveBeenCalled() }) + it('keeps a windowless macOS serve out of the Dock until it hosts a desktop window', () => { + const app = makeApp() + expect( + applyBackgroundActivationPolicy({ + app, + env: {}, + platform: 'darwin', + serveWithoutDesktopWindow: true + }) + ).toBe(true) + expect(app.setActivationPolicy).toHaveBeenLastCalledWith('accessory') + + expect(applyServeDesktopWindowActivationPolicy({ app, env: {}, platform: 'darwin' })).toBe(true) + expect(app.setActivationPolicy).toHaveBeenLastCalledWith('regular') + expect(app.dock.show).toHaveBeenCalledOnce() + }) + + it('never gives a background serve its Dock tile back', () => { + const app = makeApp() + expect( + applyServeDesktopWindowActivationPolicy({ + app, + env: { ORCA_BACKGROUND_LAUNCH: '1' }, + platform: 'darwin' + }) + ).toBe(false) + expect( + applyServeDesktopWindowActivationPolicy({ + app, + env: { ORCA_E2E_HEADFUL: '1' }, + platform: 'darwin' + }) + ).toBe(false) + expect(app.setActivationPolicy).not.toHaveBeenCalled() + }) + it('is a no-op off macOS', () => { const app = makeApp() expect( diff --git a/src/main/window/foreground-activation-policy.ts b/src/main/window/foreground-activation-policy.ts index ff73bf2acaf..9fcc3c187bc 100644 --- a/src/main/window/foreground-activation-policy.ts +++ b/src/main/window/foreground-activation-policy.ts @@ -3,7 +3,7 @@ import { app as electronApp, type BrowserWindow } from 'electron' /** Automation must not steal OS foreground. Background/sidecar requests override E2E_FOREGROUND; the hosted-Xvfb benchmark may present explicitly after startup (tests/AGENTS.md), while automatic reveals stay suppressed. */ type ActivationPolicyApp = { - dock?: { hide: () => void } + dock?: { hide: () => void; show?: () => Promise } setActivationPolicy: (policy: 'accessory' | 'prohibited' | 'regular') => void } @@ -37,17 +37,22 @@ export function isWindowlessLaunch(env: PolicyEnv = process.env): boolean { ) } +type ActivationPolicyOptions = { + app?: ActivationPolicyApp + env?: PolicyEnv + platform?: NodeJS.Platform + /** `orca serve` with no desktop window open: a server, not an app to switch to or quit (#16061). */ + serveWithoutDesktopWindow?: boolean + warn?: (message: string, error: unknown) => void +} + /** macOS accessory policy hides Dock/menu-bar claims during windowless launch/exit while retaining programmatic activation. */ -export function applyBackgroundActivationPolicy( - options: { - app?: ActivationPolicyApp - env?: PolicyEnv - platform?: NodeJS.Platform - warn?: (message: string, error: unknown) => void - } = {} -): boolean { +export function applyBackgroundActivationPolicy(options: ActivationPolicyOptions = {}): boolean { const platform = options.platform ?? process.platform - if (platform !== 'darwin' || !isWindowlessLaunch(options.env ?? process.env)) { + if ( + platform !== 'darwin' || + !(options.serveWithoutDesktopWindow || isWindowlessLaunch(options.env ?? process.env)) + ) { return false } try { @@ -61,6 +66,28 @@ export function applyBackgroundActivationPolicy( } } +/** Gives a serve process its Dock tile and menu bar back while it hosts a desktop window. */ +export function applyServeDesktopWindowActivationPolicy( + options: Omit = {} +): boolean { + const platform = options.platform ?? process.platform + // dock.show() activates the app, so automated launches keep the accessory policy. + if (platform !== 'darwin' || isBackgroundLaunch(options.env ?? process.env)) { + return false + } + try { + const app = options.app ?? electronApp + app.setActivationPolicy('regular') + void app.dock?.show?.().catch((error: unknown) => { + options.warn?.('[window] Failed to show the Dock icon', error) + }) + return true + } catch (error) { + options.warn?.('[window] Failed to apply foreground activation policy', error) + return false + } +} + /** * Reveal a window without taking the foreground: hidden entirely when windowless, * `showInactive()` for explicitly headful E2E runs.