Merge VS Code 1.26.1 (#2394)

* Squash merge commits for 1.26 (#1) (#2323)

* Polish tag search as per feedback (#55269)

* Polish tag search as per feedback

* Updated regex

* Allow users to opt-out of features that send online requests in the background (#55097)

* settings sweep #54690

* Minor css tweaks to enable eoverflow elipsis in more places (#55277)

* fix an issue with titlebarheight when not scaling with zoom

* Settings descriptions update #54690

* fixes #55209

* Settings editor - many padding fixes

* More space above level 2 label

* Fixing Cannot debug npm script using Yarn #55103

* Settings editor - show ellipsis when description overflows

* Settings editor - ... fix measuring around links, relayout

* Setting descriptions

* Settings editor - fix ... for some short lines, fix select container width

* Settings editor - overlay trees so scrollable shadow is full width

* Fix #54133 - missing extension settings after reload

* Settings color token description tweak

* Settings editor - disable overflow indicator temporarily, needs to be faster

* Added command to Run the selected npm script

* fixes #54452

* fixes #54929

* fixes #55248

* prefix command with extension name

* Contribute run selected to the context menu

* node-debug@1.26.6

* Allow terminal rendererType to be swapped out at runtime

Part of #53274
Fixes #55344

* Settings editor - fix not focusing search when restoring editor
setInput must be actually async. Will be fixed naturally when we aren't using winJS promises...

* Settings editor - TOC should only expand the section with a selected item

* Bump node-debug2

* Settings editor - Tree focus outlines

* Settings editor - don't blink the scrollbar when toc selection changes
And hide TOC correctly when the editor is narrow

* Settings editor - header rows should not be selectable

* fixes #54877

* change debug assignee to isi

* Settings sweep (#54690)

* workaround for #55051

* Settings sweep (#54690)

* settings sweep

#54690

* Don't try closing tags when you type > after another >

* Describe what implementation code lens does

Fixes #55370

* fix javadoc formatter setting description

* fixes #55325

* update to officical TS version

* Settings editor - Even more padding, use semibold instead of bold

* Fix #55357 - fix TOC twistie

* fixes #55288

* explorer: refresh on di change file system provider registration

fixes #53256

* Disable push to Linux repo to test standalone publisher

* New env var to notify log level to extensions #54001

* Disable snippets in extension search (when not in suggest dropdown) (#55281)

* Disable snippits in extension search (when not in suggest dropdown)

* Add monaco input contributions

* Fix bug preventing snippetSuggestions from taking effect in sub-editors

* Latest emmet helper to fix #52366

* Fix comment updates for threads within same file

* Allow extensions to log telemetry to log files #54001

* Pull latest css grammar

* files.exclude control - use same style for "add" vs "edit"

* files.exclude control - focus/keyboard behavior

* don't show menubar too early

* files.exclude - better styling

* Place cursor at end of extensions search box on autofill (#55254)

* Place cursor at end of extensions search box on autofill

* Use position instead of selection

* fix linux build issue (empty if block)

* Settings editor - fix extension category prefixes

* Settings editor - add simple ellipsis for first line that overflows, doesn't cover case when first line does not overflow but there is more text, TODO

* File/Text search provider docs

* Fixes #52655

* Include epoch (#55008)

* Fixes #53385

* Fixes #49480

*  VS Code Insiders (Users) not opening Fixes #55353

* Better handling of the case when the extension host fails to start

* Fixes #53966

*  Remove confusing Start from wordPartLeft commands ID

* vscode-xterm@3.6.0-beta12

Fixes #55488

* Initial size is set to infinity!! Fixes #55461

* Polish embeddedEditorBackground

* configuration service misses event

* Fix #55224 - fix duplicate results in multiroot workspace from splitting the diskseach query

* Select all not working in issue reporter on mac, fixes #55424

* Disable fuzzy matching for extensions autosuggest (#55498)

* Fix clipping of extensions search border in some third party themes (#55504)

* fixes #55538

* Fix bug causing an aria alert to not be shown the third time
 (and odd numbers thereafter)

* Settings editor - work around rendering glitch with webkit-line-clamp

* Settings editor - revert earlier '...' changes

* Settings editor - move enumDescription to its own div, because it disturbs -webkit-line-clamp for some reason

* Settings editor - better overflow indicator

* Don't show existing filters in autocomplete (#55495)

* Dont show existing filters in autocomplete

* Simplify

* Settings Editor: Add aria labels for input elements Fixes: #54836 (#55543)

* fixes #55223

* Update vscode-css-languageservice to 3.0.10-next.1

* Fix #55509 - settings navigation

* Fix #55519

* Fix #55520

* FIx #55524

* Fix #55556 - include wordSeparators in all search queries, so findTextInFiles can respect isWordMatch correctly

* oss updates for endgame

* Fix unit tests

* fixes #55522

* Avoid missing manifest error from bubbling up #54757

* Settings format crawl

* Search provider - Fix FileSearchProvider to return array, not progress

* Fix #55598

* Settings editor - fix NPE rendering settings with no description

* dont render inden guides in search box (#55600)

* fixes #55454

* More settings crawl

* Another change for #55598 - maxResults applies to FileSearch and TextSearch but not FileIndex

* Fix FileSearchProvider unit tests for progress change

* fixes #55561

* Settings description update for #54690

* Update setting descriptions for online services

* Minor edits

* fixes #55513

* fixes #55451

* Fix #55612 - fix findTextInFiles cancellation

* fixes #55539

* More setting description tweaks

* Setting to disable online experiments #54354

* fixes #55507

* fixes #55515

* Show online services action only in Insiders for now

* Settings editor - change toc behavior default to 'filter'

* Settings editor - nicer filter count style during search

* Fix #55617 - search viewlet icons

* Settings editor - better styling for element count indicator

* SearchProvider - fix NPE when searching extraFileResources

* Allow extends to work without json suffix

Fixes #16905

* Remove accessability options logic entirely

Follow up on #55451

* use latest version of DAP

* fixes #55490

* fixes #55122

* fixes #52332

* Avoid assumptions about git: URIs (fixes #36236)

* relative path for descriptions

* resourece: get rid of isFile context key

fixes #48275

* Register previous ids for compatibility (#53497)

* more tuning for #48275

* no need to always re-read "files explorer"

fixes #52003

* read out active composites properly

fixes #51967

* Update link colors for hc theme to meet color contrast ratio, fixes #55651

Also updated link color for `textLinkActiveForeground` to be the same as `textLinkForeground` as it wasn't properly updated

* detect 'winpty-agent.exe'; fixes #55672

* node-debug@1.26.7

* reset counter on new label

* Settings editor - fix multiple setting links in one description

* Settings editor - color code blocks in setting descriptions, fix #55532

* Settings editor - hover color in TOC

* Settings editor - fix navigation NPE

* Settings editor - fix text control width

* Settings editor - maybe fix #55684

* Fix bug causing cursor to not move on paste

* fixes #53582

* Use ctrlCmd instead of ctrl for go down from search box

* fixes #55264

* fixes #55456

* filter for spcaes before triggering search (#55611)

* Fix #55698 - don't lose filtered TOC counts when refreshing TOC

* fixes #55421

* fixes #28979

* fixes #55576

* only add check for updates to windows/linux help

* readonly files: append decoration to label

fixes #53022

* debug: do not show toolbar while initialising

fixes #55026

* Opening launch.json should not activate debug extensions

fixes #55029

* fixes #55435

* fixes #55434

* fixes #55439

* trigger menu only on altkey up

* Fix #50555 - fix settings editor memory leak

* Fix #55712 - no need to focus 'a' anymore when restoring control focus after tree render

* fixes #55335

* proper fix for readonly model

fixes #53022

* improve FoldingRangeKind spec (for #55686)

* Use class with static fields (fixes #55494)

* Fixes #53671

* fixes #54630

* [html] should disable ionic suggestions by default. Currently forces deprecated Ionic v1 suggestions in .html files while typing. Fixes #53324

* cleanup deps

* debug issues back to andre

* update electron for smoketest

* Fix #55757 - prevent settings tabs from overflowing

* Fix #53897 - revert setting menu defaults to old editor

* Add enum descriptions to `typescript.preferences.importModuleSpecifier`

* Fix #55767 - leaking style elements from settings editor

* Fix #55521 - prevent flashing when clicking in exclude control

* Update Git modified color for contrast ratio, fixes #53140

* Revert "Merge branch 'master' of github.com:Microsoft/vscode"

This reverts commit bf46b6bfbae0cab99c2863e1244a916181fa9fbc, reversing
changes made to e275a424483dfb4ed33b428c97d5e2c441d6b917.

* Revert "Revert "Merge branch 'master' of github.com:Microsoft/vscode""

This reverts commit 53949d963f39e40757557c6526332354a31d9154.

* don't ask to install an incomplete menu

* Fix NPE in terminal AccessibilityManager

Fixes #55744

* don't display fallback menu unless we've closed the last window

* fixes #55547

* Fix smoke tests for extension search box

* Update OSSREADME.json for Electron 2.0.5

* Update distro

Includes Chromium license changes

* fix #55455

* fix #55865

* fixes #55893

* Fix bug causing workspace recommendations to go away upon ignoring a recommendation (#55805)

* Fix bug causing workspace recommendations to go away upon ignoring a recommendation

* ONly show on @recommended or @recommended:workspace

* Make more consistant

* Fix #55911

* Understand json activity (#55926)

* Understand json file activity

* Refactoring

* adding composer.json

* Distro update for experiments

* use terminal.processId for auto-attach; fixes #55918

* Reject invalid URI with vscode.openFolder (for #55891)

* improve win32 setup system vs user detection

fixes #55840

fixes #55840

delay winreg import

related to #55840

show notification earlier

related to #55840

fix #55840

update inno setup message

related to #55840

* Fix #55593 - this code only operates on local paths, so use fsPath and Uri.file instead

* Bring back the old menu due to electron 2.0 issues (#55913)

* add the old menu back for native menus

* make menu labels match

* `vscode.openFolder`: treat missing URI schema gracefully (for #55891)

* delay EH reattach; fixes #55955

* Mark all json files under appSettingsHome as settings

* Use localized strings for telemetry opt-out

* Exception when saving file editor opened from remote file provider (fixes #55051)

* Remove terminal menu from stable

Fixes 56003

* VSCode Insiders crashes on open with TypeError: Cannot read property 'lastIndexOf' of undefined. Fixes #54933

* improve fix for #55891

* fix #55916

* Improve #55891

* increase EH debugging restart delay; fixes #55955

* Revert "Don't include non-resource entries in history quick pick"

This reverts commit 37209a838e9f7e9abe6dc53ed73cdf1e03b72060.

* Diff editor: horizontal scrollbar height is smaller (fixes #56062)

* improve openFolder uri fix (correctly treat backslashes)

* fixes #56116
repair ipc for native menubar keybindings

* Fix #56240 - Open the JSON settings editor instead of the UI editor

* Fix #55536

* uriDisplay: if no formatter is registered fall back to getPathlabel

fixes #56104

* VSCode hangs when opening python file. Fixes #56377

* VS Code Hangs When Opening Specific PowerShell File. Fixes #56430

* Fix #56433 - search extraFileResources even when no folders open

* Workaround #55649

* Fix in master #56371

* Fix tests #56371

* Fix in master #56317

* increase version to 1.26.1

* Fixes #56387: Handle SIGPIPE in extension host

* fixes #56185

* Fix merge issues (part 1)

* Fix build breaks (part 1)

* Build breaks (part 2)

* Build breaks (part 3)

* More build breaks (part 4)

* Fix build breaks (part 5)

* WIP

* Fix menus

* Render query result and message panels (#2363)

* Put back query editor hot exit changes

* Fix grid changes that broke profiler (#2365)

* Update APIs for saving query editor state

* Fix restore view state for profiler and edit data

* Updating custom default themes to support 4.5:1 contrast ratio

* Test updates

* Fix Extension Manager and Windows Setup

* Update license headers

* Add appveyor and travis files back

* Fix hidden modal dropdown issue
This commit is contained in:
Karl Burtram
2018-09-04 14:55:00 -07:00
committed by GitHub
parent 3763278366
commit 81329fa7fa
2638 changed files with 118456 additions and 64012 deletions

View File

@@ -11,18 +11,19 @@ import * as pfs from 'vs/base/node/pfs';
import * as errors from 'vs/base/common/errors';
import { assign } from 'vs/base/common/objects';
import { toDisposable, Disposable } from 'vs/base/common/lifecycle';
import { flatten, distinct } from 'vs/base/common/arrays';
import { flatten } from 'vs/base/common/arrays';
import { extract, buffer, ExtractError } from 'vs/base/node/zip';
import { TPromise } from 'vs/base/common/winjs.base';
import { TPromise, ValueCallback, ErrorCallback } from 'vs/base/common/winjs.base';
import {
IExtensionManagementService, IExtensionGalleryService, ILocalExtension,
IGalleryExtension, IExtensionManifest, IGalleryMetadata,
InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, LocalExtensionType,
StatisticType,
IExtensionIdentifier,
IReportedExtension
IReportedExtension,
InstallOperation
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { getGalleryExtensionIdFromLocal, adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, groupByExtension, getMaliciousExtensionsSet, getLocalExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { getGalleryExtensionIdFromLocal, adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, groupByExtension, getMaliciousExtensionsSet, getLocalExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, getIdFromLocalExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { localizeManifest } from '../common/extensionNls';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { Limiter, always } from 'vs/base/common/async';
@@ -52,6 +53,7 @@ const INSTALL_ERROR_LOCAL = 'local';
const INSTALL_ERROR_EXTRACTING = 'extracting';
const INSTALL_ERROR_RENAMING = 'renaming';
const INSTALL_ERROR_DELETING = 'deleting';
const INSTALL_ERROR_MALICIOUS = 'malicious';
const ERROR_UNKNOWN = 'unknown';
export class ExtensionManagementError extends Error {
@@ -102,11 +104,6 @@ interface InstallableExtension {
metadata?: IGalleryMetadata;
}
enum Operation {
Install = 1,
Update
}
export class ExtensionManagementService extends Disposable implements IExtensionManagementService {
_serviceBrand: any;
@@ -116,8 +113,8 @@ export class ExtensionManagementService extends Disposable implements IExtension
private uninstalledFileLimiter: Limiter<void>;
private reportedExtensions: TPromise<IReportedExtension[]> | undefined;
private lastReportTimestamp = 0;
private readonly installationStartTime: Map<string, number> = new Map<string, number>();
private readonly installingExtensions: Map<string, TPromise<ILocalExtension>> = new Map<string, TPromise<ILocalExtension>>();
private readonly installingExtensions: Map<string, TPromise<void>> = new Map<string, TPromise<void>>();
private readonly uninstallingExtensions: Map<string, TPromise<void>> = new Map<string, TPromise<void>>();
private readonly manifestCache: ExtensionsManifestCache;
private readonly extensionLifecycle: ExtensionsLifecycle;
@@ -144,12 +141,18 @@ export class ExtensionManagementService extends Disposable implements IExtension
this.extensionsPath = environmentService.extensionsPath;
this.uninstalledPath = path.join(this.extensionsPath, '.obsolete');
this.uninstalledFileLimiter = new Limiter(1);
this._register(toDisposable(() => this.installingExtensions.clear()));
this.manifestCache = this._register(new ExtensionsManifestCache(environmentService, this));
this.extensionLifecycle = this._register(new ExtensionsLifecycle(this.logService));
this._register(toDisposable(() => {
this.installingExtensions.forEach(promise => promise.cancel());
this.uninstallingExtensions.forEach(promise => promise.cancel());
this.installingExtensions.clear();
this.uninstallingExtensions.clear();
}));
}
install(zipPath: string): TPromise<ILocalExtension> {
install(zipPath: string): TPromise<void> {
zipPath = path.resolve(zipPath);
return validateLocalExtension(zipPath)
@@ -171,14 +174,15 @@ export class ExtensionManagementService extends Disposable implements IExtension
// Until there's a gallery for SQL Ops Studio, skip retrieving the metadata from the gallery
return this.installExtension({ zipPath, id: identifier.id, metadata: null })
.then(
local => this._onDidInstallExtension.fire({ identifier, zipPath, local }),
error => { this._onDidInstallExtension.fire({ identifier, zipPath, error }); return TPromise.wrapError(error); }
local => this._onDidInstallExtension.fire({ identifier, zipPath, local, operation: InstallOperation.Install }),
error => { this._onDidInstallExtension.fire({ identifier, zipPath, error, operation: InstallOperation.Install }); return TPromise.wrapError(error); }
);
/*
return this.getMetadata(getGalleryExtensionId(manifest.publisher, manifest.name))
.then(
metadata => this.installFromZipPath(identifier, zipPath, metadata, manifest),
error => this.installFromZipPath(identifier, zipPath, null, manifest));
local => { this.logService.info('Successfully installed the extension:', identifier.id); return local; },
*/
}
return null;
@@ -216,45 +220,73 @@ export class ExtensionManagementService extends Disposable implements IExtension
});
}
private installFromZipPath(identifier: IExtensionIdentifier, zipPath: string, metadata: IGalleryMetadata, manifest: IExtensionManifest): TPromise<ILocalExtension> {
return this.installExtension({ zipPath, id: identifier.id, metadata })
.then(local => {
if (this.galleryService.isEnabled() && local.manifest.extensionDependencies && local.manifest.extensionDependencies.length) {
return this.getDependenciesToInstall(local.manifest.extensionDependencies)
.then(dependenciesToInstall => this.downloadAndInstallExtensions(metadata ? dependenciesToInstall.filter(d => d.identifier.uuid !== metadata.id) : dependenciesToInstall))
.then(() => local, error => {
this.setUninstalled(local);
return TPromise.wrapError(new Error(nls.localize('errorInstallingDependencies', "Error while installing dependencies. {0}", error instanceof Error ? error.message : error)));
// private installFromZipPath(identifier: IExtensionIdentifier, zipPath: string, metadata: IGalleryMetadata, manifest: IExtensionManifest): TPromise<ILocalExtension> {
// return this.toNonCancellablePromise(this.getInstalled()
// .then(installed => {
// const operation = this.getOperation({ id: getIdFromLocalExtensionId(identifier.id), uuid: identifier.uuid }, installed);
// return this.installExtension({ zipPath, id: identifier.id, metadata })
// .then(local => this.installDependenciesAndPackExtensions(local, null).then(() => local, error => this.uninstall(local, true).then(() => TPromise.wrapError(error), () => TPromise.wrapError(error))))
// .then(
// local => { this._onDidInstallExtension.fire({ identifier, zipPath, local, operation }); return local; },
// error => { this._onDidInstallExtension.fire({ identifier, zipPath, operation, error }); return TPromise.wrapError(error); }
// );
// }));
// }
installFromGallery(extension: IGalleryExtension): TPromise<void> {
let installingExtension = this.installingExtensions.get(extension.identifier.id);
if (!installingExtension) {
let successCallback: ValueCallback<void>, errorCallback: ErrorCallback;
installingExtension = new TPromise((c, e) => { successCallback = c; errorCallback = e; });
this.installingExtensions.set(extension.identifier.id, installingExtension);
try {
const startTime = new Date().getTime();
const identifier = { id: getLocalExtensionIdFromGallery(extension, extension.version), uuid: extension.identifier.uuid };
const telemetryData = getGalleryExtensionTelemetryData(extension);
let operation: InstallOperation = InstallOperation.Install;
this.logService.info('Installing extension:', extension.name);
this._onInstallExtension.fire({ identifier, gallery: extension });
this.checkMalicious(extension)
.then(() => this.getInstalled(LocalExtensionType.User))
.then(installed => {
const existingExtension = installed.filter(i => areSameExtensions(i.galleryIdentifier, extension.identifier))[0];
operation = existingExtension ? InstallOperation.Update : InstallOperation.Install;
return this.downloadInstallableExtension(extension, operation)
.then(installableExtension => this.installExtension(installableExtension).then(local => always(pfs.rimraf(installableExtension.zipPath), () => null).then(() => local)))
.then(local => this.installDependenciesAndPackExtensions(local, existingExtension)
.then(() => local, error => this.uninstall(local, true).then(() => TPromise.wrapError(error), () => TPromise.wrapError(error))));
})
.then(
local => {
this.installingExtensions.delete(extension.identifier.id);
this.logService.info(`Extensions installed successfully:`, extension.identifier.id);
this._onDidInstallExtension.fire({ identifier, gallery: extension, local, operation });
this.reportTelemetry(this.getTelemetryEvent(operation), telemetryData, new Date().getTime() - startTime, void 0);
successCallback(null);
},
error => {
this.installingExtensions.delete(extension.identifier.id);
const errorCode = error && (<ExtensionManagementError>error).code ? (<ExtensionManagementError>error).code : ERROR_UNKNOWN;
this.logService.error(`Failed to install extension:`, extension.identifier.id, error ? error.message : errorCode);
this._onDidInstallExtension.fire({ identifier, gallery: extension, operation, error: errorCode });
this.reportTelemetry(this.getTelemetryEvent(operation), telemetryData, new Date().getTime() - startTime, error);
errorCallback(error);
});
}
return local;
})
.then(
local => { this._onDidInstallExtension.fire({ identifier, zipPath, local }); return local; },
error => { this._onDidInstallExtension.fire({ identifier, zipPath, error }); return TPromise.wrapError(error); }
);
} catch (error) {
this.installingExtensions.delete(extension.identifier.id);
errorCallback(error);
}
}
return installingExtension;
}
installFromGallery(extension: IGalleryExtension): TPromise<ILocalExtension> {
this.onInstallExtensions([extension]);
return this.getInstalled(LocalExtensionType.User)
.then(installed => this.collectExtensionsToInstall(extension)
.then(
extensionsToInstall => {
if (extensionsToInstall.length > 1) {
this.onInstallExtensions(extensionsToInstall.slice(1));
}
const operataions: Operation[] = extensionsToInstall.map(e => this.getOperation(e, installed));
return this.downloadAndInstallExtensions(extensionsToInstall)
.then(
locals => this.onDidInstallExtensions(extensionsToInstall, locals, operataions, [])
.then(() => locals.filter(l => areSameExtensions({ id: getGalleryExtensionIdFromLocal(l), uuid: l.identifier.uuid }, extension.identifier)[0])),
errors => this.onDidInstallExtensions(extensionsToInstall, [], operataions, errors));
},
error => this.onDidInstallExtensions([extension], [], [this.getOperation(extension, installed)], [error])));
}
reinstallFromGallery(extension: ILocalExtension): TPromise<ILocalExtension> {
reinstallFromGallery(extension: ILocalExtension): TPromise<void> {
if (!this.galleryService.isEnabled()) {
return TPromise.wrapError(new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled")));
}
@@ -271,54 +303,26 @@ export class ExtensionManagementService extends Disposable implements IExtension
});
}
private getOperation(extensionToInstall: IGalleryExtension, installed: ILocalExtension[]): Operation {
return installed.some(i => areSameExtensions({ id: getGalleryExtensionIdFromLocal(i), uuid: i.identifier.uuid }, extensionToInstall.identifier)) ? Operation.Update : Operation.Install;
// private getOperation(extensionToInstall: IGalleryExtension, installed: ILocalExtension[]): Operation {
// return installed.some(i => areSameExtensions({ id: getGalleryExtensionIdFromLocal(i), uuid: i.identifier.uuid }, extensionToInstall.identifier)) ? Operation.Update : Operation.Install;
// }
private getTelemetryEvent(operation: InstallOperation): string {
return operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install';
}
private collectExtensionsToInstall(extension: IGalleryExtension): TPromise<IGalleryExtension[]> {
return this.galleryService.loadCompatibleVersion(extension)
.then(compatible => {
if (!compatible) {
// {{SQL CARBON EDIT}}
return TPromise.wrapError<IGalleryExtension[]>(new ExtensionManagementError(nls.localize('notFoundCompatible', "Unable to install because, the extension '{0}' compatible with current version '{1}' of SQL Operations Studio is not found.", extension.identifier.id, pkg.version), INSTALL_ERROR_INCOMPATIBLE));
private checkMalicious(extension: IGalleryExtension): TPromise<void> {
return this.getExtensionsReport()
.then(report => {
if (getMaliciousExtensionsSet(report).has(extension.identifier.id)) {
throw new ExtensionManagementError(INSTALL_ERROR_MALICIOUS, nls.localize('malicious extension', "Can't install extension since it was reported to be problematic."));
} else {
return null;
}
return this.getDependenciesToInstall(compatible.properties.dependencies)
.then(
dependenciesToInstall => ([compatible, ...dependenciesToInstall.filter(d => d.identifier.uuid !== compatible.identifier.uuid)]),
error => TPromise.wrapError<IGalleryExtension[]>(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY)));
},
error => TPromise.wrapError<IGalleryExtension[]>(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY)));
});
}
private downloadAndInstallExtensions(extensions: IGalleryExtension[]): TPromise<ILocalExtension[]> {
return TPromise.join(extensions.map(extensionToInstall => this.downloadAndInstallExtension(extensionToInstall)))
.then(null, errors => this.rollback(extensions).then(() => TPromise.wrapError(errors), () => TPromise.wrapError(errors)));
}
private downloadAndInstallExtension(extension: IGalleryExtension): TPromise<ILocalExtension> {
let installingExtension = this.installingExtensions.get(extension.identifier.id);
if (!installingExtension) {
installingExtension = this.getExtensionsReport()
.then(report => {
if (getMaliciousExtensionsSet(report).has(extension.identifier.id)) {
throw new Error(nls.localize('malicious extension', "Can't install extension since it was reported to be problematic."));
} else {
return extension;
}
})
.then(extension => this.downloadInstallableExtension(extension))
.then(installableExtension => this.installExtension(installableExtension))
.then(
local => { this.installingExtensions.delete(extension.identifier.id); return local; },
e => { this.installingExtensions.delete(extension.identifier.id); return TPromise.wrapError(e); }
);
this.installingExtensions.set(extension.identifier.id, installingExtension);
}
return installingExtension;
}
private downloadInstallableExtension(extension: IGalleryExtension): TPromise<InstallableExtension> {
private downloadInstallableExtension(extension: IGalleryExtension, operation: InstallOperation): TPromise<InstallableExtension> {
const metadata = <IGalleryMetadata>{
id: extension.identifier.uuid,
publisherId: extension.publisherId,
@@ -330,10 +334,10 @@ export class ExtensionManagementService extends Disposable implements IExtension
compatible => {
if (compatible) {
this.logService.trace('Started downloading extension:', extension.name);
return this.galleryService.download(extension)
return this.galleryService.download(extension, operation)
.then(
zipPath => {
this.logService.info('Downloaded extension:', extension.name);
this.logService.info('Downloaded extension:', extension.name, zipPath);
return validateLocalExtension(zipPath)
.then(
manifest => (<InstallableExtension>{ zipPath, id: getLocalExtensionIdFromManifest(manifest), metadata }),
@@ -346,54 +350,9 @@ export class ExtensionManagementService extends Disposable implements IExtension
}
},
error => TPromise.wrapError<InstallableExtension>(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY)));
}
private onInstallExtensions(extensions: IGalleryExtension[]): void {
for (const extension of extensions) {
this.logService.info('Installing extension:', extension.name);
this.installationStartTime.set(extension.identifier.id, new Date().getTime());
const id = getLocalExtensionIdFromGallery(extension, extension.version);
this._onInstallExtension.fire({ identifier: { id, uuid: extension.identifier.uuid }, gallery: extension });
}
}
private onDidInstallExtensions(extensions: IGalleryExtension[], locals: ILocalExtension[], operations: Operation[], errors: Error[]): TPromise<any> {
extensions.forEach((gallery, index) => {
const identifier = { id: getLocalExtensionIdFromGallery(gallery, gallery.version), uuid: gallery.identifier.uuid };
const local = locals[index];
const error = errors[index];
if (local) {
this.logService.info(`Extensions installed successfully:`, gallery.identifier.id);
this._onDidInstallExtension.fire({ identifier, gallery, local });
} else {
const errorCode = error && (<ExtensionManagementError>error).code ? (<ExtensionManagementError>error).code : ERROR_UNKNOWN;
this.logService.error(`Failed to install extension:`, gallery.identifier.id, error ? error.message : errorCode);
this._onDidInstallExtension.fire({ identifier, gallery, error: errorCode });
}
const startTime = this.installationStartTime.get(gallery.identifier.id);
this.reportTelemetry(operations[index] === Operation.Install ? 'extensionGallery:install' : 'extensionGallery:update', getGalleryExtensionTelemetryData(gallery), startTime ? new Date().getTime() - startTime : void 0, error);
this.installationStartTime.delete(gallery.identifier.id);
});
return errors.length ? TPromise.wrapError(this.joinErrors(errors)) : TPromise.as(null);
}
private getDependenciesToInstall(dependencies: string[]): TPromise<IGalleryExtension[]> {
if (dependencies.length) {
return this.getInstalled()
.then(installed => {
const uninstalledDeps = dependencies.filter(d => installed.every(i => getGalleryExtensionId(i.manifest.publisher, i.manifest.name) !== d));
if (uninstalledDeps.length) {
return this.galleryService.loadAllDependencies(uninstalledDeps.map(id => (<IExtensionIdentifier>{ id })))
.then(allDependencies => allDependencies.filter(d => {
const extensionId = getLocalExtensionIdFromGallery(d, d.version);
return installed.every(({ identifier }) => identifier.id !== extensionId);
}));
}
return [];
});
}
return TPromise.as([]);
}
private installExtension(installableExtension: InstallableExtension): TPromise<ILocalExtension> {
return this.unsetUninstalledAndGetLocal(installableExtension.id)
@@ -432,7 +391,8 @@ export class ExtensionManagementService extends Disposable implements IExtension
private extractAndInstall({ zipPath, id, metadata }: InstallableExtension): TPromise<ILocalExtension> {
const tempPath = path.join(this.extensionsPath, `.${id}`);
const extensionPath = path.join(this.extensionsPath, id);
return this.extractAndRename(id, zipPath, tempPath, extensionPath)
return pfs.rimraf(extensionPath)
.then(() => this.extractAndRename(id, zipPath, tempPath, extensionPath), e => TPromise.wrapError(new ExtensionManagementError(nls.localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionPath, id), INSTALL_ERROR_DELETING)))
.then(() => {
this.logService.info('Installation completed.', id);
return this.scanExtension(id, this.extensionsPath, LocalExtensionType.User);
@@ -448,7 +408,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
private extractAndRename(id: string, zipPath: string, extractPath: string, renamePath: string): TPromise<void> {
return this.extract(id, zipPath, extractPath)
.then(() => this.rename(id, extractPath, renamePath, Date.now() + (30 * 1000) /* Retry for 30 seconds */)
.then(() => this.rename(id, extractPath, renamePath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */)
.then(
() => this.logService.info('Renamed to', renamePath),
e => {
@@ -480,22 +440,55 @@ export class ExtensionManagementService extends Disposable implements IExtension
});
}
private installDependenciesAndPackExtensions(installed: ILocalExtension, existing: ILocalExtension): TPromise<void> {
if (this.galleryService.isEnabled()) {
const dependenciesAndPackExtensions: string[] = installed.manifest.extensionDependencies || [];
if (installed.manifest.extensionPack) {
for (const extension of installed.manifest.extensionPack) {
// add only those extensions which are new in currently installed extension
if (!(existing && existing.manifest.extensionPack && existing.manifest.extensionPack.some(old => areSameExtensions({ id: old }, { id: extension })))) {
if (dependenciesAndPackExtensions.every(e => !areSameExtensions({ id: e }, { id: extension }))) {
dependenciesAndPackExtensions.push(extension);
}
}
}
}
if (dependenciesAndPackExtensions.length) {
return this.getInstalled()
.then(installed => {
// filter out installing and installed extensions
const names = dependenciesAndPackExtensions.filter(id => !this.installingExtensions.has(adoptToGalleryExtensionId(id)) && installed.every(({ galleryIdentifier }) => !areSameExtensions(galleryIdentifier, { id })));
if (names.length) {
return this.galleryService.query({ names, pageSize: dependenciesAndPackExtensions.length })
.then(galleryResult => {
const extensionsToInstall = galleryResult.firstPage;
return TPromise.join(extensionsToInstall.map(e => this.installFromGallery(e)))
.then(() => null, errors => this.rollback(extensionsToInstall).then(() => TPromise.wrapError(errors), () => TPromise.wrapError(errors)));
});
}
return null;
});
}
}
return TPromise.as(null);
}
private rollback(extensions: IGalleryExtension[]): TPromise<void> {
return this.getInstalled(LocalExtensionType.User)
.then(installed =>
TPromise.join(installed.filter(local => extensions.some(galleryExtension => local.identifier.id === getLocalExtensionIdFromGallery(galleryExtension, galleryExtension.version))) // Only check id (pub.name-version) because we want to rollback the exact version
.map(local => this.setUninstalled(local))))
.map(local => this.uninstall(local, true))))
.then(() => null, () => null);
}
uninstall(extension: ILocalExtension, force = false): TPromise<void> {
return this.getInstalled(LocalExtensionType.User)
return this.toNonCancellablePromise(this.getInstalled(LocalExtensionType.User)
.then(installed => {
const promises = installed
.filter(e => e.manifest.publisher === extension.manifest.publisher && e.manifest.name === extension.manifest.name)
.map(e => this.checkForDependenciesAndUninstall(e, installed, force));
return TPromise.join(promises).then(() => null, error => TPromise.wrapError(this.joinErrors(error)));
});
}));
}
updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): TPromise<ILocalExtension> {
@@ -552,7 +545,22 @@ export class ExtensionManagementService extends Disposable implements IExtension
private checkForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[], force: boolean): TPromise<void> {
return this.preUninstallExtension(extension)
.then(() => this.hasDependencies(extension, installed) ? this.promptForDependenciesAndUninstall(extension, installed, force) : this.promptAndUninstall(extension, installed, force))
.then(() => {
const packedExtensions = this.getAllPackExtensionsToUninstall(extension, installed);
if (packedExtensions.length) {
return this.uninstallExtensions(extension, packedExtensions, installed);
}
const dependencies = this.getDependenciesToUninstall(extension, installed);
if (dependencies.length) {
if (force) {
return this.uninstallExtensions(extension, dependencies, installed);
} else {
return this.promptForDependenciesAndUninstall(extension, dependencies, installed);
}
} else {
return this.uninstallExtensions(extension, [], installed);
}
})
.then(() => this.postUninstallExtension(extension),
error => {
this.postUninstallExtension(extension, new ExtensionManagementError(error instanceof Error ? error.message : error, INSTALL_ERROR_LOCAL));
@@ -560,68 +568,35 @@ export class ExtensionManagementService extends Disposable implements IExtension
});
}
private hasDependencies(extension: ILocalExtension, installed: ILocalExtension[]): boolean {
if (extension.manifest.extensionDependencies && extension.manifest.extensionDependencies.length) {
return installed.some(i => extension.manifest.extensionDependencies.indexOf(getGalleryExtensionIdFromLocal(i)) !== -1);
}
return false;
}
private promptForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[], force: boolean): TPromise<void> {
if (force) {
const dependencies = distinct(this.getDependenciesToUninstallRecursively(extension, installed, [])).filter(e => e !== extension);
return this.uninstallWithDependencies(extension, dependencies, installed);
}
const message = nls.localize('uninstallDependeciesConfirmation', "Would you like to uninstall '{0}' only or its dependencies also?", extension.manifest.displayName || extension.manifest.name);
private promptForDependenciesAndUninstall(extension: ILocalExtension, dependencies: ILocalExtension[], installed: ILocalExtension[]): TPromise<void> {
const message = nls.localize('uninstallDependeciesConfirmation', "Also uninstall the dependencies of the extension '{0}'?", extension.manifest.displayName || extension.manifest.name);
const buttons = [
nls.localize('uninstallOnly', "Extension Only"),
nls.localize('uninstallAll', "Uninstall All"),
nls.localize('yes', "Yes"),
nls.localize('no', "No"),
nls.localize('cancel', "Cancel")
];
this.logService.info('Requesting for confirmation to uninstall extension with dependencies', extension.identifier.id);
return this.dialogService.show(Severity.Info, message, buttons, { cancelId: 2 })
.then<void>(value => {
if (value === 0) {
return this.uninstallWithDependencies(extension, [], installed);
return this.uninstallExtensions(extension, dependencies, installed);
}
if (value === 1) {
const dependencies = distinct(this.getDependenciesToUninstallRecursively(extension, installed, [])).filter(e => e !== extension);
return this.uninstallWithDependencies(extension, dependencies, installed);
return this.uninstallExtensions(extension, [], installed);
}
this.logService.info('Cancelled uninstalling extension:', extension.identifier.id);
return TPromise.wrapError(errors.canceled());
}, error => TPromise.wrapError(errors.canceled()));
}
private promptAndUninstall(extension: ILocalExtension, installed: ILocalExtension[], force: boolean): TPromise<void> {
if (force) {
return this.uninstallWithDependencies(extension, [], installed);
}
const message = nls.localize('uninstallConfirmation', "Are you sure you want to uninstall '{0}'?", extension.manifest.displayName || extension.manifest.name);
const buttons = [
nls.localize('ok', "OK"),
nls.localize('cancel', "Cancel")
];
this.logService.info('Requesting for confirmation to uninstall extension', extension.identifier.id);
return this.dialogService.show(Severity.Info, message, buttons, { cancelId: 1 })
.then<void>(value => {
if (value === 0) {
return this.uninstallWithDependencies(extension, [], installed);
}
this.logService.info('Cancelled uninstalling extension:', extension.identifier.id);
return TPromise.wrapError(errors.canceled());
}, error => TPromise.wrapError(errors.canceled()));
}
private uninstallWithDependencies(extension: ILocalExtension, dependencies: ILocalExtension[], installed: ILocalExtension[]): TPromise<void> {
const dependenciesToUninstall = this.filterDependents(extension, dependencies, installed);
let dependents = this.getDependents(extension, installed).filter(dependent => extension !== dependent && dependenciesToUninstall.indexOf(dependent) === -1);
private uninstallExtensions(extension: ILocalExtension, otherExtensionsToUninstall: ILocalExtension[], installed: ILocalExtension[]): TPromise<void> {
const dependents = this.getDependents(extension, installed);
if (dependents.length) {
return TPromise.wrapError<void>(new Error(this.getDependentsErrorMessage(extension, dependents)));
const remainingDependents = dependents.filter(dependent => extension !== dependent && otherExtensionsToUninstall.indexOf(dependent) === -1);
if (remainingDependents.length) {
return TPromise.wrapError<void>(new Error(this.getDependentsErrorMessage(extension, remainingDependents)));
}
}
return TPromise.join([this.uninstallExtension(extension), ...dependenciesToUninstall.map(d => this.doUninstall(d))]).then(() => null);
return TPromise.join([this.uninstallExtension(extension), ...otherExtensionsToUninstall.map(d => this.doUninstall(d))]).then(() => null);
}
private getDependentsErrorMessage(extension: ILocalExtension, dependents: ILocalExtension[]): string {
@@ -637,7 +612,23 @@ export class ExtensionManagementService extends Disposable implements IExtension
extension.manifest.displayName || extension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name);
}
private getDependenciesToUninstallRecursively(extension: ILocalExtension, installed: ILocalExtension[], checked: ILocalExtension[]): ILocalExtension[] {
private getDependenciesToUninstall(extension: ILocalExtension, installed: ILocalExtension[]): ILocalExtension[] {
const dependencies = this.getAllDependenciesToUninstall(extension, installed).filter(e => e !== extension);
const dependenciesToUninstall = dependencies.slice(0);
for (let index = 0; index < dependencies.length; index++) {
const dep = dependencies[index];
const dependents = this.getDependents(dep, installed);
// Remove the dependency from the uninstall list if there is a dependent which will not be uninstalled.
if (dependents.some(e => e !== extension && dependencies.indexOf(e) === -1)) {
dependenciesToUninstall.splice(index - (dependencies.length - dependenciesToUninstall.length), 1);
}
}
return dependenciesToUninstall;
}
private getAllDependenciesToUninstall(extension: ILocalExtension, installed: ILocalExtension[], checked: ILocalExtension[] = []): ILocalExtension[] {
if (checked.indexOf(extension) !== -1) {
return [];
}
@@ -645,29 +636,32 @@ export class ExtensionManagementService extends Disposable implements IExtension
if (!extension.manifest.extensionDependencies || extension.manifest.extensionDependencies.length === 0) {
return [];
}
const dependenciesToUninstall = installed.filter(i => extension.manifest.extensionDependencies.indexOf(getGalleryExtensionIdFromLocal(i)) !== -1);
const dependenciesToUninstall = installed.filter(i => extension.manifest.extensionDependencies.some(id => areSameExtensions({ id }, i.galleryIdentifier)));
const depsOfDeps = [];
for (const dep of dependenciesToUninstall) {
depsOfDeps.push(...this.getDependenciesToUninstallRecursively(dep, installed, checked));
depsOfDeps.push(...this.getAllDependenciesToUninstall(dep, installed, checked));
}
return [...dependenciesToUninstall, ...depsOfDeps];
}
private filterDependents(extension: ILocalExtension, dependencies: ILocalExtension[], installed: ILocalExtension[]): ILocalExtension[] {
installed = installed.filter(i => i !== extension && i.manifest.extensionDependencies && i.manifest.extensionDependencies.length > 0);
let result = dependencies.slice(0);
for (let i = 0; i < dependencies.length; i++) {
const dep = dependencies[i];
const dependents = this.getDependents(dep, installed).filter(e => dependencies.indexOf(e) === -1);
if (dependents.length) {
result.splice(i - (dependencies.length - result.length), 1);
}
private getAllPackExtensionsToUninstall(extension: ILocalExtension, installed: ILocalExtension[], checked: ILocalExtension[] = []): ILocalExtension[] {
if (checked.indexOf(extension) !== -1) {
return [];
}
return result;
checked.push(extension);
if (!extension.manifest.extensionPack || extension.manifest.extensionPack.length === 0) {
return [];
}
const packedExtensions = installed.filter(i => extension.manifest.extensionPack.some(id => areSameExtensions({ id }, i.galleryIdentifier)));
const packOfPackedExtensions = [];
for (const packedExtension of packedExtensions) {
packOfPackedExtensions.push(...this.getAllPackExtensionsToUninstall(packedExtension, installed, checked));
}
return [...packedExtensions, ...packOfPackedExtensions];
}
private getDependents(extension: ILocalExtension, installed: ILocalExtension[]): ILocalExtension[] {
return installed.filter(e => e.manifest.extensionDependencies && e.manifest.extensionDependencies.indexOf(getGalleryExtensionIdFromLocal(extension)) !== -1);
return installed.filter(e => e.manifest.extensionDependencies && e.manifest.extensionDependencies.some(id => areSameExtensions({ id }, extension.galleryIdentifier)));
}
private doUninstall(extension: ILocalExtension): TPromise<void> {
@@ -681,7 +675,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
}
private preUninstallExtension(extension: ILocalExtension): TPromise<void> {
return pfs.exists(extension.path)
return pfs.exists(extension.location.fsPath)
.then(exists => exists ? null : TPromise.wrapError(new Error(nls.localize('notExists', "Could not find extension"))))
.then(() => {
this.logService.info('Uninstalling extension:', extension.identifier.id);
@@ -690,12 +684,19 @@ export class ExtensionManagementService extends Disposable implements IExtension
}
private uninstallExtension(local: ILocalExtension): TPromise<void> {
// Set all versions of the extension as uninstalled
return this.scanUserExtensions(false)
.then(userExtensions => this.setUninstalled(...userExtensions.filter(u => areSameExtensions({ id: getGalleryExtensionIdFromLocal(u), uuid: u.identifier.uuid }, { id: getGalleryExtensionIdFromLocal(local), uuid: local.identifier.uuid }))));
const id = getGalleryExtensionIdFromLocal(local);
let promise = this.uninstallingExtensions.get(id);
if (!promise) {
// Set all versions of the extension as uninstalled
promise = this.scanUserExtensions(false)
.then(userExtensions => this.setUninstalled(...userExtensions.filter(u => areSameExtensions({ id: getGalleryExtensionIdFromLocal(u), uuid: u.identifier.uuid }, { id, uuid: local.identifier.uuid }))))
.then(() => { this.uninstallingExtensions.delete(id); });
this.uninstallingExtensions.set(id, promise);
}
return promise;
}
private async postUninstallExtension(extension: ILocalExtension, error?: Error): TPromise<void> {
private async postUninstallExtension(extension: ILocalExtension, error?: Error): Promise<void> {
if (error) {
this.logService.error('Failed to uninstall extension:', extension.identifier.id, error.message);
} else {
@@ -769,8 +770,12 @@ export class ExtensionManagementService extends Disposable implements IExtension
if (manifest.extensionDependencies) {
manifest.extensionDependencies = manifest.extensionDependencies.map(id => adoptToGalleryExtensionId(id));
}
if (manifest.extensionPack) {
manifest.extensionPack = manifest.extensionPack.map(id => adoptToGalleryExtensionId(id));
}
const identifier = { id: type === LocalExtensionType.System ? folderName : getLocalExtensionIdFromManifest(manifest), uuid: metadata ? metadata.id : null };
return { type, identifier, manifest, metadata, path: extensionPath, readmeUrl, changelogUrl };
const galleryIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name), uuid: identifier.uuid };
return { type, identifier, galleryIdentifier, manifest, metadata, location: URI.file(extensionPath), readmeUrl, changelogUrl };
}))
.then(null, () => null);
}
@@ -811,7 +816,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
private removeExtension(extension: ILocalExtension, type: string): TPromise<void> {
this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id);
return pfs.rimraf(extension.path).then(() => this.logService.info('Deleted from disk', extension.identifier.id));
return pfs.rimraf(extension.location.fsPath).then(() => this.logService.info('Deleted from disk', extension.identifier.id));
}
private isUninstalled(id: string): TPromise<boolean> {
@@ -886,10 +891,25 @@ export class ExtensionManagementService extends Disposable implements IExtension
});
}
private toNonCancellablePromise<T>(promise: TPromise<T>): TPromise<T> {
return new TPromise((c, e) => promise.then(result => c(result), error => e(error)), () => this.logService.debug('Request Cancelled'));
}
private reportTelemetry(eventName: string, extensionData: any, duration: number, error?: Error): void {
const errorcode = error ? error instanceof ExtensionManagementError ? error.code : ERROR_UNKNOWN : void 0;
/* __GDPR__
"extensionGallery:install" : {
"success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
"recommendationReason": { "retiredFromVersion": "1.23.0", "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"${include}": [
"${GalleryExtensionTelemetryData}"
]
}
*/
/* __GDPR__
"extensionGallery:uninstall" : {
"success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
@@ -899,7 +919,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
}
*/
/* __GDPR__
"extensionGallery:uninstall" : {
"extensionGallery:update" : {
"success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },