Run the packaged orca CLI even when an Orca GUI instance holds the single-instance lock (#6542)

On Windows the bundled orca.cmd runs Orca.exe with the unpacked CLI
entrypoint and ELECTRON_RUN_AS_NODE=1. When that env var is dropped, Orca
boots as a GUI, loses the single-instance lock to an already-running
window, and exits silently with no stdout.

Add a packaged-CLI-entry redirect that detects a Windows launch whose argv
carries the known in-package CLI entrypoint and re-runs it in Electron node
mode before the single-instance lock gate can bounce it, then exits with the
CLI's status. Mirrors the existing maybeRedirectAppImageCliLaunch pattern.

Fixes #5774

Co-authored-by: brennanb2025 <brennanb2025@users.noreply.github.com>
This commit is contained in:
Brennan Benson
2026-06-28 00:58:57 -07:00
committed by GitHub
co-authored by brennanb2025
parent 8a39450b18
commit f768e6b503
3 changed files with 300 additions and 0 deletions
+13
View File
@@ -62,6 +62,7 @@ import {
suppressDevEducationForStore
} from './startup/dev-education-suppression'
import { maybeRedirectAppImageCliLaunch } from './startup/appimage-cli-redirect'
import { maybeRedirectPackagedCliEntryLaunch } from './startup/packaged-cli-entry-redirect'
import { startFirstWindowStartupServices } from './startup/first-window-startup-services'
import { getDevInstanceIdentity } from './startup/dev-instance-identity'
import { hydrateShellPath, mergePathSegments } from './startup/hydrate-shell-path'
@@ -183,6 +184,18 @@ let firstWindowStartupServicesReady: Promise<void> = Promise.resolve()
let localPtyStartupReady: Promise<void> = Promise.resolve()
const AGENT_STATE_CRASH_BREADCRUMB_MIN_INTERVAL_MS = 30_000
const isServeMode = process.argv.includes('--serve')
// Why: on Windows a CLI-shaped launch (Orca.exe <unpacked CLI entry>) that lost
// ELECTRON_RUN_AS_NODE would otherwise boot the GUI, lose the single-instance
// lock to a running window, and exit silently. Redirect it to node mode here,
// before the lock gate below can bounce it.
const packagedCliEntryRedirect = maybeRedirectPackagedCliEntryLaunch({
isPackaged: app.isPackaged,
resourcesPath: process.resourcesPath,
execPath: process.execPath
})
if (packagedCliEntryRedirect.redirected) {
app.exit(packagedCliEntryRedirect.status)
}
const appImageCliRedirect = maybeRedirectAppImageCliLaunch({
isPackaged: app.isPackaged,
resourcesPath: process.resourcesPath,
@@ -0,0 +1,159 @@
import { win32 } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import {
getPackagedCliEntryArgs,
maybeRedirectPackagedCliEntryLaunch
} from './packaged-cli-entry-redirect'
const resourcesPath = 'C:\\Users\\me\\AppData\\Local\\Programs\\Orca\\resources'
const execPath = 'C:\\Users\\me\\AppData\\Local\\Programs\\Orca\\Orca.exe'
const cliEntryPath = win32.join(resourcesPath, 'app.asar.unpacked', 'out', 'cli', 'index.js')
describe('packaged CLI entry redirect', () => {
it('detects Windows GUI launches that received the unpacked CLI entrypoint', () => {
expect(
getPackagedCliEntryArgs(
[execPath, cliEntryPath.toUpperCase(), 'status', '--json'],
cliEntryPath,
'win32'
)
).toEqual(['status', '--json'])
})
it('ignores normal desktop launches', () => {
expect(getPackagedCliEntryArgs([execPath, '--updated'], cliEntryPath, 'win32')).toBeNull()
})
it('ignores the entrypoint when it is only the executable itself (argv[0])', () => {
expect(getPackagedCliEntryArgs([cliEntryPath, 'status'], cliEntryPath, 'win32')).toBeNull()
})
it('does not match the entrypoint on non-Windows platforms', () => {
expect(getPackagedCliEntryArgs([execPath, cliEntryPath, 'status'], cliEntryPath, 'linux')).toBeNull()
})
it('spawns the in-package CLI in Electron node mode before the single-instance lock can win', () => {
const spawn = vi.fn((..._args: unknown[]) => ({ status: 0 }))
const result = maybeRedirectPackagedCliEntryLaunch({
argv: [execPath, cliEntryPath, 'status', '--json'],
env: {
NODE_OPTIONS: '--inspect',
NODE_REPL_EXTERNAL_MODULE: 'external-loader'
},
platform: 'win32',
isPackaged: true,
resourcesPath,
execPath,
exists: () => true,
spawn: spawn as never
})
expect(result).toEqual({ redirected: true, status: 0 })
expect(spawn).toHaveBeenCalledWith(
execPath,
[cliEntryPath, 'status', '--json'],
{
env: expect.objectContaining({
ELECTRON_RUN_AS_NODE: '1',
ORCA_PACKAGED_CLI_ENTRY_REDIRECTED: '1',
ORCA_NODE_OPTIONS: '--inspect',
ORCA_NODE_REPL_EXTERNAL_MODULE: 'external-loader'
}),
stdio: 'inherit'
}
)
const spawnOptions = spawn.mock.calls[0]?.[2] as { env: NodeJS.ProcessEnv } | undefined
expect(spawnOptions?.env).not.toHaveProperty('NODE_OPTIONS')
expect(spawnOptions?.env).not.toHaveProperty('NODE_REPL_EXTERNAL_MODULE')
})
it('never spawns an attacker-supplied script — only the computed in-package entry', () => {
const spawn = vi.fn((..._args: unknown[]) => ({ status: 0 }))
const attackerScript = 'C:\\Users\\me\\evil.js'
const result = maybeRedirectPackagedCliEntryLaunch({
// An attacker placing some other script path in argv must not cause it to run.
argv: [execPath, attackerScript, 'status'],
platform: 'win32',
isPackaged: true,
resourcesPath,
execPath,
exists: () => true,
spawn: spawn as never
})
expect(result).toEqual({ redirected: false })
expect(spawn).not.toHaveBeenCalled()
})
it('does not redirect development launches', () => {
const spawn = vi.fn()
const result = maybeRedirectPackagedCliEntryLaunch({
argv: ['C:\\dev\\Orca.exe', cliEntryPath, 'status'],
platform: 'win32',
isPackaged: false,
resourcesPath,
execPath: 'C:\\dev\\Orca.exe',
exists: () => true,
spawn: spawn as never
})
expect(result).toEqual({ redirected: false })
expect(spawn).not.toHaveBeenCalled()
})
it('reports a clear failure instead of locating a missing entrypoint', () => {
const spawn = vi.fn()
const stderrWrite = vi.spyOn(process.stderr, 'write').mockImplementation(() => true)
try {
const result = maybeRedirectPackagedCliEntryLaunch({
argv: [execPath, cliEntryPath, 'status'],
platform: 'win32',
isPackaged: true,
resourcesPath,
execPath,
exists: () => false,
spawn: spawn as never
})
expect(result).toEqual({ redirected: true, status: 1 })
expect(stderrWrite).toHaveBeenCalledWith(
`Unable to locate the Orca CLI entrypoint at ${cliEntryPath}\n`
)
expect(spawn).not.toHaveBeenCalled()
} finally {
stderrWrite.mockRestore()
}
})
it('fails clearly instead of recursively redirecting when node mode already failed once', () => {
const spawn = vi.fn()
const stderrWrite = vi.spyOn(process.stderr, 'write').mockImplementation(() => true)
try {
const result = maybeRedirectPackagedCliEntryLaunch({
argv: [execPath, cliEntryPath, 'status', '--json'],
env: {
ORCA_PACKAGED_CLI_ENTRY_REDIRECTED: '1'
},
platform: 'win32',
isPackaged: true,
resourcesPath,
execPath,
exists: () => true,
spawn: spawn as never
})
expect(result).toEqual({ redirected: true, status: 1 })
expect(stderrWrite).toHaveBeenCalledWith(
'Unable to start the Orca CLI through Electron node mode.\n'
)
expect(spawn).not.toHaveBeenCalled()
} finally {
stderrWrite.mockRestore()
}
})
})
@@ -0,0 +1,128 @@
import { spawnSync, type SpawnSyncReturns } from 'node:child_process'
import { existsSync } from 'node:fs'
import { posix, win32 } from 'node:path'
type RedirectResult =
| {
redirected: false
}
| {
redirected: true
status: number
}
type RedirectOptions = {
argv?: string[]
env?: NodeJS.ProcessEnv
platform?: NodeJS.Platform
isPackaged?: boolean
resourcesPath?: string
execPath?: string
exists?: typeof existsSync
spawn?: typeof spawnSync
}
// Why: set on the re-spawned node-mode child so a failure to honor
// ELECTRON_RUN_AS_NODE can't make us redirect forever in a tight loop.
const REDIRECT_ATTEMPT_ENV = 'ORCA_PACKAGED_CLI_ENTRY_REDIRECTED'
/**
* Why: on Windows the bundled `orca.cmd` runs `Orca.exe <unpacked CLI entry>`
* with ELECTRON_RUN_AS_NODE=1. When that env var is dropped (e.g. a wrapper or
* shell that resets it), Orca boots as a GUI, loses the single-instance lock to
* an already-running window, and exits silently with no stdout. This detects the
* CLI-shaped launch — argv carrying the known in-package CLI entry path — and
* re-runs it in Electron node mode BEFORE the lock gate, then exits with the
* CLI's status.
*
* Security: the spawned program is always `execPath` (Orca.exe) and the script
* is always `cliEntryPath`, derived solely from `resourcesPath` + a fixed
* relative path — never taken from argv. argv only contributes the trailing
* CLI arguments forwarded to the already-trusted in-package CLI, and the
* redirect only fires when an argv element exactly equals that computed path,
* so it cannot be coerced into spawning an arbitrary script.
*/
export function maybeRedirectPackagedCliEntryLaunch(options: RedirectOptions = {}): RedirectResult {
const argv = options.argv ?? process.argv
const env = options.env ?? process.env
const platform = options.platform ?? process.platform
const isPackaged = options.isPackaged ?? false
const resourcesPath = options.resourcesPath ?? process.resourcesPath
const execPath = options.execPath ?? process.execPath
const exists = options.exists ?? existsSync
const spawn = options.spawn ?? spawnSync
const cliEntryPath = buildPackagedCliEntryPath(platform, resourcesPath)
const cliArgs = getPackagedCliEntryArgs(argv, cliEntryPath, platform)
if (!isPackaged || !cliArgs) {
return { redirected: false }
}
if (env[REDIRECT_ATTEMPT_ENV] === '1') {
process.stderr.write('Unable to start the Orca CLI through Electron node mode.\n')
return { redirected: true, status: 1 }
}
if (!exists(cliEntryPath)) {
process.stderr.write(`Unable to locate the Orca CLI entrypoint at ${cliEntryPath}\n`)
return { redirected: true, status: 1 }
}
const result = spawn(execPath, [cliEntryPath, ...cliArgs], {
env: buildElectronRunAsNodeEnv(env),
stdio: 'inherit'
}) as SpawnSyncReturns<Buffer>
if (result.error) {
process.stderr.write(`${result.error.message}\n`)
return { redirected: true, status: 1 }
}
return { redirected: true, status: result.status ?? 1 }
}
/**
* Returns the CLI arguments that follow the in-package CLI entrypoint in argv,
* or null when this is not a Windows CLI-shaped launch. Scoped to win32 because
* the AppImage redirect already covers the Linux equivalent.
*/
export function getPackagedCliEntryArgs(
argv: string[],
cliEntryPath: string,
platform: NodeJS.Platform
): string[] | null {
if (platform !== 'win32') {
return null
}
const expectedCliPath = normalizePathForPlatform(cliEntryPath, platform)
const cliEntryIndex = argv.findIndex(
(arg, index) => index > 0 && normalizePathForPlatform(arg, platform) === expectedCliPath
)
return cliEntryIndex === -1 ? null : argv.slice(cliEntryIndex + 1)
}
function buildPackagedCliEntryPath(platform: NodeJS.Platform, resourcesPath: string): string {
return getPathApi(platform).join(resourcesPath, 'app.asar.unpacked', 'out', 'cli', 'index.js')
}
function normalizePathForPlatform(value: string, platform: NodeJS.Platform): string {
const pathApi = getPathApi(platform)
const normalized = pathApi.normalize(pathApi.isAbsolute(value) ? value : pathApi.resolve(value))
// Why: Windows paths are case-insensitive, so compare case-folded.
return platform === 'win32' ? normalized.toLowerCase() : normalized
}
function getPathApi(platform: NodeJS.Platform): typeof win32 | typeof posix {
return platform === 'win32' ? win32 : posix
}
function buildElectronRunAsNodeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const childEnv = { ...env }
// Why: the CLI re-reads these from the ORCA_-prefixed copies; clearing the
// originals keeps Electron's own node bootstrap from inheriting them.
childEnv.ORCA_NODE_OPTIONS = env.NODE_OPTIONS ?? ''
childEnv.ORCA_NODE_REPL_EXTERNAL_MODULE = env.NODE_REPL_EXTERNAL_MODULE ?? ''
childEnv.ELECTRON_RUN_AS_NODE = '1'
childEnv[REDIRECT_ATTEMPT_ENV] = '1'
delete childEnv.NODE_OPTIONS
delete childEnv.NODE_REPL_EXTERNAL_MODULE
return childEnv
}