Files

194 lines
5.4 KiB
TypeScript

/**
* Logger for Electron main/preload.
*
* Always uses `console` when available so output shows in the terminal if you
* start the app from a shell (dev or packaged). Preload logs typically appear
* in DevTools, not the parent terminal.
*
* In the main process only, logs are also appended asynchronously to
* userData/logs/qortalHub.log (batched, non-blocking). Rotates at ~10 MiB to
* qortalHub.log.1. Preload does not touch the filesystem here (no electron.app).
*/
import * as fs from 'fs';
import * as path from 'path';
import { createRequire } from 'module';
import { format } from 'util';
const nodeRequire = createRequire(__filename);
const noop = (..._args: unknown[]) => {};
export const DEV_LOGS_DISABLED_STORAGE_KEY = 'qortal_disable_dev_logs';
let disableDevLogs = true;
export function setDisableDevLogs(value: boolean): void {
disableDevLogs = value;
}
const IMPORTANT_LOG_PATTERNS = [
/\bfailed\b/i,
/\bfailure\b/i,
/\berror\b/i,
/\bexception\b/i,
/\btimeout\b/i,
/\btimed out\b/i,
/\bnot starting\b/i,
/\bdid not start\b/i,
/\btoo many instances\b/i,
/\bbridge missing\b/i,
/\bbridge startup\b/i,
/\bdegraded\b/i,
/\bshutdown\b/i,
];
function isImportantLog(args: readonly unknown[]): boolean {
const first = args[0];
if (typeof first !== 'string') return false;
return IMPORTANT_LOG_PATTERNS.some((pattern) => pattern.test(first));
}
function shouldEmitLog(
method: 'log' | 'error' | 'warn' | 'debug' | 'info',
args: readonly unknown[]
): boolean {
if (method === 'error' || method === 'warn') return true;
if (process.type !== 'browser') return true;
if (!disableDevLogs) return true;
return isImportantLog(args);
}
/** Stdio can be a closed pipe (AppImage, desktop launcher, detached parent). Ignore benign write failures. */
function isBenignStdioWriteError(err: unknown): boolean {
const code = (err as NodeJS.ErrnoException)?.code;
return (
code === 'EPIPE' ||
code === 'EIO' ||
code === 'ENOTCONN' ||
code === 'EBADF'
);
}
function emitToConsole(
c: Console,
method: 'log' | 'error' | 'warn' | 'debug' | 'info',
args: readonly unknown[]
): void {
try {
(c[method] as (...args: unknown[]) => void).apply(c, args);
} catch (err: unknown) {
if (!isBenignStdioWriteError(err)) throw err;
}
}
const LOG_FILE = 'qortalHub.log';
const MAX_LOG_BYTES = 10 * 1024 * 1024;
const pendingLines: string[] = [];
let flushScheduled = false;
let logFilePath: string | null = null;
let logUserDataPath: string | null = null;
/** Byte length of current log file on disk (UTF-8), best-effort. */
let currentFileBytes = 0;
let ioChain: Promise<void> = Promise.resolve();
function resolveLogFilePath(): string | null {
if (process.type !== 'browser') return null;
try {
const { app } = nodeRequire('electron') as typeof import('electron');
const userDataPath = app.getPath('userData');
if (logFilePath !== null && logUserDataPath === userDataPath) {
return logFilePath;
}
const dir = path.join(userDataPath, 'logs');
fs.mkdirSync(dir, { recursive: true });
logFilePath = path.join(dir, LOG_FILE);
logUserDataPath = userDataPath;
if (fs.existsSync(logFilePath)) {
currentFileBytes = fs.statSync(logFilePath).size;
} else {
currentFileBytes = 0;
}
return logFilePath;
} catch {
logFilePath = null;
logUserDataPath = null;
return null;
}
}
function rotateLogFileSync(): void {
if (!logFilePath) return;
const rotated = `${logFilePath}.1`;
try {
if (fs.existsSync(rotated)) fs.unlinkSync(rotated);
} catch {
/* ignore */
}
try {
if (fs.existsSync(logFilePath)) fs.renameSync(logFilePath, rotated);
} catch {
/* ignore */
}
currentFileBytes = 0;
}
function scheduleFileFlush(): void {
if (flushScheduled) return;
flushScheduled = true;
setImmediate(() => {
flushScheduled = false;
const chunk = pendingLines.splice(0, pendingLines.length).join('');
if (!chunk || !logFilePath) return;
ioChain = ioChain
.then(async () => {
const byteLen = Buffer.byteLength(chunk, 'utf8');
try {
if (currentFileBytes + byteLen > MAX_LOG_BYTES) {
rotateLogFileSync();
}
await fs.promises.appendFile(logFilePath!, chunk, 'utf8');
currentFileBytes += byteLen;
} catch {
/* disk full / permissions — avoid throwing into unhandledRejection */
}
})
.catch(() => {});
});
}
function queueFileLine(level: string, args: readonly unknown[]): void {
if (process.type !== 'browser') return;
if (!resolveLogFilePath()) return;
const line = `[${new Date().toISOString()}] [${level}] ${format(...args)}\n`;
pendingLines.push(line);
scheduleFileFlush();
}
function makeLogger(
method: 'log' | 'error' | 'warn' | 'debug' | 'info',
fileLevel: string
): (...args: unknown[]) => void {
try {
const c = typeof console !== 'undefined' ? console : null;
return c && typeof c[method] === 'function'
? (...args: unknown[]) => {
if (!shouldEmitLog(method, args)) return;
emitToConsole(c, method, args);
queueFileLine(fileLevel, args);
}
: noop;
} catch {
return noop;
}
}
export const log = makeLogger('log', 'LOG');
export const error = makeLogger('error', 'ERROR');
export const warn = makeLogger('warn', 'WARN');
export const debug = makeLogger('debug', 'DEBUG');
export const info = makeLogger('info', 'INFO');
export default { log, error, warn, debug, info };