From d030a8722de66db6c101173a25451c586e7e169a Mon Sep 17 00:00:00 2001 From: ezequielnick <107352853+ezequielnick@users.noreply.github.com> Date: Tue, 12 Aug 2025 09:07:20 +0800 Subject: [PATCH] feat: separate app logs from core logs --- src/main/config/smartOverride.ts | 5 +- src/main/core/manager.ts | 60 ++++++++-------- src/main/index.ts | 3 +- src/main/resolve/backup.ts | 3 +- src/main/resolve/floatingWindow.ts | 26 +------ src/main/resolve/server.ts | 4 +- src/main/resolve/tray.ts | 17 ++--- src/main/sys/sysproxy.ts | 13 ++-- src/main/utils/dirs.ts | 19 ++++- src/main/utils/init.ts | 7 +- src/main/utils/logger.ts | 108 +++++++++++++++++++++++++++++ src/main/utils/template.ts | 4 ++ 12 files changed, 188 insertions(+), 81 deletions(-) create mode 100644 src/main/utils/logger.ts diff --git a/src/main/config/smartOverride.ts b/src/main/config/smartOverride.ts index c0ddd95..ec8a58c 100644 --- a/src/main/config/smartOverride.ts +++ b/src/main/config/smartOverride.ts @@ -1,5 +1,6 @@ import { getAppConfig } from './app' import { addOverrideItem, removeOverrideItem, getOverrideItem } from './override' +import { overrideLogger } from '../utils/logger' const SMART_OVERRIDE_ID = 'smart-core-override' @@ -237,7 +238,7 @@ export async function createSmartOverride(): Promise { }) } } catch (error) { - console.error('Failed to create Smart override:', error) + await overrideLogger.error('Failed to create Smart override', error) throw error } } @@ -252,7 +253,7 @@ export async function removeSmartOverride(): Promise { await removeOverrideItem(SMART_OVERRIDE_ID) } } catch (error) { - console.error('Failed to remove Smart override:', error) + await overrideLogger.error('Failed to remove Smart override', error) throw error } } diff --git a/src/main/core/manager.ts b/src/main/core/manager.ts index 5baa7f0..2b9c581 100644 --- a/src/main/core/manager.ts +++ b/src/main/core/manager.ts @@ -1,7 +1,7 @@ import { ChildProcess, exec, execFile, spawn } from 'child_process' import { dataDir, - logPath, + coreLogPath, mihomoCoreDir, mihomoCorePath, mihomoProfileWorkDir, @@ -41,6 +41,7 @@ import { uploadRuntimeConfig } from '../resolve/gistApi' import { startMonitor } from '../resolve/trafficMonitor' import { safeShowErrorBox } from '../utils/init' import i18next from '../../shared/i18n' +import { managerLogger } from '../utils/logger' chokidar.watch(path.join(mihomoCoreDir(), 'meta-update'), {}).on('unlinkDir', async () => { try { @@ -96,13 +97,12 @@ export async function startCore(detached = false): Promise[]> { try { await setPublicDNS() } catch (error) { - await writeFile(logPath(), `[Manager]: set dns failed, ${error}`, { - flag: 'a' - }) + await managerLogger.error('set dns failed', error) } } - const stdout = createWriteStream(logPath(), { flags: 'a' }) - const stderr = createWriteStream(logPath(), { flags: 'a' }) + // 内核日志输出到独立的 core-日期.log 文件 + const stdout = createWriteStream(coreLogPath(), { flags: 'a' }) + const stderr = createWriteStream(coreLogPath(), { flags: 'a' }) const env = { DISABLE_LOOPBACK_DETECTOR: String(disableLoopbackDetector), DISABLE_EMBED_CA: String(disableEmbedCA), @@ -128,11 +128,9 @@ export async function startCore(detached = false): Promise[]> { }) } child.on('close', async (code, signal) => { - await writeFile(logPath(), `[Manager]: Core closed, code: ${code}, signal: ${signal}\n`, { - flag: 'a' - }) + await managerLogger.info(`Core closed, code: ${code}, signal: ${signal}`) if (retry) { - await writeFile(logPath(), `[Manager]: Try Restart Core\n`, { flag: 'a' }) + await managerLogger.info('Try Restart Core') retry-- await restartCore() } else { @@ -194,9 +192,7 @@ export async function stopCore(force = false): Promise { await recoverDNS() } } catch (error) { - await writeFile(logPath(), `[Manager]: recover dns failed, ${error}`, { - flag: 'a' - }) + await managerLogger.error('recover dns failed', error) } if (child) { @@ -214,9 +210,7 @@ export async function restartCore(): Promise { await startCore() } catch (e) { // 记录错误到日志而不是显示阻塞对话框 - await writeFile(logPath(), `[Manager]: restart core failed, ${e}\n`, { - flag: 'a' - }) + await managerLogger.error('restart core failed', e) // 重新抛出错误,让调用者处理 throw e } @@ -260,12 +254,12 @@ async function checkProfile(): Promise { mihomoTestDir() ], { env }) } catch (error) { - console.error('Profile check failed:', error) + await managerLogger.error('Profile check failed', error) if (error instanceof Error && 'stdout' in error) { const { stdout, stderr } = error as { stdout: string; stderr?: string } - console.log('Profile check stdout:', stdout) - console.log('Profile check stderr:', stderr) + await managerLogger.info('Profile check stdout', stdout) + await managerLogger.info('Profile check stderr', stderr) const errorLines = stdout .split('\n') @@ -377,15 +371,15 @@ export async function restartAsAdmin(): Promise { command = `powershell -Command "Start-Process -FilePath '${escapedExePath}' -Verb RunAs"` } - console.log('Restarting as administrator with command:', command) + await managerLogger.info('Restarting as administrator with command', command) // 执行PowerShell命令 - exec(command, { windowsHide: true }, (error, _stdout, stderr) => { + exec(command, { windowsHide: true }, async (error, _stdout, stderr) => { if (error) { - console.error('PowerShell execution error:', error) - console.error('stderr:', stderr) + await managerLogger.error('PowerShell execution error', error) + await managerLogger.error('stderr', stderr) } else { - console.log('PowerShell command executed successfully') + await managerLogger.info('PowerShell command executed successfully') } }) @@ -394,7 +388,7 @@ export async function restartAsAdmin(): Promise { const { app } = await import('electron') app.quit() } catch (error) { - console.error('Failed to restart as administrator:', error) + await managerLogger.error('Failed to restart as administrator', error) throw new Error(`Failed to restart as administrator: ${error}`) } } @@ -437,7 +431,7 @@ export async function requestTunPermissions(): Promise { export async function checkAdminRestartForTun(): Promise { if (process.argv.includes('--admin-restart-for-tun')) { - console.log('Detected admin restart for TUN mode, auto-enabling TUN...') + await managerLogger.info('Detected admin restart for TUN mode, auto-enabling TUN...') try { if (process.platform === 'win32') { @@ -446,17 +440,17 @@ export async function checkAdminRestartForTun(): Promise { await patchControledMihomoConfig({ tun: { enable: true }, dns: { enable: true } }) await restartCore() - console.log('TUN mode auto-enabled after admin restart') + await managerLogger.info('TUN mode auto-enabled after admin restart') const { mainWindow } = await import('../index') mainWindow?.webContents.send('controledMihomoConfigUpdated') ipcMain.emit('updateTrayMenu') } else { - console.warn('Admin restart detected but no admin privileges found') + await managerLogger.warn('Admin restart detected but no admin privileges found') } } } catch (error) { - console.error('Failed to auto-enable TUN after admin restart:', error) + await managerLogger.error('Failed to auto-enable TUN after admin restart', error) } } else { // 检查TUN配置与权限的匹配 @@ -475,7 +469,7 @@ export async function validateTunPermissionsOnStartup(): Promise { const hasPermissions = await checkMihomoCorePermissions() if (!hasPermissions) { - console.warn('TUN is enabled but insufficient permissions detected, auto-disabling TUN...') + await managerLogger.warn('TUN is enabled but insufficient permissions detected, auto-disabling TUN...') await patchControledMihomoConfig({ tun: { enable: false } }) @@ -483,12 +477,12 @@ export async function validateTunPermissionsOnStartup(): Promise { mainWindow?.webContents.send('controledMihomoConfigUpdated') ipcMain.emit('updateTrayMenu') - console.log('TUN auto-disabled due to insufficient permissions') + await managerLogger.info('TUN auto-disabled due to insufficient permissions') } else { - console.log('TUN permissions validated successfully') + await managerLogger.info('TUN permissions validated successfully') } } catch (error) { - console.error('Failed to validate TUN permissions on startup:', error) + await managerLogger.error('Failed to validate TUN permissions on startup', error) } } diff --git a/src/main/index.ts b/src/main/index.ts index bacfd60..ce921e6 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -20,6 +20,7 @@ import { startMonitor } from './resolve/trafficMonitor' import { showFloatingWindow } from './resolve/floatingWindow' import { initI18n } from '../shared/i18n' import i18next from 'i18next' +import { logger } from './utils/logger' // 错误处理 function showSafeErrorBox(titleKey: string, message: string): void { @@ -198,7 +199,7 @@ app.whenReady().then(async () => { try { await showFloatingWindow() } catch (error) { - console.error('Failed to create floating window on startup:', error) + await logger.error('Failed to create floating window on startup', error) } } if (!disableTray) { diff --git a/src/main/resolve/backup.ts b/src/main/resolve/backup.ts index b6fdb94..c0aab57 100644 --- a/src/main/resolve/backup.ts +++ b/src/main/resolve/backup.ts @@ -12,6 +12,7 @@ import { subStoreDir, themesDir } from '../utils/dirs' +import { systemLogger } from '../utils/logger' export async function webdavBackup(): Promise { const { createClient } = await import('webdav/dist/node/index.js') @@ -75,7 +76,7 @@ export async function webdavBackup(): Promise { } } } catch (error) { - console.error('Failed to clean up old backup files:', error) + await systemLogger.error('Failed to clean up old backup files', error) } } diff --git a/src/main/resolve/floatingWindow.ts b/src/main/resolve/floatingWindow.ts index a012b2f..0bf6e6c 100644 --- a/src/main/resolve/floatingWindow.ts +++ b/src/main/resolve/floatingWindow.ts @@ -5,33 +5,13 @@ import { join } from 'path' import { getAppConfig, patchAppConfig } from '../config' import { applyTheme } from './theme' import { buildContextMenu, showTrayIcon } from './tray' -import { writeFile } from 'fs/promises' -import { logDir } from '../utils/dirs' -import path from 'path' +import { floatingWindowLogger } from '../utils/logger' export let floatingWindow: BrowserWindow | null = null -// 悬浮窗日志记录 +// 悬浮窗日志记录 - 使用统一的日志工具 async function logFloatingWindow(message: string, error?: any): Promise { - try { - const timestamp = new Date().toISOString() - const logMessage = error - ? `[${timestamp}] [FloatingWindow] ${message}: ${error}\n` - : `[${timestamp}] [FloatingWindow] ${message}\n` - - const logPath = path.join(logDir(), 'floating-window.log') - await writeFile(logPath, logMessage, { flag: 'a' }) - - if (error) { - console.error(`[FloatingWindow] ${message}:`, error) - } else { - console.log(`[FloatingWindow] ${message}`) - } - } catch (logError) { - - console.error('[FloatingWindow] Failed to write log:', logError) - console.log(`[FloatingWindow] Original message: ${message}`, error) - } + await floatingWindowLogger.log(message, error) } async function createFloatingWindow(): Promise { diff --git a/src/main/resolve/server.ts b/src/main/resolve/server.ts index b6790e9..ce40526 100644 --- a/src/main/resolve/server.ts +++ b/src/main/resolve/server.ts @@ -11,6 +11,7 @@ import { nativeImage } from 'electron' import express from 'express' import axios from 'axios' import AdmZip from 'adm-zip' +import { systemLogger } from '../utils/logger' export let pacPort: number export let subStorePort: number @@ -168,7 +169,6 @@ export async function downloadSubStore(): Promise { ) await writeFile(tempBackendPath, Buffer.from(backendRes.data)) // 下载前端文件 - const tempFrontendDir = path.join(tempDir, 'dist') const frontendRes = await axios.get( 'https://github.com/sub-store-org/Sub-Store-Front-End/releases/latest/download/dist.zip', { @@ -192,7 +192,7 @@ export async function downloadSubStore(): Promise { await cp(path.join(tempDir, 'dist'), frontendDir, { recursive: true }) await rm(tempDir, { recursive: true }) } catch (error) { - console.error('substore.downloadFailed:', error) + await systemLogger.error('substore.downloadFailed', error) throw error } } diff --git a/src/main/resolve/tray.ts b/src/main/resolve/tray.ts index 4446c5d..329244e 100644 --- a/src/main/resolve/tray.ts +++ b/src/main/resolve/tray.ts @@ -22,14 +22,15 @@ import { triggerSysProxy } from '../sys/sysproxy' import { quitWithoutCore, restartCore, checkMihomoCorePermissions, requestTunPermissions, restartAsAdmin } from '../core/manager' import { floatingWindow, triggerFloatingWindow } from './floatingWindow' import { t } from 'i18next' +import { trayLogger } from '../utils/logger' export let tray: Tray | null = null export const buildContextMenu = async (): Promise => { // 添加调试日志 - console.log('Current translation for tray.showWindow:', t('tray.showWindow')) - console.log('Current translation for tray.hideFloatingWindow:', t('tray.hideFloatingWindow')) - console.log('Current translation for tray.showFloatingWindow:', t('tray.showFloatingWindow')) + await trayLogger.debug('Current translation for tray.showWindow', t('tray.showWindow')) + await trayLogger.debug('Current translation for tray.hideFloatingWindow', t('tray.hideFloatingWindow')) + await trayLogger.debug('Current translation for tray.showFloatingWindow', t('tray.showFloatingWindow')) const { mode, tun } = await getControledMihomoConfig() const { @@ -187,7 +188,7 @@ export const buildContextMenu = async (): Promise => { try { await restartAsAdmin() } catch (error) { - console.error('Failed to restart as admin from tray:', error) + await trayLogger.error('Failed to restart as admin from tray', error) item.checked = false ipcMain.emit('updateTrayMenu') return @@ -196,7 +197,7 @@ export const buildContextMenu = async (): Promise => { try { await requestTunPermissions() } catch (error) { - console.error('Failed to grant TUN permissions from tray:', error) + await trayLogger.error('Failed to grant TUN permissions from tray', error) item.checked = false ipcMain.emit('updateTrayMenu') return @@ -204,7 +205,7 @@ export const buildContextMenu = async (): Promise => { } } } catch (error) { - console.warn('Permission check failed in tray:', error) + await trayLogger.warn('Permission check failed in tray', error) } await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } }) @@ -418,13 +419,13 @@ export async function closeTrayIcon(): Promise { } export async function showDockIcon(): Promise { - if (process.platform === 'darwin' && !app.dock.isVisible()) { + if (process.platform === 'darwin' && app.dock && !app.dock.isVisible()) { await app.dock.show() } } export async function hideDockIcon(): Promise { - if (process.platform === 'darwin' && app.dock.isVisible()) { + if (process.platform === 'darwin' && app.dock && app.dock.isVisible()) { app.dock.hide() } } diff --git a/src/main/sys/sysproxy.ts b/src/main/sys/sysproxy.ts index f76af23..8d20f1b 100644 --- a/src/main/sys/sysproxy.ts +++ b/src/main/sys/sysproxy.ts @@ -8,6 +8,7 @@ import { resourcesFilesDir } from '../utils/dirs' import { net } from 'electron' import axios from 'axios' import fs from 'fs' +import { proxyLogger } from '../utils/logger' let defaultBypass: string[] let triggerSysProxyTimer: NodeJS.Timeout | null = null @@ -175,7 +176,7 @@ async function requestSocketRecreation(): Promise { // Wait a bit for socket recreation await new Promise(resolve => setTimeout(resolve, 1000)) } catch (error) { - console.log('Failed to send signal to helper:', error) + await proxyLogger.error('Failed to send signal to helper', error) throw error } } @@ -197,16 +198,16 @@ async function helperRequest(requestFn: () => Promise, maxRetries = 1): (error as Error).message?.includes('connect ECONNREFUSED') || (error as Error).message?.includes('ENOENT'))) { - console.log(`Helper request failed (attempt ${attempt + 1}), checking socket file...`) - + await proxyLogger.info(`Helper request failed (attempt ${attempt + 1}), checking socket file...`) + if (!isSocketFileExists()) { - console.log('Socket file missing, requesting recreation...') + await proxyLogger.info('Socket file missing, requesting recreation...') try { await requestSocketRecreation() - console.log('Socket recreation requested, retrying...') + await proxyLogger.info('Socket recreation requested, retrying...') continue } catch (signalError) { - console.log('Failed to request socket recreation:', signalError) + await proxyLogger.warn('Failed to request socket recreation', signalError) } } } diff --git a/src/main/utils/dirs.ts b/src/main/utils/dirs.ts index 13a219f..11ac41f 100644 --- a/src/main/utils/dirs.ts +++ b/src/main/utils/dirs.ts @@ -134,12 +134,27 @@ export function logDir(): string { export function logPath(): string { const date = new Date() - const name = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}` + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const name = `mihomo-party-${year}-${month}-${day}` return path.join(logDir(), `${name}.log`) } export function substoreLogPath(): string { const date = new Date() - const name = `sub-store-${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}` + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const name = `sub-store-${year}-${month}-${day}` + return path.join(logDir(), `${name}.log`) +} + +export function coreLogPath(): string { + const date = new Date() + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const name = `core-${year}-${month}-${day}` return path.join(logDir(), `${name}.log`) } diff --git a/src/main/utils/init.ts b/src/main/utils/init.ts index af1a0e2..262ddd9 100644 --- a/src/main/utils/init.ts +++ b/src/main/utils/init.ts @@ -42,6 +42,7 @@ import { import { app, dialog } from 'electron' import { startSSIDCheck } from '../sys/ssid' import i18next from '../../shared/i18n' +import { initLogger } from './logger' // 安全错误处理 export function safeShowErrorBox(titleKey: string, message: string): void { @@ -115,7 +116,7 @@ async function initDirs(): Promise { await mkdir(dir, { recursive: true }) } } catch (error) { - console.error(`Failed to create directory ${dir}:`, error) + await initLogger.error(`Failed to create directory ${dir}`, error) throw new Error(`Failed to create directory ${dir}: ${error}`) } } @@ -136,7 +137,7 @@ async function initConfig(): Promise { await writeFile(config.path, yaml.stringify(config.content)) } } catch (error) { - console.error(`Failed to create ${config.name} at ${config.path}:`, error) + await initLogger.error(`Failed to create ${config.name} at ${config.path}`, error) throw new Error(`Failed to create ${config.name}: ${error}`) } } @@ -163,7 +164,7 @@ async function initFiles(): Promise { } } } catch (error) { - console.error(`Failed to copy ${file}:`, error) + await initLogger.error(`Failed to copy ${file}`, error) if (['country.mmdb', 'geoip.dat', 'geosite.dat'].includes(file)) { throw new Error(`Failed to copy critical file ${file}: ${error}`) } diff --git a/src/main/utils/logger.ts b/src/main/utils/logger.ts new file mode 100644 index 0000000..db22cb5 --- /dev/null +++ b/src/main/utils/logger.ts @@ -0,0 +1,108 @@ +import { writeFile } from 'fs/promises' +import { logPath } from './dirs' + +export type LogLevel = 'debug' | 'info' | 'warn' | 'error' + +class Logger { + private moduleName: string + + constructor(moduleName: string) { + this.moduleName = moduleName + } + + private formatTimestamp(): string { + return new Date().toISOString() + } + + private formatLogMessage(level: LogLevel, message: string, error?: any): string { + const timestamp = this.formatTimestamp() + const errorStr = error ? `: ${error}` : '' + return `[${timestamp}] [${level.toUpperCase()}] [${this.moduleName}] ${message}${errorStr}\n` + } + + private async writeToFile(level: LogLevel, message: string, error?: any): Promise { + try { + const appLogPath = logPath() + const logMessage = this.formatLogMessage(level, message, error) + await writeFile(appLogPath, logMessage, { flag: 'a' }) + } catch (logError) { + // 如果写入日志文件失败,仍然输出到控制台 + console.error(`[Logger] Failed to write to log file:`, logError) + console.error(`[Logger] Original message: [${level.toUpperCase()}] [${this.moduleName}] ${message}`, error) + } + } + + private logToConsole(level: LogLevel, message: string, error?: any): void { + const prefix = `[${this.moduleName}] ${message}` + + switch (level) { + case 'debug': + console.debug(prefix, error || '') + break + case 'info': + console.log(prefix, error || '') + break + case 'warn': + console.warn(prefix, error || '') + break + case 'error': + console.error(prefix, error || '') + break + } + } + + async debug(message: string, error?: any): Promise { + await this.writeToFile('debug', message, error) + this.logToConsole('debug', message, error) + } + + async info(message: string, error?: any): Promise { + await this.writeToFile('info', message, error) + this.logToConsole('info', message, error) + } + + async warn(message: string, error?: any): Promise { + await this.writeToFile('warn', message, error) + this.logToConsole('warn', message, error) + } + + async error(message: string, error?: any): Promise { + await this.writeToFile('error', message, error) + this.logToConsole('error', message, error) + } + + // 兼容原有的 logFloatingWindow 函数签名 + async log(message: string, error?: any): Promise { + if (error) { + await this.error(message, error) + } else { + await this.info(message) + } + } +} + +// 创建不同模块的日志实例 +export const createLogger = (moduleName: string): Logger => { + return new Logger(moduleName) +} + +// 统一的应用日志实例 - 所有模块共享同一个日志文件 +export const appLogger = createLogger('app') + +// 为了保持向后兼容性,创建各模块的日志实例(都指向同一个应用日志) +export const floatingWindowLogger = createLogger('floating-window') +export const coreLogger = createLogger('mihomo-core') +export const apiLogger = createLogger('mihomo-api') +export const configLogger = createLogger('config') +export const systemLogger = createLogger('system') +export const trafficLogger = createLogger('traffic-monitor') +export const trayLogger = createLogger('tray') +export const initLogger = createLogger('init') +export const ipcLogger = createLogger('ipc') +export const proxyLogger = createLogger('sysproxy') +export const managerLogger = createLogger('manager') +export const factoryLogger = createLogger('factory') +export const overrideLogger = createLogger('override') + +// 默认日志实例 +export const logger = appLogger diff --git a/src/main/utils/template.ts b/src/main/utils/template.ts index 0fa09af..bc4798e 100644 --- a/src/main/utils/template.ts +++ b/src/main/utils/template.ts @@ -22,6 +22,10 @@ export const defaultConfig: IAppConfig = { controlDns: true, controlSniff: true, floatingWindowCompatMode: true, + disableLoopbackDetector: false, + disableEmbedCA: false, + disableSystemCA: false, + skipSafePathCheck: false, nameserverPolicy: {}, siderOrder: [ 'sysproxy',