fix: quiet default appimage startup

This commit is contained in:
2026-03-06 21:17:47 -08:00
parent 94abd0f372
commit ca0eec568c
10 changed files with 192 additions and 11 deletions

View File

@@ -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
<!-- SECTION:DESCRIPTION:BEGIN -->
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.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [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.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
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.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
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.
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@@ -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',

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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),
},
});

View File

@@ -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);

View File

@@ -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),
});
}

View File

@@ -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']);
});

View File

@@ -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);
}
};
}

View File

@@ -29,7 +29,7 @@ test('composeAnilistSetupHandlers returns callable setup handlers', () => {
execPath: process.execPath,
resolvePath: (value) => value,
setAsDefaultProtocolClient: () => true,
logWarn: () => {},
logDebug: () => {},
},
});