fix(linux): strip injected Chromium switches from CLI args

This commit is contained in:
Neil
2026-09-01 14:07:50 -07:00
parent de4ad981ae
commit b88f03f6fe
4 changed files with 126 additions and 17 deletions
@@ -55,6 +55,48 @@ describe('CLI launch redirect: entry-path form', () => {
).toEqual(['status'])
})
it('strips injected Chromium switches before node-mode CLI arguments', () => {
expect(
getCliLaunchArgs(
[
linux.execPath,
linux.cliEntryPath,
'--no-sandbox',
'--disable-gpu',
'--disable-features=Vulkan',
'status',
'--json'
],
linux.cliEntryPath,
linuxOptions
)
).toEqual(['status', '--json'])
expect(
getCliLaunchArgs(
[linux.execPath, linux.cliEntryPath, '--disable-features', 'Vulkan', 'skills', 'get'],
linux.cliEntryPath,
linuxOptions
)
).toEqual(['skills', 'get'])
})
it('keeps user flags after the command and malformed boolean assignments', () => {
expect(
getCliLaunchArgs(
[linux.execPath, linux.cliEntryPath, 'status', '--disable-features=Vulkan'],
linux.cliEntryPath,
linuxOptions
)
).toEqual(['status', '--disable-features=Vulkan'])
expect(
getCliLaunchArgs(
[linux.execPath, linux.cliEntryPath, '--no-sandbox=true', 'status'],
linux.cliEntryPath,
linuxOptions
)
).toEqual(['--no-sandbox=true', 'status'])
})
it('does not treat a later positional entrypoint path as the launcher', () => {
expect(
getCliLaunchArgs(
@@ -112,6 +154,13 @@ describe('CLI launch redirect: command form', () => {
linuxOptions
)
).toEqual(['serve', '--help'])
expect(
getCliLaunchArgs(
[linux.execPath, '--disable-features', 'Vulkan', 'serve', '--help'],
linux.cliEntryPath,
linuxOptions
)
).toEqual(['serve', '--help'])
})
it('treats help as a CLI launch even without a command', () => {
+62 -16
View File
@@ -21,6 +21,7 @@ export type CliLaunchRedirectOptions = {
const CLI_EARLY_EXIT_FLAGS = new Set(['--help', '-h', 'help', '--version', '-v'])
const DESKTOP_FLAGS = new Set(['--no-sandbox', '--disable-gpu'])
const DESKTOP_VALUE_FLAGS = new Set(['--disable-features'])
const CLI_LAUNCH_VALUE_FLAG_NAMES = [...VALUE_TAKING_FLAGS].map((flag) => flag.slice(2))
// Fence recursion if a wrapper drops ELECTRON_RUN_AS_NODE again.
@@ -85,23 +86,22 @@ export function getCliLaunchArgs(
if (!options.isPackaged) {
return null
}
return (
getEntryPathLaunchArgs(argv, cliEntryPath, options.platform) ??
getCommandLaunchArgs(argv, options)
)
return getEntryPathLaunchArgs(argv, cliEntryPath, options) ?? getCommandLaunchArgs(argv, options)
}
function getEntryPathLaunchArgs(
argv: string[],
cliEntryPath: string,
platform: NodeJS.Platform
options: { platform: NodeJS.Platform; commandNames: readonly string[] }
): string[] | null {
const expectedCliPath = normalizePathForPlatform(cliEntryPath, platform)
const expectedCliPath = normalizePathForPlatform(cliEntryPath, options.platform)
// The packaged launcher always passes the entrypoint as Electron's first argument.
// Matching later positional arguments can mistake a normal desktop launch for the CLI.
return argv[1] && normalizePathForPlatform(argv[1], platform) === expectedCliPath
? argv.slice(2)
: null
if (!argv[1] || normalizePathForPlatform(argv[1], options.platform) !== expectedCliPath) {
return null
}
const args = argv.slice(2)
return stripDesktopFlags(args, findCommandIndex(args, options.commandNames))
}
function getCommandLaunchArgs(
@@ -115,11 +115,8 @@ function getCommandLaunchArgs(
if (args.length === 0) {
return null
}
const commandPaths = options.commandNames.map((name) => [name])
const commandIndex = findCliCommandIndex(args, commandPaths, CLI_LAUNCH_VALUE_FLAG_NAMES)
const cliArgs = args.filter(
(arg, index) => (commandIndex !== -1 && index > commandIndex) || !DESKTOP_FLAGS.has(arg)
)
const commandIndex = findCommandIndex(args, options.commandNames)
const cliArgs = stripDesktopFlags(args, commandIndex)
const command = commandIndex === -1 ? null : args[commandIndex]
// Keep direct serve in-process so signals reach its full child tree.
if (command && command !== 'serve') {
@@ -128,6 +125,50 @@ function getCommandLaunchArgs(
return hasCliEarlyExitArg(args, commandIndex) ? cliArgs : null
}
function findCommandIndex(args: readonly string[], commandNames: readonly string[]): number {
return findCliCommandIndex(
args,
commandNames.map((name) => [name]),
CLI_LAUNCH_VALUE_FLAG_NAMES
)
}
function stripDesktopFlags(args: readonly string[], commandIndex: number): string[] {
const boundary = commandIndex === -1 ? findLeadingFlagBoundary(args) : commandIndex
const cliArgs: string[] = []
for (let index = 0; index < args.length; index += 1) {
const arg = args[index]!
if (index < boundary) {
if (DESKTOP_FLAGS.has(arg)) {
continue
}
if (DESKTOP_VALUE_FLAGS.has(flagName(arg))) {
if (!arg.includes('=') && args[index + 1] && !args[index + 1]!.startsWith('-')) {
index += 1
}
continue
}
}
cliArgs.push(arg)
}
return cliArgs
}
function findLeadingFlagBoundary(args: readonly string[]): number {
let index = 0
while (index < args.length) {
const token = args[index]!
if (token === '--' || !token.startsWith('-')) {
return index
}
index += 1
if (takesLaunchValue(token, args[index])) {
index += 1
}
}
return index
}
function hasCliEarlyExitArg(args: readonly string[], commandIndex: number): boolean {
let index = 0
let positionalCount = 0
@@ -167,8 +208,13 @@ function takesLaunchValue(token: string, next: string | undefined): boolean {
) {
return false
}
const flagName = token.startsWith('--') ? token.slice(2) : token.replace(/^-+/, '')
return !CLI_BOOLEAN_FLAGS.has(flagName)
const name = flagName(token)
return DESKTOP_VALUE_FLAGS.has(name) || !CLI_BOOLEAN_FLAGS.has(name.replace(/^-+/, ''))
}
function flagName(arg: string): string {
const equalsIndex = arg.indexOf('=')
return equalsIndex === -1 ? arg : arg.slice(0, equalsIndex)
}
function buildPackagedCliEntryPath(platform: NodeJS.Platform, resourcesPath: string): string {
+13
View File
@@ -25,6 +25,19 @@ describe('serve-mode-argv', () => {
expect(findServeSubcommandIndex(['app', '--user-data-dir', '/tmp/x', 'serve'])).toBe(3)
})
it('skips a space-separated Chromium switch value while locating serve', () => {
const argv = ['/AppRun', '--disable-features', 'Vulkan', 'serve', '--port', '6768']
expect(findServeSubcommandIndex(argv)).toBe(3)
expect(normalizeServeModeArgv(argv)).toEqual([
'/AppRun',
'--disable-features',
'Vulkan',
'--serve',
'--serve-port',
'6768'
])
})
it('refuses a help launch instead of binding a server', () => {
// Why: `--help` is not a serve flag, so it used to be swallowed and the launch bound a
// network-exposed runtime server with pairing on. The AppImage redirect routes help to the CLI.
+2 -1
View File
@@ -24,13 +24,14 @@ const CLI_TO_SERVE_VALUE_FLAG = new Map([
/**
* Flags that consume the next argv token as a value (CLI-form + Electron passthrough).
* Residual class: a flag outside this list whose space-separated value is literally `serve` would
* read as the subcommand. Chromium switches are `--flag=value` only, so no real launch does that.
* read as the subcommand. Include switches that may arrive in either argv shape.
*/
export const VALUE_TAKING_FLAGS = new Set([
...CLI_TO_SERVE_VALUE_FLAG.keys(),
'--serve-port',
'--serve-pairing-address',
'--serve-project-root',
'--disable-features',
'--user-data-dir',
'--proxy-server',
'--environment',