mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-07 03:22:17 -08:00
fix: quiet default appimage startup
This commit is contained in:
@@ -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 -->
|
||||||
@@ -1,12 +1,38 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import {
|
import {
|
||||||
|
normalizeStartupArgv,
|
||||||
sanitizeHelpEnv,
|
sanitizeHelpEnv,
|
||||||
|
sanitizeStartupEnv,
|
||||||
sanitizeBackgroundEnv,
|
sanitizeBackgroundEnv,
|
||||||
shouldDetachBackgroundLaunch,
|
shouldDetachBackgroundLaunch,
|
||||||
shouldHandleHelpOnlyAtEntry,
|
shouldHandleHelpOnlyAtEntry,
|
||||||
} from './main-entry-runtime';
|
} 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', () => {
|
test('shouldHandleHelpOnlyAtEntry detects help-only invocation', () => {
|
||||||
assert.equal(shouldHandleHelpOnlyAtEntry(['--help'], {}), true);
|
assert.equal(shouldHandleHelpOnlyAtEntry(['--help'], {}), true);
|
||||||
assert.equal(shouldHandleHelpOnlyAtEntry(['--help', '--start'], {}), false);
|
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);
|
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', () => {
|
test('sanitizeHelpEnv suppresses warnings and lsfg layer', () => {
|
||||||
const env = sanitizeHelpEnv({
|
const env = sanitizeHelpEnv({
|
||||||
VK_INSTANCE_LAYERS: 'foo:lsfg-vk:bar',
|
VK_INSTANCE_LAYERS: 'foo:lsfg-vk:bar',
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { CliArgs, parseArgs, shouldStartApp } from './cli/args';
|
import { CliArgs, parseArgs, shouldStartApp } from './cli/args';
|
||||||
|
|
||||||
const BACKGROUND_ARG = '--background';
|
const BACKGROUND_ARG = '--background';
|
||||||
|
const START_ARG = '--start';
|
||||||
|
const PASSWORD_STORE_ARG = '--password-store';
|
||||||
const BACKGROUND_CHILD_ENV = 'SUBMINER_BACKGROUND_CHILD';
|
const BACKGROUND_CHILD_ENV = 'SUBMINER_BACKGROUND_CHILD';
|
||||||
|
|
||||||
function removeLsfgLayer(env: NodeJS.ProcessEnv): void {
|
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 {
|
function parseCliArgs(argv: string[]): CliArgs {
|
||||||
return parseArgs(argv);
|
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 {
|
export function shouldDetachBackgroundLaunch(argv: string[], env: NodeJS.ProcessEnv): boolean {
|
||||||
if (env.ELECTRON_RUN_AS_NODE === '1') return false;
|
if (env.ELECTRON_RUN_AS_NODE === '1') return false;
|
||||||
if (!argv.includes(BACKGROUND_ARG)) 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);
|
return args.help && !shouldStartApp(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sanitizeHelpEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
export function sanitizeStartupEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||||
const env = { ...baseEnv };
|
const env = { ...baseEnv };
|
||||||
if (!env.NODE_NO_WARNINGS) {
|
if (!env.NODE_NO_WARNINGS) {
|
||||||
env.NODE_NO_WARNINGS = '1';
|
env.NODE_NO_WARNINGS = '1';
|
||||||
@@ -35,8 +81,12 @@ export function sanitizeHelpEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sanitizeHelpEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||||
|
return sanitizeStartupEnv(baseEnv);
|
||||||
|
}
|
||||||
|
|
||||||
export function sanitizeBackgroundEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
export function sanitizeBackgroundEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||||
const env = sanitizeHelpEnv(baseEnv);
|
const env = sanitizeStartupEnv(baseEnv);
|
||||||
env[BACKGROUND_CHILD_ENV] = '1';
|
env[BACKGROUND_CHILD_ENV] = '1';
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { printHelp } from './cli/help';
|
import { printHelp } from './cli/help';
|
||||||
import {
|
import {
|
||||||
|
normalizeStartupArgv,
|
||||||
|
sanitizeStartupEnv,
|
||||||
sanitizeBackgroundEnv,
|
sanitizeBackgroundEnv,
|
||||||
sanitizeHelpEnv,
|
sanitizeHelpEnv,
|
||||||
shouldDetachBackgroundLaunch,
|
shouldDetachBackgroundLaunch,
|
||||||
@@ -9,6 +11,21 @@ import {
|
|||||||
|
|
||||||
const DEFAULT_TEXTHOOKER_PORT = 5174;
|
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)) {
|
if (shouldDetachBackgroundLaunch(process.argv, process.env)) {
|
||||||
const child = spawn(process.execPath, process.argv.slice(1), {
|
const child = spawn(process.execPath, process.argv.slice(1), {
|
||||||
detached: true,
|
detached: true,
|
||||||
|
|||||||
@@ -375,7 +375,7 @@ if (process.platform === 'linux') {
|
|||||||
getPasswordStoreArg(process.argv) ?? getDefaultPasswordStore(),
|
getPasswordStoreArg(process.argv) ?? getDefaultPasswordStore(),
|
||||||
);
|
);
|
||||||
app.commandLine.appendSwitch('password-store', passwordStore);
|
app.commandLine.appendSwitch('password-store', passwordStore);
|
||||||
console.debug(`[main] Applied --password-store ${passwordStore}`);
|
createLogger('main').debug(`Applied --password-store ${passwordStore}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.setName('SubMiner');
|
app.setName('SubMiner');
|
||||||
@@ -1646,7 +1646,7 @@ const {
|
|||||||
appPath
|
appPath
|
||||||
? app.setAsDefaultProtocolClient(scheme, appPath, args)
|
? app.setAsDefaultProtocolClient(scheme, appPath, args)
|
||||||
: app.setAsDefaultProtocolClient(scheme),
|
: app.setAsDefaultProtocolClient(scheme),
|
||||||
logWarn: (message, details) => logger.warn(message, details),
|
logDebug: (message, details) => logger.debug(message, details),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ test('register subminer protocol client main deps builder maps callbacks', () =>
|
|||||||
execPath: '/tmp/electron',
|
execPath: '/tmp/electron',
|
||||||
resolvePath: (value) => `/abs/${value}`,
|
resolvePath: (value) => `/abs/${value}`,
|
||||||
setAsDefaultProtocolClient: () => true,
|
setAsDefaultProtocolClient: () => true,
|
||||||
logWarn: (message) => calls.push(`warn:${message}`),
|
logDebug: (message) => calls.push(`debug:${message}`),
|
||||||
})();
|
})();
|
||||||
|
|
||||||
assert.equal(deps.isDefaultApp(), true);
|
assert.equal(deps.isDefaultApp(), true);
|
||||||
|
|||||||
@@ -60,6 +60,6 @@ export function createBuildRegisterSubminerProtocolClientMainDepsHandler(
|
|||||||
resolvePath: (value: string) => deps.resolvePath(value),
|
resolvePath: (value: string) => deps.resolvePath(value),
|
||||||
setAsDefaultProtocolClient: (scheme: string, path?: string, args?: string[]) =>
|
setAsDefaultProtocolClient: (scheme: string, path?: string, args?: string[]) =>
|
||||||
deps.setAsDefaultProtocolClient(scheme, path, args),
|
deps.setAsDefaultProtocolClient(scheme, path, args),
|
||||||
logWarn: (message: string, details?: unknown) => deps.logWarn(message, details),
|
logDebug: (message: string, details?: unknown) => deps.logDebug(message, details),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,9 +56,24 @@ test('createRegisterSubminerProtocolClientHandler registers default app entry',
|
|||||||
calls.push(`register:${String(args?.[0])}`);
|
calls.push(`register:${String(args?.[0])}`);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
logWarn: (message) => calls.push(`warn:${message}`),
|
logDebug: (message) => calls.push(`debug:${message}`),
|
||||||
});
|
});
|
||||||
|
|
||||||
register();
|
register();
|
||||||
assert.deepEqual(calls, ['register:/resolved/./entry.js']);
|
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']);
|
||||||
|
});
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function createRegisterSubminerProtocolClientHandler(deps: {
|
|||||||
execPath: string;
|
execPath: string;
|
||||||
resolvePath: (value: string) => string;
|
resolvePath: (value: string) => string;
|
||||||
setAsDefaultProtocolClient: (scheme: string, path?: string, args?: string[]) => boolean;
|
setAsDefaultProtocolClient: (scheme: string, path?: string, args?: string[]) => boolean;
|
||||||
logWarn: (message: string, details?: unknown) => void;
|
logDebug: (message: string, details?: unknown) => void;
|
||||||
}) {
|
}) {
|
||||||
return (): void => {
|
return (): void => {
|
||||||
try {
|
try {
|
||||||
@@ -78,10 +78,10 @@ export function createRegisterSubminerProtocolClientHandler(deps: {
|
|||||||
])
|
])
|
||||||
: deps.setAsDefaultProtocolClient('subminer');
|
: deps.setAsDefaultProtocolClient('subminer');
|
||||||
if (!success) {
|
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) {
|
} catch (error) {
|
||||||
deps.logWarn('Failed to register subminer:// protocol handler', error);
|
deps.logDebug('Failed to register subminer:// protocol handler', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ test('composeAnilistSetupHandlers returns callable setup handlers', () => {
|
|||||||
execPath: process.execPath,
|
execPath: process.execPath,
|
||||||
resolvePath: (value) => value,
|
resolvePath: (value) => value,
|
||||||
setAsDefaultProtocolClient: () => true,
|
setAsDefaultProtocolClient: () => true,
|
||||||
logWarn: () => {},
|
logDebug: () => {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user