UNPKG

@aiot-toolkit/emulator

Version:

vela emulator tool.

867 lines (828 loc) 33.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.isHeadlessEnvironment = exports.VvdManager = void 0; var _ColorConsole = _interopRequireDefault(require("@aiot-toolkit/shared-utils/lib/ColorConsole")); var _promises = _interopRequireDefault(require("fs/promises")); var _fs = _interopRequireDefault(require("fs")); var _os = _interopRequireDefault(require("os")); var _path = _interopRequireDefault(require("path")); var _skinLayoutParser = require("../emulatorutil/skinLayoutParser"); var _child_process = require("child_process"); var _ini = require("ini"); var _constants = require("../emulatorutil/constants"); var _Vvd = require("../typing/Vvd"); var _utils = require("../utils"); var _Instance = require("../typing/Instance"); var _portfinder = require("portfinder"); var _dayjs = _interopRequireDefault(require("dayjs")); var _emulatorutil = require("../emulatorutil"); var _adb = require("@miwt/adb"); var _semver = require("semver"); var _admZip = _interopRequireDefault(require("adm-zip")); var _instance = require("../instance"); var _logcat = require("./logcat"); var _sharedUtils = require("@aiot-toolkit/shared-utils"); var _ILog = require("@aiot-toolkit/shared-utils/lib/interface/ILog"); var _grpc = require("./grpc"); var _file = require("../utils/file"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } // TODO: 升级构建工具支持 esm @xujunjie const getPort = (async () => { return (await import('get-port')).default; })(); const ipull = (async () => { return await import('ipull'); })(); const EAvdParamsToIni = { arm: { abiType: 'armeabi-v7a' }, arm64: { abiType: 'arm64-v8a' } }; const isHeadlessEnvironment = () => { // 检查是否在 Docker 容器中 if (_fs.default.existsSync('/.dockerenv')) return true; // 检查是否在 WSL 环境中 if (process.env.WSL_DISTRO_NAME) return true; // 检查是否设置了 DISPLAY 环境变量(Linux) if (process.platform === 'linux' && !process.env.DISPLAY) return true; // 检查是否在 CI 环境中 if (process.env.CI) return true; return false; }; exports.isHeadlessEnvironment = isHeadlessEnvironment; class VvdManager { // 需要复制的文件 binFiles = ['system.img', 'data.img', 'coredump.core', 'vela_data.bin', 'vela_resource.bin', 'vela_system.bin']; constructor(vvdResourcePaths) { const { vvdHome: avdHome, sdkHome } = vvdResourcePaths; this.vvdHome = avdHome || _constants.defaultVvdHome; this.sdkHome = sdkHome || _constants.defaultSDKHome; if (!_fs.default.existsSync(this.vvdHome)) { _fs.default.mkdirSync(this.vvdHome, { recursive: true }); } } static getDebuggerCfgFile() { return _path.default.resolve(__dirname, '../static/debugger_ip.cfg'); } /** * 创建Vela端的 VVD ,统一保存在 .vela/vvd 目录下 * 1. 创建.vela/advancedFeatures.ini文件 * 2. 创建.vela/vvd/${avdName}.ini文件 * 3. 创建.vela/vvd/${avdName}.vvd/config.ini文件 * @param vvdParams VVD参数,宽高、绑定的镜像路径等 * @returns */ createVvd(vvdParams) { const { name: vvdName, arch, width, height, skin, shape, flavor, density, imageDir: imagePath = _constants.defaultImageHome, customLcdRadius, imageType } = vvdParams; const vvdDir = _path.default.resolve(this.vvdHome, `${vvdName}.vvd`); const vvdIni = _path.default.resolve(this.vvdHome, `${vvdName}.ini`); const vvdConfigIni = _path.default.resolve(vvdDir, 'config.ini'); const nuttxVvdIniContent = `path=${vvdDir}\npath.rel=avd${_path.default.sep}${vvdName}.vvd`; const abiType = EAvdParamsToIni[arch]['abiType']; const configIniJson = JSON.parse(_fs.default.readFileSync(_path.default.join(__dirname, '../static/avdConfigIni.json'), 'utf-8')); configIniJson['AvdId'] = vvdName; configIniJson['abi.type'] = abiType; configIniJson['avd.ini.displayname'] = vvdName; configIniJson['hw.cpu.arch'] = arch; configIniJson['hw.lcd.shape'] = shape; configIniJson['hw.device.flavor'] = flavor; configIniJson['hw.lcd.density'] = density; configIniJson['ide.lcd.radius'] = customLcdRadius; configIniJson['ide.image.type'] = imageType; if (skin) { delete configIniJson['hw.lcd.height']; delete configIniJson['hw.lcd.width']; configIniJson['skin.dynamic'] = 'yes'; configIniJson['skin.name'] = skin; configIniJson['skin.path'] = vvdParams['skin.path']; } else { configIniJson['hw.lcd.height'] = height; configIniJson['hw.lcd.width'] = width; configIniJson['skin.dynamic'] = 'no'; delete configIniJson['skin.name']; delete configIniJson['skin.path']; } // 默认使用 'image.sysdir.2' 自定义使用 'image.sysdir.1' configIniJson['image.sysdir.2'] = imagePath; try { _fs.default.mkdirSync(vvdDir, { recursive: true }); // 写入Vela_Virtual_Device.ini文件 _fs.default.writeFileSync(vvdIni, nuttxVvdIniContent); // 写入Vela_Virtual_Device.vvd/config.ini文件 _fs.default.writeFileSync(vvdConfigIni, (0, _ini.stringify)(configIniJson)); // 在 sysdir 下创建advancedFeatures.ini文件 const advancedFeaturesIni = _path.default.resolve(imagePath, 'advancedFeatures.ini'); if (!_fs.default.existsSync(advancedFeaturesIni)) { const iniSourcePath = _path.default.join(__dirname, '../static/advancedFeatures.ini'); _fs.default.copyFileSync(iniSourcePath, advancedFeaturesIni); } // 拷贝 MODEM_SIMULATOR const modemPath = this.getSDKPart(_Vvd.SDKParts.MODEM_SIMULATOR); if (_fs.default.existsSync(modemPath)) { _sharedUtils.FileUtil.copyFiles(modemPath, _path.default.join(vvdDir, 'modem_simulator')); } return true; } catch (e) { // 出错后要删除创建的目录, 恢复现场 this.deleteVvd(vvdName); throw `createVelaAvd: ${e.message}`; } } getVvdDir(vvdName) { const maybe = [_path.default.join(this.vvdHome, `${vvdName}.ini`), _path.default.join(_os.default.homedir(), '.android', 'avd', `${vvdName}.ini`), _path.default.join(_os.default.homedir(), '.android', 'vvd', `${vvdName}.ini`)]; for (const file of maybe) { if (_fs.default.existsSync(file)) { return (0, _ini.parse)(_fs.default.readFileSync(file, 'utf-8')).path; } } throw new Error(`VVD directory for ${vvdName} not found`); } /** 根据AVD名字获取模拟器的详细信息 */ getVvdInfo(vvdName) { const vvdInfo = { name: vvdName, arch: _Vvd.IVvdArchType.arm, shape: '', height: '', width: '', skin: '', imageDir: '', customLcdRadius: '', imageType: _Vvd.VelaImageType.REL }; let currVvdDir = this.getVvdDir(vvdName); const configIni = _path.default.resolve(currVvdDir, 'config.ini'); try { const contents = _fs.default.readFileSync(configIni, 'utf-8'); const config = (0, _ini.parse)(contents); vvdInfo.arch = config['hw.cpu.arch']; vvdInfo.height = config['hw.lcd.height']; vvdInfo.width = config['hw.lcd.width']; vvdInfo.shape = config['hw.lcd.shape']; vvdInfo.skin = config['skin.name']; vvdInfo.imageDir = config['image.sysdir.2']; vvdInfo.customImagePath = config['image.sysdir.1']; vvdInfo.customLcdRadius = config['ide.lcd.radius']; vvdInfo.imageType = config['ide.image.type']; if (vvdInfo.skin) { try { vvdInfo.skinInfo = this.getSkinInfo(vvdInfo.skin, config['skin.path']); } catch (error) { _ColorConsole.default.warn(`get skin ${vvdInfo.skin} failed: ${error}`); } } return vvdInfo; } catch (err) { _ColorConsole.default.log(`getVelaAvdInfo: ${err.message}`); return vvdInfo; } } /** 根据名字删除AVD */ deleteVvd(vvdName) { const vvdDir = this.getVvdDir(vvdName); const vvdIni = _path.default.resolve(this.vvdHome, `${vvdName}.ini`); try { _fs.default.rmSync(vvdDir, { recursive: true, force: true }); _fs.default.rmSync(vvdIni, { force: true }); return true; } catch (e) { return false; } } /** 获取已经创建的模拟器列表 */ getVvdList() { const avdList = []; const files = _fs.default.readdirSync(this.vvdHome); const regex = /^([\d\D]*)\.(avd|vvd)$/; for (const fileName of files) { const matcher = fileName.match(regex); if (matcher) { const avdName = matcher[1]; const avdInfo = this.getVvdInfo(avdName); avdList.push(avdInfo); } } return avdList; } /** 获取 SDK 子目录 */ getSDKPart(name) { return _path.default.resolve(this.sdkHome, name); } getSkinInfo(skinName, skinPath) { const skinDir = skinPath; const layoutPath = _path.default.join(skinDir, 'layout'); if (_fs.default.existsSync(layoutPath)) { const layoutContent = _fs.default.readFileSync(layoutPath, 'utf-8'); const parser = new _skinLayoutParser.ConfigParser(); const skinInfo = parser.toObject(parser.parse(layoutContent)); const defaultLayout = skinInfo.layouts.portrait || skinInfo.layouts.landscape; defaultLayout.part1.value = skinInfo.parts[defaultLayout.part1.name]; defaultLayout.part2.value = skinInfo.parts[defaultLayout.part2.name]; const backgroundImage = _path.default.join(skinDir, skinInfo.parts[defaultLayout.part1.name].background.image); const maskImage = skinInfo.parts[defaultLayout.part1.name]?.foreground?.mask; const maskImagePath = maskImage ? _path.default.join(skinDir, maskImage) : undefined; return { name: skinName, path: skinPath, info: skinInfo, backgroundImage, maskImage: maskImagePath, defaultLayout }; } } /** 获取模拟器皮肤列表 */ async getVelaSkinList() { try { const skinList = []; const skinHome = this.getSDKPart(_Vvd.SDKParts.SKINS); const builinDir = _path.default.join(skinHome, 'builtin'); const userDir = _path.default.join(skinHome, 'user'); const builtinFiles = await _fs.default.promises.readdir(builinDir).catch(() => []); const userFiles = await _fs.default.promises.readdir(userDir).catch(() => []); for (const fileName of builtinFiles) { try { const item = this.getSkinInfo(fileName, _path.default.join(builinDir, fileName)); if (item) skinList.push(item); } catch (error) { _ColorConsole.default.warn(`parser skin info ${fileName} error:`, error.toString()); continue; } } for (const fileName of userFiles) { try { const item = this.getSkinInfo(fileName, _path.default.join(userDir, fileName)); if (item) skinList.push(item); } catch (error) { _ColorConsole.default.warn(`parser skin info ${fileName} error:`, error.toString()); continue; } } return skinList; } catch (e) { _ColorConsole.default.error(`get skin error:`, e.toString()); return []; } } /** 自定义模拟器的镜像目录 */ async customImageDir(vvdName, target) { const currVvdDir = this.getVvdDir(vvdName); const configIni = _path.default.resolve(currVvdDir, 'config.ini'); const contents = _fs.default.readFileSync(configIni, 'utf-8'); const config = (0, _ini.parse)(contents); config['image.sysdir.1'] = target; await _fs.default.promises.writeFile(configIni, (0, _ini.stringify)(config)); const defaultImageHome = config['image.sysdir.1']; if (!defaultImageHome) { _ColorConsole.default.warn(`defaultImageHome: ${vvdName} image dir is empty`); return; } // 如果自定义的目录缺少 binFile 则从默认目录复制 for (const file of this.binFiles) { const targetFile = _path.default.join(target, file); const originFile = _path.default.join(defaultImageHome, file); if (!_fs.default.existsSync(originFile)) continue; if (!_fs.default.existsSync(targetFile)) { // 文件不存在则直接复制 _fs.default.copyFileSync(originFile, targetFile); } else { _ColorConsole.default.warn(`${targetFile} is out-dated,if you want upadte it, please delete it,and use customImageDir again`); } } } /** 重置自定义的镜像目录 */ resetImageDir(vvdName) { const currVvdDir = this.getVvdDir(vvdName); const configIni = _path.default.resolve(currVvdDir, 'config.ini'); const contents = _fs.default.readFileSync(configIni, 'utf-8'); const config = (0, _ini.parse)(contents); const home = this.getSDKPart(_Vvd.SDKParts.SYSTEM_IMAGES); let imageDir = _path.default.resolve(home, config['ide.image.type']); config['image.sysdir.2'] = imageDir; delete config['image.sysdir.1']; _fs.default.writeFileSync(configIni, (0, _ini.stringify)(config)); } getEmulatorBinPath(sdkHome) { const osPlatform = _os.default.platform(); const arch = (0, _utils.getSystemArch)(); const platform = osPlatform === 'win32' ? 'windows' : osPlatform; const emulatorHome = _path.default.resolve(sdkHome, 'emulator'); return _path.default.resolve(emulatorHome, `${platform}-${arch}`, 'emulator'); } // 旧的模拟器迁移 async oldEmulatorMigrate(vvdName) { const configIni = _path.default.join(this.getVvdDir(vvdName), 'config.ini'); const contents = await _promises.default.readFile(configIni, 'utf-8'); const config = (0, _ini.parse)(contents); let needUpdate = false; // 检查 ramsize 调整为 1024 if (config['hw.ramSize'] < 1024) { needUpdate = true; config['hw.ramSize'] = 1024; } // 检查皮肤路径 if (config['skin.path']?.includes('.export_dev/skins')) { if (config['skin.path'].includes('.export_dev/skins/user')) { // 自定义的皮肤,将自定义皮肤迁移到新的 sdk 目录中 const oldUserSkinDir = _path.default.join(_os.default.homedir(), '.export_dev/skins/user'); const newUserSkinDir = _path.default.join(this.sdkHome, 'skins', 'user'); const userSkins = await _promises.default.readdir(oldUserSkinDir, { withFileTypes: true }); for (const skin of userSkins) { if (!skin.isDirectory()) continue; const oldSkinPath = _path.default.join(oldUserSkinDir, skin.name); const newSkinPath = _path.default.join(newUserSkinDir, skin.name); if (!_fs.default.existsSync(newSkinPath)) { await _promises.default.mkdir(newSkinPath, { recursive: true }); } await (0, _file.copyDir)(oldSkinPath, newSkinPath); } } needUpdate = true; config['skin.path'] = config['skin.path'].replace('.export_dev/skins', '.vela/sdk/skins'); } // 检查镜像路径 if (config['image.sysdir.2']?.includes('.export_dev/system-images')) { needUpdate = true; config['image.sysdir.2'] = config['image.sysdir.2'].replace('.export_dev/system-images', '.vela/sdk/system-images'); } if (needUpdate) { await _promises.default.writeFile(configIni, (0, _ini.stringify)(config)); _ColorConsole.default.log(`update ${vvdName} config.ini`); } } async getVvdStartCmd(options) { const vvdName = options.vvdName; // 获取emulator bin的绝对路径 const emulatorBin = this.getEmulatorBinPath(this.sdkHome); const vvdDir = this.getVvdDir(vvdName); // 端口映射 const degbugProt = options.debugPort || (await (await getPort)({ port: Array.from({ length: 100 }).map((_, i) => 10055 + i) })); let portMappingStr = `-network-user-mode-options hostfwd=tcp:127.0.0.1:${degbugProt}-10.0.2.15:101`; // qemu 配置 const HOST_9PFS_DIR = _path.default.join(vvdDir, 'share'); if (!_fs.default.existsSync(HOST_9PFS_DIR)) { await _fs.default.promises.mkdir(HOST_9PFS_DIR, { recursive: true }); } // 9pfs device // const qemuOption9p = `-netdev user,id=network,net=10.0.2.0/24,dhcpstart=10.0.2.16 \ // -device virtio-net-device,netdev=network,bus=virtio-mmio-bus.4 \ // -device virtio-snd,bus=virtio-mmio-bus.2 -allow-host-audio -semihosting \ // -fsdev local,security_model=none,id=fsdev0,path=${HOST_9PFS_DIR} \ // -device virtio-9p-device,id=fs0,fsdev=fsdev0,mount_tag=host ` const qemuOption = `-device virtio-snd,bus=virtio-mmio-bus.2 -allow-host-audio -semihosting`; // qt windows 配置 // 在 docker,wls,等无界面平台上用 -no-window ,否则用 -qt-hide-window // 使用 -no-window 时 extended control 不可用 // 在无界面平台上必须使用 -no-window 否则会报错 qt `Could not load the Qt platform plugin \"xcb\" const noWindowOption = isHeadlessEnvironment() ? `-no-window` : '-qt-hide-window'; const windowOption = options.qtHideWindow ? noWindowOption : ''; let grpcStr = options.grpcPort ? `-grpc ${options.grpcPort}` : ''; let serialStr = ``; if (options.serialPort) { serialStr = `-port`; // 由于模拟器方面原因,如果指定了 serialPort 则必须同时指定 grpc 参数,否则 grpc 不会默认开启,从而导致不会创建 running 文件 const grpcPort = options.grpcPort || (await (0, _portfinder.getPortPromise)()); grpcStr = `-grpc ${grpcPort}`; } const verboseOption = options.verbose ? `-verbose` : ``; // 启动模拟器的命令和参数 const cmd = `${emulatorBin} -vela -avd ${options.vvdName} ${serialStr} -show-kernel ${portMappingStr} ${windowOption} ${grpcStr} ${verboseOption} -qemu ${qemuOption}`; const vvdInfo = this.getVvdInfo(vvdName); await this.oldEmulatorMigrate(vvdName); if (!vvdInfo.imageDir) { const errMsg = `${vvdName} is not supported`; _ColorConsole.default.throw(errMsg); throw new Error(errMsg); } for (const file of this.binFiles) { const pOfVvd = _path.default.join(vvdDir, file); let pOfImageDir; if (vvdInfo.customImagePath && _fs.default.existsSync(_path.default.join(vvdInfo.customImagePath, file))) { pOfImageDir = _path.default.join(vvdInfo.customImagePath, file); } else if (vvdInfo.imageDir && _fs.default.existsSync(_path.default.join(vvdInfo.imageDir, file))) { pOfImageDir = _path.default.join(vvdInfo.imageDir, file); } else continue; if (!_fs.default.existsSync(pOfVvd)) { // 文件不存在则直接复制 _ColorConsole.default.log(`${file} not found, copy from ${pOfImageDir}`); _fs.default.copyFileSync(pOfImageDir, pOfVvd); } else { // 文件存在但过时 const statsInAvd = _fs.default.statSync(pOfVvd); const stats = _fs.default.statSync(pOfImageDir); if ((0, _dayjs.default)(stats.mtime).isAfter(statsInAvd.mtime)) { _ColorConsole.default.log(`${file} file is outdate, update from ${pOfImageDir}`); _fs.default.copyFileSync(pOfImageDir, pOfVvd); } } } // 拷贝 MODEM_SIMULATOR const modemPath = this.getSDKPart(_Vvd.SDKParts.MODEM_SIMULATOR); if (_fs.default.existsSync(modemPath) && !_fs.default.existsSync(_path.default.join(vvdDir, 'modem_simulator', 'modem_nvram.json'))) { _sharedUtils.FileUtil.copyFiles(modemPath, _path.default.join(vvdDir, 'modem_simulator')); } return cmd; } async startVvd(options) { let logger; if (options.customLogger) { logger = msg => { const prefix = _ColorConsole.default.createDefaultPrefix({ level: _ILog.Loglevel.INFO, message: msg }); options.customLogger(`${prefix[1]} ${msg?.trim()}`); }; } else { logger = _ColorConsole.default.info; } const runningVvds = await (0, _emulatorutil.getRunningVvds)(); const vvdName = options.vvdName; const onStdout = options.stdoutCallback || console.log; const onErrout = options.stderrCallback || console.log; const e = runningVvds.find(e => e['avd.name'] === vvdName); const vvdInfo = this.getVvdInfo(vvdName); // 该模拟器已经启动,则创建一个日志流返回 if (e) { // 找出来它原来使用的 debugPort const regex = /hostfwd=tcp:(.*?):(\d+)-10\.0\.2\.15:101/; const debugPort = e.cmdline?.match(regex)?.[2]; const emulatorInstance = (0, _instance.findInstance)(vvdInfo.imageType, { serialPort: e['port.serial'], vvdName: vvdName, onStdout, onErrout, debugPort, customLogger: options.customLogger }); return { coldBoot: false, emulatorInstance, getAgent: () => (0, _grpc.createGrpcClient)(e), grpcConfig: e }; } // 启动模拟器的命令和参数 const cmd = await this.getVvdStartCmd(options); const spawnArgs = cmd.split(' '); const spawnBin = spawnArgs.shift(); logger(`Start CMD: ${cmd}`); const InstanceCalss = (0, _instance.getInstanceClass)(vvdInfo.imageType); const func = InstanceCalss.isEmulatorStarted.bind(InstanceCalss); return new Promise((resolve, reject) => { const emulatorProcess = (0, _child_process.spawn)(spawnBin, spawnArgs, { stdio: 'pipe', shell: true, cwd: this.sdkHome }); if (options.origin === _Instance.IStartOrigin.Terminal) { process.stdin.pipe(emulatorProcess.stdin); } // 利用 readline 接口可解决子进程日志换行的问题 const readlines = (0, _logcat.attachReadline)(emulatorProcess, onStdout, onErrout); const emulatorStartedHandler = msg => { if (func(msg)) { const e = (0, _emulatorutil.getRunningAvdConfigByName)(vvdName); if (e) { const emulatorInstance = (0, _instance.findInstance)(vvdInfo.imageType, { serialPort: e['port.serial'], vvdName: vvdName, logcatProcess: emulatorProcess, ...readlines, onStdout, onErrout, debugPort: options.debugPort, customLogger: options.customLogger }); logger(`${options.vvdName} started successfully`); readlines.stdoutReadline.off('line', emulatorStartedHandler); resolve({ coldBoot: true, emulatorInstance, getAgent: () => (0, _grpc.createGrpcClient)(e), grpcConfig: e }); } else { reject('get emulator running config failed'); } } }; readlines.stdoutReadline.on('line', emulatorStartedHandler); // 监听模拟器的退出事件 emulatorProcess.on('exit', code => { logger(`${options.vvdName} emulator exited with code ${code}`); if (options.exitCallback) { options.exitCallback(code); } readlines.dispose(); reject(); }); // 监听模拟器的错误事件 emulatorProcess.on('error', err => { readlines.dispose(); reject(err); }); }); } stopVvd(name) { let timeout = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 10 * 1000; return new Promise((resolve, reject) => { const config = (0, _emulatorutil.getRunningAvdConfigByName)(name); if (!config) { return resolve(); } const t = setTimeout(() => { (0, _utils.killProcessByCmd)(config['avd.name']).then(resolve).catch(e => { reject(`poweroff failed ${e}`); }); }, timeout); (0, _adb.execAdbCmdAsync)(`adb -s emulator-${config['port.serial']} shell poweroff`).then(() => { clearTimeout(t); resolve(); }).catch(e => { if (e?.message?.includes(`device 'emulator-${config['port.serial']}' not found`)) { // 清理之前的文件 _fs.default.unlinkSync(config.path); } (0, _utils.killProcessByCmd)(config['avd.name']).then(() => { clearTimeout(t); resolve(); }).catch(e => { reject(`poweroff failed ${e}`); }); }); }); } //#endregion sdk manager /** 获取模拟器平台的名称,darwin-aarch64 linux-aarch64 windows-x86_64等 */ getEmulatorPlatform() { const systemOs = _os.default.platform(); const hostArch = (0, _utils.getSystemArch)(); const hostOs = systemOs === 'win32' ? 'windows' : systemOs; return `${hostOs}-${hostArch}`; } /** 获取本地镜像的构建时间 */ async getLocalImageBuildTime(imagePath) { if (!imagePath) return; if (_fs.default.existsSync(imagePath)) { const stats = await _fs.default.promises.stat(imagePath); // 兼容传递的参数是镜像目录的情况 if (stats.isDirectory()) { const stats = await _fs.default.promises.stat(_path.default.join(imagePath, 'nuttx')); return stats.mtime; } return stats.mtime; } return; } getSDKVersionPath() { return _path.default.resolve(this.sdkHome, 'versions.json'); } /** 获取本地 SDK 的版本信息 */ async getSDKVersion() { return JSON.parse(await _fs.default.promises.readFile(this.getSDKVersionPath(), 'utf-8')); } /** 获取本地已经下载的镜像 */ async getLocalSystemImage() { const home = this.getSDKPart(_Vvd.SDKParts.SYSTEM_IMAGES); const res = []; if (_fs.default.existsSync(home)) { const files = _fs.default.readdirSync(home); for (let file of files) { if (file.startsWith('.')) continue; const systemPath = _path.default.resolve(home, file, 'nuttx'); if (!_fs.default.existsSync(systemPath)) continue; const time = await this.getLocalImageBuildTime(file); res.push({ value: file, time }); } } return res; } getLocalSystemPath(imageId) { const home = this.getSDKPart(_Vvd.SDKParts.SYSTEM_IMAGES); return _path.default.resolve(home, imageId, 'nuttx'); } hasLocaleImage(imageId) { const systemPath = this.getLocalSystemPath(imageId); if (_fs.default.existsSync(systemPath)) return true; return false; } /** 判断本地的某个 vela 镜像是否需要更新 */ async isLocalImageNeedUpdate(imageId) { if (!this.hasLocaleImage(imageId)) return true; const local = await this.getLocalImageBuildTime(this.getLocalSystemPath(imageId)); const latest = (0, _dayjs.default)(_constants.VelaImageVersionList.find(t => t.value === imageId).time, 'YYYYMMDD'); if ((0, _dayjs.default)(local).isBefore(latest)) return true; return false; } async getNotInstalledSDKPart() { const allResouce = Object.values(_Vvd.SDKParts); try { const res = []; for (const resouces of allResouce) { // 镜像的版本是时间格式 if (!_fs.default.existsSync(_path.default.resolve(this.sdkHome, resouces))) { res.push(resouces); break; } } return res; } catch (error) { // 未读取到版本信息,则认为所有资源都需要更新 return allResouce; } } /** 检查 SDK 有哪些部分需要更新 */ async hasSDKPartUpdate() { const allResouce = Object.values(_Vvd.SDKParts); try { const res = []; const localVersion = await this.getSDKVersion(); for (const resouces of allResouce) { // 镜像的版本是时间格式 if (!_fs.default.existsSync(_path.default.resolve(this.sdkHome, resouces))) { res.push(resouces); continue; } if (resouces === _Vvd.SDKParts.SYSTEM_IMAGES) { const defaultImage = (0, _constants.getDefaultImage)(); if (await this.isLocalImageNeedUpdate(defaultImage)) res.push(resouces); continue; } if ((0, _semver.lt)(localVersion[resouces], _constants.EmulatorEnvVersion[resouces])) { res.push(resouces); } } return res; } catch (error) { // 未读取到版本信息,则认为所有资源都需要更新 return allResouce; } } /** * 下载 SDK,默认只下载需要更新的部分 * @param opt.force 强制下载所有部分 * * @example * async function main() { * const sdkHome = path.resolve(os.homedir(), '.export_dev1') * const velaAvdCls = new VelaAvdCls({ sdkHome }) * * const downloder = await velaAvdCls.downloadSDK({ * force: true, * cliProgress: false, * parallelDownloads: 6 * }) * * downloder.on('progress', (progress) => { * console.log( * `progress: ${progress.formattedSpeed} ${progress.formattedPercentage} ${progress.formatTotal} ${progress.formatTimeLeft}` * ) * }) * * await downloder.downlodPromise * * console.log('download success') * } */ async downloadSDK(opt) { const updateList = opt.force ? Object.values(_Vvd.SDKParts) : await this.hasSDKPartUpdate(); let urls = updateList.map(t => ({ name: t, url: (0, _constants.getSDKPartDownloadUrl)(t) })); if (opt.imageTypeArr) { for (const imgType of opt.imageTypeArr) { const needsUpdate = await this.isLocalImageNeedUpdate(imgType); const downloadUrl = (0, _constants.getImageDownloadUrl)()[imgType]; // 需要更新并且urls中不存在则添加 if (needsUpdate && urls.findIndex(u => u.url === downloadUrl) < 0) { urls.push({ name: imgType, url: downloadUrl }); } } } const downloads = urls.map(async u => { const d = await (await ipull).downloadFile({ url: u.url, directory: _path.default.resolve(this.sdkHome), skipExisting: false, parallelStreams: opt.parallelStreams || 6 }); return d; }); const downloader = await (await ipull).downloadSequence(opt, ...downloads); const p = downloader.download(); const p2 = p.then(async () => { // 如果下载被取消或者出错,就不再执行后续操作 if (downloader.downloadStatues.some(status => status.downloadStatus !== 'Finished')) { return; } _ColorConsole.default.warn('All file resources have been successfully downloaded and are being decompressed.'); for (let u of urls) { // 如果是镜像则来自默认镜像 if (u.name === _Vvd.SDKParts.SYSTEM_IMAGES) u.name = (0, _constants.getDefaultImage)(); // 解压 const targetDirName = (0, _constants.isVelaImageType)(u.name) ? _Vvd.SDKParts.SYSTEM_IMAGES : u.name; const targetDir = u.name === _Vvd.SDKParts.SYSTEM_IMAGES || (0, _constants.isVelaImageType)(u.name) ? // 镜像解压的目录位于 sdkHome/system_images/imageId _path.default.resolve(this.sdkHome, targetDirName, u.name) : // 其他资源解压的目录位于 sdkHome/resource _path.default.resolve(this.sdkHome, targetDirName); const targetDirExist = _fs.default.existsSync(targetDir); if (!targetDirExist) await _fs.default.promises.mkdir(targetDir, { recursive: true }); const zipFile = _path.default.resolve(this.sdkHome, _path.default.basename(u.url)); const zip = new _admZip.default(zipFile); zip.extractAllTo(targetDir, true, true); await _fs.default.promises.rm(zipFile, { force: true }).catch(e => { _ColorConsole.default.info(`remove ${zipFile} failed: ${e}`); // my be failed on windows as resource busy or locked, don't care return Promise.resolve(); }); } const verFile = this.getSDKVersionPath(); await _fs.default.promises.writeFile(verFile, JSON.stringify(_constants.EmulatorEnvVersion, null, 2)); }); return Object.assign(downloader, { downlodPromise: p2 }); } /** 下载vela系统镜像 */ async downloadImage(imageId, opt) { const downloadUrls = (0, _constants.getImageDownloadUrl)(); const u = downloadUrls[imageId]; const downloader = await (await ipull).downloadFile({ url: u, directory: this.getSDKPart(_Vvd.SDKParts.SYSTEM_IMAGES), skipExisting: false, ...opt }); const p = downloader.download(); const p2 = p.then(async () => { //如果下载被取消或者出错,就不再执行后续操作 if (downloader.status.downloadStatus !== 'Finished') return; const zipFile = _path.default.resolve(this.sdkHome, _Vvd.SDKParts.SYSTEM_IMAGES, _path.default.basename(u)); const zip = new _admZip.default(zipFile); zip.extractAllTo(_path.default.resolve(this.sdkHome, _Vvd.SDKParts.SYSTEM_IMAGES, imageId), true, true); await _fs.default.promises.rm(zipFile, { force: true }).catch(e => { _ColorConsole.default.info(`remove ${zipFile} failed: ${e}`); // my be failed on windows as resource busy or locked, don't care return Promise.resolve(); }); }); return Object.assign(downloader, { downlodPromise: p2 }); } } exports.VvdManager = VvdManager;