UNPKG

appcenter-cli

Version:

Command line tool for Visual Studio App Center

272 lines (235 loc) 11 kB
import { clientRequest, AppCenterClient, models } from "../../util/apis"; import { AppCommand, CommandResult } from "../../util/commandline"; import { ErrorCodes, failure, success } from "../../util/commandline"; import { help, name, position } from "../../util/commandline"; import { inspect } from "util"; import { out } from "../../util/interaction"; import { DefaultApp } from "../../util/profile"; import UploadSymbolsHelper, { SymbolType } from "./lib/symbols-uploading-helper"; import { getSymbolsZipFromXcarchive } from "./lib/subfolder-symbols-helper"; import { createTempFileFromZip } from "./lib/temp-zip-file-helper"; import { mdfind } from "./lib/mdfind"; import * as Pfs from "../../util/misc/promisfied-fs"; import * as Path from "path"; import * as JsZip from "jszip"; import * as JsZipHelper from "../../util/misc/jszip-helper"; import * as _ from "lodash"; import * as Os from "os"; import * as ChildProcess from "child_process"; const debug = require("debug")("appcenter-cli:commands:apps:crashes:upload-missing-symbols"); const bplist = require("bplist"); const MAX_SQL_INTEGER = 2147483647; @help("Upload missing crash symbols for the application (only from macOS)") export default class UploadMissingSymbols extends AppCommand { @help("Path to a dSYM package or a directory containing dSYM packages") @position(0) @name("search-path") public symbolsPath: string; public async run(client: AppCenterClient): Promise<CommandResult> { if (Os.platform() !== "darwin") { return failure(ErrorCodes.IllegalCommand, "This command must be run under macOS"); } const app: DefaultApp = this.app; await this.validateParameters(); const missingSymbolsIds: string[] = await out.progress("Getting list of missing symbols...", this.getMissingSymbolsIds(client, app)); let output: { missingSymbols: number, found: number }; if (missingSymbolsIds.length) { // there are missing symbols - find and upload them const uuidToPath = await out.progress("Searching for missing symbols...", this.searchForMissingSymbols(missingSymbolsIds, client, app)); const found = await out.progress("Uploading found symbols...", this.uploadFoundSymbols(uuidToPath, client, app)); output = { missingSymbols: missingSymbolsIds.length, found }; } else { output = { missingSymbols: 0, found: 0 }; } out.text((result) => { return `${result.missingSymbols} symbols are needed to symbolicate all crashes` + Os.EOL + `${result.found} of these symbols were found and uploaded`; }, output); return success(); } private async validateParameters(): Promise<void> { if (!_.isNil(this.symbolsPath)) { if (!(await Pfs.exists(this.symbolsPath))) { throw failure(ErrorCodes.InvalidParameter, `path ${this.symbolsPath} doesn't exist`); } } } private async getMissingSymbolsIds(client: AppCenterClient, app: DefaultApp): Promise<string[]> { try { const httpResponse = await clientRequest<models.V2MissingSymbolCrashGroupsResponse>((cb) => client.missingSymbolGroups.list(MAX_SQL_INTEGER, app.ownerName, app.appName, cb)); return _.flatten(httpResponse.result.groups .map((crashGroup) => crashGroup.missingSymbols.filter((s) => s.status === "missing").map((s) => s.symbolId))); } catch (error) { debug(`Failed to get list of missing symbols - ${inspect(error)}`); throw failure(ErrorCodes.Exception, "failed to get list of missing symbols"); } } private async searchForMissingSymbols(missingSymbolsIds: string[], client: AppCenterClient, app: DefaultApp): Promise<Map<string, string>> { console.assert(missingSymbolsIds.every((id) => /^[0-9a-f]{32}$/g.test(id)), "the API has returned abnormal missing symbols IDs"); const missingSymbolsUuids: string[] = missingSymbolsIds.map((id) => id.toUpperCase().match(/(.{8})(.{4})(.{4})(.{4})(.{12})/).slice(1).join("-")); let uuidToPath: Map<string, string>; if (_.isNil(this.symbolsPath)) { // symbols path is not specified, looking in default locations // searching with mdfind uuidToPath = await this.getMdfindResultsForUuids(missingSymbolsUuids); // check if all of the missing symbols were found const notYetFoundUuids = Array.from(uuidToPath.keys()).filter((key) => _.isNull(uuidToPath.get(key))); if (notYetFoundUuids.length) { // looking for the rest of missing symbols in Xcode Archive folder const xcodeArchivesPath = await this.getXcodeArchiveFolderLocation(); if (xcodeArchivesPath) { // xcode is installed, searching for dSYMs in Archives folder uuidToPath = new Map(Array.from(uuidToPath).concat(Array.from(await this.searchDsyms(xcodeArchivesPath, notYetFoundUuids)))); } } } else { uuidToPath = await this.searchDsyms(this.symbolsPath, _.clone(missingSymbolsUuids)); } return uuidToPath; } private async uploadFoundSymbols(uuidToPath: Map<string, string>, client: AppCenterClient, app: DefaultApp): Promise<number> { // packing and uploading each found dSYM package const helper = new UploadSymbolsHelper(client, app, debug); const paths = Array.from(uuidToPath.values()).filter((path) => !_.isNull(path)).map((path) => Path.resolve(path)); const uniquePaths = _.uniq(paths); for (const path of uniquePaths) { await this.uploadSymbolsZip(path, helper); } return paths.length; } private async getMdfindResultsForUuids(uuids: string[]): Promise<Map<string, string | null>> { const uuidToPath = new Map<string, string>(); for (const uuid of uuids) { uuidToPath.set(uuid, await this.executeMdfindSearch(uuid)); } return uuidToPath; } private executeMdfindSearch(uuid: string): Promise<string | null> { return new Promise<string>((resolve, reject) => { const context = mdfind({query: `com_apple_xcode_dsym_uuids == ${uuid}`}); let result: string = null; context.output .on("data", (data: any) => { // *.xcarchive symbols have higher priority over non-archive symbols result = data.kMDItemPath; if (Path.extname(result) === ".xcarchive") { // stop search and return xcarchive context.terminate(); resolve(result); } }) .on("error", (err: any) => reject(err)) .on("end", () => resolve(result)); // return what was found (or null if nothing was found) }).catch((error) => { debug(`Failed to find symbols for ${uuid} using mdfind - ${inspect(error)}`); throw failure(ErrorCodes.Exception, `failed to find symbols for ${uuid} using mdfind`); }); } private async getXcodeArchiveFolderLocation(): Promise<string | null> { let xcodeSettingsBuffer: Buffer; try { xcodeSettingsBuffer = await Pfs.readFile(Path.join(Os.homedir(), "Library/Preferences/com.apple.dt.Xcode.plist")); } catch (error) { if (error.code === "ENOENT") { // Xcode settings file not found, most likely xcode is not installed return null; } else { debug(`Failed to read Xcode settings file - ${inspect(error)}`); throw failure(ErrorCodes.Exception, "failed to read Xcode settings file"); } } try { const xcodeSettings = await this.parseBinaryPlist(xcodeSettingsBuffer); // return default value if custom is not specified return xcodeSettings[0].IDECustomDistributionArchivesLocation || Path.join(Os.homedir(), "Library/Developer/Xcode/Archives"); } catch (error) { debug(`Failed to process Xcode settings - ${inspect(error)}`); throw failure(ErrorCodes.Exception, "failed to process Xcode settings"); } } private parseBinaryPlist(buffer: Buffer): Promise<any> { return new Promise<any>((resolve, reject) => { bplist.parseBuffer(buffer, (error: any, result: any) => { if (error) { reject(error); } else { resolve(result); } }); }); } private async searchDsyms(path: string, uuids: string[]): Promise<Map<string, string>> { if (uuids.length) { // get list of children entities (and check the existence of path) let childrenEntities: string[]; try { childrenEntities = await Pfs.readdir(path); } catch (error) { if (error.code === "ENOENT" || error.code === "ENOTDIR") { return new Map(); } else { throw error; } } let uuidToDsym: Map<string, string>; if (Path.extname(path) === ".dSYM") { const dSymUuids = await this.extractUuidsFromDsym(path); uuidToDsym = new Map(); for (const dsymUuid of dSymUuids) { if (uuids.indexOf(dsymUuid) > -1) { // removing found uuid from uuids to quickly stop execution when all of the uuids are found _.pull(uuids, dsymUuid); uuidToDsym.set(dsymUuid, path); } } } else { let childrenEntitiesMaps: Array<[string, string]> = []; for (const childrenEntity of childrenEntities) { const pathToChildrenEntity = Path.join(path, childrenEntity); childrenEntitiesMaps = childrenEntitiesMaps.concat(Array.from(await this.searchDsyms(pathToChildrenEntity, uuids))); } uuidToDsym = new Map(childrenEntitiesMaps); } return uuidToDsym; } else { return new Map(); } } private async extractUuidsFromDsym(path: string): Promise<string[]> { try { const dwarfDumpOutput = await this.runExternalApp(`dwarfdump --uuid "${path}"`); return dwarfDumpOutput.match(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/g) || []; } catch (error) { debug(`Failed to get UUID from dSym ${path} - ${inspect(error)}`); throw failure(ErrorCodes.Exception, `failed to get UUID from dSym ${path}`); } } private runExternalApp(command: string): Promise<string> { return new Promise<string>((resolve, reject) => { ChildProcess.exec(command, (error, stdout) => { if (error) { reject(error); } else { resolve(stdout); } }); }); } private async uploadSymbolsZip(path: string, helper: UploadSymbolsHelper): Promise<void> { let zip: JsZip; if (Path.extname(path) === ".xcarchive") { // *.xcarchive has symbols inside zip = await getSymbolsZipFromXcarchive(path, debug); } else { try { zip = new JsZip(); await JsZipHelper.addFolderToZipRecursively(path, zip); } catch (error) { debug(`Unable to add ${path} to the ZIP archive - ${inspect(error)}`); throw failure(ErrorCodes.Exception, `unable to add ${path} to the ZIP archive`); } } const tempFilePath = await createTempFileFromZip(zip); await helper.uploadSymbolsZip(tempFilePath, SymbolType.Apple); } }