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:
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
id: TASK-306
|
||||||
|
title: Separate background stats daemon from regular SubMiner app
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-04-27 00:56'
|
||||||
|
updated_date: '2026-04-27 01:00'
|
||||||
|
labels:
|
||||||
|
- stats
|
||||||
|
- runtime
|
||||||
|
dependencies: []
|
||||||
|
priority: high
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Background stats mode should run only the stats data/server pieces. It must not bring up tray UI or expose the regular mpv connection surface, and stopping should remain CLI-only.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 Launching stats background mode starts a separate stats daemon process rather than booting the regular SubMiner runtime.
|
||||||
|
- [x] #2 Background stats mode does not create or keep a tray icon.
|
||||||
|
- [x] #3 Background stats mode does not start mpv IPC/client surfaces that let mpv connect to the app.
|
||||||
|
- [x] #4 Background stats mode remains stoppable through the stats stop command line path.
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
<!-- SECTION:PLAN:BEGIN -->
|
||||||
|
1. Add entry-runtime tests for public stats background/stop daemon detection.
|
||||||
|
2. Implement early public stats daemon command detection and route it before regular app boot.
|
||||||
|
3. Run targeted tests and update task status/criteria.
|
||||||
|
<!-- SECTION:PLAN:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
Implemented early public stats daemon routing in main-entry runtime. Direct `--stats-background` and `--stats-stop` now resolve to daemon control before single-instance lock and before loading `main.js`, matching the existing internal launcher daemon flags. Installed missing Bun dependencies to run targeted tests.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Summary:
|
||||||
|
- Added `resolveStatsDaemonCommandAction` and updated entry detection so public `--stats-background` / `--stats-stop` invocations route through the isolated stats daemon control path.
|
||||||
|
- Reused that action resolution in `stats-daemon-entry` so public stop commands map to stop instead of the default start path.
|
||||||
|
- Added regression coverage for public daemon detection/action resolution.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
- `bun test src/main-entry-runtime.test.ts launcher/commands/command-modules.test.ts src/main/runtime/stats-cli-command.test.ts src/stats-daemon-control.test.ts`
|
||||||
|
- `bun run typecheck`
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
id: TASK-307
|
||||||
|
title: Defer in-app stats server to running background stats daemon
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-04-27 01:57'
|
||||||
|
updated_date: '2026-04-27 02:02'
|
||||||
|
labels:
|
||||||
|
- stats
|
||||||
|
- runtime
|
||||||
|
dependencies: []
|
||||||
|
priority: high
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
When normal SubMiner app startup has stats auto-start enabled, it should detect an already-running background stats daemon and avoid starting a second in-app stats server. Stats overlay/dashboard URL resolution should point at the background daemon.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 If a live background stats daemon state exists for another process, in-app stats auto-start does not start a local stats server.
|
||||||
|
- [x] #2 Stats URL resolution returns the background daemon URL when the background daemon is live.
|
||||||
|
- [x] #3 Stale or dead background daemon state is cleared and normal in-app stats startup still works.
|
||||||
|
- [x] #4 Regression tests cover the deferral behavior.
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
<!-- SECTION:PLAN:BEGIN -->
|
||||||
|
1. Add unit tests for stats server routing decisions around live/stale background daemon state.
|
||||||
|
2. Implement a small routing helper used by main stats startup.
|
||||||
|
3. Wire `ensureStatsServerStarted()` through the helper.
|
||||||
|
4. Run targeted tests/typecheck/changelog lint and finalize the task.
|
||||||
|
<!-- SECTION:PLAN:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
Extracted stats server URL routing into `src/main/runtime/stats-server-routing.ts` and wired `main.ts` through it. The helper returns the background daemon URL without calling local server startup when a live external daemon exists; dead/self-owned stale state is removed before falling back to local startup. Added the new test to `test:core:src`.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Summary:
|
||||||
|
- Added a pure stats server routing helper that chooses between a live background daemon and local in-app stats server startup.
|
||||||
|
- Updated main stats URL resolution to defer to another process's background daemon and only start the in-app server when no live daemon is available.
|
||||||
|
- Added regression tests for live daemon deferral, dead daemon cleanup, self-owned stale state cleanup, and local server reuse.
|
||||||
|
- Added the routing test to the core source test lane and added a changelog fragment.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
- `bun test src/main/runtime/stats-server-routing.test.ts src/main-entry-runtime.test.ts src/main/runtime/stats-cli-command.test.ts src/stats-daemon-control.test.ts`
|
||||||
|
- `bun run test:core:src`
|
||||||
|
- `bun run typecheck`
|
||||||
|
- `bun run changelog:lint`
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
4
changes/306-stats-background-daemon.md
Normal file
4
changes/306-stats-background-daemon.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: stats
|
||||||
|
|
||||||
|
- Stats background mode now routes through the isolated stats daemon instead of starting the regular SubMiner app runtime.
|
||||||
4
changes/307-stats-background-deferral.md
Normal file
4
changes/307-stats-background-deferral.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: stats
|
||||||
|
|
||||||
|
- In-app stats startup now defers to an already-running background stats daemon instead of starting a second stats server.
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
"test:plugin:src": "lua scripts/test-plugin-lua-compat.lua && lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-binary-windows.lua",
|
"test:plugin:src": "lua scripts/test-plugin-lua-compat.lua && lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-binary-windows.lua",
|
||||||
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
||||||
"test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/mpv.test.ts launcher/picker.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src",
|
"test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/mpv.test.ts launcher/picker.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src",
|
||||||
"test:core:src": "bun test src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/stats-window.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/services/anilist/rate-limiter.test.ts src/core/services/jlpt-token-filter.test.ts src/core/services/subtitle-position.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/hyprland-tracker.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/setup-gate.test.ts stats/src/lib/api-client.test.ts",
|
"test:core:src": "bun test src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/stats-window.test.ts src/main/runtime/stats-server-routing.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/services/anilist/rate-limiter.test.ts src/core/services/jlpt-token-filter.test.ts src/core/services/subtitle-position.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/hyprland-tracker.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/setup-gate.test.ts stats/src/lib/api-client.test.ts",
|
||||||
"test:core:dist": "bun test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/core/services/anilist/rate-limiter.test.js dist/core/services/jlpt-token-filter.test.js dist/core/services/subtitle-position.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/hyprland-tracker.test.js dist/window-trackers/x11-tracker.test.js dist/window-trackers/windows-helper.test.js dist/window-trackers/windows-tracker.test.js",
|
"test:core:dist": "bun test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/core/services/anilist/rate-limiter.test.js dist/core/services/jlpt-token-filter.test.js dist/core/services/subtitle-position.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/hyprland-tracker.test.js dist/window-trackers/x11-tracker.test.js dist/window-trackers/windows-helper.test.js dist/window-trackers/windows-tracker.test.js",
|
||||||
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
||||||
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
|
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
normalizeLaunchMpvExtraArgs,
|
normalizeLaunchMpvExtraArgs,
|
||||||
normalizeStartupArgv,
|
normalizeStartupArgv,
|
||||||
normalizeLaunchMpvTargets,
|
normalizeLaunchMpvTargets,
|
||||||
|
resolveStatsDaemonCommandAction,
|
||||||
sanitizeHelpEnv,
|
sanitizeHelpEnv,
|
||||||
sanitizeLaunchMpvEnv,
|
sanitizeLaunchMpvEnv,
|
||||||
sanitizeStartupEnv,
|
sanitizeStartupEnv,
|
||||||
@@ -164,6 +165,51 @@ test('stats-daemon entry helper detects internal daemon commands', () => {
|
|||||||
assert.equal(shouldHandleStatsDaemonCommandAtEntry(['SubMiner.AppImage', '--start'], {}), false);
|
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', () => {
|
test('sanitizeStartupEnv suppresses warnings and lsfg layer', () => {
|
||||||
const env = sanitizeStartupEnv({
|
const env = sanitizeStartupEnv({
|
||||||
VK_INSTANCE_LAYERS: 'foo:lsfg-vk:bar',
|
VK_INSTANCE_LAYERS: 'foo:lsfg-vk:bar',
|
||||||
|
|||||||
@@ -143,7 +143,17 @@ export function shouldHandleStatsDaemonCommandAtEntry(
|
|||||||
env: NodeJS.ProcessEnv,
|
env: NodeJS.ProcessEnv,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (env.ELECTRON_RUN_AS_NODE === '1') return false;
|
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[] {
|
export function normalizeLaunchMpvTargets(argv: string[]): string[] {
|
||||||
|
|||||||
42
src/main.ts
42
src/main.ts
@@ -404,6 +404,7 @@ import {
|
|||||||
resolveBackgroundStatsServerUrl,
|
resolveBackgroundStatsServerUrl,
|
||||||
writeBackgroundStatsServerState,
|
writeBackgroundStatsServerState,
|
||||||
} from './main/runtime/stats-daemon';
|
} from './main/runtime/stats-daemon';
|
||||||
|
import { createEnsureStatsServerUrlHandler } from './main/runtime/stats-server-routing';
|
||||||
import { resolveLegacyVocabularyPosFromTokens } from './core/services/immersion-tracker/legacy-vocabulary-pos';
|
import { resolveLegacyVocabularyPosFromTokens } from './core/services/immersion-tracker/legacy-vocabulary-pos';
|
||||||
import { createAnilistUpdateQueue } from './core/services/anilist/anilist-update-queue';
|
import { createAnilistUpdateQueue } from './core/services/anilist/anilist-update-queue';
|
||||||
import {
|
import {
|
||||||
@@ -3170,11 +3171,7 @@ registerProtocolUrlHandlersHandler();
|
|||||||
const statsDistPath = path.join(__dirname, '..', 'stats', 'dist');
|
const statsDistPath = path.join(__dirname, '..', 'stats', 'dist');
|
||||||
const statsPreloadPath = path.join(__dirname, 'preload-stats.js');
|
const statsPreloadPath = path.join(__dirname, 'preload-stats.js');
|
||||||
|
|
||||||
const ensureStatsServerStarted = (): string => {
|
const startLocalStatsServer = (): void => {
|
||||||
const liveDaemon = readLiveBackgroundStatsDaemonState();
|
|
||||||
if (liveDaemon && liveDaemon.pid !== process.pid) {
|
|
||||||
return resolveBackgroundStatsServerUrl(liveDaemon);
|
|
||||||
}
|
|
||||||
const tracker = appState.immersionTracker;
|
const tracker = appState.immersionTracker;
|
||||||
if (!tracker) {
|
if (!tracker) {
|
||||||
throw new Error('Immersion tracker failed to initialize.');
|
throw new Error('Immersion tracker failed to initialize.');
|
||||||
@@ -3224,9 +3221,20 @@ const ensureStatsServerStarted = (): string => {
|
|||||||
appState.statsServer = statsServer;
|
appState.statsServer = statsServer;
|
||||||
}
|
}
|
||||||
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 = (): {
|
const ensureBackgroundStatsServerStarted = (): {
|
||||||
url: string;
|
url: string;
|
||||||
runningInCurrentProcess: boolean;
|
runningInCurrentProcess: boolean;
|
||||||
@@ -3247,13 +3255,15 @@ const ensureBackgroundStatsServerStarted = (): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const port = getResolvedConfig().stats.serverPort;
|
const port = getResolvedConfig().stats.serverPort;
|
||||||
const url = ensureStatsServerStarted();
|
const result = ensureStatsServerStarted();
|
||||||
writeBackgroundStatsServerState(statsDaemonStatePath, {
|
if (result.source === 'local') {
|
||||||
pid: process.pid,
|
writeBackgroundStatsServerState(statsDaemonStatePath, {
|
||||||
port,
|
pid: process.pid,
|
||||||
startedAtMs: Date.now(),
|
port,
|
||||||
});
|
startedAtMs: Date.now(),
|
||||||
return { url, runningInCurrentProcess: true };
|
});
|
||||||
|
}
|
||||||
|
return { url: result.url, runningInCurrentProcess: result.source === 'local' };
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopBackgroundStatsServer = async (): Promise<{ ok: boolean; stale: boolean }> => {
|
const stopBackgroundStatsServer = async (): Promise<{ ok: boolean; stale: boolean }> => {
|
||||||
@@ -3352,7 +3362,7 @@ const immersionTrackerStartupMainDeps: Parameters<
|
|||||||
registerStatsOverlayToggle({
|
registerStatsOverlayToggle({
|
||||||
staticDir: statsDistPath,
|
staticDir: statsDistPath,
|
||||||
preloadPath: statsPreloadPath,
|
preloadPath: statsPreloadPath,
|
||||||
getApiBaseUrl: () => ensureStatsServerStarted(),
|
getApiBaseUrl: () => ensureStatsServerStarted().url,
|
||||||
getToggleKey: () => getResolvedConfig().stats.toggleKey,
|
getToggleKey: () => getResolvedConfig().stats.toggleKey,
|
||||||
resolveBounds: () => getCurrentOverlayGeometry(),
|
resolveBounds: () => getCurrentOverlayGeometry(),
|
||||||
onVisibilityChanged: (visible) => {
|
onVisibilityChanged: (visible) => {
|
||||||
@@ -3407,7 +3417,7 @@ const runStatsCliCommand = createRunStatsCliCommandHandler({
|
|||||||
await createMecabTokenizerAndCheck();
|
await createMecabTokenizerAndCheck();
|
||||||
},
|
},
|
||||||
getImmersionTracker: () => appState.immersionTracker,
|
getImmersionTracker: () => appState.immersionTracker,
|
||||||
ensureStatsServerStarted: () => statsStartupRuntime.ensureStatsServerStarted(),
|
ensureStatsServerStarted: () => statsStartupRuntime.ensureStatsServerStarted().url,
|
||||||
ensureBackgroundStatsServerStarted: () =>
|
ensureBackgroundStatsServerStarted: () =>
|
||||||
statsStartupRuntime.ensureBackgroundStatsServerStarted(),
|
statsStartupRuntime.ensureBackgroundStatsServerStarted(),
|
||||||
stopBackgroundStatsServer: () => statsStartupRuntime.stopBackgroundStatsServer(),
|
stopBackgroundStatsServer: () => statsStartupRuntime.stopBackgroundStatsServer(),
|
||||||
@@ -4619,7 +4629,7 @@ async function dispatchSessionAction(request: SessionActionDispatchRequest): Pro
|
|||||||
toggleStatsOverlayWindow({
|
toggleStatsOverlayWindow({
|
||||||
staticDir: statsDistPath,
|
staticDir: statsDistPath,
|
||||||
preloadPath: statsPreloadPath,
|
preloadPath: statsPreloadPath,
|
||||||
getApiBaseUrl: () => ensureStatsServerStarted(),
|
getApiBaseUrl: () => ensureStatsServerStarted().url,
|
||||||
getToggleKey: () => getResolvedConfig().stats.toggleKey,
|
getToggleKey: () => getResolvedConfig().stats.toggleKey,
|
||||||
resolveBounds: () => getCurrentOverlayGeometry(),
|
resolveBounds: () => getCurrentOverlayGeometry(),
|
||||||
onVisibilityChanged: (visible) => {
|
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 path from 'node:path';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { shell } from 'electron';
|
import { shell } from 'electron';
|
||||||
import { sanitizeStartupEnv } from './main-entry-runtime';
|
import { resolveStatsDaemonCommandAction, sanitizeStartupEnv } from './main-entry-runtime';
|
||||||
import {
|
import {
|
||||||
isBackgroundStatsServerProcessAlive,
|
isBackgroundStatsServerProcessAlive,
|
||||||
readBackgroundStatsServerState,
|
readBackgroundStatsServerState,
|
||||||
@@ -44,7 +44,7 @@ function hasFlag(argv: string[], flag: string): boolean {
|
|||||||
|
|
||||||
function parseControlArgs(argv: string[], userDataPath: string): StatsDaemonControlArgs {
|
function parseControlArgs(argv: string[], userDataPath: string): StatsDaemonControlArgs {
|
||||||
return {
|
return {
|
||||||
action: hasFlag(argv, '--stats-daemon-stop') ? 'stop' : 'start',
|
action: resolveStatsDaemonCommandAction(argv) ?? 'start',
|
||||||
responsePath: readFlagValue(argv, '--stats-response-path'),
|
responsePath: readFlagValue(argv, '--stats-response-path'),
|
||||||
openBrowser: hasFlag(argv, '--stats-daemon-open-browser'),
|
openBrowser: hasFlag(argv, '--stats-daemon-open-browser'),
|
||||||
daemonScriptPath: path.join(__dirname, 'stats-daemon-runner.js'),
|
daemonScriptPath: path.join(__dirname, 'stats-daemon-runner.js'),
|
||||||
|
|||||||
Reference in New Issue
Block a user