UNPKG

cadb

Version:

安卓/鸿蒙系统截图/录屏工具

556 lines (517 loc) 15 kB
const { shellConfigs, logRed, projectInfoList, logGreen, } = require('../constants'); const _ = require('lodash'); const path = require('path'); const {execSync, execFileSync, spawnSync} = require('child_process'); const crypto = require('crypto'); const os = require('os'); const readline = require('readline'); const fs = require('fs'); const {once} = require('events'); let hdcPath; let adbPath; if (isWin()) { hdcPath = path.resolve(getProjectRootPath(), 'libs', 'win', 'hdc.exe'); adbPath = path.resolve(getProjectRootPath(), 'libs', 'win', 'adb.exe'); } else { hdcPath = path.resolve(getProjectRootPath(), 'libs', 'hdc'); adbPath = path.resolve(getProjectRootPath(), 'libs', 'adb'); } // 遍历当前已经存在的截图名, 并加一 function getRightFileName(prefix, destDir, regRule) { const lsString = execSync( `${isWin() ? 'dir' : 'ls'} ${destDir}`, shellConfigs, ); const numbers = []; lsString.split(/\s/).forEach(name => { if (new RegExp(regRule).test(name)) { numbers.push(+RegExp.$1); } }); let ver = 1; if (!_.isEmpty(numbers)) { ver = Math.max(...numbers) + 1; } return `${prefix}-${ver}`; } /** * 是否是linux平台 * @returns {boolean} */ function isLinux() { return process.platform === 'linux'; } /** * 是否是win平台 * @returns {boolean} */ function isWin() { return process.platform.startsWith('win'); } /** * 获取当前所在目录的module信息 * @return {{moduleName: string: 项目名, parentDirPath: string: 项目父目录全路径}} */ function getCurrModuleInfo() { const fullPath = process.cwd(); const lastIndex = fullPath.lastIndexOf('/'); return { moduleName: fullPath.substr(lastIndex + 1), parentDirPath: fullPath.substr(0, lastIndex + 1), }; } /** * 获取FileTranslate项目根路径; * 注意: 其他项目根目录时不要使用; 比如在cable项目中调用此方法, 仍然得到FileTranslate, 而非cable */ function getProjectRootPath() { const {name} = require('../../package.json'); const [p1] = __dirname.split(name); return path.join(p1, name) + '/'; } function doMd5(content) { const md5 = crypto.createHash('md5'); return md5.update(content).digest('hex'); } /** * 获取系统User目录 * @returns {*} */ function getSystemHomePath() { return process.env.HOME; } /** * 获取mac电脑本地ip * @return {string} */ function getLocalIP() { const interfaces = os.networkInterfaces(); for (const devName in interfaces) { const iface = interfaces[devName]; for (let i = 0; i < iface.length; i++) { const alias = iface[i]; if ( alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal ) { return alias.address; } } } } const ignoreFile = '.DS_Store'; const prefixNewFile = 'new file:'; const prefixDelete = 'deleted:'; const prefixModified = 'modified:'; const prefixRenamed = 'renamed:'; const prefixes = [prefixModified, prefixNewFile, prefixDelete, prefixRenamed]; function getGitChangedFileList(dir) { const lsResult = execSync('git add . && git status', { ...shellConfigs, cwd: dir, }); if ( !lsResult || lsResult.includes('nothing to commit') || !lsResult.includes('Changes to be committed:') ) { console.log('无文件改动'); return; } const lsArr = lsResult.split('\n'); const fileDataList = []; for (let item of lsArr) { item = item.trim(); if (item.endsWith(ignoreFile)) { continue; } for (const p of prefixes) { if (item.startsWith(p)) { let pathValue = item.replace(p, '').trim(); let originPath; if (pathValue.includes('->')) { const splits = pathValue.split('->'); originPath = splits[0].trim(); pathValue = splits[1].trim(); } const fileData = { desc: getFileStatus(p), prefix: p, relativePath: pathValue, originPath, }; fileDataList.push(fileData); break; } } } return fileDataList; } function getFileStatus(prefix) { if (prefix === prefixModified) { return '修改'; } else if (prefix === prefixNewFile) { return '新增'; } else if (prefix === prefixDelete) { return '删除'; } else if (prefix === prefixRenamed) { return '重命名'; } } // 重置代码至clean状态 function resetGitStatus(dir) { if (!dir) { dir = process.cwd(); } const dirConfig = {...shellConfigs, cwd: dir}; // 还原已被add的变更 execSync('git reset head', dirConfig); // 重置已改动的文件 execSync('git checkout .', dirConfig); // 删除未track的文件 execSync('git clean -f', dirConfig); // 删除未track的文件夹 execSync('git clean -fd', dirConfig); console.log('已将仓库置为clean状态'); } function isAndroidDeviceConnected() { const stdout = execFileSync(adbPath, ['devices'], shellConfigs); // 根据制表符来判断是否有安卓设备连接; stdout为null代表adb command not found if (!stdout || !stdout.includes('\t')) { logRed('\r\n当前没有安卓设备连接!'); return null; } if (stdout.includes('attached')) { const splitList = stdout.split('\n'); // 是否每个设备都offline const allOffline = splitList .filter(t => t.includes('\t')) .every(t => t.endsWith('offline')); if (allOffline) { logRed('\r\n当前没有安卓设备连接!'); return null; } return splitList.filter(t => !!t.trim()); } return null; } function isHarmonyDeviceConnected() { const stdout = execFileSync(hdcPath, ['list', 'targets'], shellConfigs); if (!stdout || stdout.includes('Empty')) { logRed('\r\n当前没有鸿蒙设备连接!'); return null; } const splitList = stdout.split('\n'); if (_.isEmpty(splitList)) { logRed('\r\n当前没有鸿蒙设备连接!'); return null; } return splitList.map(t => t.trim()).filter(t => !!t); } // 获取安卓真机的deviceId, 剔除模拟器id function chooseAndroidRealDeviceId(splitList) { if (splitList.length > 1) { const realDeList = splitList.filter( t => !t.includes('emulator-') && t.includes('\t'), ); if (realDeList.length > 1) { throw new Error('当前有多个真机连接'); } if (/(.+)\t.+/.test(realDeList[0])) { return `-s ${RegExp.$1}`; } } return ''; } /** * 优先选择通过usb连接的设备, 然后选择通过wifi连接的设备和模拟器 * atLeastOne: 是否 不要返回空 */ async function chooseHarmonyRealDeviceId(splitList, atLeastOne) { if (splitList.length > 1) { const realDeList = splitList.filter( t => !!t && !t.startsWith('127.0.0.1') && !t.includes(':'), ); // 如果有通过usb连接的设备 if (realDeList.length > 1) { logRed('当前有多个真机通过usb连接'); process.exit(); } // 如果都是远程连接或者模拟器, 则让用户选择 if (realDeList.length <= 0) { return await chooseDeviceId(splitList); } return `-t ${realDeList[0]}`; } if (atLeastOne) { return `-t ${splitList[0]}`; } return ''; } // 让用户手动选择设备id function chooseDeviceId(realDeList) { return new Promise((resolve, rej) => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); let question = '选择一个设备id: '; for (const item of realDeList) { question += `1. ${item}`; } question += '请输入一个编号:'; rl.question(question, answer => { if (!_.isNumber(answer) || +answer >= realDeList.length || +answer < 0) { logRed('回答错误'); process.exit(); return ''; } console.log(realDeList(+answer)); resolve(realDeList(+answer)); }); // 关闭接口 rl.close(); }); } function collectUserInput() { return new Promise((resolve, rej) => { // 创建接口实例 const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); // 监听用户输入的事件 rl.on('line', input => { resolve(input); // 关闭接口 rl.close(); }); }); } // 寻找一个外部接口的 MAC 地址作为电脑唯一标识 const getDeviceMacAsId = () => { const networkInterfaces = os.networkInterfaces(); for (let interfaceKey in networkInterfaces) { const networkInterface = networkInterfaces[interfaceKey]; for (let network of networkInterface) { if (!network.internal && network.mac !== '00:00:00:00:00:00') { return network.mac.replaceAll(':', '_'); // 返回第一个非内网接口的MAC地址 } } } return null; }; /** * 用法1: 若filePath是一个文件, 拿到fileName(带后缀) * 用法2: 若filePath是一个文件夹, 拿到目录名称 */ function getFileName(filePath) { // 使用 path.basename 获取文件名 return path.basename(filePath); } function createLocalId(prefix) { const mac = getDeviceMacAsId(); return `${prefix}_${mac}`; } /** * 在Finder或资源管理器打开文件所在目录 并选中 * */ function openFileInFinder(filePath) { if (isWin()) { try { execSync(`explorer /select,${filePath}`, shellConfigs); } catch (err) { // 会报错,但是功能正常, 先忽略 // console.log(err); } } else { // open -R可打开finder并选中文件 execSync(`open -R ${filePath}`); } } /** * 获取当前项目最新的commitId: git rev-parse HEAD */ function getHeadCommitId() { const commitId = execSync('git rev-parse HEAD', shellConfigs); if (commitId.includes('fatal') || commitId.includes('not a git repository')) { return ''; } return commitId.trim(); } function getCurrentWifiName() { const {stderr, stdout} = spawnSync( 'networksetup', ['-getairportnetwork', 'en0'], shellConfigs, ); if (stderr) { console.log('无法获取当前wifi名! 请确认是否已连接wifi: ' + stderr); return null; } if (/.+: (.+)/.test(stdout)) { return RegExp.$1; } else { console.log('无法获取当前wifi名! 请确认是否已连接wifi: ' + stdout); } return null; } /** * 保持进程运行并监听键盘输入; 用户输入任何字符都可以结束阻塞 * @returns {Promise<unknown>} */ function listenAnyKeyInput() { return new Promise((resolve, rej) => { process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.setEncoding('utf8'); process.stdin.on('data', key => { if (key === '\u0003') { // Ctrl + C 控制码 console.log('Exiting process.'); resolve({controlCPress: true}); } else { console.log('Stopping recording...'); resolve({controlCPress: false, key}); } }); process.on('SIGINT', () => { console.log('Process interrupted by user.'); resolve({controlCPress: true}); }); }); } async function processFile(inputFilePath, lineProcessor) { const inputStream = fs.createReadStream(inputFilePath); let totalText = ''; const rl = readline.createInterface({ input: inputStream, crlfDelay: Infinity, // 识别所有的行结束符,包括 \r 和 \n }); rl.on('line', line => { line = lineProcessor(line); // 对符合匹配规则的行进行处理 totalText += line + '\n'; // 将处理后的行添加到总文本中 // outputStream.write(line + '\n'); // 将处理后的行写入输出流 }); await once(rl, 'close'); // 异步地等待文件处理完成 const outputStream = fs.createWriteStream(inputFilePath); outputStream.write(totalText); // 将处理后的总文本写入输出流 outputStream.close(); // 关闭输出流 } const rcPath = path.join(getSystemHomePath(), '.cadbrc.json'); /** * 批量从缓存中读取或者让用户输入项目路径 */ async function loadOrCollectProjectPathBatch(appNameList, noCache) { if (_.isEmpty(appNameList)) { return []; } let rcConfig = {}; if (!fs.existsSync(rcPath)) { fs.writeFileSync(rcPath, JSON.stringify({})); } else { rcConfig = JSON.parse(fs.readFileSync(rcPath, 'utf-8')); } let originIsAllEmpty = true; let pathList = []; const {localProjectPathMap = {}} = rcConfig; for (const appName of appNameList) { let projectPath = _.get(localProjectPathMap, `${appName}.path`); if (noCache) { projectPath = ''; } if (projectPath) { originIsAllEmpty = false; } const {desc, gitRepoName} = projectInfoList[appName]; if (!projectPath) { console.log( `\r\n请输入${desc}项目的路径(比如 /Users/xxx/yyy/${gitRepoName} ): `, ); let proPath = await collectUserInput(); proPath = proPath.trim(); if (!fs.existsSync(proPath)) { logRed('请输入正确的项目路径'); process.exit(); } if (path.basename(proPath) !== gitRepoName) { logRed(`请输入${gitRepoName}的项目路径`); process.exit(); } projectPath = proPath; } console.log(`${gitRepoName} path: `, projectPath); if (!localProjectPathMap[appName]) { localProjectPathMap[appName] = {}; } localProjectPathMap[appName].path = projectPath; pathList.push(projectPath); } rcConfig.localProjectPathMap = localProjectPathMap; fs.writeFileSync(rcPath, JSON.stringify(rcConfig)); if (originIsAllEmpty) { logGreen('\r\n项目路径已写入缓存: '); console.log(JSON.stringify(localProjectPathMap, null, 2)); } return pathList; } /** * 删除指定路径的文件或文件夹 * @param {string} filepath - 要删除的文件或文件夹的路径 */ function deletePath(filepath) { try { const stats = fs.statSync(filepath); if (stats.isFile()) { // 如果是文件,使用 unlinkSync 删除 fs.unlinkSync(filepath); console.log(`File deleted successfully: ${filepath}`); } else if (stats.isDirectory()) { // 如果是文件夹,使用 rmSync 删除(递归删除) fs.rmSync(filepath, { recursive: true, force: true }); console.log(`Directory deleted successfully: ${filepath}`); } } catch (err) { console.error(`Error deleting path: ${filepath}`, err); } } module.exports = { getRightFileName, isLinux, getCurrModuleInfo, getProjectRootPath, doMd5, getSystemHomePath, getLocalIP, getGitChangedFileList, prefixNewFile, prefixDelete, prefixModified, prefixRenamed, resetGitStatus, isAndroidDeviceConnected, isHarmonyDeviceConnected, chooseAndroidRealDeviceId, chooseHarmonyRealDeviceId, collectUserInput, getDeviceMacAsId, getFileName, createLocalId, isWin, openFileInFinder, hdcPath, adbPath, getHeadCommitId, getCurrentWifiName, listenAnyKeyInput, processFile, loadOrCollectProjectPathBatch, rcPath, deletePath, };