extensionManagementService.ts 32.2 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
import { extract, buffer } from 'vs/base/node/zip';
16
import { TPromise } from 'vs/base/common/winjs.base';
J
Johannes Rieken 已提交
17 18
import {
	IExtensionManagementService, IExtensionGalleryService, ILocalExtension,
19
	IGalleryExtension, IExtensionManifest, IGalleryMetadata,
J
Joao Moreno 已提交
20
	InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, LocalExtensionType,
S
Sandeep Somavarapu 已提交
21 22
	StatisticType,
	IExtensionIdentifier
J
Joao Moreno 已提交
23
} from 'vs/platform/extensionManagement/common/extensionManagement';
S
Sandeep Somavarapu 已提交
24 25
import { getGalleryExtensionIdFromLocal, adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { getIdAndVersionFromLocalExtensionId } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
26
import { localizeManifest } from '../common/extensionNls';
J
Joao Moreno 已提交
27
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
E
Erich Gamma 已提交
28
import { Limiter } from 'vs/base/common/async';
J
Joao Moreno 已提交
29
import Event, { Emitter } from 'vs/base/common/event';
J
Joao Moreno 已提交
30
import * as semver from 'semver';
31
import { groupBy, values } from 'vs/base/common/collections';
J
João Moreno 已提交
32
import URI from 'vs/base/common/uri';
33
import { IChoiceService, Severity } from 'vs/platform/message/common/message';
S
Sandeep Somavarapu 已提交
34
import pkg from 'vs/platform/node/package';
E
Erich Gamma 已提交
35

J
Joao Moreno 已提交
36
const SystemExtensionsRoot = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', 'extensions'));
37
const INSTALL_ERROR_OBSOLETE = 'obsolete';
S
Sandeep Somavarapu 已提交
38 39 40
const INSTALL_ERROR_INCOMPATIBLE = 'incompatible';
const INSTALL_ERROR_DOWNLOADING = 'downloading';
const INSTALL_ERROR_VALIDATING = 'validating';
41 42
const INSTALL_ERROR_GALLERY = 'gallery';
const INSTALL_ERROR_LOCAL = 'local';
S
Sandeep Somavarapu 已提交
43 44 45 46 47 48 49
const INSTALL_ERROR_UNKNOWN = 'unknown';

export class InstallationError extends Error {
	constructor(message: string, readonly code: string) {
		super(message);
	}
}
J
Joao Moreno 已提交
50

J
Joao Moreno 已提交
51
function parseManifest(raw: string): TPromise<{ manifest: IExtensionManifest; metadata: IGalleryMetadata; }> {
52
	return new TPromise((c, e) => {
E
Erich Gamma 已提交
53
		try {
J
Joao Moreno 已提交
54 55 56 57
			const manifest = JSON.parse(raw);
			const metadata = manifest.__metadata || null;
			delete manifest.__metadata;
			c({ manifest, metadata });
E
Erich Gamma 已提交
58 59 60 61 62 63
		} catch (err) {
			e(new Error(nls.localize('invalidManifest', "Extension invalid: package.json is not a JSON file.")));
		}
	});
}

64
export function validateLocalExtension(zipPath: string): TPromise<IExtensionManifest> {
E
Erich Gamma 已提交
65 66
	return buffer(zipPath, 'extension/package.json')
		.then(buffer => parseManifest(buffer.toString('utf8')))
67
		.then(({ manifest }) => TPromise.as(manifest));
E
Erich Gamma 已提交
68 69
}

70 71 72 73 74
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')
R
Ron Buckton 已提交
75
			.then(null, err => err.code !== 'ENOENT' ? TPromise.wrapError<string>(err) : '{}')
76 77 78 79 80 81 82 83 84 85 86
			.then(raw => JSON.parse(raw))
	];

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

S
Sandeep Somavarapu 已提交
87 88 89
interface InstallableExtension {
	zipPath: string;
	id: string;
S
Sandeep Somavarapu 已提交
90
	metadata?: IGalleryMetadata;
S
Sandeep Somavarapu 已提交
91
	current?: ILocalExtension;
S
Sandeep Somavarapu 已提交
92 93
}

J
Joao Moreno 已提交
94
export class ExtensionManagementService implements IExtensionManagementService {
E
Erich Gamma 已提交
95

96
	_serviceBrand: any;
E
Erich Gamma 已提交
97 98

	private extensionsPath: string;
99 100
	private obsoletePath: string;
	private obsoleteFileLimiter: Limiter<void>;
101
	private disposables: IDisposable[] = [];
E
Erich Gamma 已提交
102

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

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

S
Sandeep Somavarapu 已提交
109 110
	private _onUninstallExtension = new Emitter<IExtensionIdentifier>();
	onUninstallExtension: Event<IExtensionIdentifier> = this._onUninstallExtension.event;
E
Erich Gamma 已提交
111

S
Sandeep Somavarapu 已提交
112 113
	private _onDidUninstallExtension = new Emitter<DidUninstallExtensionEvent>();
	onDidUninstallExtension: Event<DidUninstallExtensionEvent> = this._onDidUninstallExtension.event;
E
Erich Gamma 已提交
114 115

	constructor(
S
Sandeep Somavarapu 已提交
116
		@IEnvironmentService environmentService: IEnvironmentService,
117
		@IChoiceService private choiceService: IChoiceService,
118
		@IExtensionGalleryService private galleryService: IExtensionGalleryService
E
Erich Gamma 已提交
119
	) {
J
Joao Moreno 已提交
120
		this.extensionsPath = environmentService.extensionsPath;
121 122
		this.obsoletePath = path.join(this.extensionsPath, '.obsolete');
		this.obsoleteFileLimiter = new Limiter(1);
E
Erich Gamma 已提交
123 124
	}

125
	install(zipPath: string): TPromise<void> {
126 127
		zipPath = path.resolve(zipPath);

S
Sandeep Somavarapu 已提交
128
		return validateLocalExtension(zipPath)
S
Sandeep Somavarapu 已提交
129
			.then(manifest => {
S
Sandeep Somavarapu 已提交
130
				const identifier = { id: getLocalExtensionIdFromManifest(manifest) };
S
Sandeep Somavarapu 已提交
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
				return this.isObsolete(identifier.id)
					.then(isObsolete => {
						if (isObsolete) {
							return TPromise.wrapError(new Error(nls.localize('restartCodeLocal', "Please restart Code before reinstalling {0}.", manifest.displayName || manifest.name)));
						}
						return this.checkOutdated(manifest)
							.then(validated => {
								if (validated) {
									this._onInstallExtension.fire({ identifier, zipPath });
									return this.getMetadata(getGalleryExtensionId(manifest.publisher, manifest.name))
										.then(
										metadata => this.installFromZipPath(identifier, zipPath, metadata, manifest),
										error => this.installFromZipPath(identifier, zipPath, null, manifest));
								}
								return null;
							});
					});
			});
	}
150

S
Sandeep Somavarapu 已提交
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
	private checkOutdated(manifest: IExtensionManifest): TPromise<boolean> {
		const extensionIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
		return this.getInstalled()
			.then(installedExtensions => {
				const newer = installedExtensions.filter(local => areSameExtensions(extensionIdentifier, { id: getGalleryExtensionIdFromLocal(local) }) && semver.gt(local.manifest.version, manifest.version))[0];
				if (newer) {
					const message = nls.localize('installingOutdatedExtension', "A newer version of this extension is already installed. Would you like to override this with the older version?");
					const options = [
						nls.localize('override', "Override"),
						nls.localize('cancel', "Cancel")
					];
					return this.choiceService.choose(Severity.Info, message, options, 1, true)
						.then<boolean>(value => {
							if (value === 0) {
								return this.uninstall(newer, true).then(() => true);
							}
							return TPromise.wrapError(errors.canceled());
						});
				}
				return true;
171
			});
S
Sandeep Somavarapu 已提交
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
	}

	private installFromZipPath(identifier: IExtensionIdentifier, zipPath: string, metadata: IGalleryMetadata, manifest: IExtensionManifest): TPromise<void> {
		return this.installExtension({ zipPath, id: identifier.id, metadata })
			.then(local => {
				if (this.galleryService.isEnabled() && local.manifest.extensionDependencies && local.manifest.extensionDependencies.length) {
					return this.getDependenciesToInstall(local.manifest.extensionDependencies)
						.then(dependenciesToInstall => this.downloadAndInstallExtensions(metadata ? dependenciesToInstall.filter(d => d.identifier.uuid !== metadata.id) : dependenciesToInstall))
						.then(() => local, error => {
							this.uninstallExtension(local.identifier);
							return TPromise.wrapError(error);
						});
				}
				return local;
			})
			.then(
			local => this._onDidInstallExtension.fire({ identifier, zipPath, local }),
			error => { this._onDidInstallExtension.fire({ identifier, zipPath, error }); return TPromise.wrapError(error); }
			);
E
Erich Gamma 已提交
191 192
	}

S
Sandeep Somavarapu 已提交
193
	installFromGallery(extension: IGalleryExtension): TPromise<void> {
S
Sandeep Somavarapu 已提交
194 195 196
		this.onInstallExtensions([extension]);
		return this.collectExtensionsToInstall(extension)
			.then(
S
Sandeep Somavarapu 已提交
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
			extensionsToInstall => {
				if (extensionsToInstall.length > 1) {
					this.onInstallExtensions(extensionsToInstall.slice(1));
				}
				return this.downloadAndInstallExtensions(extensionsToInstall)
					.then(
					local => this.onDidInstallExtensions(extensionsToInstall, local),
					error => {
						const errorCode = error instanceof InstallationError ? error.code : INSTALL_ERROR_UNKNOWN;
						return this.onDidInstallExtensions(extensionsToInstall, null, errorCode, error);
					});
			},
			error => {
				const errorCode = error instanceof InstallationError ? error.code : INSTALL_ERROR_UNKNOWN;
				return this.onDidInstallExtensions([extension], null, errorCode, error);
			});
	}

	private collectExtensionsToInstall(extension: IGalleryExtension): TPromise<IGalleryExtension[]> {
		return this.galleryService.loadCompatibleVersion(extension)
			.then(compatible => {
				if (!compatible) {
					return TPromise.wrapError<IGalleryExtension[]>(new InstallationError(nls.localize('notFoundCopatible', "Unable to install because, the extension '{0}' compatible with current version '{1}' of VS Code is not found.", extension.identifier.id, pkg.version), INSTALL_ERROR_INCOMPATIBLE));
				}
				return this.getDependenciesToInstall(compatible.properties.dependencies)
					.then(
					dependenciesToInstall => {
						const extensionsToInstall = [compatible, ...dependenciesToInstall.filter(d => d.identifier.uuid !== compatible.identifier.uuid)];
						return this.checkForObsolete(extensionsToInstall)
							.then(
							extensionsToInstall => extensionsToInstall,
							error => TPromise.wrapError<IGalleryExtension[]>(new InstallationError(this.joinErrors(error).message, INSTALL_ERROR_OBSOLETE))
							);
					},
					error => TPromise.wrapError<IGalleryExtension[]>(new InstallationError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY)));
			},
			error => TPromise.wrapError<IGalleryExtension[]>(new InstallationError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY)));
234 235
	}

S
Sandeep Somavarapu 已提交
236
	private downloadAndInstallExtensions(extensions: IGalleryExtension[]): TPromise<ILocalExtension[]> {
S
Sandeep Somavarapu 已提交
237 238 239 240
		return this.getInstalled(LocalExtensionType.User)
			.then(installed => TPromise.join(extensions.map(extensionToInstall => this.downloadInstallableExtension(extensionToInstall, installed)))
				.then(
				installableExtensions => TPromise.join(installableExtensions.map(installableExtension => this.installExtension(installableExtension)))
S
Sandeep Somavarapu 已提交
241
					.then(null, error => this.rollback(extensions).then(() => TPromise.wrapError(error))),
S
Sandeep Somavarapu 已提交
242
				error => this.onDidInstallExtensions(extensions, null, INSTALL_ERROR_GALLERY, error)));
S
Sandeep Somavarapu 已提交
243 244 245 246 247
	}

	private checkForObsolete(extensionsToInstall: IGalleryExtension[]): TPromise<IGalleryExtension[]> {
		return this.filterObsolete(...extensionsToInstall.map(i => getLocalExtensionIdFromGallery(i, i.version)))
			.then(obsolete => obsolete.length ? TPromise.wrapError<IGalleryExtension[]>(new Error(nls.localize('restartCodeGallery', "Please restart Code before reinstalling."))) : extensionsToInstall);
248 249
	}

S
Sandeep Somavarapu 已提交
250 251
	private downloadInstallableExtension(extension: IGalleryExtension, installed: ILocalExtension[]): TPromise<InstallableExtension> {
		const current = installed.filter(i => i.identifier.uuid === extension.identifier.uuid)[0];
S
Sandeep Somavarapu 已提交
252 253
		const id = getLocalExtensionIdFromGallery(extension, extension.version);
		const metadata = <IGalleryMetadata>{
S
Sandeep Somavarapu 已提交
254
			id: extension.identifier.uuid,
S
Sandeep Somavarapu 已提交
255 256 257
			publisherId: extension.publisherId,
			publisherDisplayName: extension.publisherDisplayName,
		};
S
Sandeep Somavarapu 已提交
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275

		return this.galleryService.loadCompatibleVersion(extension)
			.then(
			compatible => {
				if (compatible) {
					return this.galleryService.download(extension)
						.then(
						zipPath => validateLocalExtension(zipPath)
							.then(
							() => (<InstallableExtension>{ zipPath, id, metadata, current }),
							error => TPromise.wrapError(new InstallationError(this.joinErrors(error).message, INSTALL_ERROR_VALIDATING))
							),
						error => TPromise.wrapError(new InstallationError(this.joinErrors(error).message, INSTALL_ERROR_DOWNLOADING)));
				} else {
					return TPromise.wrapError<InstallableExtension>(new InstallationError(nls.localize('notFoundCompatibleDependency', "Unable to install because, the depending extension '{0}' compatible with current version '{1}' of VS Code is not found.", extension.identifier.id, pkg.version), INSTALL_ERROR_INCOMPATIBLE));
				}
			},
			error => TPromise.wrapError<InstallableExtension>(new InstallationError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY)));
S
Sandeep Somavarapu 已提交
276 277 278 279
	}

	private rollback(extensions: IGalleryExtension[]): TPromise<void> {
		return this.filterOutUninstalled(extensions)
S
Sandeep Somavarapu 已提交
280
			.then(installed => TPromise.join(installed.map(local => this.uninstallExtension(local.identifier))))
S
Sandeep Somavarapu 已提交
281
			.then(() => null, () => null);
282 283
	}

S
Sandeep Somavarapu 已提交
284 285 286
	private onInstallExtensions(extensions: IGalleryExtension[]): void {
		for (const extension of extensions) {
			const id = getLocalExtensionIdFromGallery(extension, extension.version);
S
Sandeep Somavarapu 已提交
287
			this._onInstallExtension.fire({ identifier: { id, uuid: extension.identifier.uuid }, gallery: extension });
288
		}
289 290
	}

291
	private onDidInstallExtensions(extensions: IGalleryExtension[], local: ILocalExtension[], errorCode?: string, error?: any): TPromise<any> {
S
Sandeep Somavarapu 已提交
292
		extensions.forEach((gallery, index) => {
S
Sandeep Somavarapu 已提交
293
			const identifier = { id: getLocalExtensionIdFromGallery(gallery, gallery.version), uuid: gallery.identifier.uuid };
S
Sandeep Somavarapu 已提交
294
			if (errorCode) {
S
Sandeep Somavarapu 已提交
295
				this._onDidInstallExtension.fire({ identifier, gallery, error: errorCode });
S
Sandeep Somavarapu 已提交
296
			} else {
S
Sandeep Somavarapu 已提交
297
				this._onDidInstallExtension.fire({ identifier, gallery, local: local[index] });
S
Sandeep Somavarapu 已提交
298 299 300
			}
		});
		return error ? TPromise.wrapError(Array.isArray(error) ? this.joinErrors(error) : error) : TPromise.as(null);
301 302
	}

S
Sandeep Somavarapu 已提交
303 304 305 306 307 308 309 310 311
	private getDependenciesToInstall(dependencies: string[]): TPromise<IGalleryExtension[]> {
		return this.galleryService.loadAllDependencies(dependencies.map(id => (<IExtensionIdentifier>{ id })))
			.then(allDependencies => this.getInstalled()
				.then(local => {
					return allDependencies.filter(d => {
						const extensionId = getLocalExtensionIdFromGallery(d, d.version);
						return local.every(({ identifier }) => identifier.id !== extensionId);
					});
				}));
312 313
	}

J
Joao Moreno 已提交
314 315 316
	private filterOutUninstalled(extensions: IGalleryExtension[]): TPromise<ILocalExtension[]> {
		return this.getInstalled()
			.then(installed => installed.filter(local => !!this.getGalleryExtensionForLocalExtension(extensions, local)));
317
	}
318

319
	private getGalleryExtensionForLocalExtension(galleryExtensions: IGalleryExtension[], localExtension: ILocalExtension): IGalleryExtension {
S
Sandeep Somavarapu 已提交
320
		const filtered = galleryExtensions.filter(galleryExtension => areSameExtensions(localExtension.identifier, { id: getLocalExtensionIdFromGallery(galleryExtension, galleryExtension.version), uuid: galleryExtension.identifier.uuid }));
321 322 323
		return filtered.length ? filtered[0] : null;
	}

S
Sandeep Somavarapu 已提交
324
	private installExtension({ zipPath, id, metadata, current }: InstallableExtension): TPromise<ILocalExtension> {
J
Joao Moreno 已提交
325 326
		const extensionPath = path.join(this.extensionsPath, id);

J
João Moreno 已提交
327 328 329 330 331 332 333 334 335 336
		return pfs.rimraf(extensionPath).then(() => {
			return extract(zipPath, extensionPath, { sourcePath: 'extension', overwrite: true })
				.then(() => readManifest(extensionPath))
				.then(({ manifest }) => {
					return 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;
						const type = LocalExtensionType.User;
S
Sandeep Somavarapu 已提交
337
						const identifier = { id, uuid: metadata ? metadata.id : null };
J
João Moreno 已提交
338

S
Sandeep Somavarapu 已提交
339
						const local: ILocalExtension = { type, identifier, manifest, metadata, path: extensionPath, readmeUrl, changelogUrl };
J
João Moreno 已提交
340

341
						return this.saveMetadataForLocalExtension(local)
S
Sandeep Somavarapu 已提交
342
							.then(() => this.checkForRename(current, local))
J
João Moreno 已提交
343 344
							.then(() => local);
					});
J
Joao Moreno 已提交
345
				});
J
João Moreno 已提交
346
		});
E
Erich Gamma 已提交
347 348
	}

349
	uninstall(extension: ILocalExtension, force = false): TPromise<void> {
S
Sandeep Somavarapu 已提交
350 351 352 353 354 355 356
		return this.removeOutdatedExtensions()
			.then(() =>
				this.scanUserExtensions()
					.then(installed => {
						const promises = installed
							.filter(e => e.manifest.publisher === extension.manifest.publisher && e.manifest.name === extension.manifest.name)
							.map(e => this.checkForDependenciesAndUninstall(e, installed, force));
S
Sandeep Somavarapu 已提交
357
						return TPromise.join(promises).then(null, error => TPromise.wrapError(Array.isArray(error) ? this.joinErrors(error) : error));
S
Sandeep Somavarapu 已提交
358 359 360 361
					}))
			.then(() => { /* drop resolved value */ });
	}

362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378
	updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): TPromise<ILocalExtension> {
		local.metadata = metadata;
		return this.saveMetadataForLocalExtension(local);
	}

	private saveMetadataForLocalExtension(local: ILocalExtension): TPromise<ILocalExtension> {
		if (!local.metadata) {
			return TPromise.as(local);
		}
		const manifestPath = path.join(this.extensionsPath, local.identifier.id, 'package.json');
		return pfs.readFile(manifestPath, 'utf8')
			.then(raw => parseManifest(raw))
			.then(({ manifest }) => assign(manifest, { __metadata: local.metadata }))
			.then(manifest => pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t')))
			.then(() => local);
	}

S
Sandeep Somavarapu 已提交
379 380 381 382 383 384 385 386
	private getMetadata(extensionName: string): TPromise<IGalleryMetadata> {
		return this.galleryService.query({ names: [extensionName], pageSize: 1 })
			.then(galleryResult => {
				const galleryExtension = galleryResult.firstPage[0];
				return galleryExtension ? <IGalleryMetadata>{ id: galleryExtension.identifier.uuid, publisherDisplayName: galleryExtension.publisherDisplayName, publisherId: galleryExtension.publisherId } : null;
			});
	}

S
Sandeep Somavarapu 已提交
387 388 389 390 391 392 393 394 395
	private checkForRename(currentExtension: ILocalExtension, newExtension: ILocalExtension): TPromise<void> {
		// Check if the gallery id for current and new exensions are same, if not, remove the current one.
		if (currentExtension && getGalleryExtensionIdFromLocal(currentExtension) !== getGalleryExtensionIdFromLocal(newExtension)) {
			// return this.uninstallExtension(currentExtension.identifier);
			return this.setObsolete(currentExtension.identifier.id);
		}
		return TPromise.as(null);
	}

S
Sandeep Somavarapu 已提交
396 397 398 399 400 401
	private joinErrors(errors: (Error | string)[]): Error {
		if (errors.length === 1) {
			return errors[0] instanceof Error ? <Error>errors[0] : new Error(<string>errors[0]);
		}

		return errors.reduce<Error>((previousValue: Error, currentValue: Error | string) => {
S
Sandeep Somavarapu 已提交
402
			return new Error(`${previousValue.message}${previousValue.message ? ',' : ''}${currentValue instanceof Error ? currentValue.message : currentValue}`);
S
Sandeep Somavarapu 已提交
403
		}, new Error(''));
J
Joao Moreno 已提交
404 405
	}

406
	private checkForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[], force: boolean): TPromise<void> {
J
Joao Moreno 已提交
407
		return this.preUninstallExtension(extension)
408
			.then(() => this.hasDependencies(extension, installed) ? this.promptForDependenciesAndUninstall(extension, installed, force) : this.promptAndUninstall(extension, installed, force))
409
			.then(() => this.postUninstallExtension(extension),
410
			error => {
411
				this.postUninstallExtension(extension, INSTALL_ERROR_LOCAL);
412 413
				return TPromise.wrapError(error);
			});
S
Sandeep Somavarapu 已提交
414 415
	}

416 417
	private hasDependencies(extension: ILocalExtension, installed: ILocalExtension[]): boolean {
		if (extension.manifest.extensionDependencies && extension.manifest.extensionDependencies.length) {
S
Sandeep Somavarapu 已提交
418
			return installed.some(i => extension.manifest.extensionDependencies.indexOf(getGalleryExtensionIdFromLocal(i)) !== -1);
419 420 421 422
		}
		return false;
	}

423 424 425 426 427 428
	private promptForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[], force: boolean): TPromise<void> {
		if (force) {
			const dependencies = distinct(this.getDependenciesToUninstallRecursively(extension, installed, [])).filter(e => e !== extension);
			return this.uninstallWithDependencies(extension, dependencies, installed);
		}

429
		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 已提交
430 431 432 433 434
		const options = [
			nls.localize('uninstallOnly', "Only"),
			nls.localize('uninstallAll', "All"),
			nls.localize('cancel', "Cancel")
		];
435
		return this.choiceService.choose(Severity.Info, message, options, 2, true)
S
Sandeep Somavarapu 已提交
436 437
			.then<void>(value => {
				if (value === 0) {
438
					return this.uninstallWithDependencies(extension, [], installed);
S
Sandeep Somavarapu 已提交
439 440
				}
				if (value === 1) {
441 442
					const dependencies = distinct(this.getDependenciesToUninstallRecursively(extension, installed, [])).filter(e => e !== extension);
					return this.uninstallWithDependencies(extension, dependencies, installed);
S
Sandeep Somavarapu 已提交
443 444 445 446 447
				}
				return TPromise.wrapError(errors.canceled());
			}, error => TPromise.wrapError(errors.canceled()));
	}

448 449 450 451 452
	private promptAndUninstall(extension: ILocalExtension, installed: ILocalExtension[], force: boolean): TPromise<void> {
		if (force) {
			return this.uninstallWithDependencies(extension, [], installed);
		}

S
Sandeep Somavarapu 已提交
453 454
		const message = nls.localize('uninstallConfirmation', "Are you sure you want to uninstall '{0}'?", extension.manifest.displayName || extension.manifest.name);
		const options = [
D
David Hewson 已提交
455
			nls.localize('ok', "OK"),
S
Sandeep Somavarapu 已提交
456 457
			nls.localize('cancel', "Cancel")
		];
458
		return this.choiceService.choose(Severity.Info, message, options, 1, true)
S
Sandeep Somavarapu 已提交
459 460 461 462 463 464 465 466
			.then<void>(value => {
				if (value === 0) {
					return this.uninstallWithDependencies(extension, [], installed);
				}
				return TPromise.wrapError(errors.canceled());
			}, error => TPromise.wrapError(errors.canceled()));
	}

467 468
	private uninstallWithDependencies(extension: ILocalExtension, dependencies: ILocalExtension[], installed: ILocalExtension[]): TPromise<void> {
		const dependenciesToUninstall = this.filterDependents(extension, dependencies, installed);
469
		let dependents = this.getDependents(extension, installed).filter(dependent => extension !== dependent && dependenciesToUninstall.indexOf(dependent) === -1);
470
		if (dependents.length) {
471
			return TPromise.wrapError<void>(new Error(this.getDependentsErrorMessage(extension, dependents)));
472
		}
S
Sandeep Somavarapu 已提交
473
		return TPromise.join([this.uninstallExtension(extension.identifier), ...dependenciesToUninstall.map(d => this.doUninstall(d))]).then(() => null);
474 475
	}

S
Sandeep Somavarapu 已提交
476 477 478 479 480 481 482 483 484 485 486 487 488
	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);
	}

489 490 491 492 493 494 495 496
	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 [];
		}
S
Sandeep Somavarapu 已提交
497
		const dependenciesToUninstall = installed.filter(i => extension.manifest.extensionDependencies.indexOf(getGalleryExtensionIdFromLocal(i)) !== -1);
498 499 500 501 502 503 504 505 506 507 508 509
		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];
510
			const dependents = this.getDependents(dep, installed).filter(e => dependencies.indexOf(e) === -1);
511 512 513
			if (dependents.length) {
				result.splice(i - (dependencies.length - result.length), 1);
			}
S
Sandeep Somavarapu 已提交
514
		}
515
		return result;
S
Sandeep Somavarapu 已提交
516 517
	}

518
	private getDependents(extension: ILocalExtension, installed: ILocalExtension[]): ILocalExtension[] {
S
Sandeep Somavarapu 已提交
519
		return installed.filter(e => e.manifest.extensionDependencies && e.manifest.extensionDependencies.indexOf(getGalleryExtensionIdFromLocal(extension)) !== -1);
520 521
	}

J
Joao Moreno 已提交
522 523
	private doUninstall(extension: ILocalExtension): TPromise<void> {
		return this.preUninstallExtension(extension)
S
Sandeep Somavarapu 已提交
524
			.then(() => this.uninstallExtension(extension.identifier))
525
			.then(() => this.postUninstallExtension(extension),
S
Sandeep Somavarapu 已提交
526
			error => {
527
				this.postUninstallExtension(extension, INSTALL_ERROR_LOCAL);
S
Sandeep Somavarapu 已提交
528 529 530
				return TPromise.wrapError(error);
			});
	}
E
Erich Gamma 已提交
531

J
Joao Moreno 已提交
532
	private preUninstallExtension(extension: ILocalExtension): TPromise<void> {
S
Sandeep Somavarapu 已提交
533
		const extensionPath = path.join(this.extensionsPath, extension.identifier.id);
E
Erich Gamma 已提交
534
		return pfs.exists(extensionPath)
535
			.then(exists => exists ? null : TPromise.wrapError(new Error(nls.localize('notExists', "Could not find extension"))))
S
Sandeep Somavarapu 已提交
536
			.then(() => this._onUninstallExtension.fire(extension.identifier));
S
Sandeep Somavarapu 已提交
537 538
	}

S
Sandeep Somavarapu 已提交
539
	private uninstallExtension({ id }: IExtensionIdentifier): TPromise<void> {
S
Sandeep Somavarapu 已提交
540 541
		const extensionPath = path.join(this.extensionsPath, id);
		return this.setObsolete(id)
E
Erich Gamma 已提交
542
			.then(() => pfs.rimraf(extensionPath))
S
Sandeep Somavarapu 已提交
543 544 545
			.then(() => this.unsetObsolete(id));
	}

546
	private async postUninstallExtension(extension: ILocalExtension, error?: string): TPromise<void> {
547
		if (!error) {
548 549 550 551
			// only report if extension has a mapped gallery extension. UUID identifies the gallery extension.
			if (extension.identifier.uuid) {
				await this.galleryService.reportStatistic(extension.manifest.publisher, extension.manifest.name, extension.manifest.version, StatisticType.Uninstall);
			}
552 553
		}

S
Sandeep Somavarapu 已提交
554
		this._onDidUninstallExtension.fire({ identifier: extension.identifier, error });
E
Erich Gamma 已提交
555 556
	}

J
Joao Moreno 已提交
557 558 559 560 561 562 563 564 565 566 567
	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());
		}

568
		return TPromise.join<ILocalExtension[]>(promises).then(flatten);
J
Joao Moreno 已提交
569 570 571 572 573 574 575 576
	}

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

	private scanUserExtensions(): TPromise<ILocalExtension[]> {
		return this.scanExtensions(this.extensionsPath, LocalExtensionType.User).then(extensions => {
577
			const byId = values(groupBy(extensions, p => getGalleryExtensionIdFromLocal(p)));
J
Joao Moreno 已提交
578
			return byId.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]);
J
Joao Moreno 已提交
579 580 581
		});
	}

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

J
Joao Moreno 已提交
585
		return this.scanExtensionFolders(root)
586
			.then(extensionIds => TPromise.join(extensionIds.map(id => {
J
Joao Moreno 已提交
587 588 589 590 591 592 593 594 595
				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)
S
Sandeep Somavarapu 已提交
596 597 598 599
						.then<ILocalExtension>(({ manifest, metadata }) => {
							if (manifest.extensionDependencies) {
								manifest.extensionDependencies = manifest.extensionDependencies.map(id => adoptToGalleryExtensionId(id));
							}
S
Sandeep Somavarapu 已提交
600 601
							const identifier = { id, uuid: metadata ? metadata.id : null };
							return { type, identifier, manifest, metadata, path: extensionPath, readmeUrl, changelogUrl };
S
Sandeep Somavarapu 已提交
602
						});
J
Joao Moreno 已提交
603 604 605 606 607 608 609 610
				}).then(null, () => null);

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

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

J
Joao Moreno 已提交
615 616
	removeDeprecatedExtensions(): TPromise<any> {
		return TPromise.join([
617
			// Remove obsolte extensions first to avoid removing installed older extension. See #38609.
618 619
			this.removeObsoleteExtensions(),
			this.removeOutdatedExtensions()
J
Joao Moreno 已提交
620 621
		]);
	}
622

J
Joao Moreno 已提交
623 624 625 626
	private removeOutdatedExtensions(): TPromise<any> {
		return this.getOutdatedExtensionIds()
			.then(extensionIds => this.removeExtensions(extensionIds));
	}
627

J
Joao Moreno 已提交
628 629 630 631 632 633 634 635 636 637 638
	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]));
		}));
639 640
	}

J
Joao Moreno 已提交
641 642 643 644
	private getOutdatedExtensionIds(): TPromise<string[]> {
		return this.scanExtensionFolders(this.extensionsPath)
			.then(folders => {
				const galleryFolders = folders
645 646
					.map(folder => (assign({ folder }, getIdAndVersionFromLocalExtensionId(folder))))
					.filter(({ id, version }) => !!id && !!version);
J
Joao Moreno 已提交
647 648 649 650 651 652

				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 已提交
653 654
	}

J
Joao Moreno 已提交
655
	private isObsolete(id: string): TPromise<boolean> {
656 657 658 659 660 661 662 663 664 665 666 667 668
		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;
		});
669 670
	}

J
Joao Moreno 已提交
671
	private setObsolete(id: string): TPromise<void> {
J
Joao Moreno 已提交
672
		return this.withObsoleteExtensions(obsolete => assign(obsolete, { [id]: true }));
673 674
	}

J
Joao Moreno 已提交
675
	private unsetObsolete(id: string): TPromise<void> {
J
Joao Moreno 已提交
676
		return this.withObsoleteExtensions<void>(obsolete => delete obsolete[id]);
677 678
	}

J
Johannes Rieken 已提交
679
	private getObsoleteExtensions(): TPromise<{ [id: string]: boolean; }> {
J
Joao Moreno 已提交
680
		return this.withObsoleteExtensions(obsolete => obsolete);
681 682
	}

J
Johannes Rieken 已提交
683
	private withObsoleteExtensions<T>(fn: (obsolete: { [id: string]: boolean; }) => T): TPromise<T> {
684 685 686
		return this.obsoleteFileLimiter.queue(() => {
			let result: T = null;
			return pfs.readFile(this.obsoletePath, 'utf8')
R
Ron Buckton 已提交
687
				.then(null, err => err.code === 'ENOENT' ? TPromise.as('{}') : TPromise.wrapError(err))
J
Johannes Rieken 已提交
688
				.then<{ [id: string]: boolean }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; } })
689 690 691 692 693 694 695 696 697 698 699 700
				.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);
		});
	}
701

702 703 704
	dispose() {
		this.disposables = dispose(this.disposables);
	}
E
Erich Gamma 已提交
705
}
S
Sandeep Somavarapu 已提交
706 707 708 709 710 711 712 713 714 715 716 717

export function getLocalExtensionIdFromGallery(extension: IGalleryExtension, version: string): string {
	return getLocalExtensionId(extension.identifier.id, version);
}

export function getLocalExtensionIdFromManifest(manifest: IExtensionManifest): string {
	return getLocalExtensionId(getGalleryExtensionId(manifest.publisher, manifest.name), manifest.version);
}

function getLocalExtensionId(id: string, version: string): string {
	return `${id}-${version}`;
}