Files
vscode/src/vs/workbench/contrib/extensions/node/extensionsWorkbenchService.ts
T
2019-04-16 12:18:04 +02:00

1125 lines
43 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as semver from 'semver';
import { Event, Emitter } from 'vs/base/common/event';
import { index, distinct } from 'vs/base/common/arrays';
import { ThrottledDelayer } from 'vs/base/common/async';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
import { IPager, mapPager, singlePagePager } from 'vs/base/common/paging';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import {
IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions,
InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, IExtensionEnablementService, IExtensionIdentifier, EnablementState, IExtensionManagementServerService, IExtensionManagementServer
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, getMaliciousExtensionsSet, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWindowService } from 'vs/platform/windows/common/windows';
import Severity from 'vs/base/common/severity';
import { URI } from 'vs/base/common/uri';
import { IExtension, IExtensionDependencies, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions';
import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { IURLService, IURLHandler } from 'vs/platform/url/common/url';
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import product from 'vs/platform/product/node/product';
import { ILogService } from 'vs/platform/log/common/log';
import { IProgressService2, ProgressLocation } from 'vs/platform/progress/common/progress';
import { INotificationService } from 'vs/platform/notification/common/notification';
import * as resources from 'vs/base/common/resources';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IFileService } from 'vs/platform/files/common/files';
import { IExtensionManifest, ExtensionType, ExtensionIdentifierWithVersion, IExtension as IPlatformExtension } from 'vs/platform/extensions/common/extensions';
interface IExtensionStateProvider<T> {
(extension: Extension): T;
}
class Extension implements IExtension {
public enablementState: EnablementState = EnablementState.Enabled;
constructor(
private galleryService: IExtensionGalleryService,
private stateProvider: IExtensionStateProvider<ExtensionState>,
public readonly server: IExtensionManagementServer | undefined,
public local: ILocalExtension | undefined,
public gallery: IGalleryExtension | undefined,
private telemetryService: ITelemetryService,
private logService: ILogService,
private fileService: IFileService
) { }
get type(): ExtensionType | undefined {
return this.local ? this.local.type : undefined;
}
get name(): string {
return this.gallery ? this.gallery.name : this.local!.manifest.name;
}
get displayName(): string {
if (this.gallery) {
return this.gallery.displayName || this.gallery.name;
}
return this.local!.manifest.displayName || this.local!.manifest.name;
}
get identifier(): IExtensionIdentifier {
if (this.gallery) {
return this.gallery.identifier;
}
return this.local!.identifier;
}
get uuid(): string | undefined {
return this.gallery ? this.gallery.identifier.uuid : this.local!.identifier.uuid;
}
get publisher(): string {
return this.gallery ? this.gallery.publisher : this.local!.manifest.publisher;
}
get publisherDisplayName(): string {
if (this.gallery) {
return this.gallery.publisherDisplayName || this.gallery.publisher;
}
if (this.local!.metadata && this.local!.metadata.publisherDisplayName) {
return this.local!.metadata.publisherDisplayName;
}
return this.local!.manifest.publisher;
}
get version(): string {
return this.local ? this.local.manifest.version : this.latestVersion;
}
get latestVersion(): string {
return this.gallery ? this.gallery.version : this.local!.manifest.version;
}
get description(): string {
return this.gallery ? this.gallery.description : this.local!.manifest.description || '';
}
get url(): string | undefined {
if (!product.extensionsGallery || !this.gallery) {
return undefined;
}
return `${product.extensionsGallery.itemUrl}?itemName=${this.publisher}.${this.name}`;
}
get iconUrl(): string {
return this.galleryIconUrl || this.localIconUrl || this.defaultIconUrl;
}
get iconUrlFallback(): string {
return this.galleryIconUrlFallback || this.localIconUrl || this.defaultIconUrl;
}
private get localIconUrl(): string | null {
if (this.local && this.local.manifest.icon) {
return resources.joinPath(this.local.location, this.local.manifest.icon).toString();
}
return null;
}
private get galleryIconUrl(): string | null {
return this.gallery ? this.gallery.assets.icon.uri : null;
}
private get galleryIconUrlFallback(): string | null {
return this.gallery ? this.gallery.assets.icon.fallbackUri : null;
}
private get defaultIconUrl(): string {
if (this.type === ExtensionType.System && this.local) {
if (this.local.manifest && this.local.manifest.contributes) {
if (Array.isArray(this.local.manifest.contributes.themes) && this.local.manifest.contributes.themes.length) {
return require.toUrl('../electron-browser/media/theme-icon.png');
}
if (Array.isArray(this.local.manifest.contributes.grammars) && this.local.manifest.contributes.grammars.length) {
return require.toUrl('../electron-browser/media/language-icon.svg');
}
}
}
return require.toUrl('../electron-browser/media/defaultIcon.png');
}
get repository(): string | undefined {
return this.gallery && this.gallery.assets.repository ? this.gallery.assets.repository.uri : undefined;
}
get licenseUrl(): string | undefined {
return this.gallery && this.gallery.assets.license ? this.gallery.assets.license.uri : undefined;
}
get state(): ExtensionState {
return this.stateProvider(this);
}
public isMalicious: boolean = false;
get installCount(): number | undefined {
return this.gallery ? this.gallery.installCount : undefined;
}
get rating(): number | undefined {
return this.gallery ? this.gallery.rating : undefined;
}
get ratingCount(): number | undefined {
return this.gallery ? this.gallery.ratingCount : undefined;
}
get outdated(): boolean {
return !!this.gallery && this.type === ExtensionType.User && semver.gt(this.latestVersion, this.version);
}
get telemetryData(): any {
const { local, gallery } = this;
if (gallery) {
return getGalleryExtensionTelemetryData(gallery);
} else {
return getLocalExtensionTelemetryData(local!);
}
}
get preview(): boolean {
return this.gallery ? this.gallery.preview : false;
}
private isGalleryOutdated(): boolean {
return this.local && this.gallery ? semver.gt(this.local.manifest.version, this.gallery.version) : false;
}
getManifest(token: CancellationToken): Promise<IExtensionManifest | null> {
if (this.gallery && !this.isGalleryOutdated()) {
if (this.gallery.assets.manifest) {
return this.galleryService.getManifest(this.gallery, token);
}
this.logService.error(nls.localize('Manifest is not found', "Manifest is not found"), this.identifier.id);
return Promise.resolve(null);
}
return Promise.resolve(this.local!.manifest);
}
hasReadme(): boolean {
if (this.gallery && !this.isGalleryOutdated() && this.gallery.assets.readme) {
return true;
}
if (this.local && this.local.readmeUrl) {
return true;
}
return this.type === ExtensionType.System;
}
getReadme(token: CancellationToken): Promise<string> {
if (this.gallery && !this.isGalleryOutdated()) {
if (this.gallery.assets.readme) {
return this.galleryService.getReadme(this.gallery, token);
}
this.telemetryService.publicLog('extensions:NotFoundReadMe', this.telemetryData);
}
if (this.local && this.local.readmeUrl) {
return this.fileService.readFile(this.local.readmeUrl).then(content => content.value.toString());
}
if (this.type === ExtensionType.System) {
return Promise.resolve(`# ${this.displayName || this.name}
**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled.
## Features
${this.description}
`);
}
return Promise.reject(new Error('not available'));
}
hasChangelog(): boolean {
if (this.gallery && this.gallery.assets.changelog && !this.isGalleryOutdated()) {
return true;
}
if (this.local && this.local.changelogUrl) {
return true;
}
return this.type === ExtensionType.System;
}
getChangelog(token: CancellationToken): Promise<string> {
if (this.gallery && this.gallery.assets.changelog && !this.isGalleryOutdated()) {
return this.galleryService.getChangelog(this.gallery, token);
}
const changelogUrl = this.local && this.local.changelogUrl;
if (!changelogUrl) {
if (this.type === ExtensionType.System) {
return Promise.resolve('Please check the [VS Code Release Notes](command:update.showCurrentReleaseNotes) for changes to the built-in extensions.');
}
return Promise.reject(new Error('not available'));
}
return this.fileService.readFile(changelogUrl).then(content => content.value.toString());
}
get dependencies(): string[] {
const { local, gallery } = this;
if (gallery && !this.isGalleryOutdated()) {
return gallery.properties.dependencies || [];
}
if (local && local.manifest.extensionDependencies) {
return local.manifest.extensionDependencies;
}
return [];
}
get extensionPack(): string[] {
const { local, gallery } = this;
if (gallery && !this.isGalleryOutdated()) {
return gallery.properties.extensionPack || [];
}
if (local && local.manifest.extensionPack) {
return local.manifest.extensionPack;
}
return [];
}
}
class ExtensionDependencies implements IExtensionDependencies {
private _hasDependencies: boolean | null = null;
constructor(private _extension: IExtension, private _identifier: string, private _map: Map<string, IExtension>, private _dependent: IExtensionDependencies | null = null) { }
get hasDependencies(): boolean {
if (this._hasDependencies === null) {
this._hasDependencies = this.computeHasDependencies();
}
return this._hasDependencies;
}
get extension(): IExtension {
return this._extension;
}
get identifier(): string {
return this._identifier;
}
get dependent(): IExtensionDependencies | null {
return this._dependent;
}
get dependencies(): IExtensionDependencies[] {
if (!this.hasDependencies) {
return [];
}
return this._extension.dependencies.map(id => new ExtensionDependencies(this._map.get(id)!, id, this._map, this));
}
private computeHasDependencies(): boolean {
if (this._extension && this._extension.dependencies.length > 0) {
let dependent = this._dependent;
while (dependent !== null) {
if (dependent.identifier === this.identifier) {
return false;
}
dependent = dependent.dependent;
}
return true;
}
return false;
}
}
class Extensions extends Disposable {
private readonly _onChange: Emitter<Extension | undefined> = new Emitter<Extension | undefined>();
get onChange(): Event<Extension | undefined> { return this._onChange.event; }
private readonly stateProvider: IExtensionStateProvider<ExtensionState>;
private installing: Extension[] = [];
private uninstalling: Extension[] = [];
private installed: Extension[] = [];
constructor(
private readonly server: IExtensionManagementServer,
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@ILogService private readonly logService: ILogService,
@IFileService private readonly fileService: IFileService,
@IExtensionEnablementService private readonly extensionEnablementService: IExtensionEnablementService
) {
super();
this.stateProvider = ext => this.getExtensionState(ext);
this._register(server.extensionManagementService.onInstallExtension(e => this.onInstallExtension(e)));
this._register(server.extensionManagementService.onDidInstallExtension(e => this.onDidInstallExtension(e)));
this._register(server.extensionManagementService.onUninstallExtension(e => this.onUninstallExtension(e)));
this._register(server.extensionManagementService.onDidUninstallExtension(e => this.onDidUninstallExtension(e)));
this._register(extensionEnablementService.onEnablementChanged(e => this.onEnablementChanged(e)));
}
get local(): IExtension[] {
const installing = this.installing
.filter(e => !this.installed.some(installed => areSameExtensions(installed.identifier, e.identifier)))
.map(e => e);
return [...this.installed, ...installing];
}
async queryInstalled(): Promise<IExtension[]> {
const installed = await this.server.extensionManagementService.getInstalled();
const byId = index(this.installed, e => e.identifier.id);
this.installed = installed.map(local => {
const extension = byId[local.identifier.id] || new Extension(this.galleryService, this.stateProvider, this.server, local, undefined, this.telemetryService, this.logService, this.fileService);
extension.local = local;
extension.enablementState = this.extensionEnablementService.getEnablementState(local);
return extension;
});
this._onChange.fire(undefined);
return this.local;
}
async syncLocalWithGalleryExtension(gallery: IGalleryExtension, maliciousExtensionSet: Set<string>): Promise<boolean> {
const extension = this.getInstalledExtensionMatchingGallery(gallery);
if (!extension) {
return false;
}
if (maliciousExtensionSet.has(extension.identifier.id)) {
extension.isMalicious = true;
}
// Loading the compatible version only there is an engine property
// Otherwise falling back to old way so that we will not make many roundtrips
const compatible = gallery.properties.engine ? await this.galleryService.getCompatibleExtension(gallery) : gallery;
if (!compatible) {
return false;
}
// Sync the local extension with gallery extension if local extension doesnot has metadata
if (extension.local) {
const local = extension.local.metadata ? extension.local : await this.server.extensionManagementService.updateMetadata(extension.local, { id: compatible.identifier.uuid, publisherDisplayName: compatible.publisherDisplayName, publisherId: compatible.publisherId });
extension.local = local;
extension.gallery = compatible;
this._onChange.fire(extension);
return true;
}
return false;
}
private getInstalledExtensionMatchingGallery(gallery: IGalleryExtension): Extension | null {
for (const installed of this.installed) {
if (installed.uuid) { // Installed from Gallery
if (installed.uuid === gallery.identifier.uuid) {
return installed;
}
} else {
if (areSameExtensions(installed.identifier, gallery.identifier)) { // Installed from other sources
return installed;
}
}
}
return null;
}
private onInstallExtension(event: InstallExtensionEvent): void {
const { gallery } = event;
if (gallery) {
const extension = this.installed.filter(e => areSameExtensions(e.identifier, gallery.identifier))[0]
|| new Extension(this.galleryService, this.stateProvider, this.server, undefined, gallery, this.telemetryService, this.logService, this.fileService);
this.installing.push(extension);
this._onChange.fire(extension);
}
}
private onDidInstallExtension(event: DidInstallExtensionEvent): void {
const { local, zipPath, error, gallery } = event;
const installingExtension = gallery ? this.installing.filter(e => areSameExtensions(e.identifier, gallery.identifier))[0] : null;
this.installing = installingExtension ? this.installing.filter(e => e !== installingExtension) : this.installing;
let extension: Extension | undefined = installingExtension ? installingExtension : zipPath ? new Extension(this.galleryService, this.stateProvider, this.server, local, undefined, this.telemetryService, this.logService, this.fileService) : undefined;
if (extension) {
if (local) {
const installed = this.installed.filter(e => areSameExtensions(e.identifier, extension!.identifier))[0];
if (installed) {
extension = installed;
} else {
this.installed.push(extension);
}
extension.local = local;
if (!extension.gallery) {
extension.gallery = gallery;
}
}
}
this._onChange.fire(error ? undefined : extension);
}
private onUninstallExtension(identifier: IExtensionIdentifier): void {
const extension = this.installed.filter(e => areSameExtensions(e.identifier, identifier))[0];
if (extension) {
const uninstalling = this.uninstalling.filter(e => areSameExtensions(e.identifier, identifier))[0] || extension;
this.uninstalling = [uninstalling, ...this.uninstalling.filter(e => !areSameExtensions(e.identifier, identifier))];
this._onChange.fire(uninstalling);
}
}
private onDidUninstallExtension({ identifier, error }: DidUninstallExtensionEvent): void {
if (!error) {
this.installed = this.installed.filter(e => !areSameExtensions(e.identifier, identifier));
}
const uninstalling = this.uninstalling.filter(e => areSameExtensions(e.identifier, identifier))[0];
this.uninstalling = this.uninstalling.filter(e => !areSameExtensions(e.identifier, identifier));
if (uninstalling) {
this._onChange.fire(uninstalling);
}
}
private onEnablementChanged(platformExtensions: IPlatformExtension[]) {
const extensions = this.local.filter(e => platformExtensions.some(p => areSameExtensions(e.identifier, p.identifier)));
for (const extension of extensions) {
if (extension.local) {
const enablementState = this.extensionEnablementService.getEnablementState(extension.local);
if (enablementState !== extension.enablementState) {
(extension as Extension).enablementState = enablementState;
this._onChange.fire(extension as Extension);
}
}
}
}
getExtensionState(extension: Extension): ExtensionState {
if (extension.gallery && this.installing.some(e => !!e.gallery && areSameExtensions(e.gallery.identifier, extension.gallery!.identifier))) {
return ExtensionState.Installing;
}
if (this.uninstalling.some(e => areSameExtensions(e.identifier, extension.identifier))) {
return ExtensionState.Uninstalling;
}
const local = this.installed.filter(e => e === extension || (e.gallery && extension.gallery && areSameExtensions(e.gallery.identifier, extension.gallery.identifier)))[0];
return local ? ExtensionState.Installed : ExtensionState.Uninstalled;
}
}
export class ExtensionsWorkbenchService extends Disposable implements IExtensionsWorkbenchService, IURLHandler {
private static readonly SyncPeriod = 1000 * 60 * 60 * 12; // 12 hours
_serviceBrand: any;
private readonly localExtensions: Extensions;
private readonly remoteExtensions: Extensions | null;
private syncDelayer: ThrottledDelayer<void>;
private autoUpdateDelayer: ThrottledDelayer<void>;
private disposables: IDisposable[] = [];
private readonly _onChange: Emitter<IExtension | undefined> = new Emitter<IExtension | undefined>();
get onChange(): Event<IExtension | undefined> { return this._onChange.event; }
private _extensionAllowedBadgeProviders: string[];
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IEditorService private readonly editorService: IEditorService,
@IExtensionManagementService private readonly extensionService: IExtensionManagementService,
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@INotificationService private readonly notificationService: INotificationService,
@IURLService urlService: IURLService,
@IExtensionEnablementService private readonly extensionEnablementService: IExtensionEnablementService,
@IWindowService private readonly windowService: IWindowService,
@ILogService private readonly logService: ILogService,
@IProgressService2 private readonly progressService: IProgressService2,
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
@IStorageService private readonly storageService: IStorageService,
@IFileService private readonly fileService: IFileService
) {
super();
this.localExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.localExtensionManagementServer));
this._register(this.localExtensions.onChange(e => this._onChange.fire(e)));
if (this.extensionManagementServerService.remoteExtensionManagementServer) {
this.remoteExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.remoteExtensionManagementServer));
this._register(this.remoteExtensions.onChange(e => this._onChange.fire(e)));
} else {
this.remoteExtensions = null;
}
this.syncDelayer = new ThrottledDelayer<void>(ExtensionsWorkbenchService.SyncPeriod);
this.autoUpdateDelayer = new ThrottledDelayer<void>(1000);
urlService.registerHandler(this);
this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(AutoUpdateConfigurationKey)) {
if (this.isAutoUpdateEnabled()) {
this.checkForUpdates();
}
}
if (e.affectsConfiguration(AutoCheckUpdatesConfigurationKey)) {
if (this.isAutoCheckUpdatesEnabled()) {
this.checkForUpdates();
}
}
}, this, this.disposables);
this.queryLocal().then(() => {
this.resetIgnoreAutoUpdateExtensions();
this.eventuallySyncWithGallery(true);
});
}
get local(): IExtension[] {
const result = [...this.localExtensions.local];
if (!this.remoteExtensions) {
return result;
}
result.push(...this.remoteExtensions.local);
const byId = groupByExtension(result, r => r.identifier);
return byId.reduce((result, extensions) => { result.push(this.getPrimaryExtension(extensions)); return result; }, []);
}
get outdated(): IExtension[] {
const allLocal = [...this.localExtensions.local];
if (this.remoteExtensions) {
allLocal.push(...this.remoteExtensions.local);
}
return allLocal.filter(e => e.outdated && e.local && e.state === ExtensionState.Installed);
}
async queryLocal(server?: IExtensionManagementServer): Promise<IExtension[]> {
if (server) {
if (this.extensionManagementServerService.localExtensionManagementServer === server) {
return this.localExtensions.queryInstalled();
}
if (this.remoteExtensions && this.extensionManagementServerService.remoteExtensionManagementServer === server) {
return this.remoteExtensions.queryInstalled();
}
}
await this.localExtensions.queryInstalled();
if (this.remoteExtensions) {
await Promise.all([this.localExtensions.queryInstalled(), this.remoteExtensions.queryInstalled()]);
} else {
await this.localExtensions.queryInstalled();
}
return this.local;
}
queryGallery(token: CancellationToken): Promise<IPager<IExtension>>;
queryGallery(options: IQueryOptions, token: CancellationToken): Promise<IPager<IExtension>>;
queryGallery(arg1: any, arg2?: any): Promise<IPager<IExtension>> {
const options: IQueryOptions = CancellationToken.isCancellationToken(arg1) ? {} : arg1;
const token: CancellationToken = CancellationToken.isCancellationToken(arg1) ? arg1 : arg2;
return this.extensionService.getExtensionsReport()
.then(report => {
const maliciousSet = getMaliciousExtensionsSet(report);
return this.galleryService.query(options, token)
.then(result => mapPager(result, gallery => this.fromGallery(gallery, maliciousSet)))
.then(undefined, err => {
if (/No extension gallery service configured/.test(err.message)) {
return Promise.resolve(singlePagePager([]));
}
return Promise.reject<IPager<IExtension>>(err);
});
});
}
loadDependencies(extension: IExtension, token: CancellationToken): Promise<IExtensionDependencies | null> {
if (!extension.dependencies.length) {
return Promise.resolve(null);
}
return this.extensionService.getExtensionsReport()
.then(report => {
const maliciousSet = getMaliciousExtensionsSet(report);
return this.galleryService.loadAllDependencies((<Extension>extension).dependencies.map(id => ({ id })), token)
.then(galleryExtensions => {
const extensions: IExtension[] = [...this.local, ...galleryExtensions.map(galleryExtension => this.fromGallery(galleryExtension, maliciousSet))];
const map = new Map<string, IExtension>();
for (const extension of extensions) {
map.set(extension.identifier.id, extension);
}
return new ExtensionDependencies(extension, extension.identifier.id, map);
});
});
}
open(extension: IExtension, sideByside: boolean = false): Promise<any> {
return Promise.resolve(this.editorService.openEditor(this.instantiationService.createInstance(ExtensionsInput, extension), undefined, sideByside ? SIDE_GROUP : ACTIVE_GROUP));
}
private getPrimaryExtension(extensions: IExtension[]): IExtension {
if (extensions.length === 1) {
return extensions[0];
}
const pickRemoteOrFirstExtension = (from: IExtension[]): IExtension => {
const remoteExtension = from.filter(e => e.server === this.extensionManagementServerService.remoteExtensionManagementServer)[0];
return remoteExtension ? remoteExtension : from[0];
};
const enabledExtensions = extensions.filter(e => e.local && this.extensionEnablementService.isEnabled(e.local));
return enabledExtensions.length === 1 ? enabledExtensions[0] : pickRemoteOrFirstExtension(extensions);
}
private fromGallery(gallery: IGalleryExtension, maliciousExtensionSet: Set<string>): IExtension {
Promise.all([this.localExtensions.syncLocalWithGalleryExtension(gallery, maliciousExtensionSet), this.remoteExtensions ? this.remoteExtensions.syncLocalWithGalleryExtension(gallery, maliciousExtensionSet) : Promise.resolve(false)])
.then(result => {
if (result[0] || result[1]) {
this.eventuallyAutoUpdateExtensions();
}
});
const installed = this.getInstalledExtensionMatchingGallery(gallery);
if (installed) {
return installed;
}
const extension = new Extension(this.galleryService, ext => this.getExtensionState(ext), undefined, undefined, gallery, this.telemetryService, this.logService, this.fileService);
if (maliciousExtensionSet.has(extension.identifier.id)) {
extension.isMalicious = true;
}
return extension;
}
private getInstalledExtensionMatchingGallery(gallery: IGalleryExtension): IExtension | null {
for (const installed of this.local) {
if (installed.identifier.uuid) { // Installed from Gallery
if (installed.identifier.uuid === gallery.identifier.uuid) {
return installed;
}
} else {
if (areSameExtensions(installed.identifier, gallery.identifier)) { // Installed from other sources
return installed;
}
}
}
return null;
}
private getExtensionState(extension: Extension): ExtensionState {
if (this.remoteExtensions) {
const state = this.remoteExtensions.getExtensionState(extension);
if (state !== ExtensionState.Uninstalled) {
return state;
}
}
return this.localExtensions.getExtensionState(extension);
}
checkForUpdates(): Promise<void> {
return Promise.resolve(this.syncDelayer.trigger(() => this.syncWithGallery(), 0));
}
private isAutoUpdateEnabled(): boolean {
return this.configurationService.getValue(AutoUpdateConfigurationKey);
}
private isAutoCheckUpdatesEnabled(): boolean {
return this.configurationService.getValue(AutoCheckUpdatesConfigurationKey);
}
private eventuallySyncWithGallery(immediate = false): void {
const shouldSync = this.isAutoUpdateEnabled() || this.isAutoCheckUpdatesEnabled();
const loop = () => (shouldSync ? this.syncWithGallery() : Promise.resolve(undefined)).then(() => this.eventuallySyncWithGallery());
const delay = immediate ? 0 : ExtensionsWorkbenchService.SyncPeriod;
this.syncDelayer.trigger(loop, delay)
.then(undefined, err => null);
}
private syncWithGallery(): Promise<void> {
const ids: string[] = [], names: string[] = [];
for (const installed of this.local) {
if (installed.type === ExtensionType.User) {
if (installed.identifier.uuid) {
ids.push(installed.identifier.uuid);
} else {
names.push(installed.identifier.id);
}
}
}
const promises: Promise<IPager<IExtension>>[] = [];
if (ids.length) {
promises.push(this.queryGallery({ ids, pageSize: ids.length }, CancellationToken.None));
}
if (names.length) {
promises.push(this.queryGallery({ names, pageSize: names.length }, CancellationToken.None));
}
return Promise.all(promises).then(() => undefined);
}
private eventuallyAutoUpdateExtensions(): void {
this.autoUpdateDelayer.trigger(() => this.autoUpdateExtensions())
.then(undefined, err => null);
}
private autoUpdateExtensions(): Promise<any> {
if (!this.isAutoUpdateEnabled()) {
return Promise.resolve();
}
const toUpdate = this.outdated.filter(e => !this.isAutoUpdateIgnored(new ExtensionIdentifierWithVersion(e.identifier, e.version)));
return Promise.all(toUpdate.map(e => this.install(e)));
}
canInstall(extension: IExtension): boolean {
if (!(extension instanceof Extension)) {
return false;
}
if (extension.isMalicious) {
return false;
}
return !!(extension as Extension).gallery;
}
install(extension: string | IExtension): Promise<IExtension> {
if (typeof extension === 'string') {
return this.installWithProgress(async () => {
const extensionIdentifier = await this.extensionService.install(URI.file(extension));
this.checkAndEnableDisabledDependencies(extensionIdentifier);
return this.local.filter(local => areSameExtensions(local.identifier, extensionIdentifier))[0];
});
}
if (extension.isMalicious) {
return Promise.reject(new Error(nls.localize('malicious', "This extension is reported to be problematic.")));
}
const gallery = extension.gallery;
if (!gallery) {
return Promise.reject(new Error('Missing gallery'));
}
return this.installWithProgress(async () => {
await this.extensionService.installFromGallery(gallery);
this.checkAndEnableDisabledDependencies(gallery.identifier);
return this.local.filter(local => areSameExtensions(local.identifier, gallery.identifier))[0];
}, gallery.displayName);
}
setEnablement(extensions: IExtension | IExtension[], enablementState: EnablementState): Promise<void> {
extensions = Array.isArray(extensions) ? extensions : [extensions];
return this.promptAndSetEnablement(extensions, enablementState);
}
uninstall(extension: IExtension): Promise<void> {
const ext = extension.local ? extension : this.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0];
const toUninstall: ILocalExtension | null = ext && ext.local ? ext.local : null;
if (!toUninstall) {
return Promise.reject(new Error('Missing local'));
}
this.logService.info(`Requested uninstalling the extension ${extension.identifier.id} from window ${this.windowService.windowId}`);
return this.progressService.withProgress({
location: ProgressLocation.Extensions,
title: nls.localize('uninstallingExtension', 'Uninstalling extension....'),
source: `${toUninstall.identifier.id}`
}, () => this.extensionService.uninstall(toUninstall).then(() => undefined));
}
installVersion(extension: IExtension, version: string): Promise<IExtension> {
if (!(extension instanceof Extension)) {
return Promise.resolve(extension);
}
if (!extension.gallery) {
return Promise.reject(new Error('Missing gallery'));
}
return this.galleryService.getCompatibleExtension(extension.gallery.identifier, version)
.then(gallery => {
if (!gallery) {
return Promise.reject(new Error(nls.localize('incompatible', "Unable to install extension '{0}' with version '{1}' as it is not compatible with VS Code.", extension.gallery!.identifier.id, version)));
}
return this.installWithProgress(async () => {
await this.extensionService.installFromGallery(gallery);
if (extension.latestVersion !== version) {
this.ignoreAutoUpdate(new ExtensionIdentifierWithVersion(gallery.identifier, version));
}
return this.local.filter(local => areSameExtensions(local.identifier, gallery.identifier))[0];
}
, gallery.displayName);
});
}
reinstall(extension: IExtension): Promise<IExtension> {
const ext = extension.local ? extension : this.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0];
const toReinstall: ILocalExtension | null = ext && ext.local ? ext.local : null;
if (!toReinstall) {
return Promise.reject(new Error('Missing local'));
}
return this.progressService.withProgress({
location: ProgressLocation.Extensions,
source: `${toReinstall.identifier.id}`
}, () => this.extensionService.reinstallFromGallery(toReinstall).then(() => this.local.filter(local => areSameExtensions(local.identifier, extension.identifier))[0]));
}
private installWithProgress<T>(installTask: () => Promise<T>, extensionName?: string): Promise<T> {
const title = extensionName ? nls.localize('installing named extension', "Installing '{0}' extension....", extensionName) : nls.localize('installing extension', 'Installing extension....');
return this.progressService.withProgress({
location: ProgressLocation.Extensions,
title
}, () => installTask());
}
private checkAndEnableDisabledDependencies(extensionIdentifier: IExtensionIdentifier): Promise<void> {
const extension = this.local.filter(e => (e.local || e.gallery) && areSameExtensions(extensionIdentifier, e.identifier))[0];
if (extension) {
const disabledDepencies = this.getExtensionsRecursively([extension], this.local, EnablementState.Enabled, { dependencies: true, pack: false });
if (disabledDepencies.length) {
return this.setEnablement(disabledDepencies, EnablementState.Enabled);
}
}
return Promise.resolve();
}
private promptAndSetEnablement(extensions: IExtension[], enablementState: EnablementState): Promise<any> {
const enable = enablementState === EnablementState.Enabled || enablementState === EnablementState.WorkspaceEnabled;
if (enable) {
const allDependenciesAndPackedExtensions = this.getExtensionsRecursively(extensions, this.local, enablementState, { dependencies: true, pack: true });
return this.checkAndSetEnablement(extensions, allDependenciesAndPackedExtensions, enablementState);
} else {
const packedExtensions = this.getExtensionsRecursively(extensions, this.local, enablementState, { dependencies: false, pack: true });
if (packedExtensions.length) {
return this.checkAndSetEnablement(extensions, packedExtensions, enablementState);
}
return this.checkAndSetEnablement(extensions, [], enablementState);
}
}
private checkAndSetEnablement(extensions: IExtension[], otherExtensions: IExtension[], enablementState: EnablementState): Promise<any> {
const allExtensions = [...extensions, ...otherExtensions];
const enable = enablementState === EnablementState.Enabled || enablementState === EnablementState.WorkspaceEnabled;
if (!enable) {
for (const extension of extensions) {
let dependents = this.getDependentsAfterDisablement(extension, allExtensions, this.local);
if (dependents.length) {
return Promise.reject(new Error(this.getDependentsErrorMessage(extension, allExtensions, dependents)));
}
}
}
return this.doSetEnablement(allExtensions, enablementState);
}
private getExtensionsRecursively(extensions: IExtension[], installed: IExtension[], enablementState: EnablementState, options: { dependencies: boolean, pack: boolean }, checked: IExtension[] = []): IExtension[] {
const toCheck = extensions.filter(e => checked.indexOf(e) === -1);
if (toCheck.length) {
for (const extension of toCheck) {
checked.push(extension);
}
const extensionsToDisable = installed.filter(i => {
if (checked.indexOf(i) !== -1) {
return false;
}
if (i.enablementState === enablementState) {
return false;
}
const enable = enablementState === EnablementState.Enabled || enablementState === EnablementState.WorkspaceEnabled;
return (enable || i.type === ExtensionType.User) // Include all Extensions for enablement and only user extensions for disablement
&& (options.dependencies || options.pack)
&& extensions.some(extension =>
(options.dependencies && extension.dependencies.some(id => areSameExtensions({ id }, i.identifier)))
|| (options.pack && extension.extensionPack.some(id => areSameExtensions({ id }, i.identifier)))
);
});
if (extensionsToDisable.length) {
extensionsToDisable.push(...this.getExtensionsRecursively(extensionsToDisable, installed, enablementState, options, checked));
}
return extensionsToDisable;
}
return [];
}
private getDependentsAfterDisablement(extension: IExtension, extensionsToDisable: IExtension[], installed: IExtension[]): IExtension[] {
return installed.filter(i => {
if (i.dependencies.length === 0) {
return false;
}
if (i === extension) {
return false;
}
if (i.enablementState === EnablementState.WorkspaceDisabled || i.enablementState === EnablementState.Disabled) {
return false;
}
if (extensionsToDisable.indexOf(i) !== -1) {
return false;
}
return i.dependencies.some(dep => [extension, ...extensionsToDisable].some(d => areSameExtensions(d.identifier, { id: dep })));
});
}
private getDependentsErrorMessage(extension: IExtension, allDisabledExtensions: IExtension[], dependents: IExtension[]): string {
for (const e of [extension, ...allDisabledExtensions]) {
let dependentsOfTheExtension = dependents.filter(d => d.dependencies.some(id => areSameExtensions({ id }, e.identifier)));
if (dependentsOfTheExtension.length) {
return this.getErrorMessageForDisablingAnExtensionWithDependents(e, dependentsOfTheExtension);
}
}
return '';
}
private getErrorMessageForDisablingAnExtensionWithDependents(extension: IExtension, dependents: IExtension[]): string {
if (dependents.length === 1) {
return nls.localize('singleDependentError', "Cannot disable extension '{0}'. Extension '{1}' depends on this.", extension.displayName, dependents[0].displayName);
}
if (dependents.length === 2) {
return nls.localize('twoDependentsError', "Cannot disable extension '{0}'. Extensions '{1}' and '{2}' depend on this.",
extension.displayName, dependents[0].displayName, dependents[1].displayName);
}
return nls.localize('multipleDependentsError', "Cannot disable extension '{0}'. Extensions '{1}', '{2}' and others depend on this.",
extension.displayName, dependents[0].displayName, dependents[1].displayName);
}
private async doSetEnablement(extensions: IExtension[], enablementState: EnablementState): Promise<boolean[]> {
const changed = await this.extensionEnablementService.setEnablement(extensions.map(e => e.local!), enablementState);
for (let i = 0; i < changed.length; i++) {
if (changed[i]) {
/* __GDPR__
"extension:enable" : {
"${include}": [
"${GalleryExtensionTelemetryData}"
]
}
*/
/* __GDPR__
"extension:disable" : {
"${include}": [
"${GalleryExtensionTelemetryData}"
]
}
*/
this.telemetryService.publicLog(enablementState === EnablementState.Enabled || enablementState === EnablementState.WorkspaceEnabled ? 'extension:enable' : 'extension:disable', extensions[i].telemetryData);
}
}
return changed;
}
get allowedBadgeProviders(): string[] {
if (!this._extensionAllowedBadgeProviders) {
this._extensionAllowedBadgeProviders = (product.extensionAllowedBadgeProviders || []).map(s => s.toLowerCase());
}
return this._extensionAllowedBadgeProviders;
}
private onError(err: any): void {
if (isPromiseCanceledError(err)) {
return;
}
const message = err && err.message || '';
if (/getaddrinfo ENOTFOUND|getaddrinfo ENOENT|connect EACCES|connect ECONNREFUSED/.test(message)) {
return;
}
this.notificationService.error(err);
}
handleURL(uri: URI): Promise<boolean> {
if (!/^extension/.test(uri.path)) {
return Promise.resolve(false);
}
this.onOpenExtensionUrl(uri);
return Promise.resolve(true);
}
private onOpenExtensionUrl(uri: URI): void {
const match = /^extension\/([^/]+)$/.exec(uri.path);
if (!match) {
return;
}
const extensionId = match[1];
this.queryLocal().then(local => {
const extension = local.filter(local => areSameExtensions(local.identifier, { id: extensionId }))[0];
if (extension) {
return this.windowService.focusWindow()
.then(() => this.open(extension));
}
return this.queryGallery({ names: [extensionId], source: 'uri' }, CancellationToken.None).then(result => {
if (result.total < 1) {
return Promise.resolve(null);
}
const extension = result.firstPage[0];
return this.windowService.focusWindow().then(() => {
return this.open(extension).then(() => {
this.notificationService.prompt(
Severity.Info,
nls.localize('installConfirmation', "Would you like to install the '{0}' extension?", extension.displayName, extension.publisher),
[{
label: nls.localize('install', "Install"),
run: () => this.install(extension).then(undefined, error => this.onError(error))
}],
{ sticky: true }
);
});
});
});
}).then(undefined, error => this.onError(error));
}
private _ignoredAutoUpdateExtensions: string[];
private get ignoredAutoUpdateExtensions(): string[] {
if (!this._ignoredAutoUpdateExtensions) {
this._ignoredAutoUpdateExtensions = JSON.parse(this.storageService.get('extensions.ignoredAutoUpdateExtension', StorageScope.GLOBAL, '[]') || '[]');
}
return this._ignoredAutoUpdateExtensions;
}
private set ignoredAutoUpdateExtensions(extensionIds: string[]) {
this._ignoredAutoUpdateExtensions = distinct(extensionIds.map(id => id.toLowerCase()));
this.storageService.store('extensions.ignoredAutoUpdateExtension', JSON.stringify(this._ignoredAutoUpdateExtensions), StorageScope.GLOBAL);
}
private ignoreAutoUpdate(identifierWithVersion: ExtensionIdentifierWithVersion): void {
if (!this.isAutoUpdateIgnored(identifierWithVersion)) {
this.ignoredAutoUpdateExtensions = [...this.ignoredAutoUpdateExtensions, identifierWithVersion.key()];
}
}
private isAutoUpdateIgnored(identifierWithVersion: ExtensionIdentifierWithVersion): boolean {
return this.ignoredAutoUpdateExtensions.indexOf(identifierWithVersion.key()) !== -1;
}
private resetIgnoreAutoUpdateExtensions(): void {
this.ignoredAutoUpdateExtensions = this.ignoredAutoUpdateExtensions.filter(extensionId => this.local.some(local => !!local.local && new ExtensionIdentifierWithVersion(local.identifier, local.version).key() === extensionId));
}
dispose(): void {
this.syncDelayer.cancel();
this.disposables = dispose(this.disposables);
}
}