diff --git a/app/electron/cli.test.ts b/app/electron/cli.test.ts index b3e93064..6d45857b 100644 --- a/app/electron/cli.test.ts +++ b/app/electron/cli.test.ts @@ -298,6 +298,20 @@ describe('notFoundStage (non-sensitive telemetry enum for a not-found)', () => { expect(notFoundStage()).toBe('bundled-missing') }) + it.skipIf(process.platform === 'win32' || process.getuid?.() === 0)('reports bundled-denied when the bundled CLI exists but cannot be stat-ed', () => { + delete process.env.CODEBURN_BIN + const locked = join(dir, 'locked') + mkdirSync(locked) + writeFileSync(join(locked, 'launch.js'), '') + chmodSync(locked, 0o000) + process.env.CODEBURN_BUNDLED_CLI = join(locked, 'launch.js') + try { + expect(notFoundStage()).toBe('bundled-denied') + } finally { + chmodSync(locked, 0o755) + } + }) + it('reports bin-not-absolute for a relative CODEBURN_BIN override', () => { process.env.CODEBURN_BIN = 'relative/codeburn' delete process.env.CODEBURN_BUNDLED_CLI diff --git a/app/electron/cli.ts b/app/electron/cli.ts index 544c2758..6d7a6c0d 100644 --- a/app/electron/cli.ts +++ b/app/electron/cli.ts @@ -42,6 +42,8 @@ export type NotFoundStage = | 'bin-not-executable' | 'bundled-not-absolute' | 'bundled-missing' + | 'bundled-denied' + | 'bundled-unreadable' | 'spawn-error' | 'no-path-match' @@ -550,11 +552,24 @@ export function notFoundStage(): NotFoundStage { const bundled = process.env.CODEBURN_BUNDLED_CLI if (bundled) { if (!isAbsolute(bundled)) return 'bundled-not-absolute' - if (!isFile(bundled)) return 'bundled-missing' + if (!isFile(bundled)) return bundledStatStage(bundled) } return 'no-path-match' } +// isFile() swallows the errno: ENOENT is a vanished bundle, EACCES/EPERM one we were refused. +function bundledStatStage(p: string): NotFoundStage { + try { + statSync(p) + return 'bundled-missing' + } catch (err) { + const code = (err as NodeJS.ErrnoException).code + if (code === 'ENOENT' || code === 'ENOTDIR') return 'bundled-missing' + if (code === 'EACCES' || code === 'EPERM') return 'bundled-denied' + return 'bundled-unreadable' + } +} + /** Ask a child to exit, then insist. See {@link KILL_GRACE_MS}. The child is * (re-)registered as active for the whole grace: a quit landing inside that * window must still find it and SIGKILL it rather than orphan it. */