mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
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.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -60,8 +60,11 @@ import { reportProfileStateSaveDelay } from './profile-state-save-delay'
|
||||
|
||||
export async function initializeReadyFoundation(): Promise<void> {
|
||||
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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<void> }
|
||||
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<ActivationPolicyOptions, 'serveWithoutDesktopWindow'> = {}
|
||||
): 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.
|
||||
|
||||
Reference in New Issue
Block a user