extensionManagementService.ts 13.4 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

import nls = require('vs/nls');
import { tmpdir } from 'os';
import * as path from 'path';
import types = require('vs/base/common/types');
import * as pfs from 'vs/base/node/pfs';
import { assign } from 'vs/base/common/objects';
14
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
J
Joao Moreno 已提交
15
import { flatten } from 'vs/base/common/arrays';
E
Erich Gamma 已提交
16 17
import { extract, buffer } from 'vs/base/node/zip';
import { Promise, TPromise } from 'vs/base/common/winjs.base';
J
Joao Moreno 已提交
18
import { IExtensionManagementService, IExtension, IExtensionManifest, IGalleryMetadata, IGalleryVersion } from 'vs/platform/extensionManagement/common/extensionManagement';
19
import { extensionEquals, getTelemetryData } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
20
import { download, json, IRequestOptions } from 'vs/base/node/request';
21
import { getProxyAgent } from 'vs/base/node/proxy';
J
Joao Moreno 已提交
22
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
23
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
E
Erich Gamma 已提交
24
import { Limiter } from 'vs/base/common/async';
J
Joao Moreno 已提交
25
import Event, { Emitter } from 'vs/base/common/event';
26
import { UserSettings } from 'vs/workbench/node/userSettings';
J
Joao Moreno 已提交
27
import * as semver from 'semver';
28
import { groupBy, values } from 'vs/base/common/collections';
29
import { isValidExtensionVersion } from 'vs/platform/extensions/node/extensionValidator';
J
Joao Moreno 已提交
30
import pkg from 'vs/platform/package';
E
Erich Gamma 已提交
31 32 33 34 35 36 37 38 39 40 41

function parseManifest(raw: string): TPromise<IExtensionManifest> {
	return new Promise((c, e) => {
		try {
			c(JSON.parse(raw));
		} catch (err) {
			e(new Error(nls.localize('invalidManifest', "Extension invalid: package.json is not a JSON file.")));
		}
	});
}

J
Joao Moreno 已提交
42
function validate(zipPath: string, extension?: IExtension, version = extension && extension.version): TPromise<IExtension> {
E
Erich Gamma 已提交
43 44 45 46 47 48 49 50 51 52 53 54
	return buffer(zipPath, 'extension/package.json')
		.then(buffer => parseManifest(buffer.toString('utf8')))
		.then(manifest => {
			if (extension) {
				if (extension.name !== manifest.name) {
					return Promise.wrapError(Error(nls.localize('invalidName', "Extension invalid: manifest name mismatch.")));
				}

				if (extension.publisher !== manifest.publisher) {
					return Promise.wrapError(Error(nls.localize('invalidPublisher', "Extension invalid: manifest publisher mismatch.")));
				}

J
Joao Moreno 已提交
55
				if (version !== manifest.version) {
E
Erich Gamma 已提交
56 57 58 59
					return Promise.wrapError(Error(nls.localize('invalidVersion', "Extension invalid: manifest version mismatch.")));
				}
			}

A
Alex Dima 已提交
60
			return TPromise.as(manifest);
E
Erich Gamma 已提交
61 62 63
		});
}

64
function createExtension(manifest: IExtensionManifest, galleryInformation?: IGalleryMetadata, path?: string): IExtension {
E
Erich Gamma 已提交
65 66 67 68 69
	const extension: IExtension = {
		name: manifest.name,
		displayName: manifest.displayName || manifest.name,
		publisher: manifest.publisher,
		version: manifest.version,
70
		engines: { vscode: manifest.engines.vscode },
E
Erich Gamma 已提交
71 72 73 74 75 76 77 78 79 80 81 82 83 84
		description: manifest.description || ''
	};

	if (galleryInformation) {
		extension.galleryInformation = galleryInformation;
	}

	if (path) {
		extension.path = path;
	}

	return extension;
}

85
function getExtensionId(extension: IExtensionManifest, version = extension.version): string {
J
Joao Moreno 已提交
86
	return `${ extension.publisher }.${ extension.name }-${ version }`;
87 88
}

J
Joao Moreno 已提交
89
export class ExtensionManagementService implements IExtensionManagementService {
E
Erich Gamma 已提交
90

J
Joao Moreno 已提交
91
	serviceId = IExtensionManagementService;
E
Erich Gamma 已提交
92 93

	private extensionsPath: string;
94 95
	private obsoletePath: string;
	private obsoleteFileLimiter: Limiter<void>;
96
	private disposables: IDisposable[];
E
Erich Gamma 已提交
97 98

	private _onInstallExtension = new Emitter<IExtensionManifest>();
J
Joao Moreno 已提交
99
	onInstallExtension: Event<IExtensionManifest> = this._onInstallExtension.event;
E
Erich Gamma 已提交
100

101 102
	private _onDidInstallExtension = new Emitter<{ extension: IExtension; isUpdate: boolean; error?: Error; }>();
	onDidInstallExtension: Event<{ extension: IExtension; isUpdate: boolean; error?: Error; }> = this._onDidInstallExtension.event;
E
Erich Gamma 已提交
103 104

	private _onUninstallExtension = new Emitter<IExtension>();
J
Joao Moreno 已提交
105
	onUninstallExtension: Event<IExtension> = this._onUninstallExtension.event;
E
Erich Gamma 已提交
106 107

	private _onDidUninstallExtension = new Emitter<IExtension>();
J
Joao Moreno 已提交
108
	onDidUninstallExtension: Event<IExtension> = this._onDidUninstallExtension.event;
E
Erich Gamma 已提交
109 110

	constructor(
111 112
		@IEnvironmentService private environmentService: IEnvironmentService,
		@ITelemetryService telemetryService: ITelemetryService
E
Erich Gamma 已提交
113
	) {
J
Joao Moreno 已提交
114
		this.extensionsPath = environmentService.extensionsPath;
115 116
		this.obsoletePath = path.join(this.extensionsPath, '.obsolete');
		this.obsoleteFileLimiter = new Limiter(1);
117 118 119 120 121 122 123 124 125 126 127

		this.disposables = [
			this.onDidInstallExtension(({ extension, isUpdate, error }) => telemetryService.publicLog(
				isUpdate ? 'extensionGallery2:update' : 'extensionGallery2:install',
				assign(getTelemetryData(extension), { success: !error })
			)),
			this.onDidUninstallExtension(extension => telemetryService.publicLog(
				'extensionGallery2:uninstall',
				assign(getTelemetryData(extension), { success: true })
			))
		];
E
Erich Gamma 已提交
128 129
	}

J
Joao Moreno 已提交
130 131 132
	install(extension: IExtension): TPromise<IExtension>;
	install(zipPath: string): TPromise<IExtension>;
	install(arg: any): TPromise<IExtension> {
E
Erich Gamma 已提交
133 134 135 136
		if (types.isString(arg)) {
			return this.installFromZip(arg);
		}

J
Joao Moreno 已提交
137 138
		const extension = arg as IExtension;
		return this.isObsolete(extension).then(obsolete => {
139 140 141 142 143 144
			if (obsolete) {
				return TPromise.wrapError(new Error(nls.localize('restartCode', "Please restart Code before reinstalling {0}.", extension.name)));
			}

			return this.installFromGallery(arg);
		});
E
Erich Gamma 已提交
145 146 147 148 149 150 151 152 153
	}

	private installFromGallery(extension: IExtension): TPromise<IExtension> {
		const galleryInformation = extension.galleryInformation;

		if (!galleryInformation) {
			return TPromise.wrapError(new Error(nls.localize('missingGalleryInformation', "Gallery information is missing")));
		}

J
Joao Moreno 已提交
154 155
		this._onInstallExtension.fire(extension);

J
Joao Moreno 已提交
156
		return this.getLastValidExtensionVersion(extension, extension.galleryInformation.versions).then(versionInfo => {
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
			return this.getInstalled()
				.then(installed => installed.some(e => extensionEquals(e, extension)))
				.then(isUpdate => {
					const version = versionInfo.version;
					const url = versionInfo.downloadUrl;
					const headers = versionInfo.downloadHeaders;
					const zipPath = path.join(tmpdir(), galleryInformation.id);
					const extensionPath = path.join(this.extensionsPath, getExtensionId(extension, version));
					const manifestPath = path.join(extensionPath, 'package.json');

					return this.request(url)
						.then(opts => assign(opts, { headers }))
						.then(opts => download(zipPath, opts))
						.then(() => validate(zipPath, extension, version))
						.then(manifest => extract(zipPath, extensionPath, { sourcePath: 'extension', overwrite: true }).then(() => manifest))
						.then(manifest => assign({ __metadata: galleryInformation }, manifest))
						.then(manifest => pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t')))
						.then(() => { this._onDidInstallExtension.fire({ extension, isUpdate }); return extension; })
						.then(null, error => { this._onDidInstallExtension.fire({ extension, isUpdate, error }); return TPromise.wrapError(error); });
				});
177
		});
E
Erich Gamma 已提交
178 179
	}

180 181 182 183 184 185 186 187 188 189 190
	private getLastValidExtensionVersion(extension: IExtension, versions: IGalleryVersion[]): TPromise<IGalleryVersion> {
		if (!versions.length) {
			return TPromise.wrapError(new Error(nls.localize('noCompatible', "Couldn't find a compatible version of {0} with this version of Code.", extension.displayName)));
		}

		const version = versions[0];
		return this.request(version.manifestUrl)
			.then(opts => json<IExtensionManifest>(opts))
			.then(manifest => {
				const desc = {
					isBuiltin: false,
J
Joao Moreno 已提交
191 192
					engines: { vscode: manifest.engines.vscode },
					main: manifest.main
193 194
				};

195
				if (!isValidExtensionVersion(pkg.version, desc, [])) {
196 197 198 199 200 201 202
					return this.getLastValidExtensionVersion(extension, versions.slice(1));
				}

				return version;
			});
	}

E
Erich Gamma 已提交
203 204
	private installFromZip(zipPath: string): TPromise<IExtension> {
		return validate(zipPath).then(manifest => {
205
			const extensionPath = path.join(this.extensionsPath, getExtensionId(manifest));
E
Erich Gamma 已提交
206 207
			this._onInstallExtension.fire(manifest);

208 209 210 211 212 213 214 215
			return this.getInstalled()
				.then(installed => installed.some(e => extensionEquals(e, manifest)))
				.then(isUpdate => {

					return extract(zipPath, extensionPath, { sourcePath: 'extension', overwrite: true })
						.then(() => createExtension(manifest, (<any> manifest).__metadata, extensionPath))
						.then(extension => { this._onDidInstallExtension.fire({ extension, isUpdate }); return extension; });
			});
E
Erich Gamma 已提交
216 217 218
		});
	}

J
Joao Moreno 已提交
219
	uninstall(extension: IExtension): TPromise<void> {
220
		const extensionPath = extension.path || path.join(this.extensionsPath, getExtensionId(extension));
E
Erich Gamma 已提交
221 222 223 224

		return pfs.exists(extensionPath)
			.then(exists => exists ? null : Promise.wrapError(new Error(nls.localize('notExists', "Could not find extension"))))
			.then(() => this._onUninstallExtension.fire(extension))
225
			.then(() => this.setObsolete(extension))
E
Erich Gamma 已提交
226
			.then(() => pfs.rimraf(extensionPath))
227
			.then(() => this.unsetObsolete(extension))
E
Erich Gamma 已提交
228 229 230
			.then(() => this._onDidUninstallExtension.fire(extension));
	}

J
Joao Moreno 已提交
231
	getInstalled(includeDuplicateVersions: boolean = false): TPromise<IExtension[]> {
J
Joao Moreno 已提交
232 233 234 235 236 237
		const all = this.getAllInstalled();

		if (includeDuplicateVersions) {
			return all;
		}

A
Alex Dima 已提交
238 239
		return all.then(extensions => {
			const byId = values(groupBy(extensions, p => `${ p.publisher }.${ p.name }`));
J
Joao Moreno 已提交
240 241 242 243 244
			return byId.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version))[0]);
		});
	}

	private getAllInstalled(): TPromise<IExtension[]> {
E
Erich Gamma 已提交
245 246
		const limiter = new Limiter(10);

247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
		return this.getObsoleteExtensions()
			.then(obsolete => {
				return pfs.readdir(this.extensionsPath)
					.then(extensions => extensions.filter(e => !obsolete[e]))
					.then<IExtension[]>(extensions => Promise.join(extensions.map(e => {
						const extensionPath = path.join(this.extensionsPath, e);

						return limiter.queue(
							() => pfs.readFile(path.join(extensionPath, 'package.json'), 'utf8')
								.then(raw => parseManifest(raw))
								.then(manifest => createExtension(manifest, (<any> manifest).__metadata, extensionPath))
								.then(null, () => null)
						);
					})))
					.then(result => result.filter(a => !!a));
			});
E
Erich Gamma 已提交
263 264
	}

J
Joao Moreno 已提交
265 266 267
	removeDeprecatedExtensions(): TPromise<void> {
		const outdated = this.getOutdatedExtensions()
			.then(extensions => extensions.map(e => getExtensionId(e)));
268 269 270 271 272 273 274 275 276

		const obsolete = this.getObsoleteExtensions()
			.then(obsolete => Object.keys(obsolete));

		return TPromise.join([outdated, obsolete])
			.then(result => flatten(result))
			.then<void>(extensionsIds => {
				return TPromise.join(extensionsIds.map(id => {
					return pfs.rimraf(path.join(this.extensionsPath, id))
J
Joao Moreno 已提交
277
						.then(() => this.withObsoleteExtensions(obsolete => delete obsolete[id]));
278 279 280 281
				}));
			});
	}

J
Joao Moreno 已提交
282 283 284 285 286 287 288 289 290 291
	private getOutdatedExtensions(): TPromise<IExtension[]> {
		return this.getAllInstalled().then(plugins => {
			const byId = values(groupBy(plugins, p => `${ p.publisher }.${ p.name }`));
			const extensions = flatten(byId.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version)).slice(1)));

			return extensions
				.filter(e => !!e.path);
		});
	}

292 293 294 295 296
	private isObsolete(extension: IExtension): TPromise<boolean> {
		const id = getExtensionId(extension);
		return this.withObsoleteExtensions(obsolete => !!obsolete[id]);
	}

297 298
	private setObsolete(extension: IExtension): TPromise<void> {
		const id = getExtensionId(extension);
J
Joao Moreno 已提交
299
		return this.withObsoleteExtensions(obsolete => assign(obsolete, { [id]: true }));
300 301 302 303
	}

	private unsetObsolete(extension: IExtension): TPromise<void> {
		const id = getExtensionId(extension);
J
Joao Moreno 已提交
304
		return this.withObsoleteExtensions<void>(obsolete => delete obsolete[id]);
305 306 307
	}

	private getObsoleteExtensions(): TPromise<{ [id:string]: boolean; }> {
J
Joao Moreno 已提交
308
		return this.withObsoleteExtensions(obsolete => obsolete);
309 310
	}

J
Joao Moreno 已提交
311
	private withObsoleteExtensions<T>(fn: (obsolete: { [id:string]: boolean; }) => T): TPromise<T> {
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
		return this.obsoleteFileLimiter.queue(() => {
			let result: T = null;
			return pfs.readFile(this.obsoletePath, 'utf8')
				.then(null, err => err.code === 'ENOENT' ? TPromise.as('{}') : TPromise.wrapError(err))
				.then<{ [id: string]: boolean }>(raw => JSON.parse(raw))
				.then(obsolete => { result = fn(obsolete); return obsolete; })
				.then(obsolete => {
					if (Object.keys(obsolete).length === 0) {
						return pfs.rimraf(this.obsoletePath);
					} else {
						const raw = JSON.stringify(obsolete);
						return pfs.writeFile(this.obsoletePath, raw);
					}
				})
				.then(() => result);
		});
	}
329 330 331 332 333

	// Helper for proxy business... shameful.
	// This should be pushed down and not rely on the context service
	private request(url: string): TPromise<IRequestOptions> {
		const settings = TPromise.join([
J
Joao Moreno 已提交
334 335 336
			// TODO@Joao we need a nice configuration service here!
			UserSettings.getValue(this.environmentService.userDataPath, 'http.proxy'),
			UserSettings.getValue(this.environmentService.userDataPath, 'http.proxyStrictSSL')
337 338 339 340 341 342 343 344 345 346
		]);

		return settings.then(settings => {
			const proxyUrl: string = settings[0];
			const strictSSL: boolean = settings[1];
			const agent = getProxyAgent(url, { proxyUrl, strictSSL });

			return { url, agent, strictSSL };
		});
	}
347 348 349 350

	dispose() {
		this.disposables = dispose(this.disposables);
	}
E
Erich Gamma 已提交
351
}