support managing web extensions

- Allow installing extensions with web resource asset type
- Allow uninstalling
This commit is contained in:
Sandeep Somavarapu 2020-06-25 19:25:49 +02:00
parent 95f3c81dd3
commit 41f32e0bd6
6 changed files with 216 additions and 14 deletions

View file

@ -520,7 +520,8 @@
"**/vs/workbench/api/**/common/**",
"vscode-textmate",
"vscode-oniguruma",
"iconv-lite-umd"
"iconv-lite-umd",
"semver-umd"
]
},
{

View file

@ -295,6 +295,8 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
installCount: getStatistic(galleryExtension.statistics, 'install'),
rating: getStatistic(galleryExtension.statistics, 'averagerating'),
ratingCount: getStatistic(galleryExtension.statistics, 'ratingcount'),
assetUri: URI.parse(version.assetUri),
assetTypes: version.files.map(({ assetType }) => assetType),
assets,
properties: {
dependencies: getExtensions(version, PropertyType.Dependency),

View file

@ -78,6 +78,8 @@ export interface IGalleryExtension {
installCount: number;
rating: number;
ratingCount: number;
assetUri: URI;
assetTypes: string[];
assets: IGalleryExtensionAssets;
properties: IGalleryExtensionProperties;
telemetryData: any;

View file

@ -7,7 +7,7 @@ import { Event } from 'vs/base/common/event';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
import { IExtension, IScannedExtension, ExtensionType } from 'vs/platform/extensions/common/extensions';
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionManagementService, IGalleryExtension, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IWorkspace, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { IStringDictionary } from 'vs/base/common/collections';
@ -142,4 +142,6 @@ export const IWebExtensionsScannerService = createDecorator<IWebExtensionsScanne
export interface IWebExtensionsScannerService {
readonly _serviceBrand: undefined;
scanExtensions(type?: ExtensionType): Promise<IScannedExtension[]>;
addExtension(galleryExtension: IGalleryExtension): Promise<IScannedExtension>;
removeExtension(identifier: IExtensionIdentifier, version?: string): Promise<void>;
}

View file

@ -4,27 +4,39 @@
*--------------------------------------------------------------------------------------------*/
import { ExtensionType, IExtensionIdentifier, IExtensionManifest, IScannedExtension } from 'vs/platform/extensions/common/extensions';
import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, IGalleryExtension, IReportedExtension, IGalleryMetadata } from 'vs/platform/extensionManagement/common/extensionManagement';
import { Event } from 'vs/base/common/event';
import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, IGalleryExtension, IReportedExtension, IGalleryMetadata, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
import { Event, Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { IRequestService, isSuccess, asText } from 'vs/platform/request/common/request';
import { CancellationToken } from 'vs/base/common/cancellation';
import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ILogService } from 'vs/platform/log/common/log';
import { Disposable } from 'vs/base/common/lifecycle';
export class WebExtensionManagementService implements IExtensionManagementService {
export class WebExtensionManagementService extends Disposable implements IExtensionManagementService {
declare readonly _serviceBrand: undefined;
onInstallExtension: Event<InstallExtensionEvent> = Event.None;
onDidInstallExtension: Event<DidInstallExtensionEvent> = Event.None;
onUninstallExtension: Event<IExtensionIdentifier> = Event.None;
onDidUninstallExtension: Event<DidUninstallExtensionEvent> = Event.None;
private readonly _onInstallExtension = this._register(new Emitter<InstallExtensionEvent>());
readonly onInstallExtension: Event<InstallExtensionEvent> = this._onInstallExtension.event;
private readonly _onDidInstallExtension = this._register(new Emitter<DidInstallExtensionEvent>());
readonly onDidInstallExtension: Event<DidInstallExtensionEvent> = this._onDidInstallExtension.event;
private readonly _onUninstallExtension = this._register(new Emitter<IExtensionIdentifier>());
readonly onUninstallExtension: Event<IExtensionIdentifier> = this._onUninstallExtension.event;
private _onDidUninstallExtension = this._register(new Emitter<DidUninstallExtensionEvent>());
onDidUninstallExtension: Event<DidUninstallExtensionEvent> = this._onDidUninstallExtension.event;
constructor(
@IWebExtensionsScannerService private readonly webExtensionsScannerService: IWebExtensionsScannerService,
@IRequestService private readonly requestService: IRequestService,
@ILogService private readonly logService: ILogService,
) {
super();
}
async getInstalled(type?: ExtensionType): Promise<ILocalExtension[]> {
@ -32,6 +44,45 @@ export class WebExtensionManagementService implements IExtensionManagementServic
return Promise.all(extensions.map(e => this.toLocalExtension(e)));
}
async installFromGallery(gallery: IGalleryExtension): Promise<ILocalExtension> {
this.logService.info('Installing extension:', gallery.identifier.id);
this._onInstallExtension.fire({ identifier: gallery.identifier, gallery });
try {
const existingExtension = await this.getUserExtension(gallery.identifier);
if (existingExtension && existingExtension.manifest.version !== gallery.version) {
await this.webExtensionsScannerService.removeExtension(existingExtension.identifier, existingExtension.manifest.version);
}
const scannedExtension = await this.webExtensionsScannerService.addExtension(gallery);
const local = await this.toLocalExtension(scannedExtension);
this._onDidInstallExtension.fire({ local, identifier: gallery.identifier, operation: InstallOperation.Install, gallery });
return local;
} catch (error) {
this._onDidInstallExtension.fire({ error, identifier: gallery.identifier, operation: InstallOperation.Install, gallery });
throw error;
}
}
async uninstall(extension: ILocalExtension): Promise<void> {
this._onUninstallExtension.fire(extension.identifier);
try {
await this.webExtensionsScannerService.removeExtension(extension.identifier);
this._onDidUninstallExtension.fire({ identifier: extension.identifier });
} catch (error) {
this.logService.error(error);
this._onDidUninstallExtension.fire({ error, identifier: extension.identifier });
throw error;
}
}
async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension> {
return local;
}
private async getUserExtension(identifier: IExtensionIdentifier): Promise<ILocalExtension | undefined> {
const userExtensions = await this.getInstalled(ExtensionType.User);
return userExtensions.find(e => areSameExtensions(e.identifier, identifier));
}
private async toLocalExtension(scannedExtension: IScannedExtension): Promise<ILocalExtension> {
let manifest = scannedExtension.packageJSON;
if (scannedExtension.packageNLSUrl) {
@ -60,10 +111,7 @@ export class WebExtensionManagementService implements IExtensionManagementServic
unzip(zipLocation: URI): Promise<IExtensionIdentifier> { throw new Error('unsupported'); }
getManifest(vsix: URI): Promise<IExtensionManifest> { throw new Error('unsupported'); }
install(vsix: URI, isMachineScoped?: boolean): Promise<ILocalExtension> { throw new Error('unsupported'); }
installFromGallery(extension: IGalleryExtension, isMachineScoped?: boolean): Promise<ILocalExtension> { throw new Error('unsupported'); }
uninstall(extension: ILocalExtension, force?: boolean): Promise<void> { throw new Error('unsupported'); }
reinstallFromGallery(extension: ILocalExtension): Promise<void> { throw new Error('unsupported'); }
getExtensionsReport(): Promise<IReportedExtension[]> { throw new Error('unsupported'); }
updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension> { throw new Error('unsupported'); }
}

View file

@ -3,11 +3,44 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IBuiltinExtensionsScannerService, IScannedExtension, ExtensionType } from 'vs/platform/extensions/common/extensions';
import * as semver from 'semver-umd';
import { IBuiltinExtensionsScannerService, IScannedExtension, ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { isWeb } from 'vs/base/common/platform';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { joinPath } from 'vs/base/common/resources';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { Queue } from 'vs/base/common/async';
import { VSBuffer } from 'vs/base/common/buffer';
import { asText, isSuccess, IRequestService } from 'vs/platform/request/common/request';
import { ILogService } from 'vs/platform/log/common/log';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { groupByExtension, areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
interface IUserExtension {
identifier: IExtensionIdentifier;
version: string;
uri: URI;
readmeUri?: URI;
changelogUri?: URI;
packageNLSUri?: URI;
}
interface IStoredUserExtension {
identifier: IExtensionIdentifier;
version: string;
uri: UriComponents;
readmeUri?: UriComponents;
changelogUri?: UriComponents;
packageNLSUri?: UriComponents;
}
const AssetTypeWebResource = 'Microsoft.VisualStudio.Code.WebResources';
function getExtensionLocation(assetUri: URI): URI { return joinPath(assetUri, AssetTypeWebResource, 'extension'); }
export class WebExtensionsScannerService implements IWebExtensionsScannerService {
@ -15,11 +48,18 @@ export class WebExtensionsScannerService implements IWebExtensionsScannerService
private readonly systemExtensionsPromise: Promise<IScannedExtension[]>;
private readonly staticExtensions: IScannedExtension[];
private readonly extensionsResource: URI;
private readonly userExtensionsResourceLimiter: Queue<IUserExtension[]>;
constructor(
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IBuiltinExtensionsScannerService private readonly builtinExtensionsScannerService: IBuiltinExtensionsScannerService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService
@IFileService private readonly fileService: IFileService,
@IRequestService private readonly requestService: IRequestService,
@ILogService private readonly logService: ILogService,
) {
this.extensionsResource = joinPath(environmentService.userRoamingDataHome, 'extensions.json');
this.userExtensionsResourceLimiter = new Queue<IUserExtension[]>();
this.systemExtensionsPromise = isWeb ? this.builtinExtensionsScannerService.scanBuiltinExtensions() : Promise.resolve([]);
const staticExtensions = environmentService.options && Array.isArray(environmentService.options.staticExtensions) ? environmentService.options.staticExtensions : [];
this.staticExtensions = staticExtensions.map(data => <IScannedExtension>{
@ -37,10 +77,117 @@ export class WebExtensionsScannerService implements IWebExtensionsScannerService
}
if (type === undefined || type === ExtensionType.User) {
extensions.push(...this.staticExtensions);
const userExtensions = await this.scanUserExtensions();
extensions.push(...userExtensions);
}
return extensions;
}
async addExtension(galleryExtension: IGalleryExtension): Promise<IScannedExtension> {
if (!galleryExtension.assetTypes.some(type => type.startsWith(AssetTypeWebResource))) {
throw new Error(`Missing ${AssetTypeWebResource} asset type`);
}
const packageNLSUri = joinPath(getExtensionLocation(galleryExtension.assetUri), 'package.nls.json');
const context = await this.requestService.request({ type: 'GET', url: packageNLSUri.toString() }, CancellationToken.None);
const packageNLSExists = isSuccess(context);
const userExtensions = await this.readUserExtensions();
const userExtension: IUserExtension = {
identifier: galleryExtension.identifier,
version: galleryExtension.version,
uri: galleryExtension.assetUri,
readmeUri: galleryExtension.assets.readme ? URI.parse(galleryExtension.assets.readme.uri) : undefined,
changelogUri: galleryExtension.assets.changelog ? URI.parse(galleryExtension.assets.changelog.uri) : undefined,
packageNLSUri: packageNLSExists ? packageNLSUri : undefined
};
userExtensions.push(userExtension);
await this.writeUserExtensions(userExtensions);
const scannedExtension = await this.toScannedExtension(userExtension);
if (scannedExtension) {
return scannedExtension;
}
throw new Error('Error while scanning extension');
}
async removeExtension(identifier: IExtensionIdentifier, version?: string): Promise<void> {
let userExtensions = await this.readUserExtensions();
userExtensions = userExtensions.filter(extension => !(areSameExtensions(extension.identifier, identifier) && version ? extension.version === version : true));
await this.writeUserExtensions(userExtensions);
}
private async scanUserExtensions(): Promise<IScannedExtension[]> {
let userExtensions = await this.readUserExtensions();
const byExtension: IUserExtension[][] = groupByExtension(userExtensions, e => e.identifier);
userExtensions = byExtension.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version))[0]);
const scannedExtensions: IScannedExtension[] = [];
await Promise.all(userExtensions.map(async userExtension => {
try {
const scannedExtension = await this.toScannedExtension(userExtension);
if (scannedExtension) {
scannedExtensions.push(scannedExtension);
}
} catch (error) {
this.logService.error(error, 'Error while scanning user extension', userExtension.identifier.id);
}
}));
return scannedExtensions;
}
private async toScannedExtension(userExtension: IUserExtension): Promise<IScannedExtension | null> {
const context = await this.requestService.request({ type: 'GET', url: joinPath(userExtension.uri, 'Microsoft.VisualStudio.Code.Manifest').toString() }, CancellationToken.None);
if (isSuccess(context)) {
const content = await asText(context);
if (content) {
const packageJSON = JSON.parse(content);
return {
identifier: userExtension.identifier,
location: getExtensionLocation(userExtension.uri),
packageJSON,
type: ExtensionType.User,
readmeUrl: userExtension.readmeUri,
changelogUrl: userExtension.changelogUri,
packageNLSUrl: userExtension.packageNLSUri,
};
}
}
return null;
}
private readUserExtensions(): Promise<IUserExtension[]> {
return this.userExtensionsResourceLimiter.queue(async () => {
try {
const content = await this.fileService.readFile(this.extensionsResource);
const storedUserExtensions: IStoredUserExtension[] = JSON.parse(content.value.toString());
return storedUserExtensions.map(e => ({
identifier: e.identifier,
version: e.version,
uri: URI.revive(e.uri),
readmeUri: URI.revive(e.readmeUri),
changelogUri: URI.revive(e.changelogUri),
packageNLSUri: URI.revive(e.packageNLSUri),
}));
} catch (error) { /* Ignore */ }
return [];
});
}
private writeUserExtensions(userExtensions: IUserExtension[]): Promise<IUserExtension[]> {
return this.userExtensionsResourceLimiter.queue(async () => {
const storedUserExtensions: IStoredUserExtension[] = userExtensions.map(e => ({
identifier: e.identifier,
version: e.version,
uri: e.uri.toJSON(),
readmeUri: e.readmeUri?.toJSON(),
changelogUri: e.changelogUri?.toJSON(),
packageNLSUri: e.packageNLSUri?.toJSON(),
}));
await this.fileService.writeFile(this.extensionsResource, VSBuffer.fromString(JSON.stringify(storedUserExtensions)));
return userExtensions;
});
}
}
registerSingleton(IWebExtensionsScannerService, WebExtensionsScannerService);