Files
SubMiner/src/stats-daemon-entry.ts
sudacode a954f62f55 Decouple stats daemon and preserve final mine OSD status
- Run `subminer stats -b` as a dedicated daemon process, independent from the overlay app
- Stop Anki progress spinner before showing final `✓`/`x` mine result so it is not overwritten
- Keep grammar/noise subtitle tokens hoverable while stripping annotation metadata
2026-03-18 23:49:27 -07:00

136 lines
4.1 KiB
TypeScript

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 {
isBackgroundStatsServerProcessAlive,
readBackgroundStatsServerState,
removeBackgroundStatsServerState,
resolveBackgroundStatsServerUrl,
} from './main/runtime/stats-daemon';
import {
createRunStatsDaemonControlHandler,
type StatsDaemonControlArgs,
} from './stats-daemon-control';
import {
type StatsCliCommandResponse,
writeStatsCliCommandResponse,
} from './main/runtime/stats-cli-command';
const STATS_DAEMON_RESPONSE_TIMEOUT_MS = 12_000;
function readFlagValue(argv: string[], flag: string): string | undefined {
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (!arg) continue;
if (arg === flag) {
const value = argv[i + 1];
if (value && !value.startsWith('--')) {
return value;
}
return undefined;
}
if (arg.startsWith(`${flag}=`)) {
return arg.split('=', 2)[1];
}
}
return undefined;
}
function hasFlag(argv: string[], flag: string): boolean {
return argv.includes(flag);
}
function parseControlArgs(argv: string[], userDataPath: string): StatsDaemonControlArgs {
return {
action: hasFlag(argv, '--stats-daemon-stop') ? 'stop' : 'start',
responsePath: readFlagValue(argv, '--stats-response-path'),
openBrowser: hasFlag(argv, '--stats-daemon-open-browser'),
daemonScriptPath: path.join(__dirname, 'stats-daemon-runner.js'),
userDataPath,
};
}
async function waitForDaemonResponse(responsePath: string): Promise<StatsCliCommandResponse> {
const deadline = Date.now() + STATS_DAEMON_RESPONSE_TIMEOUT_MS;
while (Date.now() < deadline) {
try {
if (fs.existsSync(responsePath)) {
return JSON.parse(fs.readFileSync(responsePath, 'utf8')) as StatsCliCommandResponse;
}
} catch {
// retry until timeout
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
return {
ok: false,
error: 'Timed out waiting for stats daemon startup response.',
};
}
export async function runStatsDaemonControlFromProcess(userDataPath: string): Promise<number> {
const args = parseControlArgs(process.argv, userDataPath);
const statePath = path.join(userDataPath, 'stats-daemon.json');
const writeFailureResponse = (message: string): void => {
if (args.responsePath) {
try {
writeStatsCliCommandResponse(args.responsePath, {
ok: false,
error: message,
});
} catch {
// ignore secondary response-write failures
}
}
};
const handler = createRunStatsDaemonControlHandler({
statePath,
readState: () => readBackgroundStatsServerState(statePath),
removeState: () => {
removeBackgroundStatsServerState(statePath);
},
isProcessAlive: (pid) => isBackgroundStatsServerProcessAlive(pid),
resolveUrl: (state) => resolveBackgroundStatsServerUrl(state),
spawnDaemon: async (options) => {
const childArgs = [options.scriptPath, '--stats-user-data-path', options.userDataPath];
if (options.responsePath) {
childArgs.push('--stats-response-path', options.responsePath);
}
const logLevel = readFlagValue(process.argv, '--log-level');
if (logLevel) {
childArgs.push('--log-level', logLevel);
}
const child = spawn(process.execPath, childArgs, {
detached: true,
stdio: 'ignore',
env: {
...sanitizeStartupEnv(process.env),
ELECTRON_RUN_AS_NODE: '1',
},
});
child.unref();
return child.pid ?? 0;
},
waitForDaemonResponse,
openExternal: async (url) => shell.openExternal(url),
writeResponse: writeStatsCliCommandResponse,
killProcess: (pid, signal) => {
process.kill(pid, signal);
},
sleep: async (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
});
try {
return await handler(args);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
writeFailureResponse(message);
return 1;
}
}