diff --git a/src/main/startup/cli-launch-redirect.test.ts b/src/main/startup/cli-launch-redirect.test.ts index 975e15a5d7c..7a4b29fe60e 100644 --- a/src/main/startup/cli-launch-redirect.test.ts +++ b/src/main/startup/cli-launch-redirect.test.ts @@ -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', () => { diff --git a/src/main/startup/cli-launch-redirect.ts b/src/main/startup/cli-launch-redirect.ts index e9b3be31348..88c0e8062cb 100644 --- a/src/main/startup/cli-launch-redirect.ts +++ b/src/main/startup/cli-launch-redirect.ts @@ -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 { diff --git a/src/main/startup/serve-mode-argv.test.ts b/src/main/startup/serve-mode-argv.test.ts index e82c02189d8..e3c8973427b 100644 --- a/src/main/startup/serve-mode-argv.test.ts +++ b/src/main/startup/serve-mode-argv.test.ts @@ -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. diff --git a/src/main/startup/serve-mode-argv.ts b/src/main/startup/serve-mode-argv.ts index 70159e35057..55f94d895e6 100644 --- a/src/main/startup/serve-mode-argv.ts +++ b/src/main/startup/serve-mode-argv.ts @@ -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',