diff --git a/src/main/index.ts b/src/main/index.ts index cdebf9abc..c969d302e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -27,7 +27,7 @@ import { registerAltJsRuntimeHandlers } from './altJsRuntimes'; import { registerAppInfoHandlers } from './ipc/appInfo'; import { registerFileSystemHandlers } from './ipc/fileSystem'; import { registerLocaleHandlers } from './ipc/locale'; -import { registerDesktopSmokeHandlers } from './ipc/desktopSmoke'; +import { desktopSmokeRendererArguments, registerDesktopSmokeHandlers } from './ipc/desktopSmoke'; import { registerEnvHandlers } from './ipc/env'; import { registerLspHandlers, disposeLspBridge } from './ipc/lsp'; import { registerProfileHandlers } from './ipc/profile'; @@ -171,6 +171,7 @@ const createWindow = async () => { contextIsolation: true, nodeIntegration: false, sandbox: true, + additionalArguments: desktopSmokeRendererArguments(), // Pin the same-origin policy explicitly. This is the // Electron default, but pinning it ensures a future edit cannot // silently disable web security (which would let a compromised diff --git a/src/main/ipc/desktopSmoke.ts b/src/main/ipc/desktopSmoke.ts index c7e5ef59c..4a77e84ed 100644 --- a/src/main/ipc/desktopSmoke.ts +++ b/src/main/ipc/desktopSmoke.ts @@ -6,8 +6,8 @@ import { getBlockedOfflineSmokeUrls, isOfflineSmokeRequested, } from '../offlineSmoke'; +import { DESKTOP_SMOKE_FLAG } from '../../shared/desktopSmoke'; -const DESKTOP_SMOKE_FLAG = '--lingua-desktop-smoke'; const SMOKE_ARTIFACT_DIR_PREFIX = '--lingua-smoke-artifact-dir='; const SMOKE_LICENSE_TOKEN_PREFIX = '--lingua-smoke-license-token='; const SMOKE_LAUNCHED_AT_ENV = 'LINGUA_SMOKE_LAUNCHED_AT_MS'; @@ -27,6 +27,14 @@ function isDesktopSmokeEnabled(): boolean { ); } +/** + * Renderer arguments that let the sandboxed preload see smoke mode. A packaged + * renderer does not inherit the launcher's environment or main's argv. + */ +export function desktopSmokeRendererArguments(): string[] { + return isDesktopSmokeEnabled() ? [DESKTOP_SMOKE_FLAG] : []; +} + /** * packaged-subset gate. When the smoke harness is * launched against a release artifact (the `.app` bundle on macOS), diff --git a/src/preload/index.ts b/src/preload/index.ts index 41c80097b..d50d26059 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -12,6 +12,7 @@ import type { PythonDebuggerBridge } from '../shared/pythonDebugger'; import type { GoDebuggerBridge } from '../shared/goDebugger'; import type { RustDebuggerBridge } from '../shared/rustDebugger'; import { typedInvoke, typedOn, typedSend } from './ipcTyped'; +import { DESKTOP_SMOKE_FLAG } from '../shared/desktopSmoke'; const lsp: LspBridge = { resolveTarget: (rootId: RootId, uri: string) => typedInvoke('lsp:resolve-target', rootId, uri), @@ -48,7 +49,7 @@ const lsp: LspBridge = { const desktopSmokeEnabled = process.env.LINGUA_DESKTOP_SMOKE === '1' || - process.argv.includes('--lingua-desktop-smoke'); + process.argv.includes(DESKTOP_SMOKE_FLAG); const http: HttpDesktopAPI = { execute: (runId, request, options) => diff --git a/src/shared/desktopSmoke.ts b/src/shared/desktopSmoke.ts new file mode 100644 index 000000000..5bd87b0c0 --- /dev/null +++ b/src/shared/desktopSmoke.ts @@ -0,0 +1,2 @@ +/** Read by main and by the sandboxed preload, which receives it via additionalArguments. */ +export const DESKTOP_SMOKE_FLAG = '--lingua-desktop-smoke'; diff --git a/tests/ipc/desktopSmoke.test.ts b/tests/ipc/desktopSmoke.test.ts index 7080e350f..e64ec13eb 100644 --- a/tests/ipc/desktopSmoke.test.ts +++ b/tests/ipc/desktopSmoke.test.ts @@ -1,4 +1,7 @@ +import { readFileSync } from 'node:fs'; +import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { DESKTOP_SMOKE_FLAG } from '#src/shared/desktopSmoke'; const handlers = new Map unknown>(); const listeners = new Map unknown>(); @@ -283,4 +286,29 @@ describe('desktop smoke IPC handlers', () => { reason: 'smoke-disabled', }); }); + + it('forwards smoke mode to the sandboxed renderer only when main is in smoke mode', async () => { + const { desktopSmokeRendererArguments } = await import('#src/main/ipc/desktopSmoke'); + expect(desktopSmokeRendererArguments()).toEqual(['--lingua-desktop-smoke']); + + delete process.env.LINGUA_DESKTOP_SMOKE; + process.argv = ['electron', 'main.js']; + expect(desktopSmokeRendererArguments()).toEqual([]); + + process.argv = ['electron', 'main.js', '--lingua-desktop-smoke']; + expect(desktopSmokeRendererArguments()).toEqual(['--lingua-desktop-smoke']); + }); + + it('wires the forwarded flag into the main window and reads the same flag in the preload', () => { + const root = path.resolve(__dirname, '../..'); + const main = readFileSync(path.join(root, 'src/main/index.ts'), 'utf8'); + const webPreferences = main.slice(main.indexOf('webPreferences: {')); + expect(webPreferences.slice(0, webPreferences.indexOf('}'))).toContain( + 'additionalArguments: desktopSmokeRendererArguments()' + ); + + const preload = readFileSync(path.join(root, 'src/preload/index.ts'), 'utf8'); + expect(preload).toContain('process.argv.includes(DESKTOP_SMOKE_FLAG)'); + expect(preload).not.toContain(`'${DESKTOP_SMOKE_FLAG}'`); + }); });