main.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. const { app, BrowserWindow, ipcMain } = require('electron')
  2. const path = require('path')
  3. const os = require('os')
  4. const fs = require('fs')
  5. const unpackedRoot = app.isPackaged
  6. ? path.join(path.dirname(process.execPath), 'resources', 'app.asar.unpacked')
  7. : path.join(__dirname, '..')
  8. const config = require(path.join(unpackedRoot, 'configs', 'config.js'))
  9. const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged
  10. const staticDir = path.join(unpackedRoot, 'static')
  11. if (!fs.existsSync(staticDir)) {
  12. fs.mkdirSync(staticDir, { recursive: true })
  13. }
  14. // 修复缓存权限:userData 与缓存目录设到临时目录,避免安装目录只读导致 "Unable to move the cache (0x5)"
  15. if (process.platform === 'win32') {
  16. const userDataPath = path.join(os.tmpdir(), 'AndroidRemoteController')
  17. if (!fs.existsSync(userDataPath)) {
  18. fs.mkdirSync(userDataPath, { recursive: true })
  19. }
  20. const cacheDir = path.join(userDataPath, 'Cache')
  21. const gpuCacheDir = path.join(userDataPath, 'GPUCache')
  22. if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true })
  23. if (!fs.existsSync(gpuCacheDir)) fs.mkdirSync(gpuCacheDir, { recursive: true })
  24. app.setPath('userData', userDataPath)
  25. app.commandLine.appendSwitch('disk-cache-dir', cacheDir)
  26. app.commandLine.appendSwitch('gpu-disk-cache-dir', gpuCacheDir)
  27. app.commandLine.appendSwitch('disable-gpu-shader-disk-cache')
  28. }
  29. // 保存主窗口引用,用于推送消息
  30. let mainWindowInstance = null
  31. function createWindow() {
  32. const mainWindow = new BrowserWindow({
  33. width: config.window.width,
  34. height: config.window.height,
  35. autoHideMenuBar: config.window.autoHideMenuBar, // 从配置文件读取
  36. webPreferences: {
  37. nodeIntegration: false,
  38. contextIsolation: true,
  39. preload: path.join(__dirname, 'preload.js')
  40. }
  41. })
  42. // 保存窗口引用
  43. mainWindowInstance = mainWindow
  44. if (isDev) {
  45. const vitePort = config.vite?.port || 5173
  46. const viteHost = config.vite?.host || 'localhost'
  47. console.log(`Loading Vite dev server at http://${viteHost}:${vitePort}`)
  48. mainWindow.loadURL(`http://${viteHost}:${vitePort}`)
  49. // 根据配置文件决定是否打开调试侧边栏
  50. if (config.devTools.enabled) {
  51. mainWindow.webContents.openDevTools()
  52. }
  53. } else {
  54. mainWindow.loadFile(path.join(unpackedRoot, 'dist', 'index.html'))
  55. }
  56. }
  57. const { spawn } = require('child_process')
  58. // 存储运行中的进程
  59. const runningProcesses = new Map()
  60. // Execute Node.js script
  61. ipcMain.handle('run-nodejs-script', async (event, scriptName, ...parameters) => {
  62. return new Promise((resolve, reject) => {
  63. const scriptPath = path.join(unpackedRoot, 'nodejs', `${scriptName}.js`)
  64. const processKey = `${scriptName}-${parameters.join('-')}`
  65. // 如果进程已运行,先停止它
  66. if (runningProcesses.has(processKey)) {
  67. const oldProcess = runningProcesses.get(processKey)
  68. oldProcess.kill()
  69. runningProcesses.delete(processKey)
  70. }
  71. const nodeProcess = spawn('node', [scriptPath, ...parameters])
  72. runningProcesses.set(processKey, nodeProcess)
  73. let stdout = ''
  74. let stderr = ''
  75. let resolved = false
  76. const finish = (result) => {
  77. if (resolved) return
  78. resolved = true
  79. resolve(result)
  80. }
  81. nodeProcess.stdout.on('data', (data) => {
  82. const dataStr = data.toString()
  83. stdout += dataStr
  84. const isLongRunning = scriptName.includes('screenshot') || scriptName.includes('adb/')
  85. try {
  86. const lines = dataStr.trim().split('\n')
  87. for (const line of lines) {
  88. if (line.trim().startsWith('{')) {
  89. const json = JSON.parse(line.trim())
  90. if (json.success && isLongRunning) {
  91. finish({ success: true, stdout: line.trim(), stderr: stderr.trim(), exitCode: null })
  92. return
  93. }
  94. }
  95. }
  96. } catch (e) {}
  97. })
  98. nodeProcess.stderr.on('data', (data) => {
  99. stderr += data.toString()
  100. })
  101. const waitForExit = scriptName === 'run-process' || scriptName === 'enable-wirless-connect'
  102. const timeoutId = waitForExit ? null : setTimeout(() => {
  103. if (!resolved) {
  104. finish({
  105. success: false,
  106. stdout: stdout.trim(),
  107. stderr: stderr.trim(),
  108. exitCode: 1,
  109. message: 'Script is running in background'
  110. })
  111. }
  112. }, 5000)
  113. nodeProcess.on('close', (code) => {
  114. if (timeoutId) clearTimeout(timeoutId)
  115. runningProcesses.delete(processKey)
  116. const exitCode = (code !== null && code !== undefined) ? code : 1
  117. finish({
  118. success: exitCode === 0,
  119. stdout: stdout.trim(),
  120. stderr: stderr.trim(),
  121. exitCode
  122. })
  123. })
  124. nodeProcess.on('error', (error) => {
  125. clearTimeout(timeoutId)
  126. runningProcesses.delete(processKey)
  127. if (!resolved) {
  128. resolved = true
  129. reject(error)
  130. }
  131. })
  132. })
  133. })
  134. /** 停止指定的 Node.js 脚本进程 */
  135. ipcMain.handle('kill-nodejs-script', async (event, scriptName, ...parameters) => {
  136. const processKey = `${scriptName}-${parameters.join('-')}`
  137. if (runningProcesses.has(processKey)) {
  138. runningProcesses.get(processKey).kill()
  139. runningProcesses.delete(processKey)
  140. return { killed: true }
  141. }
  142. return { killed: false }
  143. })
  144. /** 检查 scrcpy 是否仍在运行(读 pid 文件并用 process.kill(pid,0) 探测),用于用户直接关窗口时同步按钮状态 */
  145. ipcMain.handle('check-scrcpy-running', async () => {
  146. const pidFile = path.join(unpackedRoot, 'static', 'scrcpy-pid.json')
  147. if (!fs.existsSync(pidFile)) return { running: false }
  148. let pid
  149. try {
  150. pid = JSON.parse(fs.readFileSync(pidFile, 'utf-8')).pid
  151. } catch (_) {
  152. return { running: false }
  153. }
  154. try {
  155. process.kill(pid, 0)
  156. return { running: true }
  157. } catch (_) {
  158. return { running: false }
  159. }
  160. })
  161. // Execute Python script
  162. ipcMain.handle('run-python-script', async (event, scriptName, ...parameters) => {
  163. return new Promise((resolve, reject) => {
  164. let pythonPath = 'python'
  165. if (config.pythonPath?.path) {
  166. const configPythonPath = path.join(config.pythonPath.path, 'python.exe')
  167. if (fs.existsSync(configPythonPath)) {
  168. pythonPath = configPythonPath
  169. }
  170. }
  171. const scriptPath = path.join(unpackedRoot, 'python', 'scripts', `${scriptName}.py`)
  172. if (!fs.existsSync(scriptPath)) {
  173. reject({
  174. success: false,
  175. error: `Script file not found: ${scriptPath}`,
  176. stderr: `Script file not found: ${scriptPath}`
  177. })
  178. return
  179. }
  180. const pythonProcess = spawn(pythonPath, [scriptPath, ...parameters])
  181. let stdout = ''
  182. let stderr = ''
  183. pythonProcess.stdout.on('data', (data) => {
  184. stdout += data.toString()
  185. })
  186. pythonProcess.stderr.on('data', (data) => {
  187. stderr += data.toString()
  188. })
  189. pythonProcess.on('close', (code) => {
  190. resolve({
  191. success: code === 0,
  192. stdout: stdout.trim(),
  193. stderr: stderr.trim(),
  194. exitCode: code
  195. })
  196. })
  197. pythonProcess.on('error', (error) => {
  198. reject({
  199. success: false,
  200. error: error.message,
  201. stderr: error.message
  202. })
  203. })
  204. })
  205. })
  206. // IPC 实时通信处理
  207. // 处理前端请求(异步,返回 Promise)
  208. ipcMain.handle('ipc-request', async (event, channel, data) => {
  209. // 可以根据不同的 channel 处理不同的请求
  210. // 例如:'run-nodejs', 'get-status' 等
  211. if (channel === 'run-nodejs') {
  212. // 通过 IPC 调用 run-nodejs-script
  213. const { scriptName, ...parameters } = data
  214. return new Promise((resolve, reject) => {
  215. const scriptPath = path.join(unpackedRoot, 'nodejs', `${scriptName}.js`)
  216. const processKey = `${scriptName}-${parameters.join('-')}`
  217. if (runningProcesses.has(processKey)) {
  218. const oldProcess = runningProcesses.get(processKey)
  219. oldProcess.kill()
  220. runningProcesses.delete(processKey)
  221. }
  222. const nodeProcess = spawn('node', [scriptPath, ...Object.values(parameters)])
  223. runningProcesses.set(processKey, nodeProcess)
  224. let stdout = ''
  225. let stderr = ''
  226. let resolved = false
  227. nodeProcess.stdout.on('data', (chunk) => {
  228. stdout += chunk.toString()
  229. try {
  230. const lines = chunk.toString().trim().split('\n')
  231. for (const line of lines) {
  232. if (line.trim().startsWith('{')) {
  233. const json = JSON.parse(line.trim())
  234. if (json.success && !resolved) {
  235. resolved = true
  236. resolve({ success: true, stdout: line.trim(), stderr: stderr.trim(), exitCode: null })
  237. }
  238. }
  239. }
  240. } catch (e) {}
  241. })
  242. nodeProcess.stderr.on('data', (chunk) => {
  243. stderr += chunk.toString()
  244. })
  245. nodeProcess.on('close', (code) => {
  246. runningProcesses.delete(processKey)
  247. if (!resolved) {
  248. resolve({ success: code === 0, stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code })
  249. }
  250. })
  251. nodeProcess.on('error', (error) => {
  252. runningProcesses.delete(processKey)
  253. if (!resolved) {
  254. reject(error)
  255. }
  256. })
  257. setTimeout(() => {
  258. if (!resolved) {
  259. resolved = true
  260. resolve({ success: true, stdout: stdout.trim(), stderr: stderr.trim(), exitCode: null, message: 'Script is running in background' })
  261. }
  262. }, 5000)
  263. })
  264. }
  265. // 默认响应
  266. return { success: true, channel, data, timestamp: Date.now() }
  267. })
  268. // 监听前端发送的消息(不需要响应)
  269. ipcMain.on('ipc-message', (event, channel, data) => {
  270. // 处理前端发送的消息
  271. console.log(`[IPC] Received message on channel "${channel}":`, data)
  272. })
  273. // 推送消息到前端的辅助函数
  274. function pushToFrontend(channel, data) {
  275. if (mainWindowInstance && !mainWindowInstance.isDestroyed()) {
  276. mainWindowInstance.webContents.send(channel, data)
  277. }
  278. }
  279. // 导出推送函数供其他模块使用
  280. global.pushToFrontend = pushToFrontend
  281. app.whenReady().then(() => {
  282. createWindow()
  283. app.on('activate', () => {
  284. if (BrowserWindow.getAllWindows().length === 0) {
  285. createWindow()
  286. }
  287. })
  288. })
  289. app.on('window-all-closed', () => {
  290. if (process.platform !== 'darwin') {
  291. app.quit()
  292. }
  293. })