main.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. import { app, BrowserWindow, session, ipcMain } from 'electron';
  2. import { fileURLToPath } from 'url';
  3. import path from 'path';
  4. import { exec } from 'child_process';
  5. import { promisify } from 'util';
  6. import os from 'os';
  7. const execAsync = promisify(exec);
  8. const __filename = fileURLToPath(import.meta.url);
  9. const __dirname = path.dirname(__filename);
  10. const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
  11. /**
  12. * 设置内容安全策略 (Content Security Policy, CSP)
  13. *
  14. * 功能说明:
  15. * - 为 Electron 应用设置 CSP 规则,防止 XSS 攻击
  16. * - 开发环境:允许 localhost 连接,便于开发调试
  17. * - 生产环境:严格限制资源加载,提高安全性
  18. *
  19. * CSP 规则说明:
  20. * - default-src 'self': 默认只允许同源资源
  21. * - script-src: 控制脚本加载源
  22. * - style-src: 控制样式表加载源
  23. * - connect-src: 控制网络请求源(如 fetch, WebSocket)
  24. * - img-src: 控制图片加载源
  25. * - font-src: 控制字体加载源
  26. * - worker-src: 控制 Web Worker 加载源
  27. */
  28. function setContentSecurityPolicy() {
  29. session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
  30. const csp = isDev
  31. ? "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:;"
  32. : "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data: https:; font-src 'self' data:;";
  33. const responseHeaders = Object.assign({}, details.responseHeaders);
  34. responseHeaders['Content-Security-Policy'] = [csp];
  35. callback({ responseHeaders });
  36. });
  37. }
  38. /**
  39. * 创建主窗口
  40. *
  41. * 功能说明:
  42. * - 创建 Electron 应用的主窗口
  43. * - 配置窗口大小和 Web 安全设置
  44. * - 根据环境加载不同的内容源
  45. *
  46. * 窗口配置:
  47. * - 宽度: 1200px
  48. * - 高度: 800px
  49. * - preload: 预加载脚本路径(用于安全地暴露 Node.js API)
  50. * - nodeIntegration: false(禁用 Node.js 集成,提高安全性)
  51. * - contextIsolation: true(启用上下文隔离,防止 XSS 攻击)
  52. *
  53. * 加载逻辑:
  54. * - 开发环境: 加载 Vite 开发服务器 (http://localhost:5173) 并打开开发者工具
  55. * - 生产环境: 加载构建后的静态文件 (dist/index.html)
  56. */
  57. function createWindow() {
  58. const mainWindow = new BrowserWindow({
  59. width: 1200,
  60. height: 800,
  61. webPreferences: {
  62. preload: path.join(__dirname, 'preload.cjs'),
  63. nodeIntegration: false,
  64. contextIsolation: true
  65. }
  66. });
  67. if (isDev) {
  68. mainWindow.loadURL('http://localhost:5173');
  69. mainWindow.webContents.openDevTools();
  70. } else {
  71. mainWindow.loadFile(path.join(__dirname, 'dist/index.html'));
  72. }
  73. }
  74. /**
  75. * 获取本机局域网 IP 地址
  76. *
  77. * 功能说明:
  78. * - 遍历所有网络接口,查找第一个非内部的 IPv4 地址
  79. * - 用于确定局域网网段,以便扫描同网段的设备
  80. *
  81. * 查找逻辑:
  82. * - 遍历所有网络接口(如以太网、Wi-Fi 等)
  83. * - 过滤条件:IPv4 地址 && 非内部地址(非 127.0.0.1)
  84. * - 返回找到的第一个符合条件的 IP 地址
  85. *
  86. * @returns {string} 本机局域网 IP 地址,如果未找到则返回默认值 '192.168.1.1'
  87. *
  88. * @example
  89. * // 如果本机 IP 是 192.168.2.35,则返回 '192.168.2.35'
  90. * const localIP = getLocalIP(); // '192.168.2.35'
  91. */
  92. function getLocalIP() {
  93. const interfaces = os.networkInterfaces();
  94. for (const name of Object.keys(interfaces)) {
  95. for (const iface of interfaces[name]) {
  96. if (iface.family === 'IPv4' && !iface.internal) {
  97. return iface.address;
  98. }
  99. }
  100. }
  101. return '192.168.1.1';
  102. }
  103. /**
  104. * 扫描局域网内端口 5555 的 ADB 设备
  105. *
  106. * 功能说明:
  107. * - 获取本机 IP 地址,确定局域网网段(如 192.168.2.x)
  108. * - 扫描该网段内所有可能的 IP 地址(1-254)
  109. * - 对每个 IP 尝试连接 ADB 端口 5555
  110. * - 使用并行扫描提高效率,每批同时扫描 50 个 IP
  111. *
  112. * 扫描策略:
  113. * - 根据本机 IP 确定网段(如 192.168.2.35 → 网段 192.168.2.x)
  114. * - 扫描范围:192.168.2.1 到 192.168.2.254
  115. * - 每批处理 50 个 IP,避免同时发起过多连接
  116. * - 每个连接超时时间:300ms(快速失败,提高扫描速度)
  117. * - 忽略连接失败的错误(大部分 IP 不会有设备)
  118. *
  119. * 性能优化:
  120. * - 使用 Promise.all 并行执行,大幅提升扫描速度
  121. * - 分批处理避免系统资源耗尽
  122. * - 扫描完成后等待 500ms,确保所有连接已建立
  123. *
  124. * @returns {Promise<void>} 扫描完成(不返回结果,结果通过 getADBDevices 获取)
  125. *
  126. * @example
  127. * // 如果本机 IP 是 192.168.2.35
  128. * // 会扫描 192.168.2.1:5555 到 192.168.2.254:5555
  129. * await scanADBDevices();
  130. */
  131. async function scanADBDevices() {
  132. const localIP = getLocalIP();
  133. const ipParts = localIP.split('.');
  134. const baseIP = `${ipParts[0]}.${ipParts[1]}.${ipParts[2]}`;
  135. // 并行扫描,每批 50 个 IP
  136. const batchSize = 100;
  137. const promises = [];
  138. for (let i = 1; i <= 254; i += batchSize) {
  139. const batch = [];
  140. for (let j = i; j < i + batchSize && j <= 254; j++) {
  141. const ip = `${baseIP}.${j}`;
  142. batch.push(
  143. execAsync(`adb connect ${ip}:5555`, { timeout: 300 })
  144. .catch(() => {}) // 忽略连接失败
  145. );
  146. }
  147. promises.push(Promise.all(batch));
  148. }
  149. await Promise.all(promises);
  150. // 等待连接建立
  151. await new Promise(resolve => setTimeout(resolve, 500));
  152. }
  153. /**
  154. * 获取已连接的 ADB 设备列表
  155. *
  156. * 功能说明:
  157. * - 执行 `adb devices` 命令获取当前所有已连接的设备
  158. * - 解析命令输出,提取设备 ID 和状态
  159. * - 只返回状态为 'device' 的设备(已授权且可用的设备)
  160. *
  161. * 命令输出格式:
  162. * ```
  163. * List of devices attached
  164. * 192.168.2.5:5555 device
  165. * 3764756281ZZZZZ device
  166. * ```
  167. *
  168. * 解析逻辑:
  169. * - 跳过第一行标题("List of devices attached")
  170. * - 每行按空白字符分割,提取设备 ID 和状态
  171. * - 只保留状态为 'device' 的设备(排除 'offline', 'unauthorized' 等)
  172. *
  173. * 返回数据格式:
  174. * ```javascript
  175. * [
  176. * { id: '192.168.2.5:5555', status: 'device' },
  177. * { id: '3764756281ZZZZZ', status: 'device' }
  178. * ]
  179. * ```
  180. *
  181. * @returns {Promise<Array<{id: string, status: string}>>} 设备列表数组
  182. * - id: 设备标识符(IP:端口 或 USB 序列号)
  183. * - status: 设备状态(通常为 'device')
  184. *
  185. * @example
  186. * const devices = await getADBDevices();
  187. * // [{ id: '192.168.2.5:5555', status: 'device' }]
  188. */
  189. async function getADBDevices() {
  190. try {
  191. const { stdout } = await execAsync('adb devices');
  192. const lines = stdout.split('\n').slice(1);
  193. const devices = [];
  194. for (const line of lines) {
  195. const parts = line.trim().split(/\s+/);
  196. if (parts.length >= 2 && parts[0] && parts[1] === 'device') {
  197. devices.push({
  198. id: parts[0],
  199. status: parts[1]
  200. });
  201. }
  202. }
  203. return devices;
  204. } catch (error) {
  205. console.error('获取设备列表失败:', error);
  206. return [];
  207. }
  208. }
  209. /**
  210. * IPC 处理程序:扫描 ADB 设备
  211. *
  212. * 功能说明:
  213. * - 接收渲染进程的扫描请求
  214. * - 先执行扫描操作(scanADBDevices)
  215. * - 然后获取设备列表(getADBDevices)
  216. * - 返回扫描后的设备列表
  217. *
  218. * 调用方式(从渲染进程):
  219. * ```javascript
  220. * const devices = await window.electronAPI.scanADBDevices();
  221. * ```
  222. *
  223. * @returns {Promise<Array<{id: string, status: string}>>} 设备列表数组
  224. */
  225. ipcMain.handle('scan-adb-devices', async () => {
  226. try {
  227. await scanADBDevices();
  228. return await getADBDevices();
  229. } catch (error) {
  230. console.error('扫描设备失败:', error);
  231. return [];
  232. }
  233. });
  234. /**
  235. * IPC 处理程序:获取 ADB 设备列表
  236. *
  237. * 功能说明:
  238. * - 接收渲染进程的获取设备列表请求
  239. * - 直接返回当前已连接的设备列表(不执行扫描)
  240. * - 用于刷新设备列表,查看当前连接状态
  241. *
  242. * 调用方式(从渲染进程):
  243. * ```javascript
  244. * const devices = await window.electronAPI.getADBDevices();
  245. * ```
  246. *
  247. * @returns {Promise<Array<{id: string, status: string}>>} 设备列表数组
  248. */
  249. ipcMain.handle('get-adb-devices', async () => {
  250. return await getADBDevices();
  251. });
  252. /**
  253. * IPC 处理程序:连接 ADB 设备
  254. *
  255. * 功能说明:
  256. * - 接收渲染进程的连接设备请求
  257. * - 执行 `adb connect` 命令连接指定设备
  258. * - 返回连接结果(成功或失败)
  259. *
  260. * 调用方式(从渲染进程):
  261. * ```javascript
  262. * const result = await window.electronAPI.connectADBDevice('192.168.2.5:5555');
  263. * // { success: true } 或 { success: false, error: '错误信息' }
  264. * ```
  265. *
  266. * @param {Electron.IpcMainInvokeEvent} event - IPC 事件对象
  267. * @param {string} ipPort - 设备 IP 地址和端口(格式:'192.168.2.5:5555')
  268. *
  269. * @returns {Promise<{success: boolean, error?: string}>} 连接结果
  270. * - success: true 表示连接成功,false 表示连接失败
  271. * - error: 连接失败时的错误信息(仅在失败时存在)
  272. */
  273. ipcMain.handle('connect-adb-device', async (event, ipPort) => {
  274. try {
  275. await execAsync(`adb connect ${ipPort}`);
  276. return { success: true };
  277. } catch (error) {
  278. console.error('连接设备失败:', error);
  279. return { success: false, error: error.message };
  280. }
  281. });
  282. /**
  283. * 应用启动逻辑
  284. *
  285. * 当 Electron 应用准备就绪时:
  286. * 1. 设置内容安全策略(CSP)
  287. * 2. 创建主窗口
  288. * 3. 监听 'activate' 事件(macOS 特有,当应用被激活时)
  289. * - 如果没有窗口,则创建新窗口
  290. */
  291. app.whenReady().then(() => {
  292. setContentSecurityPolicy();
  293. createWindow();
  294. app.on('activate', () => {
  295. if (BrowserWindow.getAllWindows().length === 0) {
  296. createWindow();
  297. }
  298. });
  299. });
  300. /**
  301. * 应用关闭逻辑
  302. *
  303. * 当所有窗口都关闭时:
  304. * - macOS: 不退出应用(保持应用在 Dock 中运行)
  305. * - Windows/Linux: 退出应用
  306. *
  307. * 这是 Electron 的标准行为,符合各平台的应用生命周期管理
  308. */
  309. app.on('window-all-closed', () => {
  310. if (process.platform !== 'darwin') {
  311. app.quit();
  312. }
  313. });