mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 19:17:28 +00:00
fix(serve): start Linux headless serve on X11 so an inherited Wayland session can't abort it (#29260)
* fix(serve): pin headless Linux serve to X11 so an inherited Wayland session can't abort startup Fixes #25400 * fix(serve): put --ozone-platform=x11 on the launch argv for Linux headless serve Chromium caches its Ozone platform in PreSandboxStartup, before main JS runs, so app.commandLine changes cannot move it off Wayland. Pin X11 on the CLI and orcad serve spawns, and re-exec in-process serve (same pid) when Electron already resolved Wayland.
This commit is contained in:
@@ -3,6 +3,7 @@ import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promise
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { headlessServeOzonePlatformSwitches } from '../../shared/headless-serve-ozone-platform'
|
||||
import { encodePairingOffer, PAIRING_OFFER_VERSION } from '../../shared/pairing'
|
||||
import {
|
||||
getServeUpdateHandoffPath,
|
||||
@@ -72,6 +73,8 @@ const INVALID_SSH_RECIPE_JSON = SSH_RECIPE_JSON.replace('/workspace/repo', 'rela
|
||||
const IGNORED_NON_RECIPE_STDOUT = '[serve] ignored non-recipe stdout'
|
||||
const PROFILE = resolve('/profiles/serve')
|
||||
const PIN_PROFILE = `--user-data-dir=${PROFILE}`
|
||||
// Linux CI runners get the X11 pin; macOS and Windows hosts get nothing.
|
||||
const HOST_SERVE_OZONE_SWITCHES = headlessServeOzonePlatformSwitches(process.platform)
|
||||
|
||||
function restoreEnv(key: string, value: string | undefined): void {
|
||||
if (value === undefined) {
|
||||
@@ -439,7 +442,7 @@ describe('serveOrcaApp', () => {
|
||||
|
||||
expect(spawnMock).toHaveBeenCalledWith(
|
||||
'/Applications/Orca.app/Contents/MacOS/Orca',
|
||||
[PIN_PROFILE, '--serve', '--serve-json'],
|
||||
[PIN_PROFILE, '--serve', '--serve-json', ...HOST_SERVE_OZONE_SWITCHES],
|
||||
expect.objectContaining({
|
||||
cwd: resolve(__dirname, '../../..')
|
||||
})
|
||||
@@ -479,7 +482,8 @@ describe('serveOrcaApp', () => {
|
||||
'6768',
|
||||
'--serve-pairing-address',
|
||||
'100.64.1.20',
|
||||
'--serve-mobile-pairing'
|
||||
'--serve-mobile-pairing',
|
||||
...HOST_SERVE_OZONE_SWITCHES
|
||||
],
|
||||
expect.objectContaining({
|
||||
cwd: resolve(__dirname, '../../..')
|
||||
@@ -513,7 +517,8 @@ describe('serveOrcaApp', () => {
|
||||
'--serve',
|
||||
'--serve-json',
|
||||
'--serve-port',
|
||||
'6768'
|
||||
'6768',
|
||||
...HOST_SERVE_OZONE_SWITCHES
|
||||
],
|
||||
expect.objectContaining({
|
||||
cwd: resolve(__dirname, '../../..')
|
||||
@@ -574,7 +579,7 @@ describe('serveOrcaApp', () => {
|
||||
)
|
||||
expect(spawnMock).toHaveBeenCalledWith(
|
||||
executable,
|
||||
[...expectedPrefix, PIN_PROFILE, '--serve', '--serve-json'],
|
||||
[...expectedPrefix, PIN_PROFILE, '--serve', '--serve-json', '--ozone-platform=x11'],
|
||||
// Foreground serve must share POSIX job-control signals with its CLI supervisor.
|
||||
expect.objectContaining({ detached: false })
|
||||
)
|
||||
@@ -616,7 +621,8 @@ describe('serveOrcaApp', () => {
|
||||
'wss://sandbox.example.com',
|
||||
'--serve-recipe-json',
|
||||
'--serve-project-root',
|
||||
'/workspace/repo'
|
||||
'/workspace/repo',
|
||||
...HOST_SERVE_OZONE_SWITCHES
|
||||
],
|
||||
expect.objectContaining({
|
||||
cwd: resolve(__dirname, '../../..'),
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
resolveLaunchUserDataPath
|
||||
} from './launch-user-data-path'
|
||||
import { foregroundServeIsolatedProfileSwitches } from '../../shared/electron-isolated-profile-switches'
|
||||
import { headlessServeOzonePlatformSwitches } from '../../shared/headless-serve-ozone-platform'
|
||||
import { ELECTRON_NO_SANDBOX_REQUEST_ENV } from '../../shared/electron-no-sandbox-request'
|
||||
import { needsElectronNoSandbox } from './electron-sandbox-switch'
|
||||
import { getMacAppBundlePath } from './mac-app-update-bundle'
|
||||
@@ -225,6 +226,7 @@ function spawnElectronServe(
|
||||
// Why: the isolated profile has no login keychain, and the probe that discovers this blocks the
|
||||
// foreground process on the Keychain authorization prompt before it advertises readiness (#25453).
|
||||
childArgs.push(...foregroundServeIsolatedProfileSwitches())
|
||||
childArgs.push(...headlessServeOzonePlatformSwitches(process.platform))
|
||||
const handoffPath =
|
||||
args.recipeJson !== true && getMacAppBundlePath(executable)
|
||||
? getServeUpdateHandoffPath(userDataPath)
|
||||
|
||||
@@ -20,6 +20,7 @@ vi.mock('@orca/process-host', async (importOriginal) => ({
|
||||
}))
|
||||
|
||||
import { BROWSER_SIDECAR_OWNER_ENV } from '../../shared/browser-sidecar-lifetime'
|
||||
import { headlessServeOzonePlatformSwitches } from '../../shared/headless-serve-ozone-platform'
|
||||
import { ElectronServeBrowserProcess } from './electron-serve-browser-process'
|
||||
import { startOrcadBrowserProvider } from './orcad-browser-startup'
|
||||
import { resolveOrcadBrowserProvider } from './orcad-browser-provider'
|
||||
@@ -205,7 +206,14 @@ describe('ElectronServeBrowserProcess start-up', () => {
|
||||
expect(spec.program).toBe(INSTALLED_EXECUTABLE)
|
||||
const args = [...(spec.args ?? [])]
|
||||
expect(args).toEqual(
|
||||
expect.arrayContaining(['--serve', '--serve-port', '--serve-json', '--serve-no-pairing'])
|
||||
expect.arrayContaining([
|
||||
'--serve',
|
||||
'--serve-port',
|
||||
'--serve-json',
|
||||
'--serve-no-pairing',
|
||||
// The sidecar is a headless serve too, so on Linux it must not inherit Wayland.
|
||||
...headlessServeOzonePlatformSwitches(process.platform)
|
||||
])
|
||||
)
|
||||
const port = Number(args[args.indexOf('--serve-port') + 1])
|
||||
expect(Number.isInteger(port) && port > 0 && port < 65_536).toBe(true)
|
||||
|
||||
@@ -14,6 +14,7 @@ import { spawnProcess } from '@orca/process-host'
|
||||
import { isProcessAlive } from '../../shared/serve-bundle-coordination'
|
||||
import { BROWSER_SIDECAR_OWNER_ENV } from '../../shared/browser-sidecar-lifetime'
|
||||
import { electronIsolatedProfileSwitches } from '../../shared/electron-isolated-profile-switches'
|
||||
import { headlessServeOzonePlatformSwitches } from '../../shared/headless-serve-ozone-platform'
|
||||
import { sendOrcadSidecarRequest } from './orcad-sidecar-runtime-client'
|
||||
import {
|
||||
ElectronSidecarTabRegistry,
|
||||
@@ -114,6 +115,7 @@ export class ElectronServeBrowserProcess {
|
||||
'--serve-json',
|
||||
'--serve-no-pairing',
|
||||
...electronIsolatedProfileSwitches(process.env),
|
||||
...headlessServeOzonePlatformSwitches(process.platform),
|
||||
`--user-data-dir=${userDataPath}`
|
||||
],
|
||||
env: { ...electronServeEnvironment(userDataPath), [BROWSER_SIDECAR_OWNER_ENV]: '1' },
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
type CliLaunchRedirectOptions
|
||||
} from './cli-launch-redirect'
|
||||
|
||||
vi.mock('electron', () => ({ app: { commandLine: { getSwitchValue: () => '' } } }))
|
||||
|
||||
const COMMAND_NAMES = ['project', 'serve', 'status', 'skills', 'worktree']
|
||||
|
||||
const linux = {
|
||||
@@ -472,3 +474,38 @@ describe('CLI launch redirect: spawning', () => {
|
||||
expect(result).toEqual({ redirected: true, status: 1 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('CLI launch redirect: in-process serve on Wayland (#25400)', () => {
|
||||
const redirect = (argv: string[], readOzonePlatform: () => string): ReturnType<typeof vi.fn> => {
|
||||
const execve = vi.fn()
|
||||
maybeRedirectCliLaunch({
|
||||
argv,
|
||||
env: { XDG_SESSION_TYPE: 'wayland' },
|
||||
platform: 'linux',
|
||||
isPackaged: true,
|
||||
resourcesPath: linux.resourcesPath,
|
||||
execPath: linux.execPath,
|
||||
commandNames: COMMAND_NAMES,
|
||||
exists: () => true,
|
||||
execve,
|
||||
readOzonePlatform
|
||||
})
|
||||
return execve
|
||||
}
|
||||
|
||||
it('re-execs a direct --serve onto X11 when Electron resolved Wayland', () => {
|
||||
const execve = redirect([linux.execPath, '--serve', '--serve-port', '6768'], () => 'wayland')
|
||||
expect(execve).toHaveBeenCalledExactlyOnceWith(
|
||||
linux.execPath,
|
||||
[linux.execPath, '--ozone-platform=x11', '--serve', '--serve-port', '6768'],
|
||||
{ XDG_SESSION_TYPE: 'wayland' }
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ name: 'a serve already on X11', argv: [linux.execPath, '--serve'], platform: 'x11' },
|
||||
{ name: 'a desktop launch', argv: [linux.execPath], platform: 'wayland' }
|
||||
])('leaves $name alone', ({ argv, platform }) => {
|
||||
expect(redirect(argv, () => platform)).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,7 +4,8 @@ import { runProcessSync } from '@orca/process-host'
|
||||
import { CLI_BOOLEAN_FLAGS, findCliCommandIndex } from '../../shared/cli-argument-boundary'
|
||||
import { ELECTRON_NO_SANDBOX_REQUEST_ENV } from '../../shared/electron-no-sandbox-request'
|
||||
import { CLI_COMMAND_NAMES } from './cli-command-names'
|
||||
import { VALUE_TAKING_FLAGS } from './serve-mode-argv'
|
||||
import { reexecHeadlessServeOnX11IfNeeded } from './headless-serve-x11-reexec'
|
||||
import { argvRequestsServeMode, VALUE_TAKING_FLAGS } from './serve-mode-argv'
|
||||
|
||||
export type CliLaunchRedirectResult = { redirected: false } | { redirected: true; status: number }
|
||||
|
||||
@@ -20,6 +21,8 @@ export type CliLaunchRedirectOptions = {
|
||||
run?: typeof runProcessSync
|
||||
/** Replaces this process; null when the runtime cannot, which keeps serve in-process. */
|
||||
execve?: ((file: string, args: string[], env: NodeJS.ProcessEnv) => void) | null
|
||||
/** The Ozone platform Electron resolved before main ran; defaults to Electron's switch. */
|
||||
readOzonePlatform?: () => string
|
||||
}
|
||||
|
||||
const CLI_EARLY_EXIT_FLAGS = new Set(['--help', '-h', 'help', '--version', '-v'])
|
||||
@@ -59,6 +62,16 @@ export function maybeRedirectCliLaunch(
|
||||
if (serve && execve && exists(cliEntryPath)) {
|
||||
execDirectServe(execve, execPath, cliEntryPath, serve, env)
|
||||
}
|
||||
// In-process serve (direct `--serve`, or a CLI exec that failed) never got the CLI's X11 pin.
|
||||
reexecHeadlessServeOnX11IfNeeded({
|
||||
isServeMode: argvRequestsServeMode(argv),
|
||||
argv,
|
||||
platform,
|
||||
execPath,
|
||||
env,
|
||||
execve: execve ?? null,
|
||||
readResolvedOzonePlatform: options.readOzonePlatform
|
||||
})
|
||||
return { redirected: false }
|
||||
}
|
||||
if (env[REDIRECT_ATTEMPT_ENV] === '1') {
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { headlessServeOzonePlatformSwitches } from '../../shared/headless-serve-ozone-platform'
|
||||
import { reexecHeadlessServeOnX11IfNeeded } from './headless-serve-x11-reexec'
|
||||
|
||||
vi.mock('electron', () => ({ app: { commandLine: { getSwitchValue: () => 'wayland' } } }))
|
||||
|
||||
const EXEC_PATH = '/opt/Orca/orca-ide'
|
||||
|
||||
/**
|
||||
* Electron's pre-main `--ozone-platform-hint=auto` resolution, verified against Electron 43 on
|
||||
* Linux: `XDG_SESSION_TYPE=wayland` alone selects Wayland, and app.commandLine changes made
|
||||
* afterwards do not move it, so only argv decides.
|
||||
*/
|
||||
function resolveOzonePlatform(argv: readonly string[], env: NodeJS.ProcessEnv): string {
|
||||
const explicit = argv.find((arg) => arg.startsWith('--ozone-platform='))
|
||||
if (explicit) {
|
||||
return explicit.slice('--ozone-platform='.length)
|
||||
}
|
||||
const hint = env.ELECTRON_OZONE_PLATFORM_HINT ?? 'auto'
|
||||
const wayland =
|
||||
hint === 'wayland' ||
|
||||
(hint === 'auto' && (env.XDG_SESSION_TYPE === 'wayland' || Boolean(env.WAYLAND_DISPLAY)))
|
||||
return wayland ? 'wayland' : 'x11'
|
||||
}
|
||||
|
||||
/** Boots a fake Electron and returns the platform Chromium ends up on, following any exec. */
|
||||
function bootServe(argv: string[], env: NodeJS.ProcessEnv): { platform: string; execs: number } {
|
||||
let execs = 0
|
||||
let current = argv
|
||||
for (;;) {
|
||||
let next: string[] | null = null
|
||||
reexecHeadlessServeOnX11IfNeeded({
|
||||
isServeMode: current.includes('--serve'),
|
||||
readResolvedOzonePlatform: () => resolveOzonePlatform(current, env),
|
||||
argv: current,
|
||||
platform: 'linux',
|
||||
execPath: EXEC_PATH,
|
||||
env,
|
||||
execve: (_file, args) => {
|
||||
next = args
|
||||
}
|
||||
})
|
||||
if (!next || execs > 2) {
|
||||
return { platform: resolveOzonePlatform(current, env), execs }
|
||||
}
|
||||
execs += 1
|
||||
current = next
|
||||
}
|
||||
}
|
||||
|
||||
const SERVE_ARGS = ['--serve', '--serve-port', '6768', '--serve-no-pairing']
|
||||
|
||||
const matrix = (['wayland', 'x11', undefined] as const).flatMap((sessionType) =>
|
||||
([':0', undefined] as const).flatMap((display) =>
|
||||
(['wayland-0', undefined] as const).flatMap((waylandDisplay) =>
|
||||
(['direct --serve', 'CLI-spawned'] as const).map((launch) => ({
|
||||
sessionType,
|
||||
display,
|
||||
waylandDisplay,
|
||||
launch
|
||||
}))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
describe('reexecHeadlessServeOnX11IfNeeded', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it.each(matrix)(
|
||||
'$launch serve runs on X11 with XDG_SESSION_TYPE=$sessionType DISPLAY=$display WAYLAND_DISPLAY=$waylandDisplay',
|
||||
({ sessionType, display, waylandDisplay, launch }) => {
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...(sessionType ? { XDG_SESSION_TYPE: sessionType } : {}),
|
||||
...(display ? { DISPLAY: display } : {}),
|
||||
...(waylandDisplay ? { WAYLAND_DISPLAY: waylandDisplay } : {})
|
||||
}
|
||||
const argv =
|
||||
launch === 'CLI-spawned'
|
||||
? [EXEC_PATH, ...SERVE_ARGS, ...headlessServeOzonePlatformSwitches('linux')]
|
||||
: [EXEC_PATH, ...SERVE_ARGS]
|
||||
const booted = bootServe(argv, env)
|
||||
|
||||
expect(booted.platform).toBe('x11')
|
||||
// The CLI path pins on the spawn, so it never pays for an exec.
|
||||
expect(booted.execs).toBe(
|
||||
launch === 'direct --serve' && resolveOzonePlatform(argv, env) === 'wayland' ? 1 : 0
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it('pins an inherited ELECTRON_OZONE_PLATFORM_HINT=wayland and an argv hint', () => {
|
||||
const booted = bootServe([EXEC_PATH, '--ozone-platform-hint=auto', ...SERVE_ARGS], {
|
||||
ELECTRON_OZONE_PLATFORM_HINT: 'wayland'
|
||||
})
|
||||
expect(booted).toEqual({ platform: 'x11', execs: 1 })
|
||||
})
|
||||
|
||||
it('keeps the pid, env and argv order, with the switch ahead of a `--` terminator', () => {
|
||||
const execve = vi.fn()
|
||||
const env = { XDG_SESSION_TYPE: 'wayland', HOME: '/home/orca' }
|
||||
reexecHeadlessServeOnX11IfNeeded({
|
||||
isServeMode: true,
|
||||
readResolvedOzonePlatform: () => 'wayland',
|
||||
argv: [EXEC_PATH, '--serve', '--', '--ozone-platform=wayland'],
|
||||
platform: 'linux',
|
||||
execPath: EXEC_PATH,
|
||||
env,
|
||||
execve
|
||||
})
|
||||
expect(execve).toHaveBeenCalledWith(
|
||||
EXEC_PATH,
|
||||
[EXEC_PATH, '--ozone-platform=x11', '--serve', '--', '--ozone-platform=wayland'],
|
||||
env
|
||||
)
|
||||
})
|
||||
|
||||
it("reads Electron's resolved switch by default", () => {
|
||||
const execve = vi.fn()
|
||||
reexecHeadlessServeOnX11IfNeeded({
|
||||
isServeMode: true,
|
||||
argv: [EXEC_PATH, '--serve'],
|
||||
platform: 'linux',
|
||||
execPath: EXEC_PATH,
|
||||
execve
|
||||
})
|
||||
expect(execve).toHaveBeenCalledWith(
|
||||
EXEC_PATH,
|
||||
[EXEC_PATH, '--ozone-platform=x11', '--serve'],
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it('leaves an explicit --ozone-platform alone', () => {
|
||||
const booted = bootServe([EXEC_PATH, '--ozone-platform=wayland', ...SERVE_ARGS], {
|
||||
XDG_SESSION_TYPE: 'wayland'
|
||||
})
|
||||
expect(booted).toEqual({ platform: 'wayland', execs: 0 })
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ name: 'desktop launches', isServeMode: true, platform: 'darwin' as const },
|
||||
{ name: 'non-serve Linux launches', isServeMode: false, platform: 'linux' as const }
|
||||
])('does nothing for $name', ({ isServeMode, platform }) => {
|
||||
const execve = vi.fn()
|
||||
reexecHeadlessServeOnX11IfNeeded({
|
||||
isServeMode,
|
||||
readResolvedOzonePlatform: () => 'wayland',
|
||||
argv: [EXEC_PATH],
|
||||
platform,
|
||||
execve
|
||||
})
|
||||
expect(execve).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps serving in-process when the runtime cannot exec', () => {
|
||||
const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true)
|
||||
const args = {
|
||||
isServeMode: true,
|
||||
readResolvedOzonePlatform: () => 'wayland',
|
||||
argv: [EXEC_PATH, '--serve'],
|
||||
platform: 'linux' as const
|
||||
}
|
||||
expect(() => reexecHeadlessServeOnX11IfNeeded({ ...args, execve: null })).not.toThrow()
|
||||
reexecHeadlessServeOnX11IfNeeded({
|
||||
...args,
|
||||
execve: () => {
|
||||
throw new Error('EACCES')
|
||||
}
|
||||
})
|
||||
expect(stderr).toHaveBeenCalledWith('[serve] Could not restart on X11: EACCES\n')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
import { app } from 'electron'
|
||||
import {
|
||||
HEADLESS_SERVE_OZONE_PLATFORM_SWITCH,
|
||||
hasExplicitOzonePlatform
|
||||
} from '../../shared/headless-serve-ozone-platform'
|
||||
|
||||
export type HeadlessServeX11ReexecOptions = {
|
||||
isServeMode: boolean
|
||||
/** The platform Electron already resolved into `--ozone-platform`; read only for Linux serve. */
|
||||
readResolvedOzonePlatform?: () => string
|
||||
argv?: string[]
|
||||
platform?: NodeJS.Platform
|
||||
execPath?: string
|
||||
env?: NodeJS.ProcessEnv
|
||||
/** Replaces this process; null when the runtime cannot. */
|
||||
execve: ((file: string, args: string[], env: NodeJS.ProcessEnv) => void) | null
|
||||
}
|
||||
|
||||
/**
|
||||
* A direct `--serve` launch (no CLI to add the switch) that Chromium already put on Wayland is
|
||||
* exec'd again with X11 pinned. Exec keeps the pid, so systemd and the AppImage runtime still own
|
||||
* it. Returns only when no exec was needed or it failed; serve then continues as before.
|
||||
*/
|
||||
export function reexecHeadlessServeOnX11IfNeeded(options: HeadlessServeX11ReexecOptions): void {
|
||||
const argv = options.argv ?? process.argv
|
||||
if (
|
||||
!options.isServeMode ||
|
||||
(options.platform ?? process.platform) !== 'linux' ||
|
||||
// Also the loop fence: the exec'd argv carries the switch.
|
||||
hasExplicitOzonePlatform(argv.slice(1)) ||
|
||||
!options.execve ||
|
||||
readOzonePlatform(options).trim().toLowerCase() !== 'wayland'
|
||||
) {
|
||||
return
|
||||
}
|
||||
const execPath = options.execPath ?? process.execPath
|
||||
try {
|
||||
// Why first: a switch after a `--` terminator would be read as a positional.
|
||||
options.execve(
|
||||
execPath,
|
||||
[execPath, HEADLESS_SERVE_OZONE_PLATFORM_SWITCH, ...argv.slice(1)],
|
||||
options.env ?? process.env
|
||||
)
|
||||
} catch (error) {
|
||||
process.stderr.write(
|
||||
`[serve] Could not restart on X11: ${error instanceof Error ? error.message : String(error)}\n`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function readOzonePlatform(options: HeadlessServeX11ReexecOptions): string {
|
||||
return options.readResolvedOzonePlatform
|
||||
? options.readResolvedOzonePlatform()
|
||||
: app.commandLine.getSwitchValue('ozone-platform')
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
hasExplicitOzonePlatform,
|
||||
headlessServeOzonePlatformSwitches
|
||||
} from './headless-serve-ozone-platform'
|
||||
|
||||
describe('headless serve Ozone platform switches', () => {
|
||||
it('pins X11 only for Linux serve launches', () => {
|
||||
expect(headlessServeOzonePlatformSwitches('linux')).toEqual(['--ozone-platform=x11'])
|
||||
expect(headlessServeOzonePlatformSwitches('darwin')).toEqual([])
|
||||
expect(headlessServeOzonePlatformSwitches('win32')).toEqual([])
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ argv: ['--ozone-platform=wayland'], explicit: true },
|
||||
{ argv: ['--ozone-platform', 'x11'], explicit: true },
|
||||
{ argv: ['--ozone-platform-hint=auto'], explicit: false },
|
||||
{ argv: ['--', '--ozone-platform=x11'], explicit: false },
|
||||
{ argv: ['--serve'], explicit: false }
|
||||
])('reads $argv as explicit=$explicit', ({ argv, explicit }) => {
|
||||
expect(hasExplicitOzonePlatform(argv)).toBe(explicit)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Headless serve renders browser panes in hidden windows on an X display (often its own Xvfb), so
|
||||
* it must run Chromium on X11. Chromium picks its platform before Orca's main code runs, from
|
||||
* XDG_SESSION_TYPE / WAYLAND_DISPLAY / ELECTRON_OZONE_PLATFORM_HINT, and `app.commandLine` changes
|
||||
* cannot move it afterwards. An inherited `XDG_SESSION_TYPE=wayland` with no Wayland compositor
|
||||
* makes it exit in platform init (#25400), so the switch must be on the launch argv.
|
||||
*/
|
||||
export const HEADLESS_SERVE_OZONE_PLATFORM_SWITCH = '--ozone-platform=x11'
|
||||
|
||||
/** True when argv already names a platform; a `--ozone-platform-hint` is only a preference. */
|
||||
export function hasExplicitOzonePlatform(argv: readonly string[]): boolean {
|
||||
for (const arg of argv) {
|
||||
if (arg === '--') {
|
||||
return false
|
||||
}
|
||||
if (arg === '--ozone-platform' || arg.startsWith('--ozone-platform=')) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Switches for an Electron serve launch on `platform`. */
|
||||
export function headlessServeOzonePlatformSwitches(platform: NodeJS.Platform): string[] {
|
||||
return platform === 'linux' ? [HEADLESS_SERVE_OZONE_PLATFORM_SWITCH] : []
|
||||
}
|
||||
Reference in New Issue
Block a user