From ca0eec568c613a1ecccc3fdcd37b05bafa1d2b5e Mon Sep 17 00:00:00 2001 From: sudacode Date: Fri, 6 Mar 2026 21:17:47 -0800 Subject: [PATCH] fix: quiet default appimage startup --- ...-startup-and-implicit-background-launch.md | 65 +++++++++++++++++++ src/main-entry-runtime.test.ts | 34 ++++++++++ src/main-entry-runtime.ts | 54 ++++++++++++++- src/main-entry.ts | 17 +++++ src/main.ts | 4 +- .../anilist-setup-protocol-main-deps.test.ts | 2 +- .../anilist-setup-protocol-main-deps.ts | 2 +- .../runtime/anilist-setup-protocol.test.ts | 17 ++++- src/main/runtime/anilist-setup-protocol.ts | 6 +- .../composers/anilist-setup-composer.test.ts | 2 +- 10 files changed, 192 insertions(+), 11 deletions(-) create mode 100644 backlog/tasks/task-102 - Quiet-default-AppImage-startup-and-implicit-background-launch.md diff --git a/backlog/tasks/task-102 - Quiet-default-AppImage-startup-and-implicit-background-launch.md b/backlog/tasks/task-102 - Quiet-default-AppImage-startup-and-implicit-background-launch.md new file mode 100644 index 0000000..7b206c9 --- /dev/null +++ b/backlog/tasks/task-102 - Quiet-default-AppImage-startup-and-implicit-background-launch.md @@ -0,0 +1,65 @@ +--- +id: TASK-102 +title: Quiet default AppImage startup and implicit background launch +status: Done +assignee: + - codex +created_date: '2026-03-06 21:20' +updated_date: '2026-03-06 21:33' +labels: [] +dependencies: [] +references: + - /home/sudacode/projects/japanese/SubMiner/src/main-entry-runtime.ts + - /home/sudacode/projects/japanese/SubMiner/src/core/services/cli-command.ts + - /home/sudacode/projects/japanese/SubMiner/src/main.ts +priority: medium +--- + +## Description + + +Make the packaged Linux no-arg launch path behave like a quiet background start instead of surfacing startup-only noise. + +Scope: + +- Treat default background entry launches as implicit `--start --background`. +- Keep the `--password-store` diagnostic out of normal startup output. +- Suppress known startup-only `node:sqlite` and `lsfg-vk` warnings for the entry/background launch path. +- Avoid noisy protocol-registration warnings during normal startup when registration is unsupported. + + +## Acceptance Criteria + + +- [x] #1 Initial background launch reaches the start path without logging `No running instance. Use --start to launch the app.` +- [x] #2 Default startup no longer emits the `Applied --password-store gnome-libsecret` line at normal log levels. +- [x] #3 Entry/background launch sanitization suppresses the observed `ExperimentalWarning: SQLite...` and `lsfg-vk ... unsupported configuration version` startup noise. +- [x] #4 Regression coverage documents the new startup behavior. + + +## Implementation Notes + + +Normalized no-arg/password-store-only entry launches to append implicit `--start --background`, and upgraded `--background`-only entry launches to include `--start`. + +Applied shared entry env sanitization before loading the main process so default startup strips the `lsfg-vk` Vulkan layer and sets `NODE_NO_WARNINGS=1`; background children keep the same sanitized env. + +Downgraded startup-only protocol-registration failure logging to debug, and routed the Linux password-store diagnostic through the scoped debug logger instead of raw console output. + +Verification: + +- `bun test src/main-entry-runtime.test.ts src/main/runtime/anilist-setup-protocol.test.ts src/main/runtime/anilist-setup-protocol-main-deps.test.ts` +- `bun run test:fast` + +Note: the final `node --experimental-sqlite --test dist/main/runtime/registry.test.js` step in `bun run test:fast` still prints Node's own experimental SQLite warning because that test command explicitly enables the feature flag outside the app entrypoint. + + +## Final Summary + + +Default packaged startup is now quiet and behaves like an implicit `--start --background` launch. + +- No-arg AppImage entry launches now append `--start --background`, and `--background`-only launches append the missing `--start`. +- Entry/background startup sanitization now suppresses the observed `lsfg-vk` and `node:sqlite` warnings on the app launch path. +- Linux password-store and unsupported protocol-registration diagnostics now stay at debug level instead of normal startup output. + diff --git a/src/main-entry-runtime.test.ts b/src/main-entry-runtime.test.ts index a35f937..1514af0 100644 --- a/src/main-entry-runtime.test.ts +++ b/src/main-entry-runtime.test.ts @@ -1,12 +1,38 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { + normalizeStartupArgv, sanitizeHelpEnv, + sanitizeStartupEnv, sanitizeBackgroundEnv, shouldDetachBackgroundLaunch, shouldHandleHelpOnlyAtEntry, } from './main-entry-runtime'; +test('normalizeStartupArgv defaults no-arg startup to --start --background', () => { + assert.deepEqual(normalizeStartupArgv(['SubMiner.AppImage'], {}), [ + 'SubMiner.AppImage', + '--start', + '--background', + ]); + assert.deepEqual( + normalizeStartupArgv( + ['SubMiner.AppImage', '--password-store', 'gnome-libsecret'], + {}, + ), + ['SubMiner.AppImage', '--password-store', 'gnome-libsecret', '--start', '--background'], + ); + assert.deepEqual(normalizeStartupArgv(['SubMiner.AppImage', '--background'], {}), [ + 'SubMiner.AppImage', + '--background', + '--start', + ]); + assert.deepEqual(normalizeStartupArgv(['SubMiner.AppImage', '--help'], {}), [ + 'SubMiner.AppImage', + '--help', + ]); +}); + test('shouldHandleHelpOnlyAtEntry detects help-only invocation', () => { assert.equal(shouldHandleHelpOnlyAtEntry(['--help'], {}), true); assert.equal(shouldHandleHelpOnlyAtEntry(['--help', '--start'], {}), false); @@ -14,6 +40,14 @@ test('shouldHandleHelpOnlyAtEntry detects help-only invocation', () => { assert.equal(shouldHandleHelpOnlyAtEntry(['--help'], { ELECTRON_RUN_AS_NODE: '1' }), false); }); +test('sanitizeStartupEnv suppresses warnings and lsfg layer', () => { + const env = sanitizeStartupEnv({ + VK_INSTANCE_LAYERS: 'foo:lsfg-vk:bar', + }); + assert.equal(env.NODE_NO_WARNINGS, '1'); + assert.equal('VK_INSTANCE_LAYERS' in env, false); +}); + test('sanitizeHelpEnv suppresses warnings and lsfg layer', () => { const env = sanitizeHelpEnv({ VK_INSTANCE_LAYERS: 'foo:lsfg-vk:bar', diff --git a/src/main-entry-runtime.ts b/src/main-entry-runtime.ts index 3970c5c..0ca555e 100644 --- a/src/main-entry-runtime.ts +++ b/src/main-entry-runtime.ts @@ -1,6 +1,8 @@ import { CliArgs, parseArgs, shouldStartApp } from './cli/args'; const BACKGROUND_ARG = '--background'; +const START_ARG = '--start'; +const PASSWORD_STORE_ARG = '--password-store'; const BACKGROUND_CHILD_ENV = 'SUBMINER_BACKGROUND_CHILD'; function removeLsfgLayer(env: NodeJS.ProcessEnv): void { @@ -9,10 +11,54 @@ function removeLsfgLayer(env: NodeJS.ProcessEnv): void { } } +function removePassiveStartupArgs(argv: string[]): string[] { + const filtered: string[] = []; + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (!arg) continue; + + if (arg === PASSWORD_STORE_ARG) { + const value = argv[i + 1]; + if (value && !value.startsWith('--')) { + i += 1; + } + continue; + } + + if (arg.startsWith(`${PASSWORD_STORE_ARG}=`)) { + continue; + } + + filtered.push(arg); + } + + return filtered; +} + function parseCliArgs(argv: string[]): CliArgs { return parseArgs(argv); } +export function normalizeStartupArgv(argv: string[], env: NodeJS.ProcessEnv): string[] { + if (env.ELECTRON_RUN_AS_NODE === '1') return argv; + + const effectiveArgs = removePassiveStartupArgs(argv.slice(1)); + if (effectiveArgs.length === 0) { + return [...argv, START_ARG, BACKGROUND_ARG]; + } + + if ( + effectiveArgs.length === 1 && + effectiveArgs[0] === BACKGROUND_ARG && + !argv.includes(START_ARG) + ) { + return [...argv, START_ARG]; + } + + return argv; +} + export function shouldDetachBackgroundLaunch(argv: string[], env: NodeJS.ProcessEnv): boolean { if (env.ELECTRON_RUN_AS_NODE === '1') return false; if (!argv.includes(BACKGROUND_ARG)) return false; @@ -26,7 +72,7 @@ export function shouldHandleHelpOnlyAtEntry(argv: string[], env: NodeJS.ProcessE return args.help && !shouldStartApp(args); } -export function sanitizeHelpEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv { +export function sanitizeStartupEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv { const env = { ...baseEnv }; if (!env.NODE_NO_WARNINGS) { env.NODE_NO_WARNINGS = '1'; @@ -35,8 +81,12 @@ export function sanitizeHelpEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv { return env; } +export function sanitizeHelpEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + return sanitizeStartupEnv(baseEnv); +} + export function sanitizeBackgroundEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv { - const env = sanitizeHelpEnv(baseEnv); + const env = sanitizeStartupEnv(baseEnv); env[BACKGROUND_CHILD_ENV] = '1'; return env; } diff --git a/src/main-entry.ts b/src/main-entry.ts index 4abe1bb..d31b871 100644 --- a/src/main-entry.ts +++ b/src/main-entry.ts @@ -1,6 +1,8 @@ import { spawn } from 'node:child_process'; import { printHelp } from './cli/help'; import { + normalizeStartupArgv, + sanitizeStartupEnv, sanitizeBackgroundEnv, sanitizeHelpEnv, shouldDetachBackgroundLaunch, @@ -9,6 +11,21 @@ import { const DEFAULT_TEXTHOOKER_PORT = 5174; +function applySanitizedEnv(sanitizedEnv: NodeJS.ProcessEnv): void { + if (sanitizedEnv.NODE_NO_WARNINGS) { + process.env.NODE_NO_WARNINGS = sanitizedEnv.NODE_NO_WARNINGS; + } + + if (sanitizedEnv.VK_INSTANCE_LAYERS) { + process.env.VK_INSTANCE_LAYERS = sanitizedEnv.VK_INSTANCE_LAYERS; + } else { + delete process.env.VK_INSTANCE_LAYERS; + } +} + +process.argv = normalizeStartupArgv(process.argv, process.env); +applySanitizedEnv(sanitizeStartupEnv(process.env)); + if (shouldDetachBackgroundLaunch(process.argv, process.env)) { const child = spawn(process.execPath, process.argv.slice(1), { detached: true, diff --git a/src/main.ts b/src/main.ts index 0dfc493..502444f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -375,7 +375,7 @@ if (process.platform === 'linux') { getPasswordStoreArg(process.argv) ?? getDefaultPasswordStore(), ); app.commandLine.appendSwitch('password-store', passwordStore); - console.debug(`[main] Applied --password-store ${passwordStore}`); + createLogger('main').debug(`Applied --password-store ${passwordStore}`); } app.setName('SubMiner'); @@ -1646,7 +1646,7 @@ const { appPath ? app.setAsDefaultProtocolClient(scheme, appPath, args) : app.setAsDefaultProtocolClient(scheme), - logWarn: (message, details) => logger.warn(message, details), + logDebug: (message, details) => logger.debug(message, details), }, }); diff --git a/src/main/runtime/anilist-setup-protocol-main-deps.test.ts b/src/main/runtime/anilist-setup-protocol-main-deps.test.ts index a6c4c06..60106f6 100644 --- a/src/main/runtime/anilist-setup-protocol-main-deps.test.ts +++ b/src/main/runtime/anilist-setup-protocol-main-deps.test.ts @@ -76,7 +76,7 @@ test('register subminer protocol client main deps builder maps callbacks', () => execPath: '/tmp/electron', resolvePath: (value) => `/abs/${value}`, setAsDefaultProtocolClient: () => true, - logWarn: (message) => calls.push(`warn:${message}`), + logDebug: (message) => calls.push(`debug:${message}`), })(); assert.equal(deps.isDefaultApp(), true); diff --git a/src/main/runtime/anilist-setup-protocol-main-deps.ts b/src/main/runtime/anilist-setup-protocol-main-deps.ts index 6532908..f32ddd4 100644 --- a/src/main/runtime/anilist-setup-protocol-main-deps.ts +++ b/src/main/runtime/anilist-setup-protocol-main-deps.ts @@ -60,6 +60,6 @@ export function createBuildRegisterSubminerProtocolClientMainDepsHandler( resolvePath: (value: string) => deps.resolvePath(value), setAsDefaultProtocolClient: (scheme: string, path?: string, args?: string[]) => deps.setAsDefaultProtocolClient(scheme, path, args), - logWarn: (message: string, details?: unknown) => deps.logWarn(message, details), + logDebug: (message: string, details?: unknown) => deps.logDebug(message, details), }); } diff --git a/src/main/runtime/anilist-setup-protocol.test.ts b/src/main/runtime/anilist-setup-protocol.test.ts index 183b938..6563757 100644 --- a/src/main/runtime/anilist-setup-protocol.test.ts +++ b/src/main/runtime/anilist-setup-protocol.test.ts @@ -56,9 +56,24 @@ test('createRegisterSubminerProtocolClientHandler registers default app entry', calls.push(`register:${String(args?.[0])}`); return true; }, - logWarn: (message) => calls.push(`warn:${message}`), + logDebug: (message) => calls.push(`debug:${message}`), }); register(); assert.deepEqual(calls, ['register:/resolved/./entry.js']); }); + +test('createRegisterSubminerProtocolClientHandler keeps unsupported registration at debug level', () => { + const calls: string[] = []; + const register = createRegisterSubminerProtocolClientHandler({ + isDefaultApp: () => false, + getArgv: () => ['SubMiner.AppImage'], + execPath: '/tmp/SubMiner.AppImage', + resolvePath: (value) => value, + setAsDefaultProtocolClient: () => false, + logDebug: (message) => calls.push(`debug:${message}`), + }); + + register(); + assert.deepEqual(calls, ['debug:Failed to register default protocol handler for subminer:// URLs']); +}); diff --git a/src/main/runtime/anilist-setup-protocol.ts b/src/main/runtime/anilist-setup-protocol.ts index 951b05f..90d75e6 100644 --- a/src/main/runtime/anilist-setup-protocol.ts +++ b/src/main/runtime/anilist-setup-protocol.ts @@ -67,7 +67,7 @@ export function createRegisterSubminerProtocolClientHandler(deps: { execPath: string; resolvePath: (value: string) => string; setAsDefaultProtocolClient: (scheme: string, path?: string, args?: string[]) => boolean; - logWarn: (message: string, details?: unknown) => void; + logDebug: (message: string, details?: unknown) => void; }) { return (): void => { try { @@ -78,10 +78,10 @@ export function createRegisterSubminerProtocolClientHandler(deps: { ]) : deps.setAsDefaultProtocolClient('subminer'); if (!success) { - deps.logWarn('Failed to register default protocol handler for subminer:// URLs'); + deps.logDebug('Failed to register default protocol handler for subminer:// URLs'); } } catch (error) { - deps.logWarn('Failed to register subminer:// protocol handler', error); + deps.logDebug('Failed to register subminer:// protocol handler', error); } }; } diff --git a/src/main/runtime/composers/anilist-setup-composer.test.ts b/src/main/runtime/composers/anilist-setup-composer.test.ts index 8914125..df22dd9 100644 --- a/src/main/runtime/composers/anilist-setup-composer.test.ts +++ b/src/main/runtime/composers/anilist-setup-composer.test.ts @@ -29,7 +29,7 @@ test('composeAnilistSetupHandlers returns callable setup handlers', () => { execPath: process.execPath, resolvePath: (value) => value, setAsDefaultProtocolClient: () => true, - logWarn: () => {}, + logDebug: () => {}, }, });