| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- 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<void>} 扫描完成(不返回结果,结果通过 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<Array<{id: string, status: string}>>} 设备列表数组
- * - 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<Array<{id: string, status: string}>>} 设备列表数组
- */
- 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<Array<{id: string, status: string}>>} 设备列表数组
- */
- 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();
- }
- });
|