cadb
Version:
安卓/鸿蒙系统截图/录屏工具
556 lines (517 loc) • 15 kB
JavaScript
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,
};