mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
132 lines
3.3 KiB
TypeScript
132 lines
3.3 KiB
TypeScript
import { appendLogLine, resolveDefaultLogFilePath } from '../shared/log-files';
|
|
|
|
export type FatalErrorReportOptions = {
|
|
title: string;
|
|
context: string;
|
|
};
|
|
|
|
export type FatalErrorReporterDeps = {
|
|
showErrorBox: (title: string, details: string) => void;
|
|
consoleError?: (message: string, error?: unknown) => void;
|
|
appendLogLine?: (line: string) => void;
|
|
resolveLogFilePath?: () => string;
|
|
now?: () => Date;
|
|
};
|
|
|
|
export type FatalErrorReporter = (
|
|
error: unknown,
|
|
options?: Partial<FatalErrorReportOptions>,
|
|
) => void;
|
|
|
|
const DEFAULT_TITLE = 'SubMiner crashed';
|
|
const DEFAULT_CONTEXT = 'SubMiner encountered a fatal error';
|
|
|
|
let fatalErrorReported = false;
|
|
|
|
function pad(value: number): string {
|
|
return String(value).padStart(2, '0');
|
|
}
|
|
|
|
function formatTimestamp(date: Date): string {
|
|
return [
|
|
date.getFullYear(),
|
|
'-',
|
|
pad(date.getMonth() + 1),
|
|
'-',
|
|
pad(date.getDate()),
|
|
' ',
|
|
pad(date.getHours()),
|
|
':',
|
|
pad(date.getMinutes()),
|
|
':',
|
|
pad(date.getSeconds()),
|
|
].join('');
|
|
}
|
|
|
|
function stringifyUnknownError(error: unknown): string {
|
|
if (error instanceof Error) {
|
|
return error.stack || error.message || error.name;
|
|
}
|
|
if (typeof error === 'string') {
|
|
return error;
|
|
}
|
|
try {
|
|
return JSON.stringify(error) ?? String(error);
|
|
} catch {
|
|
return String(error);
|
|
}
|
|
}
|
|
|
|
export function buildFatalErrorDetails(options: {
|
|
context: string;
|
|
error: unknown;
|
|
logFilePath: string;
|
|
}): string {
|
|
return [
|
|
options.context,
|
|
'',
|
|
stringifyUnknownError(options.error),
|
|
'',
|
|
`Log file: ${options.logFilePath}`,
|
|
].join('\n');
|
|
}
|
|
|
|
export function createFatalErrorReporter(deps: FatalErrorReporterDeps): FatalErrorReporter {
|
|
return (error, options = {}) => {
|
|
if (fatalErrorReported) {
|
|
return;
|
|
}
|
|
fatalErrorReported = true;
|
|
|
|
const title = options.title ?? DEFAULT_TITLE;
|
|
const context = options.context ?? DEFAULT_CONTEXT;
|
|
const logFilePath = deps.resolveLogFilePath?.() ?? resolveDefaultLogFilePath('app');
|
|
const details = buildFatalErrorDetails({ context, error, logFilePath });
|
|
const timestamp = formatTimestamp(deps.now?.() ?? new Date());
|
|
const line = `[subminer] - ${timestamp} - ERROR - [main:fatal] ${details.replace(/\r?\n/g, ' | ')}`;
|
|
|
|
try {
|
|
(deps.appendLogLine ?? ((entry: string) => appendLogLine(logFilePath, entry)))(line);
|
|
} catch {
|
|
// Fatal reporting must never throw while handling the original failure.
|
|
}
|
|
|
|
try {
|
|
deps.consoleError?.(line, error);
|
|
} catch {
|
|
// ignore console sink failures
|
|
}
|
|
|
|
try {
|
|
deps.showErrorBox(title, details);
|
|
} catch {
|
|
// If native dialogs are unavailable, the file log above is still the source of truth.
|
|
}
|
|
};
|
|
}
|
|
|
|
export function registerFatalErrorHandlers(deps: {
|
|
reportFatalError: FatalErrorReporter;
|
|
exit: (code: number) => void;
|
|
}): void {
|
|
process.on('uncaughtException', (error) => {
|
|
deps.reportFatalError(error, {
|
|
title: 'SubMiner crashed',
|
|
context: 'SubMiner main process threw an uncaught exception.',
|
|
});
|
|
deps.exit(1);
|
|
});
|
|
|
|
process.on('unhandledRejection', (reason) => {
|
|
deps.reportFatalError(reason, {
|
|
title: 'SubMiner crashed',
|
|
context: 'SubMiner main process had an unhandled promise rejection.',
|
|
});
|
|
deps.exit(1);
|
|
});
|
|
}
|
|
|
|
export function resetFatalErrorReporterForTests(): void {
|
|
fatalErrorReported = false;
|
|
}
|