diff --git a/package.json b/package.json index 80a0481..e07cc62 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "pack": "zx script/pack.mjs", "start": "electron ./dist/main/index.js --inspect=9229", "lint": "eslint \"src/**/*.{ts,tsx}\"", + "test": "node --experimental-strip-types --disable-warning=ExperimentalWarning --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --test src/main/lib/hdc/bundleInfos.test.ts", "init": "npm run resources", "resources": "zx script/resources.mjs", "format": "lsla prettier \"src/**/*.{ts,tsx,scss,css,json}\" \"*.{js,ts,json}\" \"script/*.mjs\" --write", diff --git a/src/main/lib/hdc/bundle.ts b/src/main/lib/hdc/bundle.ts index 911cacb..0700ce0 100644 --- a/src/main/lib/hdc/bundle.ts +++ b/src/main/lib/hdc/bundle.ts @@ -1,5 +1,4 @@ import { - IBundleInfo, IpcCleanBundleCache, IpcCleanBundleData, IpcGetBundleInfos, @@ -21,6 +20,7 @@ import map from 'licia/map' import startWith from 'licia/startWith' import filter from 'licia/filter' import contain from 'licia/contain' +import { getBundleInfosFromDumps } from './bundleInfos' const logger = log('hdcBundle') @@ -65,48 +65,15 @@ function isSystemBundle(bundle: string) { } const getBundleInfos: IpcGetBundleInfos = async (connectKey, bundleNames) => { - const result: IBundleInfo[] = [] - const dumpInfos = await shell( connectKey, map(bundleNames, (name) => `bm dump -n ${name}`) ) - const infos = map(dumpInfos, (dump) => { - const lines = dump.split('\n') - return JSON.parse(lines.slice(1).join('\n')) - }) - - for (let i = 0, len = bundleNames.length; i < len; i++) { - const bundleName = bundleNames[i] - const bundleInfo: IBundleInfo = { - bundleName, - label: bundleName, - icon: '', - system: false, - versionName: '', - apiTargetVersion: 0, - vendor: '', - installTime: 0, - releaseType: '', - } - - const info = infos[i] - const applicationInfo = info.applicationInfo - bundleInfo.system = applicationInfo.isSystemApp - bundleInfo.versionName = applicationInfo.versionName - bundleInfo.apiTargetVersion = applicationInfo.apiTargetVersion - bundleInfo.vendor = applicationInfo.vendor - bundleInfo.installTime = info.installTime - bundleInfo.releaseType = info.releaseType - - const mainEntry = info.mainEntry - if (mainEntry) { - const mainModuleInfo = - info.hapModuleInfos[info.hapModuleNames.indexOf(mainEntry)] - bundleInfo.mainAbility = - mainModuleInfo.mainAbility || mainModuleInfo.abilityInfos[0].name - } + const result = getBundleInfosFromDumps(bundleNames, dumpInfos) + for (let i = 0, len = result.length; i < len; i++) { + const bundleInfo = result[i] + const bundleName = bundleInfo.bundleName if (!bundleInfo.system && !startWith(bundleName, 'com.huawei')) { try { const onlineInfo = await getOnlineBundleInfo(bundleName) @@ -120,7 +87,6 @@ const getBundleInfos: IpcGetBundleInfos = async (connectKey, bundleNames) => { logger.error(e) } } - result.push(bundleInfo) } return result diff --git a/src/main/lib/hdc/bundleInfos.test.ts b/src/main/lib/hdc/bundleInfos.test.ts new file mode 100644 index 0000000..60d1ae0 --- /dev/null +++ b/src/main/lib/hdc/bundleInfos.test.ts @@ -0,0 +1,121 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { getBundleInfosFromDumps } from './bundleInfos.ts' + +function dumpText(bundleName: string, info: unknown) { + return `${bundleName}:\n${JSON.stringify(info, null, 4)}` +} + +function happyDump(bundleName: string) { + return dumpText(bundleName, { + installTime: 1710000000000, + releaseType: 'Release', + mainEntry: 'entry', + hapModuleNames: ['entry'], + hapModuleInfos: [ + { + name: 'entry', + mainAbility: 'EntryAbility', + abilityInfos: [{ name: 'EntryAbility' }], + }, + ], + applicationInfo: { + isSystemApp: false, + versionName: '1.2.3', + apiTargetVersion: 12, + vendor: 'Example', + }, + }) +} + +function missingMainEntryDump(bundleName: string) { + return dumpText(bundleName, { + installTime: 1710000001000, + releaseType: 'Release', + mainEntry: 'entry', + hapModuleNames: ['feature'], + hapModuleInfos: [ + { + name: 'feature', + mainAbility: 'FeatureAbility', + abilityInfos: [{ name: 'FeatureAbility' }], + }, + ], + applicationInfo: { + isSystemApp: false, + versionName: '2.0.0', + apiTargetVersion: 11, + vendor: 'Other', + }, + }) +} + +describe('getBundleInfosFromDumps', () => { + it('returns bundle info on the happy path', () => { + const bundleName = 'com.example.good' + assert.deepEqual( + getBundleInfosFromDumps([bundleName], [happyDump(bundleName)]), + [ + { + bundleName, + label: bundleName, + icon: '', + system: false, + versionName: '1.2.3', + apiTargetVersion: 12, + vendor: 'Example', + installTime: 1710000000000, + releaseType: 'Release', + mainAbility: 'EntryAbility', + }, + ], + ) + }) + + it('keeps other bundles when one dump is not JSON', () => { + const good = 'com.example.good' + const bad = 'com.example.broken' + const infos = getBundleInfosFromDumps( + [good, bad], + [happyDump(good), `${bad}:\nnot-json {{{`], + ) + + assert.equal(infos.length, 1) + assert.equal(infos[0].bundleName, good) + assert.equal(infos[0].mainAbility, 'EntryAbility') + }) + + it('lists a bundle when mainEntry indexOf is -1', () => { + const orphan = 'com.example.orphan' + const infos = getBundleInfosFromDumps( + [orphan], + [missingMainEntryDump(orphan)], + ) + + assert.equal(infos.length, 1) + assert.equal(infos[0].bundleName, orphan) + assert.equal(infos[0].versionName, '2.0.0') + assert.equal(infos[0].mainAbility, undefined) + }) + + it('does not empty the list when a bad JSON dump is mixed with indexOf -1', () => { + const good = 'com.example.good' + const bad = 'com.example.broken' + const orphan = 'com.example.orphan' + const infos = getBundleInfosFromDumps( + [good, bad, orphan], + [ + happyDump(good), + `${bad}:\n[truncated dump`, + missingMainEntryDump(orphan), + ], + ) + + assert.deepEqual( + infos.map((info) => info.bundleName), + [good, orphan], + ) + assert.equal(infos[0].mainAbility, 'EntryAbility') + assert.equal(infos[1].mainAbility, undefined) + }) +}) diff --git a/src/main/lib/hdc/bundleInfos.ts b/src/main/lib/hdc/bundleInfos.ts new file mode 100644 index 0000000..20d2f04 --- /dev/null +++ b/src/main/lib/hdc/bundleInfos.ts @@ -0,0 +1,85 @@ +export interface BundleInfo { + bundleName: string + versionName: string + icon: string + label: string + system: boolean + apiTargetVersion: number + vendor: string + installTime: number + releaseType: string + mainAbility?: string +} + +export function parseBmDumpJson(dump: string): any { + const lines = dump.split('\n') + return JSON.parse(lines.slice(1).join('\n')) +} + +export function createBundleInfo(bundleName: string, info: any): BundleInfo { + const bundleInfo: BundleInfo = { + bundleName, + label: bundleName, + icon: '', + system: false, + versionName: '', + apiTargetVersion: 0, + vendor: '', + installTime: 0, + releaseType: '', + } + + const applicationInfo = info.applicationInfo + bundleInfo.system = applicationInfo.isSystemApp + bundleInfo.versionName = applicationInfo.versionName + bundleInfo.apiTargetVersion = applicationInfo.apiTargetVersion + bundleInfo.vendor = applicationInfo.vendor + bundleInfo.installTime = info.installTime + bundleInfo.releaseType = info.releaseType + + const mainEntry = info.mainEntry + if (mainEntry) { + const hapModuleNames = info.hapModuleNames + const hapModuleInfos = info.hapModuleInfos + const index = Array.isArray(hapModuleNames) + ? hapModuleNames.indexOf(mainEntry) + : -1 + const mainModuleInfo = + index === -1 || !Array.isArray(hapModuleInfos) + ? undefined + : hapModuleInfos[index] + if (mainModuleInfo) { + const fallbackAbility = Array.isArray(mainModuleInfo.abilityInfos) + ? mainModuleInfo.abilityInfos[0]?.name + : undefined + if (mainModuleInfo.mainAbility || fallbackAbility) { + bundleInfo.mainAbility = mainModuleInfo.mainAbility || fallbackAbility + } + } + } + + return bundleInfo +} + +export function getBundleInfosFromDumps( + bundleNames: string[], + dumpInfos: string[], +): BundleInfo[] { + const result: BundleInfo[] = [] + + for (let i = 0, len = bundleNames.length; i < len; i++) { + const bundleName = bundleNames[i] + try { + const dump = dumpInfos[i] + if (typeof dump !== 'string') { + continue + } + const info = parseBmDumpJson(dump) + result.push(createBundleInfo(bundleName, info)) + } catch { + continue + } + } + + return result +}