From 546d005c197cbef98f28ac114cd4feac6195d235 Mon Sep 17 00:00:00 2001 From: Misaka Date: Sat, 21 Mar 2026 10:33:33 +0800 Subject: [PATCH] refactor: split main process bootstrap flow --- src/main/bootstrap/main-window.ts | 56 ++++++++ src/main/bootstrap/process-guards.ts | 40 ++++++ src/main/bootstrap/runtime.ts | 84 +++++++++++ src/main/index.ts | 199 +++------------------------ 4 files changed, 196 insertions(+), 183 deletions(-) create mode 100644 src/main/bootstrap/main-window.ts create mode 100644 src/main/bootstrap/process-guards.ts create mode 100644 src/main/bootstrap/runtime.ts diff --git a/src/main/bootstrap/main-window.ts b/src/main/bootstrap/main-window.ts new file mode 100644 index 0000000..172b470 --- /dev/null +++ b/src/main/bootstrap/main-window.ts @@ -0,0 +1,56 @@ +import { BrowserWindow, app, shell } from 'electron' +import { join } from 'path' +import { is } from '@electron-toolkit/utils' +import icon from '../../../resources/icon.png?asset' +import { fileURLToPath } from 'url' +import { dirname } from 'path' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +export function createMainWindow(): BrowserWindow { + const mainWindow = new BrowserWindow({ + width: 1200, + height: 670, + show: false, + autoHideMenuBar: true, + ...(process.platform === 'linux' ? { icon } : {}), + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + contextIsolation: true, + sandbox: true, + nodeIntegration: false + } + }) + + mainWindow.on('ready-to-show', () => { + mainWindow.show() + }) + + mainWindow.webContents.setWindowOpenHandler((details) => { + shell.openExternal(details.url) + return { action: 'deny' } + }) + + if (is.dev && process.env['ELECTRON_RENDERER_URL']) { + mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) + } else { + mainWindow.loadFile(join(__dirname, '../renderer/index.html')) + } + + return mainWindow +} + +export function registerMainWindowLifecycle(): void { + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createMainWindow() + } + }) + + app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } + }) +} diff --git a/src/main/bootstrap/process-guards.ts b/src/main/bootstrap/process-guards.ts new file mode 100644 index 0000000..e917fff --- /dev/null +++ b/src/main/bootstrap/process-guards.ts @@ -0,0 +1,40 @@ +import { app } from 'electron' +import logger from '../services/logger/index' +import { logAudit } from '../services/logger/audit-logger' + +export function setupProcessGuards(): void { + process.on('uncaughtException', async (err) => { + logger.error('Uncaught exception', { error: err }) + await logAudit('SYSTEM_CRASH', 'system', { + username: 'system', + computerName: process.env.COMPUTERNAME || 'unknown', + resource: 'main-process', + status: 'failure', + metadata: { error: err.message, stack: err.stack } + }) + console.error('Uncaught exception:', err) + setTimeout(() => process.exit(1), 1000) + }) + + process.on('unhandledRejection', async (reason) => { + logger.error('Unhandled Rejection', { reason: String(reason) }) + await logAudit('SYSTEM_ERROR', 'system', { + username: 'system', + computerName: process.env.COMPUTERNAME || 'unknown', + resource: 'main-process', + status: 'failure', + metadata: { reason: String(reason) } + }) + console.error('Unhandled Rejection:', reason) + }) + + app.on('render-process-gone', (_, webContents, details) => { + logger.error('Render process gone', { details, webContentsId: webContents.id }) + console.error('Render process gone:', details) + }) + + app.on('child-process-gone', (_, details) => { + logger.error('Child process gone', { details }) + console.error('Child process gone:', details) + }) +} diff --git a/src/main/bootstrap/runtime.ts b/src/main/bootstrap/runtime.ts new file mode 100644 index 0000000..34b0a39 --- /dev/null +++ b/src/main/bootstrap/runtime.ts @@ -0,0 +1,84 @@ +import { app, dialog } from 'electron' +import { electronApp, optimizer } from '@electron-toolkit/utils' +import fs from 'fs' +import { join } from 'path' +import { ConfigManager } from '../services/config/config-manager' +import { UpdateService } from '../services/update/update-service' + +export function configurePlaywrightBrowsersPath(): string { + const browsersPath = join(app.getPath('userData'), 'ms-playwright') + process.env.PLAYWRIGHT_BROWSERS_PATH = browsersPath + return browsersPath +} + +export function setupElectronRuntime(): void { + electronApp.setAppUserModelId('com.electron') + + app.on('browser-window-created', (_, window) => { + optimizer.watchWindowShortcuts(window) + }) +} + +export function ensurePlaywrightRuntime(browsersPath: string): void { + try { + fs.mkdirSync(browsersPath, { recursive: true }) + } catch (error) { + console.error('Failed to create browsers directory:', error) + } + + const newChromiumPath = join(browsersPath, 'chromium-1208', 'chrome-win64', 'chrome.exe') + const oldChromiumPath = join(browsersPath, 'chromium-win32', 'chrome.exe') + const chromiumPath = fs.existsSync(newChromiumPath) ? newChromiumPath : oldChromiumPath + + if (fs.existsSync(chromiumPath)) { + return + } + + let foundRevision = false + try { + const entries = fs.readdirSync(browsersPath) + for (const entry of entries) { + if (entry.startsWith('chromium-') && !entry.includes('headless')) { + const revisionPath = join(browsersPath, entry, 'chrome-win64', 'chrome.exe') + if (fs.existsSync(revisionPath)) { + console.log('Found Chromium revision:', entry) + foundRevision = true + break + } + } + } + } catch { + // Ignore browser directory probing failures and fall through to the warning dialog. + } + + if (foundRevision) { + return + } + + dialog.showErrorBox( + '浏览器文件未找到', + `Playwright 浏览器文件不存在。\n\n` + + `期望路径:${newChromiumPath}\n` + + `或:${oldChromiumPath}\n\n` + + `当前目录内容:${fs.existsSync(browsersPath) ? fs.readdirSync(browsersPath).join(', ') : '目录不存在'}\n\n` + + `请运行以下命令安装浏览器:\n` + + `npx playwright install chromium` + ) + console.warn( + 'Playwright browser not found. Available:', + fs.existsSync(browsersPath) ? fs.readdirSync(browsersPath) : 'none' + ) +} + +export async function initializeMainProcessServices(): Promise { + try { + const configManager = ConfigManager.getInstance() + await configManager.initialize() + UpdateService.getInstance().initialize() + } catch (error) { + console.error('Failed to initialize ConfigManager:', error) + } + + const { registerIpcHandlers } = await import('../ipc') + registerIpcHandlers() +} diff --git a/src/main/index.ts b/src/main/index.ts index a8a8d85..ee08f1e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,191 +1,24 @@ -import { app, shell, BrowserWindow, ipcMain, dialog } from 'electron' -import { join } from 'path' -import { electronApp, optimizer, is } from '@electron-toolkit/utils' -import icon from '../../resources/icon.png?asset' -import { registerIpcHandlers } from './ipc' -import { ConfigManager } from './services/config/config-manager' -import logger from './services/logger/index' -import { logAudit } from './services/logger/audit-logger' -import { fileURLToPath } from 'url' -import { dirname } from 'path' -import fs from 'fs' -import { UpdateService } from './services/update/update-service' +import { app, ipcMain } from 'electron' +import { createMainWindow, registerMainWindowLifecycle } from './bootstrap/main-window' +import { + configurePlaywrightBrowsersPath, + ensurePlaywrightRuntime, + initializeMainProcessServices, + setupElectronRuntime +} from './bootstrap/runtime' +import { setupProcessGuards } from './bootstrap/process-guards' -// Set Playwright browsers path BEFORE any playwright import -process.env.PLAYWRIGHT_BROWSERS_PATH = join(app.getPath('userData'), 'ms-playwright') +const playwrightBrowsersPath = configurePlaywrightBrowsersPath() -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) +setupProcessGuards() +registerMainWindowLifecycle() -function createWindow(): void { - // Create the browser window. - const mainWindow = new BrowserWindow({ - width: 1200, - height: 670, - show: false, - autoHideMenuBar: true, - ...(process.platform === 'linux' ? { icon } : {}), - webPreferences: { - preload: join(__dirname, '../preload/index.js'), - sandbox: true - } - }) - - mainWindow.on('ready-to-show', () => { - mainWindow.show() - }) - - mainWindow.webContents.setWindowOpenHandler((details) => { - shell.openExternal(details.url) - return { action: 'deny' } - }) - - // HMR for renderer base on electron-vite cli. - // Load the remote URL for development or the local html file for production. - if (is.dev && process.env['ELECTRON_RENDERER_URL']) { - mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) - } else { - mainWindow.loadFile(join(__dirname, '../renderer/index.html')) - } -} - -// This method will be called when Electron has finished -// initialization and is ready to create browser windows. -// Some APIs can only be used after this event occurs. app.whenReady().then(async () => { - // Validate Playwright browser path - const browsersPath = process.env.PLAYWRIGHT_BROWSERS_PATH! + ensurePlaywrightRuntime(playwrightBrowsersPath) + await initializeMainProcessServices() + setupElectronRuntime() - // Create directory if it doesn't exist - try { - fs.mkdirSync(browsersPath, { recursive: true }) - } catch (error) { - console.error('Failed to create browsers directory:', error) - } - - // Check if chromium browser exists (supports both old and new Playwright directory structures) - // New format (v1.48+): chromium-1208/chrome-win64/chrome.exe - // Old format: chromium-win32/chrome.exe - const newChromiumPath = join(browsersPath, 'chromium-1208', 'chrome-win64', 'chrome.exe') - const oldChromiumPath = join(browsersPath, 'chromium-win32', 'chrome.exe') - const chromiumPath = fs.existsSync(newChromiumPath) ? newChromiumPath : oldChromiumPath - - if (!fs.existsSync(chromiumPath)) { - // Try to find any chromium revision - let foundRevision = false - try { - const entries = fs.readdirSync(browsersPath) - for (const entry of entries) { - if (entry.startsWith('chromium-') && !entry.includes('headless')) { - const revisionPath = join(browsersPath, entry, 'chrome-win64', 'chrome.exe') - if (fs.existsSync(revisionPath)) { - console.log('Found Chromium revision:', entry) - foundRevision = true - break - } - } - } - } catch { - // Ignore - } - - if (!foundRevision) { - dialog.showErrorBox( - '浏览器文件未找到', - `Playwright 浏览器文件不存在。\n\n` + - `期望路径:${newChromiumPath}\n` + - `或:${oldChromiumPath}\n\n` + - `当前目录内容:${fs.existsSync(browsersPath) ? fs.readdirSync(browsersPath).join(', ') : '目录不存在'}\n\n` + - `请运行以下命令安装浏览器:\n` + - `npx playwright install chromium` - ) - console.warn( - 'Playwright browser not found. Available:', - fs.existsSync(browsersPath) ? fs.readdirSync(browsersPath) : 'none' - ) - } - } - - // Initialize ConfigManager BEFORE registering IPC handlers - // This ensures config is loaded before any service tries to use it - try { - const configManager = ConfigManager.getInstance() - await configManager.initialize() - UpdateService.getInstance().initialize() - } catch (error) { - console.error('Failed to initialize ConfigManager:', error) - // Continue anyway - default config will be created - } - - // Set app user model id for windows - electronApp.setAppUserModelId('com.electron') - - // Default open or close DevTools by F12 in development - // and ignore CommandOrControl + R in production. - // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils - app.on('browser-window-created', (_, window) => { - optimizer.watchWindowShortcuts(window) - }) - - // Register IPC handlers (after ConfigManager is initialized) - registerIpcHandlers() - - // IPC test ipcMain.on('ping', () => console.log('pong')) - createWindow() - - app.on('activate', function () { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (BrowserWindow.getAllWindows().length === 0) createWindow() - }) + createMainWindow() }) - -// Quit when all windows are closed, except on macOS. There, it's common -// for applications and their menu bar to stay active until the user quits -// explicitly with Cmd + Q. -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit() - } -}) - -// Global exception handlers to prevent crashes without logging -process.on('uncaughtException', async (err) => { - logger.error('Uncaught exception', { error: err }) - await logAudit('SYSTEM_CRASH', 'system', { - username: 'system', - computerName: process.env.COMPUTERNAME || 'unknown', - resource: 'main-process', - status: 'failure', - metadata: { error: err.message, stack: err.stack } - }) - console.error('Uncaught exception:', err) - setTimeout(() => process.exit(1), 1000) -}) - -process.on('unhandledRejection', async (reason) => { - logger.error('Unhandled Rejection', { reason: String(reason) }) - await logAudit('SYSTEM_ERROR', 'system', { - username: 'system', - computerName: process.env.COMPUTERNAME || 'unknown', - resource: 'main-process', - status: 'failure', - metadata: { reason: String(reason) } - }) - console.error('Unhandled Rejection:', reason) -}) - -app.on('render-process-gone', (_, webContents, details) => { - logger.error('Render process gone', { details, webContentsId: webContents.id }) - console.error('Render process gone:', details) -}) - -app.on('child-process-gone', (_, details) => { - logger.error('Child process gone', { details }) - console.error('Child process gone:', details) -}) - -// In this file you can include the rest of your app's specific main process -// code. You can also put them in separate files and require them here.