mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 03:16:46 -07:00
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
This commit is contained in:
@@ -150,16 +150,17 @@ test('stats command launches attached app command with response path', async ()
|
||||
assert.equal(handled, true);
|
||||
assert.deepEqual(forwarded, [
|
||||
[
|
||||
'--stats',
|
||||
'--stats-daemon-start',
|
||||
'--stats-response-path',
|
||||
'/tmp/subminer-stats-test/response.json',
|
||||
'--stats-daemon-open-browser',
|
||||
'--log-level',
|
||||
'debug',
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
test('stats background command launches detached app command with response path', async () => {
|
||||
test('stats background command launches attached daemon control command with response path', async () => {
|
||||
const context = createContext();
|
||||
context.args.stats = true;
|
||||
(context.args as typeof context.args & { statsBackground?: boolean }).statsBackground = true;
|
||||
@@ -168,11 +169,9 @@ test('stats background command launches detached app command with response path'
|
||||
const handled = await runStatsCommand(context, {
|
||||
createTempDir: () => '/tmp/subminer-stats-test',
|
||||
joinPath: (...parts) => parts.join('/'),
|
||||
runAppCommandAttached: async () => {
|
||||
throw new Error('attached path should not run for stats -b');
|
||||
},
|
||||
launchAppCommandDetached: (_appPath, appArgs) => {
|
||||
runAppCommandAttached: async (_appPath, appArgs) => {
|
||||
forwarded.push(appArgs);
|
||||
return 0;
|
||||
},
|
||||
waitForStatsResponse: async () => ({ ok: true, url: 'http://127.0.0.1:5175' }),
|
||||
removeDir: () => {},
|
||||
@@ -181,10 +180,9 @@ test('stats background command launches detached app command with response path'
|
||||
assert.equal(handled, true);
|
||||
assert.deepEqual(forwarded, [
|
||||
[
|
||||
'--stats',
|
||||
'--stats-daemon-start',
|
||||
'--stats-response-path',
|
||||
'/tmp/subminer-stats-test/response.json',
|
||||
'--stats-background',
|
||||
],
|
||||
]);
|
||||
});
|
||||
@@ -215,7 +213,12 @@ test('stats command returns after startup response even if app process stays run
|
||||
const final = await statsCommand;
|
||||
assert.equal(final, true);
|
||||
assert.deepEqual(forwarded, [
|
||||
['--stats', '--stats-response-path', '/tmp/subminer-stats-test/response.json'],
|
||||
[
|
||||
'--stats-daemon-start',
|
||||
'--stats-response-path',
|
||||
'/tmp/subminer-stats-test/response.json',
|
||||
'--stats-daemon-open-browser',
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -268,7 +271,11 @@ test('stats stop command forwards stop flag to the app', async () => {
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.deepEqual(forwarded, [
|
||||
['--stats', '--stats-response-path', '/tmp/subminer-stats-test/response.json', '--stats-stop'],
|
||||
[
|
||||
'--stats-daemon-stop',
|
||||
'--stats-response-path',
|
||||
'/tmp/subminer-stats-test/response.json',
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { launchAppCommandDetached, runAppCommandAttached } from '../mpv.js';
|
||||
import { runAppCommandAttached } from '../mpv.js';
|
||||
import { sleep } from '../util.js';
|
||||
import type { LauncherCommandContext } from './context.js';
|
||||
|
||||
@@ -20,12 +20,6 @@ type StatsCommandDeps = {
|
||||
logLevel: LauncherCommandContext['args']['logLevel'],
|
||||
label: string,
|
||||
) => Promise<number>;
|
||||
launchAppCommandDetached: (
|
||||
appPath: string,
|
||||
appArgs: string[],
|
||||
logLevel: LauncherCommandContext['args']['logLevel'],
|
||||
label: string,
|
||||
) => void;
|
||||
waitForStatsResponse: (responsePath: string) => Promise<StatsCommandResponse>;
|
||||
removeDir: (targetPath: string) => void;
|
||||
};
|
||||
@@ -37,8 +31,6 @@ const defaultDeps: StatsCommandDeps = {
|
||||
joinPath: (...parts) => path.join(...parts),
|
||||
runAppCommandAttached: (appPath, appArgs, logLevel, label) =>
|
||||
runAppCommandAttached(appPath, appArgs, logLevel, label),
|
||||
launchAppCommandDetached: (appPath, appArgs, logLevel, label) =>
|
||||
launchAppCommandDetached(appPath, appArgs, logLevel, label),
|
||||
waitForStatsResponse: async (responsePath) => {
|
||||
const deadline = Date.now() + STATS_STARTUP_RESPONSE_TIMEOUT_MS;
|
||||
while (Date.now() < deadline) {
|
||||
@@ -75,12 +67,15 @@ export async function runStatsCommand(
|
||||
const responsePath = resolvedDeps.joinPath(tempDir, 'response.json');
|
||||
|
||||
try {
|
||||
const forwarded = ['--stats', '--stats-response-path', responsePath];
|
||||
if (args.statsBackground) {
|
||||
forwarded.push('--stats-background');
|
||||
}
|
||||
if (args.statsStop) {
|
||||
forwarded.push('--stats-stop');
|
||||
const forwarded = args.statsCleanup
|
||||
? ['--stats', '--stats-response-path', responsePath]
|
||||
: [
|
||||
args.statsStop ? '--stats-daemon-stop' : '--stats-daemon-start',
|
||||
'--stats-response-path',
|
||||
responsePath,
|
||||
];
|
||||
if (!args.statsCleanup && !args.statsBackground && !args.statsStop) {
|
||||
forwarded.push('--stats-daemon-open-browser');
|
||||
}
|
||||
if (args.statsCleanup) {
|
||||
forwarded.push('--stats-cleanup');
|
||||
@@ -94,14 +89,6 @@ export async function runStatsCommand(
|
||||
if (args.logLevel !== 'info') {
|
||||
forwarded.push('--log-level', args.logLevel);
|
||||
}
|
||||
if (args.statsBackground) {
|
||||
resolvedDeps.launchAppCommandDetached(appPath, forwarded, args.logLevel, 'stats');
|
||||
const startupResult = await resolvedDeps.waitForStatsResponse(responsePath);
|
||||
if (!startupResult.ok) {
|
||||
throw new Error(startupResult.error || 'Stats dashboard failed to start.');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const attachedExitPromise = resolvedDeps.runAppCommandAttached(
|
||||
appPath,
|
||||
forwarded,
|
||||
|
||||
@@ -276,6 +276,16 @@ export function parseCliPrograms(
|
||||
if (statsBackground && statsStop) {
|
||||
throw new Error('Stats background and stop flags cannot be combined.');
|
||||
}
|
||||
if (
|
||||
normalizedAction &&
|
||||
normalizedAction !== 'cleanup' &&
|
||||
normalizedAction !== 'rebuild' &&
|
||||
normalizedAction !== 'backfill'
|
||||
) {
|
||||
throw new Error(
|
||||
'Invalid stats action. Valid values are cleanup, rebuild, or backfill.',
|
||||
);
|
||||
}
|
||||
if (normalizedAction && (statsBackground || statsStop)) {
|
||||
throw new Error('Stats background and stop flags cannot be combined with stats actions.');
|
||||
}
|
||||
|
||||
@@ -536,7 +536,7 @@ exit 0
|
||||
assert.equal(result.status, 0, `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
|
||||
assert.match(
|
||||
fs.readFileSync(capturePath, 'utf8'),
|
||||
/^--stats\n--stats-response-path\n.+\n--log-level\ndebug\n$/,
|
||||
/^--stats-daemon-start\n--stats-response-path\n.+\n--stats-daemon-open-browser\n--log-level\ndebug\n$/,
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -45,6 +45,8 @@ export function parseMpvArgString(input: string): string[] {
|
||||
let inSingleQuote = false;
|
||||
let inDoubleQuote = false;
|
||||
let escaping = false;
|
||||
const canEscape = (nextChar: string | undefined): boolean =>
|
||||
nextChar === undefined || nextChar === '"' || nextChar === "'" || nextChar === '\\' || /\s/.test(nextChar);
|
||||
|
||||
for (let i = 0; i < chars.length; i += 1) {
|
||||
const ch = chars[i] || '';
|
||||
@@ -65,7 +67,11 @@ export function parseMpvArgString(input: string): string[] {
|
||||
|
||||
if (inDoubleQuote) {
|
||||
if (ch === '\\') {
|
||||
escaping = true;
|
||||
if (canEscape(chars[i + 1])) {
|
||||
escaping = true;
|
||||
} else {
|
||||
current += ch;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') {
|
||||
@@ -77,7 +83,11 @@ export function parseMpvArgString(input: string): string[] {
|
||||
}
|
||||
|
||||
if (ch === '\\') {
|
||||
escaping = true;
|
||||
if (canEscape(chars[i + 1])) {
|
||||
escaping = true;
|
||||
} else {
|
||||
current += ch;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ch === "'") {
|
||||
@@ -857,8 +867,14 @@ export function runAppCommandAttached(
|
||||
proc.once('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
proc.once('exit', (code) => {
|
||||
resolve(code ?? 0);
|
||||
proc.once('exit', (code, signal) => {
|
||||
if (code !== null) {
|
||||
resolve(code);
|
||||
} else if (signal) {
|
||||
resolve(128);
|
||||
} else {
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user