/** * 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 = 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 };