mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-28 04:19:27 -07:00
Route stats background mode through isolated daemon and defer in-app startup to live daemon (#58)
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
normalizeLaunchMpvExtraArgs,
|
||||
normalizeStartupArgv,
|
||||
normalizeLaunchMpvTargets,
|
||||
resolveStatsDaemonCommandAction,
|
||||
sanitizeHelpEnv,
|
||||
sanitizeLaunchMpvEnv,
|
||||
sanitizeStartupEnv,
|
||||
@@ -164,6 +165,51 @@ test('stats-daemon entry helper detects internal daemon commands', () => {
|
||||
assert.equal(shouldHandleStatsDaemonCommandAtEntry(['SubMiner.AppImage', '--start'], {}), false);
|
||||
});
|
||||
|
||||
test('stats-daemon entry helper detects public background stats commands', () => {
|
||||
assert.equal(
|
||||
shouldHandleStatsDaemonCommandAtEntry(
|
||||
['SubMiner.AppImage', '--stats', '--stats-background'],
|
||||
{},
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldHandleStatsDaemonCommandAtEntry(['SubMiner.AppImage', '--stats', '--stats-stop'], {}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldHandleStatsDaemonCommandAtEntry(['SubMiner.AppImage', '--stats-background'], {}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldHandleStatsDaemonCommandAtEntry(['SubMiner.AppImage', '--stats-background'], {
|
||||
ELECTRON_RUN_AS_NODE: '1',
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(shouldHandleStatsDaemonCommandAtEntry(['SubMiner.AppImage', '--stats'], {}), false);
|
||||
});
|
||||
|
||||
test('stats-daemon entry helper resolves daemon action for public and internal commands', () => {
|
||||
assert.equal(
|
||||
resolveStatsDaemonCommandAction(['SubMiner.AppImage', '--stats-daemon-start']),
|
||||
'start',
|
||||
);
|
||||
assert.equal(
|
||||
resolveStatsDaemonCommandAction(['SubMiner.AppImage', '--stats-daemon-stop']),
|
||||
'stop',
|
||||
);
|
||||
assert.equal(
|
||||
resolveStatsDaemonCommandAction(['SubMiner.AppImage', '--stats', '--stats-background']),
|
||||
'start',
|
||||
);
|
||||
assert.equal(
|
||||
resolveStatsDaemonCommandAction(['SubMiner.AppImage', '--stats', '--stats-stop']),
|
||||
'stop',
|
||||
);
|
||||
assert.equal(resolveStatsDaemonCommandAction(['SubMiner.AppImage', '--stats']), null);
|
||||
});
|
||||
|
||||
test('sanitizeStartupEnv suppresses warnings and lsfg layer', () => {
|
||||
const env = sanitizeStartupEnv({
|
||||
VK_INSTANCE_LAYERS: 'foo:lsfg-vk:bar',
|
||||
|
||||
@@ -143,7 +143,17 @@ export function shouldHandleStatsDaemonCommandAtEntry(
|
||||
env: NodeJS.ProcessEnv,
|
||||
): boolean {
|
||||
if (env.ELECTRON_RUN_AS_NODE === '1') return false;
|
||||
return argv.includes('--stats-daemon-start') || argv.includes('--stats-daemon-stop');
|
||||
return resolveStatsDaemonCommandAction(argv) !== null;
|
||||
}
|
||||
|
||||
export function resolveStatsDaemonCommandAction(argv: string[]): 'start' | 'stop' | null {
|
||||
if (argv.includes('--stats-daemon-stop') || argv.includes('--stats-stop')) {
|
||||
return 'stop';
|
||||
}
|
||||
if (argv.includes('--stats-daemon-start') || argv.includes('--stats-background')) {
|
||||
return 'start';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function normalizeLaunchMpvTargets(argv: string[]): string[] {
|
||||
|
||||
42
src/main.ts
42
src/main.ts
@@ -404,6 +404,7 @@ import {
|
||||
resolveBackgroundStatsServerUrl,
|
||||
writeBackgroundStatsServerState,
|
||||
} from './main/runtime/stats-daemon';
|
||||
import { createEnsureStatsServerUrlHandler } from './main/runtime/stats-server-routing';
|
||||
import { resolveLegacyVocabularyPosFromTokens } from './core/services/immersion-tracker/legacy-vocabulary-pos';
|
||||
import { createAnilistUpdateQueue } from './core/services/anilist/anilist-update-queue';
|
||||
import {
|
||||
@@ -3170,11 +3171,7 @@ registerProtocolUrlHandlersHandler();
|
||||
const statsDistPath = path.join(__dirname, '..', 'stats', 'dist');
|
||||
const statsPreloadPath = path.join(__dirname, 'preload-stats.js');
|
||||
|
||||
const ensureStatsServerStarted = (): string => {
|
||||
const liveDaemon = readLiveBackgroundStatsDaemonState();
|
||||
if (liveDaemon && liveDaemon.pid !== process.pid) {
|
||||
return resolveBackgroundStatsServerUrl(liveDaemon);
|
||||
}
|
||||
const startLocalStatsServer = (): void => {
|
||||
const tracker = appState.immersionTracker;
|
||||
if (!tracker) {
|
||||
throw new Error('Immersion tracker failed to initialize.');
|
||||
@@ -3224,9 +3221,20 @@ const ensureStatsServerStarted = (): string => {
|
||||
appState.statsServer = statsServer;
|
||||
}
|
||||
appState.statsServer = statsServer;
|
||||
return `http://127.0.0.1:${getResolvedConfig().stats.serverPort}`;
|
||||
};
|
||||
|
||||
const ensureStatsServerStarted = createEnsureStatsServerUrlHandler({
|
||||
currentPid: process.pid,
|
||||
readBackgroundState: () => readBackgroundStatsServerState(statsDaemonStatePath),
|
||||
removeBackgroundState: () => {
|
||||
removeBackgroundStatsServerState(statsDaemonStatePath);
|
||||
},
|
||||
isProcessAlive: (pid) => isBackgroundStatsServerProcessAlive(pid),
|
||||
hasLocalStatsServer: () => statsServer !== null,
|
||||
startLocalStatsServer,
|
||||
getConfiguredPort: () => getResolvedConfig().stats.serverPort,
|
||||
});
|
||||
|
||||
const ensureBackgroundStatsServerStarted = (): {
|
||||
url: string;
|
||||
runningInCurrentProcess: boolean;
|
||||
@@ -3247,13 +3255,15 @@ const ensureBackgroundStatsServerStarted = (): {
|
||||
}
|
||||
|
||||
const port = getResolvedConfig().stats.serverPort;
|
||||
const url = ensureStatsServerStarted();
|
||||
writeBackgroundStatsServerState(statsDaemonStatePath, {
|
||||
pid: process.pid,
|
||||
port,
|
||||
startedAtMs: Date.now(),
|
||||
});
|
||||
return { url, runningInCurrentProcess: true };
|
||||
const result = ensureStatsServerStarted();
|
||||
if (result.source === 'local') {
|
||||
writeBackgroundStatsServerState(statsDaemonStatePath, {
|
||||
pid: process.pid,
|
||||
port,
|
||||
startedAtMs: Date.now(),
|
||||
});
|
||||
}
|
||||
return { url: result.url, runningInCurrentProcess: result.source === 'local' };
|
||||
};
|
||||
|
||||
const stopBackgroundStatsServer = async (): Promise<{ ok: boolean; stale: boolean }> => {
|
||||
@@ -3352,7 +3362,7 @@ const immersionTrackerStartupMainDeps: Parameters<
|
||||
registerStatsOverlayToggle({
|
||||
staticDir: statsDistPath,
|
||||
preloadPath: statsPreloadPath,
|
||||
getApiBaseUrl: () => ensureStatsServerStarted(),
|
||||
getApiBaseUrl: () => ensureStatsServerStarted().url,
|
||||
getToggleKey: () => getResolvedConfig().stats.toggleKey,
|
||||
resolveBounds: () => getCurrentOverlayGeometry(),
|
||||
onVisibilityChanged: (visible) => {
|
||||
@@ -3407,7 +3417,7 @@ const runStatsCliCommand = createRunStatsCliCommandHandler({
|
||||
await createMecabTokenizerAndCheck();
|
||||
},
|
||||
getImmersionTracker: () => appState.immersionTracker,
|
||||
ensureStatsServerStarted: () => statsStartupRuntime.ensureStatsServerStarted(),
|
||||
ensureStatsServerStarted: () => statsStartupRuntime.ensureStatsServerStarted().url,
|
||||
ensureBackgroundStatsServerStarted: () =>
|
||||
statsStartupRuntime.ensureBackgroundStatsServerStarted(),
|
||||
stopBackgroundStatsServer: () => statsStartupRuntime.stopBackgroundStatsServer(),
|
||||
@@ -4619,7 +4629,7 @@ async function dispatchSessionAction(request: SessionActionDispatchRequest): Pro
|
||||
toggleStatsOverlayWindow({
|
||||
staticDir: statsDistPath,
|
||||
preloadPath: statsPreloadPath,
|
||||
getApiBaseUrl: () => ensureStatsServerStarted(),
|
||||
getApiBaseUrl: () => ensureStatsServerStarted().url,
|
||||
getToggleKey: () => getResolvedConfig().stats.toggleKey,
|
||||
resolveBounds: () => getCurrentOverlayGeometry(),
|
||||
onVisibilityChanged: (visible) => {
|
||||
|
||||
86
src/main/runtime/stats-server-routing.test.ts
Normal file
86
src/main/runtime/stats-server-routing.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createEnsureStatsServerUrlHandler } from './stats-server-routing';
|
||||
|
||||
function createHarness(options?: {
|
||||
state?: { pid: number; port: number; startedAtMs: number } | null;
|
||||
processAlive?: boolean;
|
||||
localServerStarted?: boolean;
|
||||
}) {
|
||||
const calls: string[] = [];
|
||||
let localServerStarted = options?.localServerStarted ?? false;
|
||||
const handler = createEnsureStatsServerUrlHandler({
|
||||
currentPid: 100,
|
||||
readBackgroundState: () => {
|
||||
calls.push('readBackgroundState');
|
||||
return options?.state ?? null;
|
||||
},
|
||||
removeBackgroundState: () => {
|
||||
calls.push('removeBackgroundState');
|
||||
},
|
||||
isProcessAlive: () => {
|
||||
calls.push('isProcessAlive');
|
||||
return options?.processAlive ?? true;
|
||||
},
|
||||
hasLocalStatsServer: () => localServerStarted,
|
||||
startLocalStatsServer: () => {
|
||||
calls.push('startLocalStatsServer');
|
||||
localServerStarted = true;
|
||||
},
|
||||
getConfiguredPort: () => 6969,
|
||||
});
|
||||
|
||||
return {
|
||||
calls,
|
||||
handler,
|
||||
};
|
||||
}
|
||||
|
||||
test('stats server routing defers to a live background daemon from another process', () => {
|
||||
const { calls, handler } = createHarness({
|
||||
state: { pid: 200, port: 7979, startedAtMs: 1 },
|
||||
processAlive: true,
|
||||
});
|
||||
|
||||
assert.deepEqual(handler(), { url: 'http://127.0.0.1:7979', source: 'foreign' });
|
||||
assert.deepEqual(calls, ['readBackgroundState', 'isProcessAlive']);
|
||||
});
|
||||
|
||||
test('stats server routing clears dead daemon state and starts local server', () => {
|
||||
const { calls, handler } = createHarness({
|
||||
state: { pid: 200, port: 7979, startedAtMs: 1 },
|
||||
processAlive: false,
|
||||
});
|
||||
|
||||
assert.deepEqual(handler(), { url: 'http://127.0.0.1:6969', source: 'local' });
|
||||
assert.deepEqual(calls, [
|
||||
'readBackgroundState',
|
||||
'isProcessAlive',
|
||||
'removeBackgroundState',
|
||||
'startLocalStatsServer',
|
||||
]);
|
||||
});
|
||||
|
||||
test('stats server routing clears self-owned stale state and starts local server', () => {
|
||||
const { calls, handler } = createHarness({
|
||||
state: { pid: 100, port: 7979, startedAtMs: 1 },
|
||||
processAlive: true,
|
||||
});
|
||||
|
||||
assert.deepEqual(handler(), { url: 'http://127.0.0.1:6969', source: 'local' });
|
||||
assert.deepEqual(calls, [
|
||||
'readBackgroundState',
|
||||
'removeBackgroundState',
|
||||
'startLocalStatsServer',
|
||||
]);
|
||||
});
|
||||
|
||||
test('stats server routing reuses a started local stats server', () => {
|
||||
const { calls, handler } = createHarness({
|
||||
state: null,
|
||||
localServerStarted: true,
|
||||
});
|
||||
|
||||
assert.deepEqual(handler(), { url: 'http://127.0.0.1:6969', source: 'local' });
|
||||
assert.deepEqual(calls, ['readBackgroundState', 'removeBackgroundState']);
|
||||
});
|
||||
41
src/main/runtime/stats-server-routing.ts
Normal file
41
src/main/runtime/stats-server-routing.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { BackgroundStatsServerState } from './stats-daemon';
|
||||
|
||||
type EnsureStatsServerUrlDeps = {
|
||||
currentPid: number;
|
||||
readBackgroundState: () => BackgroundStatsServerState | null;
|
||||
removeBackgroundState: () => void;
|
||||
isProcessAlive: (pid: number) => boolean;
|
||||
hasLocalStatsServer: () => boolean;
|
||||
startLocalStatsServer: () => void;
|
||||
getConfiguredPort: () => number;
|
||||
};
|
||||
|
||||
function formatStatsServerUrl(port: number): string {
|
||||
return `http://127.0.0.1:${port}`;
|
||||
}
|
||||
|
||||
export type EnsureStatsServerUrlResult =
|
||||
| { url: string; source: 'foreign' }
|
||||
| { url: string; source: 'local' };
|
||||
|
||||
export function createEnsureStatsServerUrlHandler(
|
||||
deps: EnsureStatsServerUrlDeps,
|
||||
): () => EnsureStatsServerUrlResult {
|
||||
return () => {
|
||||
const state = deps.readBackgroundState();
|
||||
if (!state) {
|
||||
deps.removeBackgroundState();
|
||||
} else if (state.pid === deps.currentPid && !deps.hasLocalStatsServer()) {
|
||||
deps.removeBackgroundState();
|
||||
} else if (!deps.isProcessAlive(state.pid)) {
|
||||
deps.removeBackgroundState();
|
||||
} else if (state.pid !== deps.currentPid) {
|
||||
return { url: formatStatsServerUrl(state.port), source: 'foreign' };
|
||||
}
|
||||
|
||||
if (!deps.hasLocalStatsServer()) {
|
||||
deps.startLocalStatsServer();
|
||||
}
|
||||
return { url: formatStatsServerUrl(deps.getConfiguredPort()), source: 'local' };
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { shell } from 'electron';
|
||||
import { sanitizeStartupEnv } from './main-entry-runtime';
|
||||
import { resolveStatsDaemonCommandAction, sanitizeStartupEnv } from './main-entry-runtime';
|
||||
import {
|
||||
isBackgroundStatsServerProcessAlive,
|
||||
readBackgroundStatsServerState,
|
||||
@@ -44,7 +44,7 @@ function hasFlag(argv: string[], flag: string): boolean {
|
||||
|
||||
function parseControlArgs(argv: string[], userDataPath: string): StatsDaemonControlArgs {
|
||||
return {
|
||||
action: hasFlag(argv, '--stats-daemon-stop') ? 'stop' : 'start',
|
||||
action: resolveStatsDaemonCommandAction(argv) ?? 'start',
|
||||
responsePath: readFlagValue(argv, '--stats-response-path'),
|
||||
openBrowser: hasFlag(argv, '--stats-daemon-open-browser'),
|
||||
daemonScriptPath: path.join(__dirname, 'stats-daemon-runner.js'),
|
||||
|
||||
Reference in New Issue
Block a user