|
|
@@ -1,12 +1,33 @@
|
|
|
-import { app, BrowserWindow, session } from 'electron';
|
|
|
+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;
|
|
|
|
|
|
-// Set Content Security Policy
|
|
|
+/**
|
|
|
+ * 设置内容安全策略 (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
|
|
|
@@ -20,6 +41,25 @@ function setContentSecurityPolicy() {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 创建主窗口
|
|
|
+ *
|
|
|
+ * 功能说明:
|
|
|
+ * - 创建 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,
|
|
|
@@ -39,6 +79,233 @@ function createWindow() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 获取本机局域网 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();
|
|
|
@@ -50,6 +317,15 @@ app.whenReady().then(() => {
|
|
|
});
|
|
|
});
|
|
|
|
|
|
+/**
|
|
|
+ * 应用关闭逻辑
|
|
|
+ *
|
|
|
+ * 当所有窗口都关闭时:
|
|
|
+ * - macOS: 不退出应用(保持应用在 Dock 中运行)
|
|
|
+ * - Windows/Linux: 退出应用
|
|
|
+ *
|
|
|
+ * 这是 Electron 的标准行为,符合各平台的应用生命周期管理
|
|
|
+ */
|
|
|
app.on('window-all-closed', () => {
|
|
|
if (process.platform !== 'darwin') {
|
|
|
app.quit();
|