// 主进程入口 import { app, dialog, BrowserWindow } from 'electron' import { join } from 'node:path' import { existsSync, writeFileSync, readFileSync } from 'node:fs' import { randomUUID } from 'node:crypto' import { hostname, platform } from 'node:os' import { paths } from './paths' import { getSettings, ensureDownloadDir } from './settings' import { ensureIconFile } from './icons' import { createMainWindow, broadcastToRenderer } from './window' import { createTray } from './tray' import { Discovery } from './discovery' import { ChatServer } from './chat-server' import { ChatClient } from './chat-client' import { FileServer } from './file-server' import { registerIpc, bindNetworkContext } from './ipc' import { listDevices, totalUnread, getUnread } from './db' import { setBadgeCount } from './notify' import { DEFAULT_PORTS, PROTOCOL_VERSION } from './protocol' import type { DeviceInfo } from './protocol' const gotLock = app.requestSingleInstanceLock() if (!gotLock) { app.quit() } else { ;(global as any).__quitting = false app.on('second-instance', () => { const w = BrowserWindow.getAllWindows()[0] if (w) { if (w.isMinimized()) w.restore(); w.show(); w.focus() } }) app.whenReady().then(main).catch((e) => { console.error('startup error', e) dialog.showErrorBox('启动失败', String(e?.stack || e)) app.quit() }) } app.on('window-all-closed', () => { if (!(global as any).__quitting) { // 不退出, 隐藏到托盘 } }) app.on('before-quit', () => { (global as any).__quitting = true }) app.on('will-quit', async () => { /* 网络资源由各模块 stop */ }) let stopFns: Array<() => Promise | void> = [] async function main() { ensureIconFile(paths.iconPng, 32) ensureDownloadDir() setBadgeCount(totalUnread()) const self = buildSelf() const discovery = new Discovery(self) const chatServer = new ChatServer(DEFAULT_PORTS.chat) const chatClient = new ChatClient(self) const fileServer = new FileServer(DEFAULT_PORTS.file, ensureDownloadDir()) const chatPort = await chatServer.start() const filePort = await fileServer.start() self.chatPort = chatPort self.filePort = filePort // 后台清理 24h+ 残留 .tmp (不阻塞启动) fileServer.cleanupStaleTmp().catch(() => {}) stopFns.push(() => chatServer.stop(), () => chatClient.stop(), () => discovery.stop(), () => fileServer.stop()) await discovery.start() bindNetworkContext({ self, discovery, chatServer, chatClient, fileServer }) // 已入库的设备: 启动后主动尝试连接 for (const d of listDevices()) { if (d.device_id === self.deviceId) continue chatClient.connectTo({ deviceId: d.device_id, name: d.name, hostname: d.hostname || '', platform: d.platform || '', appVersion: d.app_version || '', address: d.last_ip || '127.0.0.1', chatPort: DEFAULT_PORTS.chat, filePort: DEFAULT_PORTS.file, version: PROTOCOL_VERSION, ts: d.last_seen, } as DeviceInfo) } createMainWindow() createTray() registerIpc() setTimeout(pushInitialState, 400) } function buildSelf(): DeviceInfo { const idFile = join(paths.appData, 'device.id') let id: string if (existsSync(idFile)) id = readFileSync(idFile, 'utf8').trim() else { id = randomUUID(); writeFileSync(idFile, id, 'utf8') } const s = getSettings() return { deviceId: id, name: s.deviceName || hostname() || 'User', hostname: hostname() || '', platform: platform(), version: PROTOCOL_VERSION, appVersion: app.getVersion(), address: '0.0.0.0', chatPort: 0, filePort: 0, ts: Date.now(), } } function pushInitialState() { broadcastToRenderer('boot:ready', { settings: getSettings(), unread: getUnread(), }) }