mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 03:16:46 -07:00
feat(stats): add v1 immersion stats dashboard (#19)
This commit is contained in:
@@ -7,6 +7,7 @@ import { runConfigCommand } from './config-command.js';
|
||||
import { runDictionaryCommand } from './dictionary-command.js';
|
||||
import { runDoctorCommand } from './doctor-command.js';
|
||||
import { runMpvPreAppCommand } from './mpv-command.js';
|
||||
import { runStatsCommand } from './stats-command.js';
|
||||
|
||||
class ExitSignal extends Error {
|
||||
code: number;
|
||||
@@ -47,6 +48,64 @@ function createContext(overrides: Partial<LauncherCommandContext> = {}): Launche
|
||||
};
|
||||
}
|
||||
|
||||
type StatsTestArgOverrides = {
|
||||
stats?: boolean;
|
||||
statsBackground?: boolean;
|
||||
statsCleanup?: boolean;
|
||||
statsCleanupVocab?: boolean;
|
||||
statsCleanupLifetime?: boolean;
|
||||
statsStop?: boolean;
|
||||
logLevel?: LauncherCommandContext['args']['logLevel'];
|
||||
};
|
||||
|
||||
function createStatsTestHarness(overrides: StatsTestArgOverrides = {}) {
|
||||
const context = createContext();
|
||||
const forwarded: string[][] = [];
|
||||
const removedPaths: string[] = [];
|
||||
const createTempDir = (_prefix: string) => {
|
||||
const created = `/tmp/subminer-stats-test`;
|
||||
return created;
|
||||
};
|
||||
const joinPath = (...parts: string[]) => parts.join('/');
|
||||
const removeDir = (targetPath: string) => {
|
||||
removedPaths.push(targetPath);
|
||||
};
|
||||
const runAppCommandAttachedStub = async (
|
||||
_appPath: string,
|
||||
appArgs: string[],
|
||||
_logLevel: LauncherCommandContext['args']['logLevel'],
|
||||
_label: string,
|
||||
) => {
|
||||
forwarded.push(appArgs);
|
||||
return 0;
|
||||
};
|
||||
const waitForStatsResponseStub = async () => ({ ok: true, url: 'http://127.0.0.1:5175' });
|
||||
|
||||
context.args = {
|
||||
...context.args,
|
||||
stats: true,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
return {
|
||||
context,
|
||||
forwarded,
|
||||
removedPaths,
|
||||
createTempDir,
|
||||
joinPath,
|
||||
removeDir,
|
||||
runAppCommandAttachedStub,
|
||||
waitForStatsResponseStub,
|
||||
commandDeps: {
|
||||
createTempDir,
|
||||
joinPath,
|
||||
runAppCommandAttached: runAppCommandAttachedStub,
|
||||
waitForStatsResponse: waitForStatsResponseStub,
|
||||
removeDir,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('config command writes newline-terminated path via process adapter', () => {
|
||||
const writes: string[] = [];
|
||||
const context = createContext();
|
||||
@@ -76,11 +135,37 @@ test('doctor command exits non-zero for missing hard dependencies', () => {
|
||||
commandExists: () => false,
|
||||
configExists: () => true,
|
||||
resolveMainConfigPath: () => '/tmp/SubMiner/config.jsonc',
|
||||
runAppCommandWithInherit: () => {
|
||||
throw new Error('unexpected app handoff');
|
||||
},
|
||||
}),
|
||||
(error: unknown) => error instanceof ExitSignal && error.code === 1,
|
||||
);
|
||||
});
|
||||
|
||||
test('doctor command forwards refresh-known-words to app binary', () => {
|
||||
const context = createContext();
|
||||
context.args.doctor = true;
|
||||
context.args.doctorRefreshKnownWords = true;
|
||||
const forwarded: string[][] = [];
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
runDoctorCommand(context, {
|
||||
commandExists: () => false,
|
||||
configExists: () => true,
|
||||
resolveMainConfigPath: () => '/tmp/SubMiner/config.jsonc',
|
||||
runAppCommandWithInherit: (_appPath, appArgs) => {
|
||||
forwarded.push(appArgs);
|
||||
throw new ExitSignal(0);
|
||||
},
|
||||
}),
|
||||
(error: unknown) => error instanceof ExitSignal && error.code === 0,
|
||||
);
|
||||
|
||||
assert.deepEqual(forwarded, [['--refresh-known-words']]);
|
||||
});
|
||||
|
||||
test('mpv pre-app command exits non-zero when socket is not ready', async () => {
|
||||
const context = createContext();
|
||||
context.args.mpvStatus = true;
|
||||
@@ -128,3 +213,309 @@ test('dictionary command throws if app handoff unexpectedly returns', () => {
|
||||
/unexpectedly returned/,
|
||||
);
|
||||
});
|
||||
|
||||
test('stats command launches attached app command with response path', async () => {
|
||||
const harness = createStatsTestHarness({ stats: true, logLevel: 'debug' });
|
||||
const handled = await runStatsCommand(harness.context, harness.commandDeps);
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.deepEqual(harness.forwarded, [
|
||||
[
|
||||
'--stats',
|
||||
'--stats-response-path',
|
||||
'/tmp/subminer-stats-test/response.json',
|
||||
'--log-level',
|
||||
'debug',
|
||||
],
|
||||
]);
|
||||
assert.equal(harness.removedPaths.length, 1);
|
||||
});
|
||||
|
||||
test('stats background command launches attached daemon control command with response path', async () => {
|
||||
const harness = createStatsTestHarness({ stats: true, statsBackground: true });
|
||||
const handled = await runStatsCommand(harness.context, harness.commandDeps);
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.deepEqual(harness.forwarded, [
|
||||
[
|
||||
'--stats-daemon-start',
|
||||
'--stats-response-path',
|
||||
'/tmp/subminer-stats-test/response.json',
|
||||
],
|
||||
]);
|
||||
assert.equal(harness.removedPaths.length, 1);
|
||||
});
|
||||
|
||||
test('stats command waits for attached app exit after startup response', async () => {
|
||||
const harness = createStatsTestHarness({ stats: true });
|
||||
const started = new Promise<number>((resolve) => setTimeout(() => resolve(0), 20));
|
||||
|
||||
const statsCommand = runStatsCommand(harness.context, {
|
||||
...harness.commandDeps,
|
||||
runAppCommandAttached: async (...args) => {
|
||||
await harness.runAppCommandAttachedStub(...args);
|
||||
return started;
|
||||
},
|
||||
});
|
||||
const result = await Promise.race([
|
||||
statsCommand.then(() => 'resolved'),
|
||||
new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), 5)),
|
||||
]);
|
||||
|
||||
assert.equal(result, 'timeout');
|
||||
|
||||
const final = await statsCommand;
|
||||
assert.equal(final, true);
|
||||
assert.deepEqual(harness.forwarded, [
|
||||
[
|
||||
'--stats',
|
||||
'--stats-response-path',
|
||||
'/tmp/subminer-stats-test/response.json',
|
||||
],
|
||||
]);
|
||||
assert.equal(harness.removedPaths.length, 1);
|
||||
});
|
||||
|
||||
test('stats command throws when attached app exits non-zero after startup response', async () => {
|
||||
const harness = createStatsTestHarness({ stats: true });
|
||||
|
||||
await assert.rejects(async () => {
|
||||
await runStatsCommand(harness.context, {
|
||||
...harness.commandDeps,
|
||||
runAppCommandAttached: async (...args) => {
|
||||
await harness.runAppCommandAttachedStub(...args);
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
return 3;
|
||||
},
|
||||
});
|
||||
}, /Stats app exited with status 3\./);
|
||||
|
||||
assert.equal(harness.removedPaths.length, 1);
|
||||
});
|
||||
|
||||
test('stats cleanup command forwards cleanup vocab flags to the app', async () => {
|
||||
const harness = createStatsTestHarness({
|
||||
stats: true,
|
||||
statsCleanup: true,
|
||||
statsCleanupVocab: true,
|
||||
});
|
||||
const handled = await runStatsCommand(harness.context, {
|
||||
...harness.commandDeps,
|
||||
waitForStatsResponse: async () => ({ ok: true }),
|
||||
});
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.deepEqual(harness.forwarded, [
|
||||
[
|
||||
'--stats',
|
||||
'--stats-response-path',
|
||||
'/tmp/subminer-stats-test/response.json',
|
||||
'--stats-cleanup',
|
||||
'--stats-cleanup-vocab',
|
||||
],
|
||||
]);
|
||||
assert.equal(harness.removedPaths.length, 1);
|
||||
});
|
||||
|
||||
test('stats stop command forwards stop flag to the app', async () => {
|
||||
const harness = createStatsTestHarness({ stats: true, statsStop: true });
|
||||
|
||||
const handled = await runStatsCommand(harness.context, {
|
||||
...harness.commandDeps,
|
||||
waitForStatsResponse: async () => ({ ok: true }),
|
||||
});
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.deepEqual(harness.forwarded, [
|
||||
[
|
||||
'--stats-daemon-stop',
|
||||
'--stats-response-path',
|
||||
'/tmp/subminer-stats-test/response.json',
|
||||
],
|
||||
]);
|
||||
assert.equal(harness.removedPaths.length, 1);
|
||||
});
|
||||
|
||||
test('stats stop command exits on process exit without waiting for startup response', async () => {
|
||||
const harness = createStatsTestHarness({ stats: true, statsStop: true });
|
||||
let waitedForResponse = false;
|
||||
|
||||
const handled = await runStatsCommand(harness.context, {
|
||||
...harness.commandDeps,
|
||||
runAppCommandAttached: async (...args) => {
|
||||
await harness.runAppCommandAttachedStub(...args);
|
||||
return 0;
|
||||
},
|
||||
waitForStatsResponse: async () => {
|
||||
waitedForResponse = true;
|
||||
return { ok: true };
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.equal(waitedForResponse, false);
|
||||
assert.equal(harness.removedPaths.length, 1);
|
||||
});
|
||||
|
||||
test('stats cleanup command forwards lifetime rebuild flag to the app', async () => {
|
||||
const harness = createStatsTestHarness({
|
||||
stats: true,
|
||||
statsCleanup: true,
|
||||
statsCleanupLifetime: true,
|
||||
});
|
||||
const handled = await runStatsCommand(harness.context, {
|
||||
...harness.commandDeps,
|
||||
waitForStatsResponse: async () => ({ ok: true }),
|
||||
});
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.deepEqual(harness.forwarded, [
|
||||
[
|
||||
'--stats',
|
||||
'--stats-response-path',
|
||||
'/tmp/subminer-stats-test/response.json',
|
||||
'--stats-cleanup',
|
||||
'--stats-cleanup-lifetime',
|
||||
],
|
||||
]);
|
||||
assert.equal(harness.removedPaths.length, 1);
|
||||
});
|
||||
|
||||
test('stats command throws when stats response reports an error', async () => {
|
||||
const harness = createStatsTestHarness({ stats: true });
|
||||
|
||||
await assert.rejects(async () => {
|
||||
await runStatsCommand(harness.context, {
|
||||
...harness.commandDeps,
|
||||
runAppCommandAttached: async (...args) => {
|
||||
await harness.runAppCommandAttachedStub(...args);
|
||||
return 0;
|
||||
},
|
||||
waitForStatsResponse: async () => ({
|
||||
ok: false,
|
||||
error: 'Immersion tracking is disabled in config.',
|
||||
}),
|
||||
});
|
||||
}, /Immersion tracking is disabled in config\./);
|
||||
|
||||
assert.equal(harness.removedPaths.length, 1);
|
||||
});
|
||||
|
||||
test('stats cleanup command fails if attached app exits before startup response', async () => {
|
||||
const harness = createStatsTestHarness({
|
||||
stats: true,
|
||||
statsCleanup: true,
|
||||
statsCleanupVocab: true,
|
||||
});
|
||||
|
||||
await assert.rejects(async () => {
|
||||
await runStatsCommand(harness.context, {
|
||||
...harness.commandDeps,
|
||||
runAppCommandAttached: async (...args) => {
|
||||
await harness.runAppCommandAttachedStub(...args);
|
||||
return 2;
|
||||
},
|
||||
waitForStatsResponse: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
return { ok: true, url: 'http://127.0.0.1:5175' };
|
||||
},
|
||||
});
|
||||
}, /Stats app exited before startup response \(status 2\)\./);
|
||||
|
||||
assert.equal(harness.removedPaths.length, 1);
|
||||
});
|
||||
|
||||
test('stats command aborts pending response wait when app exits before startup response', async () => {
|
||||
const harness = createStatsTestHarness({ stats: true });
|
||||
let aborted = false;
|
||||
|
||||
await assert.rejects(async () => {
|
||||
await runStatsCommand(harness.context, {
|
||||
...harness.commandDeps,
|
||||
runAppCommandAttached: async (...args) => {
|
||||
await harness.runAppCommandAttachedStub(...args);
|
||||
return 2;
|
||||
},
|
||||
waitForStatsResponse: async (_responsePath, signal) =>
|
||||
await new Promise((resolve) => {
|
||||
signal?.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
aborted = true;
|
||||
resolve({ ok: false, error: 'aborted' });
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
}),
|
||||
});
|
||||
}, /Stats app exited before startup response \(status 2\)\./);
|
||||
|
||||
assert.equal(aborted, true);
|
||||
assert.equal(harness.removedPaths.length, 1);
|
||||
});
|
||||
|
||||
test('stats command aborts pending response wait when attached app fails to spawn', async () => {
|
||||
const harness = createStatsTestHarness({ stats: true });
|
||||
const spawnError = new Error('spawn failed');
|
||||
let aborted = false;
|
||||
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
await runStatsCommand(harness.context, {
|
||||
...harness.commandDeps,
|
||||
runAppCommandAttached: async (...args) => {
|
||||
await harness.runAppCommandAttachedStub(...args);
|
||||
throw spawnError;
|
||||
},
|
||||
waitForStatsResponse: async (_responsePath, signal) =>
|
||||
await new Promise((resolve) => {
|
||||
signal?.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
aborted = true;
|
||||
resolve({ ok: false, error: 'aborted' });
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
}),
|
||||
});
|
||||
},
|
||||
(error: unknown) => error === spawnError,
|
||||
);
|
||||
|
||||
assert.equal(aborted, true);
|
||||
assert.equal(harness.removedPaths.length, 1);
|
||||
});
|
||||
|
||||
test('stats cleanup command aborts pending response wait when app exits before startup response', async () => {
|
||||
const harness = createStatsTestHarness({
|
||||
stats: true,
|
||||
statsCleanup: true,
|
||||
statsCleanupVocab: true,
|
||||
});
|
||||
let aborted = false;
|
||||
|
||||
await assert.rejects(async () => {
|
||||
await runStatsCommand(harness.context, {
|
||||
...harness.commandDeps,
|
||||
runAppCommandAttached: async (...args) => {
|
||||
await harness.runAppCommandAttachedStub(...args);
|
||||
return 2;
|
||||
},
|
||||
waitForStatsResponse: async (_responsePath, signal) =>
|
||||
await new Promise((resolve) => {
|
||||
signal?.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
aborted = true;
|
||||
resolve({ ok: false, error: 'aborted' });
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
}),
|
||||
});
|
||||
}, /Stats app exited before startup response \(status 2\)\./);
|
||||
|
||||
assert.equal(aborted, true);
|
||||
assert.equal(harness.removedPaths.length, 1);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from 'node:fs';
|
||||
import { log } from '../log.js';
|
||||
import { runAppCommandWithInherit } from '../mpv.js';
|
||||
import { commandExists } from '../util.js';
|
||||
import { resolveMainConfigPath } from '../config-path.js';
|
||||
import type { LauncherCommandContext } from './context.js';
|
||||
@@ -8,12 +9,14 @@ interface DoctorCommandDeps {
|
||||
commandExists(command: string): boolean;
|
||||
configExists(path: string): boolean;
|
||||
resolveMainConfigPath(): string;
|
||||
runAppCommandWithInherit(appPath: string, appArgs: string[]): never;
|
||||
}
|
||||
|
||||
const defaultDeps: DoctorCommandDeps = {
|
||||
commandExists,
|
||||
configExists: fs.existsSync,
|
||||
resolveMainConfigPath,
|
||||
runAppCommandWithInherit,
|
||||
};
|
||||
|
||||
export function runDoctorCommand(
|
||||
@@ -72,14 +75,21 @@ export function runDoctorCommand(
|
||||
},
|
||||
];
|
||||
|
||||
const hasHardFailure = checks.some((entry) =>
|
||||
entry.label === 'app binary' || entry.label === 'mpv' ? !entry.ok : false,
|
||||
);
|
||||
|
||||
for (const check of checks) {
|
||||
log(check.ok ? 'info' : 'warn', args.logLevel, `[doctor] ${check.label}: ${check.detail}`);
|
||||
}
|
||||
|
||||
if (args.doctorRefreshKnownWords) {
|
||||
if (!appPath) {
|
||||
processAdapter.exit(1);
|
||||
return true;
|
||||
}
|
||||
deps.runAppCommandWithInherit(appPath, ['--refresh-known-words']);
|
||||
}
|
||||
|
||||
const hasHardFailure = checks.some((entry) =>
|
||||
entry.label === 'app binary' || entry.label === 'mpv' ? !entry.ok : false,
|
||||
);
|
||||
processAdapter.exit(hasHardFailure ? 1 : 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
180
launcher/commands/stats-command.ts
Normal file
180
launcher/commands/stats-command.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { runAppCommandAttached } from '../mpv.js';
|
||||
import { sleep } from '../util.js';
|
||||
import type { LauncherCommandContext } from './context.js';
|
||||
|
||||
type StatsCommandResponse = {
|
||||
ok: boolean;
|
||||
url?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type StatsCommandDeps = {
|
||||
createTempDir: (prefix: string) => string;
|
||||
joinPath: (...parts: string[]) => string;
|
||||
runAppCommandAttached: (
|
||||
appPath: string,
|
||||
appArgs: string[],
|
||||
logLevel: LauncherCommandContext['args']['logLevel'],
|
||||
label: string,
|
||||
) => Promise<number>;
|
||||
waitForStatsResponse: (
|
||||
responsePath: string,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<StatsCommandResponse>;
|
||||
removeDir: (targetPath: string) => void;
|
||||
};
|
||||
|
||||
const STATS_STARTUP_RESPONSE_TIMEOUT_MS = 12_000;
|
||||
|
||||
type StatsResponseWait = {
|
||||
controller: AbortController;
|
||||
promise: Promise<{ kind: 'response'; response: StatsCommandResponse }>;
|
||||
};
|
||||
|
||||
type StatsStartupResult =
|
||||
| { kind: 'response'; response: StatsCommandResponse }
|
||||
| { kind: 'exit'; status: number }
|
||||
| { kind: 'spawn-error'; error: unknown };
|
||||
|
||||
const defaultDeps: StatsCommandDeps = {
|
||||
createTempDir: (prefix) => fs.mkdtempSync(path.join(os.tmpdir(), prefix)),
|
||||
joinPath: (...parts) => path.join(...parts),
|
||||
runAppCommandAttached: (appPath, appArgs, logLevel, label) =>
|
||||
runAppCommandAttached(appPath, appArgs, logLevel, label),
|
||||
waitForStatsResponse: async (responsePath, signal) => {
|
||||
const deadline = Date.now() + STATS_STARTUP_RESPONSE_TIMEOUT_MS;
|
||||
while (Date.now() < deadline) {
|
||||
if (signal?.aborted) {
|
||||
return {
|
||||
ok: false,
|
||||
error: 'Cancelled waiting for stats dashboard startup response.',
|
||||
};
|
||||
}
|
||||
try {
|
||||
if (fs.existsSync(responsePath)) {
|
||||
return JSON.parse(fs.readFileSync(responsePath, 'utf8')) as StatsCommandResponse;
|
||||
}
|
||||
} catch {
|
||||
// retry until timeout
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: 'Timed out waiting for stats dashboard startup response.',
|
||||
};
|
||||
},
|
||||
removeDir: (targetPath) => {
|
||||
fs.rmSync(targetPath, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
|
||||
async function performStartupHandshake(
|
||||
createResponseWait: () => StatsResponseWait,
|
||||
attachedExitPromise: Promise<number>,
|
||||
): Promise<boolean> {
|
||||
const responseWait = createResponseWait();
|
||||
const startupResult = await Promise.race<StatsStartupResult>([
|
||||
responseWait.promise,
|
||||
attachedExitPromise.then(
|
||||
(status) => ({ kind: 'exit' as const, status }),
|
||||
(error) => ({ kind: 'spawn-error' as const, error }),
|
||||
),
|
||||
]);
|
||||
|
||||
if (startupResult.kind === 'spawn-error') {
|
||||
responseWait.controller.abort();
|
||||
throw startupResult.error;
|
||||
}
|
||||
|
||||
if (startupResult.kind === 'exit') {
|
||||
if (startupResult.status !== 0) {
|
||||
responseWait.controller.abort();
|
||||
throw new Error(`Stats app exited before startup response (status ${startupResult.status}).`);
|
||||
}
|
||||
|
||||
const response = await responseWait.promise.then((result) => result.response);
|
||||
if (!response.ok) {
|
||||
throw new Error(response.error || 'Stats dashboard failed to start.');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!startupResult.response.ok) {
|
||||
throw new Error(startupResult.response.error || 'Stats dashboard failed to start.');
|
||||
}
|
||||
|
||||
const exitStatus = await attachedExitPromise;
|
||||
if (exitStatus !== 0) {
|
||||
throw new Error(`Stats app exited with status ${exitStatus}.`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function runStatsCommand(
|
||||
context: LauncherCommandContext,
|
||||
deps: Partial<StatsCommandDeps> = {},
|
||||
): Promise<boolean> {
|
||||
const resolvedDeps: StatsCommandDeps = { ...defaultDeps, ...deps };
|
||||
const { args, appPath } = context;
|
||||
if (!args.stats || !appPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tempDir = resolvedDeps.createTempDir('subminer-stats-');
|
||||
const responsePath = resolvedDeps.joinPath(tempDir, 'response.json');
|
||||
|
||||
const createResponseWait = () => {
|
||||
const controller = new AbortController();
|
||||
return {
|
||||
controller,
|
||||
promise: resolvedDeps
|
||||
.waitForStatsResponse(responsePath, controller.signal)
|
||||
.then((response) => ({ kind: 'response' as const, response })),
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
const forwarded = args.statsCleanup
|
||||
? ['--stats', '--stats-response-path', responsePath]
|
||||
: args.statsStop
|
||||
? ['--stats-daemon-stop', '--stats-response-path', responsePath]
|
||||
: args.statsBackground
|
||||
? ['--stats-daemon-start', '--stats-response-path', responsePath]
|
||||
: ['--stats', '--stats-response-path', responsePath];
|
||||
if (args.statsCleanup) {
|
||||
forwarded.push('--stats-cleanup');
|
||||
}
|
||||
if (args.statsCleanupVocab) {
|
||||
forwarded.push('--stats-cleanup-vocab');
|
||||
}
|
||||
if (args.statsCleanupLifetime) {
|
||||
forwarded.push('--stats-cleanup-lifetime');
|
||||
}
|
||||
if (args.logLevel !== 'info') {
|
||||
forwarded.push('--log-level', args.logLevel);
|
||||
}
|
||||
const attachedExitPromise = resolvedDeps.runAppCommandAttached(
|
||||
appPath,
|
||||
forwarded,
|
||||
args.logLevel,
|
||||
'stats',
|
||||
);
|
||||
|
||||
if (args.statsStop) {
|
||||
const status = await attachedExitPromise;
|
||||
if (status !== 0) {
|
||||
throw new Error(`Stats app exited with status ${status}.`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return await performStartupHandshake(createResponseWait, attachedExitPromise);
|
||||
} finally {
|
||||
resolvedDeps.removeDir(tempDir);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user