import { app, BrowserWindow, session, ipcMain } from 'electron'; import { fileURLToPath } from 'url'; import path from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; import os from 'os'; const execAsync = promisify(exec); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged; /** * 设置内容安全策略 (Content Security Policy, CSP) * * 功能说明: * - 为 Electron 应用设置 CSP 规则,防止 XSS 攻击 * - 开发环境:允许 localhost 连接,便于开发调试 * - 生产环境:严格限制资源加载,提高安全性 * * CSP 规则说明: * - default-src 'self': 默认只允许同源资源 * - script-src: 控制脚本加载源 * - style-src: 控制样式表加载源 * - connect-src: 控制网络请求源(如 fetch, WebSocket) * - img-src: 控制图片加载源 * - font-src: 控制字体加载源 * - worker-src: 控制 Web Worker 加载源 */ function setContentSecurityPolicy() { session.defaultSession.webRequest.onHeadersReceived((details, callback) => { const csp = isDev ? "default-src 'self'; script-src 'self' 'unsafe-inline' http://localhost:*; style-src 'self' 'unsafe-inline'; connect-src 'self' http://localhost:* ws://localhost:*; img-src 'self' data: https: blob:; font-src 'self' data:; worker-src 'self' blob:;" : "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data: https:; font-src 'self' data:;"; const responseHeaders = Object.assign({}, details.responseHeaders); responseHeaders['Content-Security-Policy'] = [csp]; callback({ responseHeaders }); }); } /** * 创建主窗口 * * 功能说明: * - 创建 Electron 应用的主窗口 * - 配置窗口大小和 Web 安全设置 * - 根据环境加载不同的内容源 * * 窗口配置: * - 宽度: 1200px * - 高度: 800px * - preload: 预加载脚本路径(用于安全地暴露 Node.js API) * - nodeIntegration: false(禁用 Node.js 集成,提高安全性) * - contextIsolation: true(启用上下文隔离,防止 XSS 攻击) * * 加载逻辑: * - 开发环境: 加载 Vite 开发服务器 (http://localhost:5173) 并打开开发者工具 * - 生产环境: 加载构建后的静态文件 (dist/index.html) */ function createWindow() { const mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { preload: path.join(__dirname, 'preload.cjs'), nodeIntegration: false, contextIsolation: true } }); if (isDev) { mainWindow.loadURL('http://localhost:5173'); mainWindow.webContents.openDevTools(); } else { mainWindow.loadFile(path.join(__dirname, 'dist/index.html')); } } /** * 获取本机局域网 IP 地址 * * 功能说明: * - 遍历所有网络接口,查找第一个非内部的 IPv4 地址 * - 用于确定局域网网段,以便扫描同网段的设备 * * 查找逻辑: * - 遍历所有网络接口(如以太网、Wi-Fi 等) * - 过滤条件:IPv4 地址 && 非内部地址(非 127.0.0.1) * - 返回找到的第一个符合条件的 IP 地址 * * @returns {string} 本机局域网 IP 地址,如果未找到则返回默认值 '192.168.1.1' * * @example * // 如果本机 IP 是 192.168.2.35,则返回 '192.168.2.35' * const localIP = getLocalIP(); // '192.168.2.35' */ function getLocalIP() { const interfaces = os.networkInterfaces(); for (const name of Object.keys(interfaces)) { for (const iface of interfaces[name]) { if (iface.family === 'IPv4' && !iface.internal) { return iface.address; } } } return '192.168.1.1'; } /** * 扫描局域网内端口 5555 的 ADB 设备 * * 功能说明: * - 获取本机 IP 地址,确定局域网网段(如 192.168.2.x) * - 扫描该网段内所有可能的 IP 地址(1-254) * - 对每个 IP 尝试连接 ADB 端口 5555 * - 使用并行扫描提高效率,每批同时扫描 50 个 IP * * 扫描策略: * - 根据本机 IP 确定网段(如 192.168.2.35 → 网段 192.168.2.x) * - 扫描范围:192.168.2.1 到 192.168.2.254 * - 每批处理 50 个 IP,避免同时发起过多连接 * - 每个连接超时时间:300ms(快速失败,提高扫描速度) * - 忽略连接失败的错误(大部分 IP 不会有设备) * * 性能优化: * - 使用 Promise.all 并行执行,大幅提升扫描速度 * - 分批处理避免系统资源耗尽 * - 扫描完成后等待 500ms,确保所有连接已建立 * * @returns {Promise} 扫描完成(不返回结果,结果通过 getADBDevices 获取) * * @example * // 如果本机 IP 是 192.168.2.35 * // 会扫描 192.168.2.1:5555 到 192.168.2.254:5555 * await scanADBDevices(); */ async function scanADBDevices() { const localIP = getLocalIP(); const ipParts = localIP.split('.'); const baseIP = `${ipParts[0]}.${ipParts[1]}.${ipParts[2]}`; // 并行扫描,每批 50 个 IP const batchSize = 100; const promises = []; for (let i = 1; i <= 254; i += batchSize) { const batch = []; for (let j = i; j < i + batchSize && j <= 254; j++) { const ip = `${baseIP}.${j}`; batch.push( execAsync(`adb connect ${ip}:5555`, { timeout: 300 }) .catch(() => {}) // 忽略连接失败 ); } promises.push(Promise.all(batch)); } await Promise.all(promises); // 等待连接建立 await new Promise(resolve => setTimeout(resolve, 500)); } /** * 获取已连接的 ADB 设备列表 * * 功能说明: * - 执行 `adb devices` 命令获取当前所有已连接的设备 * - 解析命令输出,提取设备 ID 和状态 * - 只返回状态为 'device' 的设备(已授权且可用的设备) * * 命令输出格式: * ``` * List of devices attached * 192.168.2.5:5555 device * 3764756281ZZZZZ device * ``` * * 解析逻辑: * - 跳过第一行标题("List of devices attached") * - 每行按空白字符分割,提取设备 ID 和状态 * - 只保留状态为 'device' 的设备(排除 'offline', 'unauthorized' 等) * * 返回数据格式: * ```javascript * [ * { id: '192.168.2.5:5555', status: 'device' }, * { id: '3764756281ZZZZZ', status: 'device' } * ] * ``` * * @returns {Promise>} 设备列表数组 * - id: 设备标识符(IP:端口 或 USB 序列号) * - status: 设备状态(通常为 'device') * * @example * const devices = await getADBDevices(); * // [{ id: '192.168.2.5:5555', status: 'device' }] */ async function getADBDevices() { try { const { stdout } = await execAsync('adb devices'); const lines = stdout.split('\n').slice(1); const devices = []; for (const line of lines) { const parts = line.trim().split(/\s+/); if (parts.length >= 2 && parts[0] && parts[1] === 'device') { devices.push({ id: parts[0], status: parts[1] }); } } return devices; } catch (error) { console.error('获取设备列表失败:', error); return []; } } /** * IPC 处理程序:扫描 ADB 设备 * * 功能说明: * - 接收渲染进程的扫描请求 * - 先执行扫描操作(scanADBDevices) * - 然后获取设备列表(getADBDevices) * - 返回扫描后的设备列表 * * 调用方式(从渲染进程): * ```javascript * const devices = await window.electronAPI.scanADBDevices(); * ``` * * @returns {Promise>} 设备列表数组 */ ipcMain.handle('scan-adb-devices', async () => { try { await scanADBDevices(); return await getADBDevices(); } catch (error) { console.error('扫描设备失败:', error); return []; } }); /** * IPC 处理程序:获取 ADB 设备列表 * * 功能说明: * - 接收渲染进程的获取设备列表请求 * - 直接返回当前已连接的设备列表(不执行扫描) * - 用于刷新设备列表,查看当前连接状态 * * 调用方式(从渲染进程): * ```javascript * const devices = await window.electronAPI.getADBDevices(); * ``` * * @returns {Promise>} 设备列表数组 */ ipcMain.handle('get-adb-devices', async () => { return await getADBDevices(); }); /** * IPC 处理程序:连接 ADB 设备 * * 功能说明: * - 接收渲染进程的连接设备请求 * - 执行 `adb connect` 命令连接指定设备 * - 返回连接结果(成功或失败) * * 调用方式(从渲染进程): * ```javascript * const result = await window.electronAPI.connectADBDevice('192.168.2.5:5555'); * // { success: true } 或 { success: false, error: '错误信息' } * ``` * * @param {Electron.IpcMainInvokeEvent} event - IPC 事件对象 * @param {string} ipPort - 设备 IP 地址和端口(格式:'192.168.2.5:5555') * * @returns {Promise<{success: boolean, error?: string}>} 连接结果 * - success: true 表示连接成功,false 表示连接失败 * - error: 连接失败时的错误信息(仅在失败时存在) */ ipcMain.handle('connect-adb-device', async (event, ipPort) => { try { await execAsync(`adb connect ${ipPort}`); return { success: true }; } catch (error) { console.error('连接设备失败:', error); return { success: false, error: error.message }; } }); /** * 应用启动逻辑 * * 当 Electron 应用准备就绪时: * 1. 设置内容安全策略(CSP) * 2. 创建主窗口 * 3. 监听 'activate' 事件(macOS 特有,当应用被激活时) * - 如果没有窗口,则创建新窗口 */ app.whenReady().then(() => { setContentSecurityPolicy(); createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); /** * 应用关闭逻辑 * * 当所有窗口都关闭时: * - macOS: 不退出应用(保持应用在 Dock 中运行) * - Windows/Linux: 退出应用 * * 这是 Electron 的标准行为,符合各平台的应用生命周期管理 */ app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } });