@aiot-toolkit/emulator
Version:
vela emulator tool.
867 lines (828 loc) • 33.4 kB
JavaScript
;
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;