const { app, BrowserWindow, ipcMain } = require('electron') const path = require('path') const os = require('os') const fs = require('fs') const unpackedRoot = app.isPackaged ? path.join(path.dirname(process.execPath), 'resources', 'app.asar.unpacked') : path.join(__dirname, '..') // static 不打包,放在打包根目录(与 exe 同级)作为沙盒目录 const sandboxRoot = app.isPackaged ? path.dirname(process.execPath) : path.join(__dirname, '..') const staticDir = path.join(sandboxRoot, 'static') if (!fs.existsSync(staticDir)) { fs.mkdirSync(staticDir, { recursive: true }) } const config = require(path.join(unpackedRoot, 'configs', 'config.js')) const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged // 打包后:userData 放在 exe 同目录 UserData,实现「开包即用」— 整包复制到任意电脑即可使用 // 开发时:使用临时目录,避免污染项目 if (process.platform === 'win32') { const userDataPath = app.isPackaged ? path.join(sandboxRoot, 'UserData') : path.join(os.tmpdir(), 'AndroidRemoteController') if (!fs.existsSync(userDataPath)) { fs.mkdirSync(userDataPath, { recursive: true }) } const cacheDir = path.join(userDataPath, 'Cache') const gpuCacheDir = path.join(userDataPath, 'GPUCache') if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true }) if (!fs.existsSync(gpuCacheDir)) fs.mkdirSync(gpuCacheDir, { recursive: true }) app.setPath('userData', userDataPath) app.commandLine.appendSwitch('disk-cache-dir', cacheDir) app.commandLine.appendSwitch('gpu-disk-cache-dir', gpuCacheDir) app.commandLine.appendSwitch('disable-gpu-shader-disk-cache') } // 保存主窗口引用,用于推送消息 let mainWindowInstance = null // 启动日志:写入 userData/startup.log,便于排查“无 UI”问题 const startupLogPath = path.join(app.getPath('userData'), 'startup.log') function startupLog (msg) { const line = `[${new Date().toISOString()}] ${msg}\n` try { fs.appendFileSync(startupLogPath, line) } catch (e) {} console.log(msg) } function createWindow() { startupLog(`createWindow: isPackaged=${app.isPackaged} isDev=${isDev} unpackedRoot=${unpackedRoot}`) const mainWindow = new BrowserWindow({ width: config.window.width, height: config.window.height, autoHideMenuBar: config.window.autoHideMenuBar, // 从配置文件读取 webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') } }) // 保存窗口引用 mainWindowInstance = mainWindow if (isDev) { const vitePort = config.vite?.port || 5173 const viteHost = config.vite?.host || 'localhost' startupLog(`Loading Vite dev server at http://${viteHost}:${vitePort}`) mainWindow.loadURL(`http://${viteHost}:${vitePort}`) // 根据配置文件决定是否打开调试侧边栏 if (config.devTools.enabled) { mainWindow.webContents.openDevTools() } } else { // 前端 dist(含 assets)通过 extraFiles 拷贝到 exe 同级 dist/,从沙盒根加载 const indexPath = path.join(sandboxRoot, 'dist', 'index.html') const indexExists = fs.existsSync(indexPath) startupLog(`Packaged load: index.html path=${indexPath} exists=${indexExists}`) if (!indexExists) { startupLog('ERROR: dist/index.html 不存在,请先 npm run build 再打包,并确认 extraFiles 含 dist') } mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame) => { startupLog(`did-fail-load: mainFrame=${isMainFrame} code=${errorCode} desc=${errorDescription} url=${validatedURL}`) }) mainWindow.webContents.on('did-finish-load', () => { startupLog(`did-finish-load: url=${mainWindow.webContents.getURL()}`) }) mainWindow.loadFile(indexPath) } startupLog(`启动日志文件: ${startupLogPath}`) } const { spawn } = require('child_process') // 存储运行中的进程 const runningProcesses = new Map() // Execute Node.js script ipcMain.handle('run-nodejs-script', async (event, scriptName, ...parameters) => { return new Promise((resolve, reject) => { const scriptPath = path.join(unpackedRoot, 'nodejs', `${scriptName}.js`) const processKey = `${scriptName}-${parameters.join('-')}` // 如果进程已运行,先停止它 if (runningProcesses.has(processKey)) { const oldProcess = runningProcesses.get(processKey) oldProcess.kill() runningProcesses.delete(processKey) } const nodeProcess = spawn('node', [scriptPath, ...parameters], { env: { ...process.env, STATIC_ROOT: staticDir }, cwd: unpackedRoot }) runningProcesses.set(processKey, nodeProcess) let stdout = '' let stderr = '' let resolved = false const finish = (result) => { if (resolved) return resolved = true resolve(result) } nodeProcess.stdout.on('data', (data) => { const dataStr = data.toString() stdout += dataStr const isLongRunning = scriptName.includes('screenshot') || scriptName.includes('adb/') try { const lines = dataStr.trim().split('\n') for (const line of lines) { if (line.trim().startsWith('{')) { const json = JSON.parse(line.trim()) if (json.success && isLongRunning) { finish({ success: true, stdout: line.trim(), stderr: stderr.trim(), exitCode: null }) return } } } } catch (e) {} }) nodeProcess.stderr.on('data', (data) => { stderr += data.toString() }) const waitForExit = scriptName === 'run-process' || scriptName === 'enable-wirless-connect' const timeoutId = waitForExit ? null : setTimeout(() => { if (!resolved) { finish({ success: false, stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 1, message: 'Script is running in background' }) } }, 5000) nodeProcess.on('close', (code) => { if (timeoutId) clearTimeout(timeoutId) runningProcesses.delete(processKey) const exitCode = (code !== null && code !== undefined) ? code : 1 finish({ success: exitCode === 0, stdout: stdout.trim(), stderr: stderr.trim(), exitCode }) }) nodeProcess.on('error', (error) => { clearTimeout(timeoutId) runningProcesses.delete(processKey) if (!resolved) { resolved = true reject(error) } }) }) }) /** 停止指定的 Node.js 脚本进程 */ ipcMain.handle('kill-nodejs-script', async (event, scriptName, ...parameters) => { const processKey = `${scriptName}-${parameters.join('-')}` if (runningProcesses.has(processKey)) { runningProcesses.get(processKey).kill() runningProcesses.delete(processKey) return { killed: true } } return { killed: false } }) /** 检查 scrcpy 是否仍在运行(读 pid 文件并用 process.kill(pid,0) 探测),用于用户直接关窗口时同步按钮状态 */ ipcMain.handle('check-scrcpy-running', async () => { const pidFile = path.join(staticDir, 'scrcpy-pid.json') if (!fs.existsSync(pidFile)) return { running: false } let pid try { pid = JSON.parse(fs.readFileSync(pidFile, 'utf-8')).pid } catch (_) { return { running: false } } try { process.kill(pid, 0) return { running: true } } catch (_) { return { running: false } } }) // Execute Python script ipcMain.handle('run-python-script', async (event, scriptName, ...parameters) => { return new Promise((resolve, reject) => { let pythonPath = 'python' if (config.pythonPath?.path) { const configPythonPath = path.join(config.pythonPath.path, 'python.exe') if (fs.existsSync(configPythonPath)) { pythonPath = configPythonPath } } const scriptPath = path.join(unpackedRoot, 'python', 'scripts', `${scriptName}.py`) if (!fs.existsSync(scriptPath)) { reject({ success: false, error: `Script file not found: ${scriptPath}`, stderr: `Script file not found: ${scriptPath}` }) return } const pythonProcess = spawn(pythonPath, [scriptPath, ...parameters]) let stdout = '' let stderr = '' pythonProcess.stdout.on('data', (data) => { stdout += data.toString() }) pythonProcess.stderr.on('data', (data) => { stderr += data.toString() }) pythonProcess.on('close', (code) => { resolve({ success: code === 0, stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code }) }) pythonProcess.on('error', (error) => { reject({ success: false, error: error.message, stderr: error.message }) }) }) }) // IPC 实时通信处理 // 处理前端请求(异步,返回 Promise) ipcMain.handle('ipc-request', async (event, channel, data) => { // 可以根据不同的 channel 处理不同的请求 // 例如:'run-nodejs', 'get-status' 等 if (channel === 'run-nodejs') { // 通过 IPC 调用 run-nodejs-script const { scriptName, ...parameters } = data return new Promise((resolve, reject) => { const scriptPath = path.join(unpackedRoot, 'nodejs', `${scriptName}.js`) const processKey = `${scriptName}-${parameters.join('-')}` if (runningProcesses.has(processKey)) { const oldProcess = runningProcesses.get(processKey) oldProcess.kill() runningProcesses.delete(processKey) } const nodeProcess = spawn('node', [scriptPath, ...Object.values(parameters)], { env: { ...process.env, STATIC_ROOT: staticDir }, cwd: unpackedRoot }) runningProcesses.set(processKey, nodeProcess) let stdout = '' let stderr = '' let resolved = false nodeProcess.stdout.on('data', (chunk) => { stdout += chunk.toString() try { const lines = chunk.toString().trim().split('\n') for (const line of lines) { if (line.trim().startsWith('{')) { const json = JSON.parse(line.trim()) if (json.success && !resolved) { resolved = true resolve({ success: true, stdout: line.trim(), stderr: stderr.trim(), exitCode: null }) } } } } catch (e) {} }) nodeProcess.stderr.on('data', (chunk) => { stderr += chunk.toString() }) nodeProcess.on('close', (code) => { runningProcesses.delete(processKey) if (!resolved) { resolve({ success: code === 0, stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code }) } }) nodeProcess.on('error', (error) => { runningProcesses.delete(processKey) if (!resolved) { reject(error) } }) setTimeout(() => { if (!resolved) { resolved = true resolve({ success: true, stdout: stdout.trim(), stderr: stderr.trim(), exitCode: null, message: 'Script is running in background' }) } }, 5000) }) } // 默认响应 return { success: true, channel, data, timestamp: Date.now() } }) // 监听前端发送的消息(不需要响应) ipcMain.on('ipc-message', (event, channel, data) => { // 处理前端发送的消息 console.log(`[IPC] Received message on channel "${channel}":`, data) }) // 推送消息到前端的辅助函数 function pushToFrontend(channel, data) { if (mainWindowInstance && !mainWindowInstance.isDestroyed()) { mainWindowInstance.webContents.send(channel, data) } } // 导出推送函数供其他模块使用 global.pushToFrontend = pushToFrontend app.whenReady().then(() => { startupLog('========== 本次启动 ==========') createWindow() app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow() } }) }) // 退出时结束 adb.exe,避免残留在后台 function killAdbOnExit() { try { if (process.platform === 'win32') { require('child_process').execSync('taskkill /IM adb.exe /F', { stdio: 'ignore', windowsHide: true }) } else { require('child_process').execSync('pkill -x adb || true', { stdio: 'ignore' }) } } catch (e) {} } app.on('before-quit', () => { killAdbOnExit() }) app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() } })