extensionManagementService.ts 25.4 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6 7 8 9 10
/*---------------------------------------------------------------------------------------------
 *  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 * as path from 'path';
import * as pfs from 'vs/base/node/pfs';
11
import * as errors from 'vs/base/common/errors';
E
Erich Gamma 已提交
12
import { assign } from 'vs/base/common/objects';
13
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
14
import { flatten, distinct } from 'vs/base/common/arrays';
E
Erich Gamma 已提交
15 16
import { extract, buffer } from 'vs/base/node/zip';
import { Promise, TPromise } from 'vs/base/common/winjs.base';
J
Johannes Rieken 已提交
17 18
import {
	IExtensionManagementService, IExtensionGalleryService, ILocalExtension,
J
Joao Moreno 已提交
19
	IGalleryExtension, IExtensionIdentity, IExtensionManifest, IGalleryMetadata,
S
Sandeep Somavarapu 已提交
20
	InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, LocalExtensionType
J
Joao Moreno 已提交
21
} from 'vs/platform/extensionManagement/common/extensionManagement';
22
import { localizeManifest } from '../common/extensionNls';
J
Joao Moreno 已提交
23
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
E
Erich Gamma 已提交
24
import { Limiter } from 'vs/base/common/async';
J
Joao Moreno 已提交
25
import Event, { Emitter } from 'vs/base/common/event';
J
Joao Moreno 已提交
26
import * as semver from 'semver';
27
import { groupBy, values } from 'vs/base/common/collections';
J
João Moreno 已提交
28
import URI from 'vs/base/common/uri';
29
import { IChoiceService, Severity } from 'vs/platform/message/common/message';
E
Erich Gamma 已提交
30

J
Joao Moreno 已提交
31 32
const SystemExtensionsRoot = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', 'extensions'));

J
Joao Moreno 已提交
33
function parseManifest(raw: string): TPromise<{ manifest: IExtensionManifest; metadata: IGalleryMetadata; }> {
E
Erich Gamma 已提交
34 35
	return new Promise((c, e) => {
		try {
J
Joao Moreno 已提交
36 37 38 39
			const manifest = JSON.parse(raw);
			const metadata = manifest.__metadata || null;
			delete manifest.__metadata;
			c({ manifest, metadata });
E
Erich Gamma 已提交
40 41 42 43 44 45
		} catch (err) {
			e(new Error(nls.localize('invalidManifest', "Extension invalid: package.json is not a JSON file.")));
		}
	});
}

J
Joao Moreno 已提交
46
function validate(zipPath: string, extension?: IExtensionIdentity, version?: string): TPromise<IExtensionManifest> {
E
Erich Gamma 已提交
47 48
	return buffer(zipPath, 'extension/package.json')
		.then(buffer => parseManifest(buffer.toString('utf8')))
J
Joao Moreno 已提交
49
		.then(({ manifest }) => {
E
Erich Gamma 已提交
50 51 52 53 54 55 56 57 58
			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 已提交
59
				if (version !== manifest.version) {
E
Erich Gamma 已提交
60 61 62 63
					return Promise.wrapError(Error(nls.localize('invalidVersion', "Extension invalid: manifest version mismatch.")));
				}
			}

A
Alex Dima 已提交
64
			return TPromise.as(manifest);
E
Erich Gamma 已提交
65 66 67
		});
}

68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
function readManifest(extensionPath: string): TPromise<{ manifest: IExtensionManifest; metadata: IGalleryMetadata; }> {
	const promises = [
		pfs.readFile(path.join(extensionPath, 'package.json'), 'utf8')
			.then(raw => parseManifest(raw)),
		pfs.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8')
			.then<string>(null, err => err.code !== 'ENOENT' ? TPromise.wrapError(err) : '{}')
			.then(raw => JSON.parse(raw))
	];

	return TPromise.join<any>(promises).then(([{ manifest, metadata }, translations]) => {
		return {
			manifest: localizeManifest(manifest, translations),
			metadata
		};
	});
}

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

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

91
	_serviceBrand: any;
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

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

J
Joao Moreno 已提交
101 102
	private _onDidInstallExtension = new Emitter<DidInstallExtensionEvent>();
	onDidInstallExtension: Event<DidInstallExtensionEvent> = this._onDidInstallExtension.event;
E
Erich Gamma 已提交
103

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

S
Sandeep Somavarapu 已提交
107 108
	private _onDidUninstallExtension = new Emitter<DidUninstallExtensionEvent>();
	onDidUninstallExtension: Event<DidUninstallExtensionEvent> = this._onDidUninstallExtension.event;
E
Erich Gamma 已提交
109 110

	constructor(
111
		@IEnvironmentService private environmentService: IEnvironmentService,
112
		@IChoiceService private choiceService: IChoiceService,
113
		@IExtensionGalleryService private galleryService: IExtensionGalleryService
E
Erich Gamma 已提交
114
	) {
J
Joao Moreno 已提交
115
		this.extensionsPath = environmentService.extensionsPath;
116 117
		this.obsoletePath = path.join(this.extensionsPath, '.obsolete');
		this.obsoleteFileLimiter = new Limiter(1);
E
Erich Gamma 已提交
118 119
	}

120
	install(zipPath: string): TPromise<void> {
121 122
		zipPath = path.resolve(zipPath);

123 124
		return validate(zipPath).then<void>(manifest => {
			const id = getExtensionId(manifest, manifest.version);
125

126 127 128 129
			return this.isObsolete(id).then(isObsolete => {
				if (isObsolete) {
					return TPromise.wrapError(new Error(nls.localize('restartCode', "Please restart Code before reinstalling {0}.", manifest.displayName || manifest.name)));
				}
J
Joao Moreno 已提交
130

131
				this._onInstallExtension.fire({ id, zipPath });
J
Joao Moreno 已提交
132

133 134
				return this.installExtension(zipPath, id)
					.then(
J
Johannes Rieken 已提交
135 136
					local => this._onDidInstallExtension.fire({ id, zipPath, local }),
					error => { this._onDidInstallExtension.fire({ id, zipPath, error }); return TPromise.wrapError(error); }
137
					);
138
			});
139
		});
E
Erich Gamma 已提交
140 141
	}

S
Sandeep Somavarapu 已提交
142
	installFromGallery(extension: IGalleryExtension, promptToInstallDependencies: boolean = true): TPromise<void> {
143
		const id = getExtensionId(extension, extension.version);
J
Joao Moreno 已提交
144

145 146 147 148 149
		return this.isObsolete(id).then(isObsolete => {
			if (isObsolete) {
				return TPromise.wrapError<void>(new Error(nls.localize('restartCode', "Please restart Code before reinstalling {0}.", extension.displayName || extension.name)));
			}
			this._onInstallExtension.fire({ id, gallery: extension });
S
Sandeep Somavarapu 已提交
150
			return this.installCompatibleVersion(extension, true, promptToInstallDependencies)
S
Sandeep Somavarapu 已提交
151
				.then(
J
Johannes Rieken 已提交
152 153 154 155 156
				local => this._onDidInstallExtension.fire({ id, local, gallery: extension }),
				error => {
					this._onDidInstallExtension.fire({ id, gallery: extension, error });
					return TPromise.wrapError(error);
				}
S
Sandeep Somavarapu 已提交
157
				);
158 159 160
		});
	}

161
	private installCompatibleVersion(extension: IGalleryExtension, installDependencies: boolean, promptToInstallDependencies: boolean): TPromise<ILocalExtension> {
162
		return this.galleryService.loadCompatibleVersion(extension)
163
			.then(compatibleVersion => this.getDependenciesToInstall(extension, installDependencies)
164 165
				.then(dependencies => {
					if (!dependencies.length) {
166
						return this.downloadAndInstall(compatibleVersion);
167 168
					}
					if (promptToInstallDependencies) {
169
						const message = nls.localize('installDependeciesConfirmation', "Installing '{0}' also installs its dependencies. Would you like to continue?", extension.displayName);
170
						const options = [
171 172
							nls.localize('install', "Yes"),
							nls.localize('doNotInstall', "No")
173
						];
S
Sandeep Somavarapu 已提交
174
						return this.choiceService.choose(Severity.Info, message, options, true)
175
							.then(value => {
176 177
								if (value === 0) {
									return this.installWithDependencies(compatibleVersion);
178
								}
179
								return TPromise.wrapError(errors.canceled());
180 181 182
							}, error => TPromise.wrapError(errors.canceled()));
					} else {
						return this.installWithDependencies(compatibleVersion);
183 184
					}
				})
J
Johannes Rieken 已提交
185
			);
186 187
	}

188 189 190 191 192
	private getDependenciesToInstall(extension: IGalleryExtension, checkDependecies: boolean): TPromise<string[]> {
		if (!checkDependecies) {
			return TPromise.wrap([]);
		}
		// Filter out self
193
		const extensionName = `${extension.publisher}.${extension.name}`;
194 195 196 197 198 199 200 201
		const dependencies = extension.properties.dependencies ? extension.properties.dependencies.filter(name => name !== extensionName) : [];
		if (!dependencies.length) {
			return TPromise.wrap([]);
		}
		// Filter out installed dependencies
		return this.getInstalled().then(installed => {
			return dependencies.filter(dep => installed.every(i => `${i.manifest.publisher}.${i.manifest.name}` !== dep));
		});
202 203 204 205
	}

	private installWithDependencies(extension: IGalleryExtension): TPromise<ILocalExtension> {
		return this.galleryService.getAllDependencies(extension)
206
			.then(allDependencies => this.filterDependenciesToInstall(extension, allDependencies))
207 208 209 210 211 212 213 214
			.then(toInstall => this.filterObsolete(...toInstall.map(i => getExtensionId(i, i.version)))
				.then((obsolete) => {
					if (obsolete.length) {
						return TPromise.wrapError<ILocalExtension>(new Error(nls.localize('restartCode', "Please restart Code before reinstalling {0}.", extension.displayName || extension.name)));
					}
					return this.bulkInstallWithDependencies(extension, toInstall);
				})
			);
215 216 217
	}

	private bulkInstallWithDependencies(extension: IGalleryExtension, dependecies: IGalleryExtension[]): TPromise<ILocalExtension> {
218 219 220 221
		for (const dependency of dependecies) {
			const id = getExtensionId(dependency, dependency.version);
			this._onInstallExtension.fire({ id, gallery: dependency });
		}
222
		return this.downloadAndInstall(extension)
223
			.then(localExtension => {
224
				return TPromise.join(dependecies.map((dep) => this.installCompatibleVersion(dep, false, false)))
225
					.then(installedLocalExtensions => {
226 227 228 229 230
						for (const installedLocalExtension of installedLocalExtensions) {
							const gallery = this.getGalleryExtensionForLocalExtension(dependecies, installedLocalExtension);
							this._onDidInstallExtension.fire({ id: installedLocalExtension.id, local: installedLocalExtension, gallery });
						}
						return localExtension;
231
					}, error => {
232 233 234 235
						return this.rollback(localExtension, dependecies).then(() => {
							return TPromise.wrapError(Array.isArray(error) ? error[error.length - 1] : error);
						});
					});
236 237 238 239 240 241
			})
			.then(localExtension => localExtension, error => {
				for (const dependency of dependecies) {
					this._onDidInstallExtension.fire({ id: getExtensionId(dependency, dependency.version), gallery: dependency, error });
				}
				return TPromise.wrapError(error);
242
			});
243 244 245
	}

	private rollback(localExtension: ILocalExtension, dependecies: IGalleryExtension[]): TPromise<void> {
S
Sandeep Somavarapu 已提交
246
		return this.doUninstall(localExtension.id)
J
Joao Moreno 已提交
247
			.then(() => this.filterOutUninstalled(dependecies))
S
Sandeep Somavarapu 已提交
248
			.then(installed => TPromise.join(installed.map((i) => this.doUninstall(i.id))))
J
Joao Moreno 已提交
249
			.then(() => null);
250 251
	}

252
	private filterDependenciesToInstall(extension: IGalleryExtension, dependencies: IGalleryExtension[]): TPromise<IGalleryExtension[]> {
J
Joao Moreno 已提交
253 254
		return this.getInstalled()
			.then(local => {
255
				return dependencies.filter(d => {
256
					if (extension.uuid === d.uuid) {
257 258 259
						return false;
					}
					const extensionId = getExtensionId(d, d.version);
J
Joao Moreno 已提交
260 261
					return local.every(local => local.id !== extensionId);
				});
262
			});
263 264
	}

J
Joao Moreno 已提交
265 266 267
	private filterOutUninstalled(extensions: IGalleryExtension[]): TPromise<ILocalExtension[]> {
		return this.getInstalled()
			.then(installed => installed.filter(local => !!this.getGalleryExtensionForLocalExtension(extensions, local)));
268
	}
269

270 271 272 273 274
	private getGalleryExtensionForLocalExtension(galleryExtensions: IGalleryExtension[], localExtension: ILocalExtension): IGalleryExtension {
		const filtered = galleryExtensions.filter(galleryExtension => getExtensionId(galleryExtension, galleryExtension.version) === localExtension.id);
		return filtered.length ? filtered[0] : null;
	}

275
	private downloadAndInstall(extension: IGalleryExtension): TPromise<ILocalExtension> {
276 277
		const id = getExtensionId(extension, extension.version);
		const metadata = {
278
			uuid: extension.uuid,
J
Joao Moreno 已提交
279 280 281 282
			publisherId: extension.publisherId,
			publisherDisplayName: extension.publisherDisplayName,
		};

283 284 285
		return this.galleryService.download(extension)
			.then(zipPath => validate(zipPath).then(() => zipPath))
			.then(zipPath => this.installExtension(zipPath, id, metadata));
286
	}
J
Joao Moreno 已提交
287

288
	private installExtension(zipPath: string, id: string, metadata: IGalleryMetadata = null): TPromise<ILocalExtension> {
J
Joao Moreno 已提交
289 290 291
		const extensionPath = path.join(this.extensionsPath, id);

		return extract(zipPath, extensionPath, { sourcePath: 'extension', overwrite: true })
292
			.then(() => readManifest(extensionPath))
J
Joao Moreno 已提交
293
			.then(({ manifest }) => {
J
Joao Moreno 已提交
294 295
				return pfs.readdir(extensionPath).then(children => {
					const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0];
J
João Moreno 已提交
296
					const readmeUrl = readme ? URI.file(path.join(extensionPath, readme)).toString() : null;
297 298
					const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0];
					const changelogUrl = changelog ? URI.file(path.join(extensionPath, changelog)).toString() : null;
J
Joao Moreno 已提交
299
					const type = LocalExtensionType.User;
300

J
Joao Moreno 已提交
301
					const local: ILocalExtension = { type, id, manifest, metadata, path: extensionPath, readmeUrl, changelogUrl };
302
					const manifestPath = path.join(extensionPath, 'package.json');
J
Joao Moreno 已提交
303

304 305 306 307
					return pfs.readFile(manifestPath, 'utf8')
						.then(raw => parseManifest(raw))
						.then(({ manifest }) => assign(manifest, { __metadata: metadata }))
						.then(manifest => pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t')))
308
						.then(() => local);
J
Joao Moreno 已提交
309
				});
310
			});
E
Erich Gamma 已提交
311 312
	}

J
Joao Moreno 已提交
313
	uninstall(extension: ILocalExtension): TPromise<void> {
J
Joao Moreno 已提交
314 315 316 317
		return this.removeOutdatedExtensions().then(() => {
			return this.scanUserExtensions().then<void>(installed => {
				const promises = installed
					.filter(e => e.manifest.publisher === extension.manifest.publisher && e.manifest.name === extension.manifest.name)
S
Sandeep Somavarapu 已提交
318
					.map(e => this.checkForDependenciesAndUninstall(e, installed));
J
Joao Moreno 已提交
319 320
				return TPromise.join(promises);
			});
J
Joao Moreno 已提交
321 322 323
		});
	}

S
Sandeep Somavarapu 已提交
324
	private checkForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[]): TPromise<void> {
325
		return this.preUninstallExtension(extension.id)
S
Sandeep Somavarapu 已提交
326
			.then(() => this.hasDependencies(extension, installed) ? this.promptForDependenciesAndUninstall(extension, installed) : this.promptAndUninstall(extension, installed))
327 328 329 330 331
			.then(() => this.postUninstallExtension(extension.id),
			error => {
				this.postUninstallExtension(extension.id, error);
				return TPromise.wrapError(error);
			});
S
Sandeep Somavarapu 已提交
332 333
	}

334 335 336 337 338 339 340
	private hasDependencies(extension: ILocalExtension, installed: ILocalExtension[]): boolean {
		if (extension.manifest.extensionDependencies && extension.manifest.extensionDependencies.length) {
			return installed.some(i => extension.manifest.extensionDependencies.indexOf(`${i.manifest.publisher}.${i.manifest.name}`) !== -1);
		}
		return false;
	}

S
Sandeep Somavarapu 已提交
341
	private promptForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[]): TPromise<void> {
342
		const message = nls.localize('uninstallDependeciesConfirmation', "Would you like to uninstall '{0}' only or its dependencies also?", extension.manifest.displayName || extension.manifest.name);
S
Sandeep Somavarapu 已提交
343 344 345 346 347
		const options = [
			nls.localize('uninstallOnly', "Only"),
			nls.localize('uninstallAll', "All"),
			nls.localize('cancel', "Cancel")
		];
S
Sandeep Somavarapu 已提交
348
		return this.choiceService.choose(Severity.Info, message, options, true)
S
Sandeep Somavarapu 已提交
349 350
			.then<void>(value => {
				if (value === 0) {
351
					return this.uninstallWithDependencies(extension, [], installed);
S
Sandeep Somavarapu 已提交
352 353
				}
				if (value === 1) {
354 355
					const dependencies = distinct(this.getDependenciesToUninstallRecursively(extension, installed, [])).filter(e => e !== extension);
					return this.uninstallWithDependencies(extension, dependencies, installed);
S
Sandeep Somavarapu 已提交
356 357 358 359 360
				}
				return TPromise.wrapError(errors.canceled());
			}, error => TPromise.wrapError(errors.canceled()));
	}

S
Sandeep Somavarapu 已提交
361 362 363 364 365 366
	private promptAndUninstall(extension: ILocalExtension, installed: ILocalExtension[]): TPromise<void> {
		const message = nls.localize('uninstallConfirmation', "Are you sure you want to uninstall '{0}'?", extension.manifest.displayName || extension.manifest.name);
		const options = [
			nls.localize('ok', "Ok"),
			nls.localize('cancel', "Cancel")
		];
S
Sandeep Somavarapu 已提交
367
		return this.choiceService.choose(Severity.Info, message, options, true)
S
Sandeep Somavarapu 已提交
368 369 370 371 372 373 374 375
			.then<void>(value => {
				if (value === 0) {
					return this.uninstallWithDependencies(extension, [], installed);
				}
				return TPromise.wrapError(errors.canceled());
			}, error => TPromise.wrapError(errors.canceled()));
	}

376 377
	private uninstallWithDependencies(extension: ILocalExtension, dependencies: ILocalExtension[], installed: ILocalExtension[]): TPromise<void> {
		const dependenciesToUninstall = this.filterDependents(extension, dependencies, installed);
378
		let dependents = this.getDependents(extension, installed).filter(dependent => extension !== dependent && dependenciesToUninstall.indexOf(dependent) === -1);
379
		if (dependents.length) {
S
Sandeep Somavarapu 已提交
380
			return TPromise.wrapError<void>(this.getDependentsErrorMessage(extension, dependents));
381
		}
382 383 384
		return TPromise.join([this.uninstallExtension(extension.id), ...dependenciesToUninstall.map(d => this.doUninstall(d.id))]).then(() => null);
	}

S
Sandeep Somavarapu 已提交
385 386 387 388 389 390 391 392 393 394 395 396 397
	private getDependentsErrorMessage(extension: ILocalExtension, dependents: ILocalExtension[]): string {
		if (dependents.length === 1) {
			return nls.localize('singleDependentError', "Cannot uninstall extension '{0}'. Extension '{1}' depends on this.",
				extension.manifest.displayName || extension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name);
		}
		if (dependents.length === 2) {
			return nls.localize('twoDependentsError', "Cannot uninstall extension '{0}'. Extensions '{1}' and '{2}' depend on this.",
				extension.manifest.displayName || extension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name);
		}
		return nls.localize('multipleDependentsError', "Cannot uninstall extension '{0}'. Extensions '{1}', '{2}' and others depend on this.",
			extension.manifest.displayName || extension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name);
	}

398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
	private getDependenciesToUninstallRecursively(extension: ILocalExtension, installed: ILocalExtension[], checked: ILocalExtension[]): ILocalExtension[] {
		if (checked.indexOf(extension) !== -1) {
			return [];
		}
		checked.push(extension);
		if (!extension.manifest.extensionDependencies || extension.manifest.extensionDependencies.length === 0) {
			return [];
		}
		const dependenciesToUninstall = installed.filter(i => extension.manifest.extensionDependencies.indexOf(`${i.manifest.publisher}.${i.manifest.name}`) !== -1);
		const depsOfDeps = [];
		for (const dep of dependenciesToUninstall) {
			depsOfDeps.push(...this.getDependenciesToUninstallRecursively(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];
419
			const dependents = this.getDependents(dep, installed).filter(e => dependencies.indexOf(e) === -1);
420 421 422
			if (dependents.length) {
				result.splice(i - (dependencies.length - result.length), 1);
			}
S
Sandeep Somavarapu 已提交
423
		}
424
		return result;
S
Sandeep Somavarapu 已提交
425 426
	}

427 428 429 430
	private getDependents(extension: ILocalExtension, installed: ILocalExtension[]): ILocalExtension[] {
		return installed.filter(e => e.manifest.extensionDependencies && e.manifest.extensionDependencies.indexOf(`${extension.manifest.publisher}.${extension.manifest.name}`) !== -1);
	}

S
Sandeep Somavarapu 已提交
431 432 433 434 435 436 437 438 439
	private doUninstall(id: string): TPromise<void> {
		return this.preUninstallExtension(id)
			.then(() => this.uninstallExtension(id))
			.then(() => this.postUninstallExtension(id),
			error => {
				this.postUninstallExtension(id, error);
				return TPromise.wrapError(error);
			});
	}
E
Erich Gamma 已提交
440

S
Sandeep Somavarapu 已提交
441 442
	private preUninstallExtension(id: string): TPromise<void> {
		const extensionPath = path.join(this.extensionsPath, id);
E
Erich Gamma 已提交
443 444
		return pfs.exists(extensionPath)
			.then(exists => exists ? null : Promise.wrapError(new Error(nls.localize('notExists', "Could not find extension"))))
S
Sandeep Somavarapu 已提交
445 446 447 448 449 450
			.then(() => this._onUninstallExtension.fire(id));
	}

	private uninstallExtension(id: string): TPromise<void> {
		const extensionPath = path.join(this.extensionsPath, id);
		return this.setObsolete(id)
E
Erich Gamma 已提交
451
			.then(() => pfs.rimraf(extensionPath))
S
Sandeep Somavarapu 已提交
452 453 454 455 456
			.then(() => this.unsetObsolete(id));
	}

	private postUninstallExtension(id: string, error?: any): TPromise<void> {
		return this._onDidUninstallExtension.fire({ id, error });
E
Erich Gamma 已提交
457 458
	}

J
Joao Moreno 已提交
459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478
	getInstalled(type: LocalExtensionType = null): TPromise<ILocalExtension[]> {
		const promises = [];

		if (type === null || type === LocalExtensionType.System) {
			promises.push(this.scanSystemExtensions());
		}

		if (type === null || type === LocalExtensionType.User) {
			promises.push(this.scanUserExtensions());
		}

		return TPromise.join(promises).then(flatten);
	}

	private scanSystemExtensions(): TPromise<ILocalExtension[]> {
		return this.scanExtensions(SystemExtensionsRoot, LocalExtensionType.System);
	}

	private scanUserExtensions(): TPromise<ILocalExtension[]> {
		return this.scanExtensions(this.extensionsPath, LocalExtensionType.User).then(extensions => {
J
Johannes Rieken 已提交
479
			const byId = values(groupBy(extensions, p => `${p.manifest.publisher}.${p.manifest.name}`));
J
Joao Moreno 已提交
480
			return byId.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]);
J
Joao Moreno 已提交
481 482 483
		});
	}

J
Joao Moreno 已提交
484
	private scanExtensions(root: string, type: LocalExtensionType): TPromise<ILocalExtension[]> {
E
Erich Gamma 已提交
485 486
		const limiter = new Limiter(10);

J
Joao Moreno 已提交
487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506
		return this.scanExtensionFolders(root)
			.then(extensionIds => Promise.join(extensionIds.map(id => {
				const extensionPath = path.join(root, id);

				const each = () => pfs.readdir(extensionPath).then(children => {
					const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0];
					const readmeUrl = readme ? URI.file(path.join(extensionPath, readme)).toString() : null;
					const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0];
					const changelogUrl = changelog ? URI.file(path.join(extensionPath, changelog)).toString() : null;

					return readManifest(extensionPath)
						.then<ILocalExtension>(({ manifest, metadata }) => ({ type, id, manifest, metadata, path: extensionPath, readmeUrl, changelogUrl }));
				}).then(null, () => null);

				return limiter.queue(each);
			})))
			.then(result => result.filter(a => !!a));
	}

	private scanExtensionFolders(root: string): TPromise<string[]> {
507
		return this.getObsoleteExtensions()
J
Joao Moreno 已提交
508
			.then(obsolete => pfs.readdir(root).then(extensions => extensions.filter(id => !obsolete[id])));
E
Erich Gamma 已提交
509 510
	}

J
Joao Moreno 已提交
511 512 513 514 515 516
	removeDeprecatedExtensions(): TPromise<any> {
		return TPromise.join([
			this.removeOutdatedExtensions(),
			this.removeObsoleteExtensions()
		]);
	}
517

J
Joao Moreno 已提交
518 519 520 521
	private removeOutdatedExtensions(): TPromise<any> {
		return this.getOutdatedExtensionIds()
			.then(extensionIds => this.removeExtensions(extensionIds));
	}
522

J
Joao Moreno 已提交
523 524 525 526 527 528 529 530 531 532 533
	private removeObsoleteExtensions(): TPromise<any> {
		return this.getObsoleteExtensions()
			.then(obsolete => Object.keys(obsolete))
			.then(extensionIds => this.removeExtensions(extensionIds));
	}

	private removeExtensions(extensionsIds: string[]): TPromise<any> {
		return TPromise.join(extensionsIds.map(id => {
			return pfs.rimraf(path.join(this.extensionsPath, id))
				.then(() => this.withObsoleteExtensions(obsolete => delete obsolete[id]));
		}));
534 535
	}

J
Joao Moreno 已提交
536 537 538 539 540 541 542 543 544 545 546 547 548
	private getOutdatedExtensionIds(): TPromise<string[]> {
		return this.scanExtensionFolders(this.extensionsPath)
			.then(folders => {
				const galleryFolders = folders
					.map(folder => ({ folder, match: /^([^.]+\..+)-(\d+\.\d+\.\d+)$/.exec(folder) }))
					.filter(({ match }) => !!match)
					.map(({ folder, match }) => ({ folder, id: match[1], version: match[2] }));

				const byId = values(groupBy(galleryFolders, p => p.id));

				return flatten(byId.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version)).slice(1)))
					.map(a => a.folder);
			});
J
Joao Moreno 已提交
549 550
	}

J
Joao Moreno 已提交
551
	private isObsolete(id: string): TPromise<boolean> {
552 553 554 555 556 557 558 559 560 561 562 563 564
		return this.filterObsolete(id).then(obsolete => obsolete.length === 1);
	}

	private filterObsolete(...ids: string[]): TPromise<string[]> {
		return this.withObsoleteExtensions(allObsolete => {
			const obsolete = [];
			for (const id of ids) {
				if (!!allObsolete[id]) {
					obsolete.push(id);
				}
			}
			return obsolete;
		});
565 566
	}

J
Joao Moreno 已提交
567
	private setObsolete(id: string): TPromise<void> {
J
Joao Moreno 已提交
568
		return this.withObsoleteExtensions(obsolete => assign(obsolete, { [id]: true }));
569 570
	}

J
Joao Moreno 已提交
571
	private unsetObsolete(id: string): TPromise<void> {
J
Joao Moreno 已提交
572
		return this.withObsoleteExtensions<void>(obsolete => delete obsolete[id]);
573 574
	}

J
Johannes Rieken 已提交
575
	private getObsoleteExtensions(): TPromise<{ [id: string]: boolean; }> {
J
Joao Moreno 已提交
576
		return this.withObsoleteExtensions(obsolete => obsolete);
577 578
	}

J
Johannes Rieken 已提交
579
	private withObsoleteExtensions<T>(fn: (obsolete: { [id: string]: boolean; }) => T): TPromise<T> {
580 581 582
		return this.obsoleteFileLimiter.queue(() => {
			let result: T = null;
			return pfs.readFile(this.obsoletePath, 'utf8')
D
Dirk Baeumer 已提交
583
				.then<string>(null, err => err.code === 'ENOENT' ? TPromise.as('{}') : TPromise.wrapError(err))
J
Johannes Rieken 已提交
584
				.then<{ [id: string]: boolean }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; } })
585 586 587 588 589 590 591 592 593 594 595 596
				.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);
		});
	}
597

598 599 600
	dispose() {
		this.disposables = dispose(this.disposables);
	}
E
Erich Gamma 已提交
601
}