From 0a064bdbb8fe16c8727d3c95bd1dd3acf6043da9 Mon Sep 17 00:00:00 2001 From: ezequielnick <107352853+ezequielnick@users.noreply.github.com> Date: Wed, 6 Aug 2025 10:32:09 +0800 Subject: [PATCH] fix: windows first startup issue --- src/main/core/manager.ts | 5 +- src/main/index.ts | 37 +++++++++--- src/main/utils/init.ts | 121 +++++++++++++++++++++++++-------------- 3 files changed, 109 insertions(+), 54 deletions(-) diff --git a/src/main/core/manager.ts b/src/main/core/manager.ts index 1c2b23d..5b54e1c 100644 --- a/src/main/core/manager.ts +++ b/src/main/core/manager.ts @@ -39,6 +39,7 @@ import os from 'os' import { createWriteStream, existsSync } from 'fs' import { uploadRuntimeConfig } from '../resolve/gistApi' import { startMonitor } from '../resolve/trafficMonitor' +import { safeShowErrorBox } from '../utils/init' import i18next from '../../shared/i18n' chokidar.watch(path.join(mihomoCoreDir(), 'meta-update'), {}).on('unlinkDir', async () => { @@ -46,7 +47,7 @@ chokidar.watch(path.join(mihomoCoreDir(), 'meta-update'), {}).on('unlinkDir', as await stopCore(true) await startCore() } catch (e) { - dialog.showErrorBox(i18next.t('mihomo.error.coreStartFailed'), `${e}`) + safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`) } }) @@ -228,7 +229,7 @@ export async function keepCoreAlive(): Promise { await writeFile(path.join(dataDir(), 'core.pid'), child.pid.toString()) } } catch (e) { - dialog.showErrorBox(i18next.t('mihomo.error.coreStartFailed'), `${e}`) + safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`) } } diff --git a/src/main/index.ts b/src/main/index.ts index 93dada2..9b27aa8 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -18,12 +18,31 @@ import { initProfileUpdater } from './core/profileUpdater' import { existsSync, writeFileSync } from 'fs' import { exePath, taskDir } from './utils/dirs' import path from 'path' +import iconv from 'iconv-lite' import { startMonitor } from './resolve/trafficMonitor' import { showFloatingWindow } from './resolve/floatingWindow' -import iconv from 'iconv-lite' import { initI18n } from '../shared/i18n' import i18next from 'i18next' +// 错误处理 +function showSafeErrorBox(titleKey: string, message: string): void { + let title: string + try { + title = i18next.t(titleKey) + if (!title || title === titleKey) throw new Error('Translation not ready') + } catch { + const isZh = app.getLocale().startsWith('zh') + const fallbacks: Record = { + 'common.error.initFailed': { zh: '应用初始化失败', en: 'Application initialization failed' }, + 'mihomo.error.coreStartFailed': { zh: '内核启动出错', en: 'Core start failed' }, + 'profiles.error.importFailed': { zh: '配置导入失败', en: 'Profile import failed' }, + 'common.error.adminRequired': { zh: '需要管理员权限', en: 'Administrator privileges required' } + } + title = fallbacks[titleKey] ? (isZh ? fallbacks[titleKey].zh : fallbacks[titleKey].en) : (isZh ? '错误' : 'Error') + } + dialog.showErrorBox(title, message) +} + async function fixUserDataPermissions(): Promise { if (process.platform !== 'darwin') return @@ -50,6 +69,7 @@ async function fixUserDataPermissions(): Promise { let quitTimeout: NodeJS.Timeout | null = null export let mainWindow: BrowserWindow | null = null +// Windows 管理员权限检查(仅在生产模式下) if (process.platform === 'win32' && !is.dev && !process.argv.includes('noadmin')) { try { createElevateTask() @@ -74,10 +94,7 @@ if (process.platform === 'win32' && !is.dev && !process.argv.includes('noadmin') } catch { // ignore } - dialog.showErrorBox( - i18next.t('common.error.adminRequired'), - `${i18next.t('common.error.adminRequired')}\n${createErrorStr}\n${eStr}` - ) + showSafeErrorBox('common.error.adminRequired', `${createErrorStr}\n${eStr}`) } finally { app.exit() } @@ -171,6 +188,9 @@ app.whenReady().then(async () => { electronApp.setAppUserModelId('party.mihomo.app') try { + // 首先等待初始化完成,确保配置文件和目录都已创建 + await initPromise + const appConfig = await getAppConfig() // 如果配置中没有语言设置,则使用系统语言 if (!appConfig.language) { @@ -179,9 +199,8 @@ app.whenReady().then(async () => { appConfig.language = systemLanguage } await initI18n({ lng: appConfig.language }) - await initPromise } catch (e) { - dialog.showErrorBox(i18next.t('common.error.initFailed'), `${e}`) + showSafeErrorBox('common.error.initFailed', `${e}`) app.quit() } try { @@ -190,7 +209,7 @@ app.whenReady().then(async () => { await initProfileUpdater() }) } catch (e) { - dialog.showErrorBox(i18next.t('mihomo.error.coreStartFailed'), `${e}`) + showSafeErrorBox('mihomo.error.coreStartFailed', `${e}`) } try { await startMonitor() @@ -242,7 +261,7 @@ async function handleDeepLink(url: string): Promise { new Notification({ title: i18next.t('profiles.notification.importSuccess') }).show() break } catch (e) { - dialog.showErrorBox(i18next.t('profiles.error.importFailed'), `${url}\n${e}`) + showSafeErrorBox('profiles.error.importFailed', `${url}\n${e}`) } } } diff --git a/src/main/utils/init.ts b/src/main/utils/init.ts index f4fb539..fc50bfe 100644 --- a/src/main/utils/init.ts +++ b/src/main/utils/init.ts @@ -39,8 +39,25 @@ import { patchAppConfig, patchControledMihomoConfig } from '../config' -import { app } from 'electron' +import { app, dialog } from 'electron' import { startSSIDCheck } from '../sys/ssid' +import i18next from '../../shared/i18n' + +// 安全错误处理 +export function safeShowErrorBox(titleKey: string, message: string): void { + let title: string + try { + title = i18next.t(titleKey) + if (!title || title === titleKey) throw new Error('Translation not ready') + } catch { + const isZh = process.env.LANG?.startsWith('zh') || process.env.LC_ALL?.startsWith('zh') + const fallbacks: Record = { + 'mihomo.error.coreStartFailed': { zh: '内核启动出错', en: 'Core start failed' } + } + title = fallbacks[titleKey] ? (isZh ? fallbacks[titleKey].zh : fallbacks[titleKey].en) : (isZh ? '错误' : 'Error') + } + dialog.showErrorBox(title, message) +} async function fixDataDirPermissions(): Promise { if (process.platform !== 'darwin') return @@ -68,47 +85,48 @@ async function fixDataDirPermissions(): Promise { async function initDirs(): Promise { await fixDataDirPermissions() - if (!existsSync(dataDir())) { - await mkdir(dataDir()) - } - if (!existsSync(themesDir())) { - await mkdir(themesDir()) - } - if (!existsSync(profilesDir())) { - await mkdir(profilesDir()) - } - if (!existsSync(overrideDir())) { - await mkdir(overrideDir()) - } - if (!existsSync(mihomoWorkDir())) { - await mkdir(mihomoWorkDir()) - } - if (!existsSync(logDir())) { - await mkdir(logDir()) - } - if (!existsSync(mihomoTestDir())) { - await mkdir(mihomoTestDir()) - } - if (!existsSync(subStoreDir())) { - await mkdir(subStoreDir()) + // 按依赖顺序创建目录 + const dirsToCreate = [ + dataDir(), + themesDir(), + profilesDir(), + overrideDir(), + mihomoWorkDir(), + logDir(), + mihomoTestDir(), + subStoreDir() + ] + + for (const dir of dirsToCreate) { + try { + if (!existsSync(dir)) { + await mkdir(dir, { recursive: true }) + } + } catch (error) { + console.error(`Failed to create directory ${dir}:`, error) + throw new Error(`Failed to create directory ${dir}: ${error}`) + } } } async function initConfig(): Promise { - if (!existsSync(appConfigPath())) { - await writeFile(appConfigPath(), yaml.stringify(defaultConfig)) - } - if (!existsSync(profileConfigPath())) { - await writeFile(profileConfigPath(), yaml.stringify(defaultProfileConfig)) - } - if (!existsSync(overrideConfigPath())) { - await writeFile(overrideConfigPath(), yaml.stringify(defaultOverrideConfig)) - } - if (!existsSync(profilePath('default'))) { - await writeFile(profilePath('default'), yaml.stringify(defaultProfile)) - } - if (!existsSync(controledMihomoConfigPath())) { - await writeFile(controledMihomoConfigPath(), yaml.stringify(defaultControledMihomoConfig)) + const configs = [ + { path: appConfigPath(), content: defaultConfig, name: 'app config' }, + { path: profileConfigPath(), content: defaultProfileConfig, name: 'profile config' }, + { path: overrideConfigPath(), content: defaultOverrideConfig, name: 'override config' }, + { path: profilePath('default'), content: defaultProfile, name: 'default profile' }, + { path: controledMihomoConfigPath(), content: defaultControledMihomoConfig, name: 'mihomo config' } + ] + + for (const config of configs) { + try { + if (!existsSync(config.path)) { + await writeFile(config.path, yaml.stringify(config.content)) + } + } catch (error) { + console.error(`Failed to create ${config.name} at ${config.path}:`, error) + throw new Error(`Failed to create ${config.name}: ${error}`) + } } } @@ -117,13 +135,30 @@ async function initFiles(): Promise { const targetPath = path.join(mihomoWorkDir(), file) const testTargetPath = path.join(mihomoTestDir(), file) const sourcePath = path.join(resourcesFilesDir(), file) - if (!existsSync(targetPath) && existsSync(sourcePath)) { - await cp(sourcePath, targetPath, { recursive: true }) - } - if (!existsSync(testTargetPath) && existsSync(sourcePath)) { - await cp(sourcePath, testTargetPath, { recursive: true }) + + try { + if (!existsSync(targetPath) && existsSync(sourcePath)) { + await cp(sourcePath, targetPath, { recursive: true }) + } + if (!existsSync(testTargetPath) && existsSync(sourcePath)) { + await cp(sourcePath, testTargetPath, { recursive: true }) + } + } catch (error) { + console.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}`) + } } } + + // 确保工作目录存在 + if (!existsSync(mihomoWorkDir())) { + await mkdir(mihomoWorkDir(), { recursive: true }) + } + if (!existsSync(mihomoTestDir())) { + await mkdir(mihomoTestDir(), { recursive: true }) + } + await Promise.all([ copy('country.mmdb'), copy('geoip.metadb'),