|
|
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'url';
|
|
|
import path from 'path';
|
|
|
import { exec } from 'child_process';
|
|
|
import { promisify } from 'util';
|
|
|
+import { existsSync, readFileSync } from 'fs';
|
|
|
|
|
|
const execAsync = promisify(exec);
|
|
|
|
|
|
@@ -10,6 +11,67 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
const __dirname = path.dirname(__filename);
|
|
|
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
|
|
|
|
|
|
+// 读取配置文件
|
|
|
+function loadConfig() {
|
|
|
+ try {
|
|
|
+ const configPath = path.join(__dirname, 'config.js');
|
|
|
+ if (existsSync(configPath)) {
|
|
|
+ const configContent = readFileSync(configPath, 'utf-8');
|
|
|
+ // 解析 JSON(移除可能的注释和尾随逗号)
|
|
|
+ const jsonContent = configContent.replace(/\/\/.*$/gm, '').replace(/,(\s*[}\]])/g, '$1');
|
|
|
+ return JSON.parse(jsonContent);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('Failed to load config.js:', error.message);
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+// 查找 ADB 可执行文件路径
|
|
|
+function getAdbPath() {
|
|
|
+ // 首先尝试从配置文件读取
|
|
|
+ const config = loadConfig();
|
|
|
+ if (config && config['adb-path']) {
|
|
|
+ const configAdbPath = path.join(config['adb-path'], 'adb.exe');
|
|
|
+ if (existsSync(configAdbPath)) {
|
|
|
+ console.log('Using ADB path from config.js:', configAdbPath);
|
|
|
+ return configAdbPath;
|
|
|
+ }
|
|
|
+ // 如果配置的路径不存在,尝试直接使用配置的路径(可能已经是完整路径)
|
|
|
+ if (existsSync(config['adb-path'])) {
|
|
|
+ console.log('Using ADB path from config.js:', config['adb-path']);
|
|
|
+ return config['adb-path'];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果配置文件没有或路径不存在,使用常见 ADB 安装位置(按优先级排序)
|
|
|
+ const possiblePaths = [
|
|
|
+ path.join(process.env.LOCALAPPDATA || '', 'Android', 'Sdk', 'platform-tools', 'adb.exe'),
|
|
|
+ path.join(process.env.USERPROFILE || '', 'AppData', 'Local', 'Android', 'Sdk', 'platform-tools', 'adb.exe'),
|
|
|
+ path.join(process.env.ProgramFiles || '', 'Android', 'android-sdk', 'platform-tools', 'adb.exe'),
|
|
|
+ path.join(process.env['ProgramFiles(x86)'] || '', 'Android', 'android-sdk', 'platform-tools', 'adb.exe'),
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 检查常见位置
|
|
|
+ for (const adbPath of possiblePaths) {
|
|
|
+ if (existsSync(adbPath)) {
|
|
|
+ return adbPath;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果都找不到,尝试使用 PATH 中的 adb
|
|
|
+ return 'adb';
|
|
|
+}
|
|
|
+
|
|
|
+// 缓存 ADB 路径
|
|
|
+let adbPathCache = null;
|
|
|
+function getCachedAdbPath() {
|
|
|
+ if (!adbPathCache) {
|
|
|
+ adbPathCache = getAdbPath();
|
|
|
+ }
|
|
|
+ return adbPathCache;
|
|
|
+}
|
|
|
+
|
|
|
// 设置内容安全策略(CSP),防止 XSS 攻击
|
|
|
function setContentSecurityPolicy() {
|
|
|
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
|
|
@@ -24,9 +86,12 @@ function setContentSecurityPolicy() {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+// 全局主窗口引用,用于发送实时事件
|
|
|
+let mainWindow = null;
|
|
|
+
|
|
|
// 创建主窗口,根据环境加载不同内容源
|
|
|
function createWindow() {
|
|
|
- const mainWindow = new BrowserWindow({
|
|
|
+ mainWindow = new BrowserWindow({
|
|
|
width: 1200,
|
|
|
height: 800,
|
|
|
webPreferences: {
|
|
|
@@ -47,7 +112,8 @@ function createWindow() {
|
|
|
// 获取已连接的 ADB 设备列表
|
|
|
async function getADBDevices() {
|
|
|
try {
|
|
|
- const { stdout } = await execAsync('adb devices');
|
|
|
+ const adbPath = getCachedAdbPath();
|
|
|
+ const { stdout } = await execAsync(`${adbPath} devices`);
|
|
|
const lines = stdout.split('\n').slice(1);
|
|
|
const devices = [];
|
|
|
|
|
|
@@ -67,15 +133,113 @@ async function getADBDevices() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// 网络扫描:从 192.168.0.1 开始扫描网段,尝试连接设备(实时推送结果)
|
|
|
+async function scanNetworkDevices(event) {
|
|
|
+ const adbPath = getCachedAdbPath();
|
|
|
+ const baseIP = '192.168.0';
|
|
|
+ const port = 5555;
|
|
|
+ const maxConcurrent = 20; // 限制并发数,避免过载
|
|
|
+ const connectTimeout = 1500; // 连接超时时间(毫秒)
|
|
|
+
|
|
|
+ // 生成 IP 地址列表
|
|
|
+ const ipList = [];
|
|
|
+ for (let i = 1; i <= 255; i++) {
|
|
|
+ ipList.push(`${baseIP}.${i}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const foundDevices = new Set(); // 使用 Set 避免重复
|
|
|
+
|
|
|
+ // 分批并发扫描
|
|
|
+ for (let i = 0; i < ipList.length; i += maxConcurrent) {
|
|
|
+ const batch = ipList.slice(i, i + maxConcurrent);
|
|
|
+ const promises = batch.map(async (ip) => {
|
|
|
+ const ipPort = `${ip}:${port}`;
|
|
|
+ try {
|
|
|
+ // 尝试连接设备(超时时间短,避免等待太久)
|
|
|
+ await execAsync(`${adbPath} connect ${ipPort}`, {
|
|
|
+ timeout: connectTimeout,
|
|
|
+ maxBuffer: 1024 * 1024
|
|
|
+ });
|
|
|
+
|
|
|
+ // 连接后稍等片刻,检查设备是否真的连接成功
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
+
|
|
|
+ // 检查设备是否在列表中
|
|
|
+ try {
|
|
|
+ const { stdout } = await execAsync(`${adbPath} devices`, {
|
|
|
+ timeout: 2000,
|
|
|
+ maxBuffer: 1024 * 1024
|
|
|
+ });
|
|
|
+
|
|
|
+ const lines = stdout.split('\n').slice(1);
|
|
|
+ for (const line of lines) {
|
|
|
+ const parts = line.trim().split(/\s+/);
|
|
|
+ if (parts.length >= 2 && parts[0] === ipPort && parts[1] === 'device') {
|
|
|
+ // 发现设备,实时推送
|
|
|
+ if (!foundDevices.has(ipPort)) {
|
|
|
+ foundDevices.add(ipPort);
|
|
|
+ const device = {
|
|
|
+ id: ipPort,
|
|
|
+ status: 'device'
|
|
|
+ };
|
|
|
+ // 实时发送发现的设备
|
|
|
+ if (event && event.sender) {
|
|
|
+ event.sender.send('device-found', device);
|
|
|
+ } else if (mainWindow) {
|
|
|
+ mainWindow.webContents.send('device-found', device);
|
|
|
+ }
|
|
|
+ console.log('发现设备:', ipPort);
|
|
|
+ }
|
|
|
+ return ipPort;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (checkError) {
|
|
|
+ // 检查失败,忽略
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ // 连接失败是正常的,忽略错误
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ });
|
|
|
+
|
|
|
+ await Promise.all(promises);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 返回所有发现的设备
|
|
|
+ return Array.from(foundDevices).map(ipPort => ({
|
|
|
+ id: ipPort,
|
|
|
+ status: 'device'
|
|
|
+ }));
|
|
|
+}
|
|
|
+
|
|
|
+// IPC 处理程序:获取 ADB 路径配置
|
|
|
+ipcMain.handle('get-adb-path-config', async () => {
|
|
|
+ const config = loadConfig();
|
|
|
+ return config ? config['adb-path'] : null;
|
|
|
+});
|
|
|
+
|
|
|
// IPC 处理程序:获取 ADB 设备列表
|
|
|
ipcMain.handle('get-adb-devices', async () => {
|
|
|
return await getADBDevices();
|
|
|
});
|
|
|
|
|
|
+// IPC 处理程序:扫描网络设备(支持实时推送)
|
|
|
+ipcMain.handle('scan-adb-devices', async (event) => {
|
|
|
+ try {
|
|
|
+ const devices = await scanNetworkDevices(event);
|
|
|
+ return devices;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('网络扫描失败:', error);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
// IPC 处理程序:连接 ADB 设备
|
|
|
ipcMain.handle('connect-adb-device', async (event, ipPort) => {
|
|
|
try {
|
|
|
- await execAsync(`adb connect ${ipPort}`);
|
|
|
+ const adbPath = getCachedAdbPath();
|
|
|
+ await execAsync(`${adbPath} connect ${ipPort}`);
|
|
|
return { success: true };
|
|
|
} catch (error) {
|
|
|
console.error('连接设备失败:', error);
|
|
|
@@ -90,7 +254,8 @@ ipcMain.handle('get-device-resolution', async (event, ipPort) => {
|
|
|
}
|
|
|
try {
|
|
|
// 使用 wm size 命令获取设备分辨率
|
|
|
- const { stdout } = await execAsync(`adb -s ${ipPort} shell wm size`);
|
|
|
+ const adbPath = getCachedAdbPath();
|
|
|
+ const { stdout } = await execAsync(`${adbPath} -s ${ipPort} shell wm size`);
|
|
|
// 输出格式通常是: "Physical size: 1080x2400" 或 "1080x2400"
|
|
|
const match = stdout.match(/(\d+)x(\d+)/);
|
|
|
if (match) {
|
|
|
@@ -115,7 +280,8 @@ ipcMain.handle('capture-screenshot', async (event, ipPort) => {
|
|
|
return { success: false, error: '缺少设备 ID' };
|
|
|
}
|
|
|
try {
|
|
|
- const { stdout } = await execAsync(`adb -s ${ipPort} exec-out screencap -p`, {
|
|
|
+ const adbPath = getCachedAdbPath();
|
|
|
+ const { stdout } = await execAsync(`${adbPath} -s ${ipPort} exec-out screencap -p`, {
|
|
|
encoding: 'buffer',
|
|
|
maxBuffer: 25 * 1024 * 1024,
|
|
|
});
|
|
|
@@ -135,7 +301,8 @@ ipcMain.handle('send-tap', async (event, ipPort, x, y) => {
|
|
|
return { success: false, error: '坐标必须是数字' };
|
|
|
}
|
|
|
try {
|
|
|
- const command = `adb -s ${ipPort} shell input tap ${x} ${y}`;
|
|
|
+ const adbPath = getCachedAdbPath();
|
|
|
+ const command = `${adbPath} -s ${ipPort} shell input tap ${x} ${y}`;
|
|
|
await execAsync(command, {
|
|
|
timeout: 5000,
|
|
|
maxBuffer: 1024 * 1024
|
|
|
@@ -156,7 +323,8 @@ ipcMain.handle('send-swipe', async (event, ipPort, x1, y1, x2, y2, duration = 30
|
|
|
return { success: false, error: '坐标必须是数字' };
|
|
|
}
|
|
|
try {
|
|
|
- const command = `adb -s ${ipPort} shell input swipe ${x1} ${y1} ${x2} ${y2} ${duration}`;
|
|
|
+ const adbPath = getCachedAdbPath();
|
|
|
+ const command = `${adbPath} -s ${ipPort} shell input swipe ${x1} ${y1} ${x2} ${y2} ${duration}`;
|
|
|
await execAsync(command, {
|
|
|
timeout: 5000,
|
|
|
maxBuffer: 1024 * 1024
|
|
|
@@ -177,6 +345,7 @@ ipcMain.handle('send-text', async (event, ipPort, text) => {
|
|
|
return { success: false, error: '文字必须是字符串' };
|
|
|
}
|
|
|
try {
|
|
|
+ const adbPath = getCachedAdbPath();
|
|
|
// ADB input text 需要转义特殊字符
|
|
|
// 使用单引号包裹,并转义单引号
|
|
|
const escapedText = text
|
|
|
@@ -219,14 +388,14 @@ ipcMain.handle('send-text', async (event, ipPort, text) => {
|
|
|
.replace(/\$/g, '\\$')
|
|
|
.replace(/"/g, '\\"');
|
|
|
|
|
|
- await execAsync(`adb -s ${ipPort} shell input text "${escapedLine}"`, {
|
|
|
+ await execAsync(`${adbPath} -s ${ipPort} shell input text "${escapedLine}"`, {
|
|
|
timeout: 5000,
|
|
|
maxBuffer: 1024 * 1024
|
|
|
});
|
|
|
}
|
|
|
// 发送换行(除了最后一行)
|
|
|
if (i < lines.length - 1) {
|
|
|
- await execAsync(`adb -s ${ipPort} shell input keyevent KEYCODE_ENTER`, {
|
|
|
+ await execAsync(`${adbPath} -s ${ipPort} shell input keyevent KEYCODE_ENTER`, {
|
|
|
timeout: 5000,
|
|
|
maxBuffer: 1024 * 1024
|
|
|
});
|
|
|
@@ -234,7 +403,7 @@ ipcMain.handle('send-text', async (event, ipPort, text) => {
|
|
|
}
|
|
|
} else {
|
|
|
// 没有换行,直接发送
|
|
|
- const command = `adb -s ${ipPort} shell input text "${escapedText}"`;
|
|
|
+ const command = `${adbPath} -s ${ipPort} shell input text "${escapedText}"`;
|
|
|
await execAsync(command, {
|
|
|
timeout: 5000,
|
|
|
maxBuffer: 1024 * 1024
|
|
|
@@ -257,7 +426,8 @@ ipcMain.handle('send-key-event', async (event, ipPort, keyCode) => {
|
|
|
return { success: false, error: '按键代码必须是字符串' };
|
|
|
}
|
|
|
try {
|
|
|
- const command = `adb -s ${ipPort} shell input keyevent ${keyCode}`;
|
|
|
+ const adbPath = getCachedAdbPath();
|
|
|
+ const command = `${adbPath} -s ${ipPort} shell input keyevent ${keyCode}`;
|
|
|
await execAsync(command, {
|
|
|
timeout: 5000,
|
|
|
maxBuffer: 1024 * 1024
|