extensionManagementService.ts 16.1 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';
J
Joao Moreno 已提交
14
import { flatten } 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
Joao Moreno 已提交
17 18 19 20
import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension,
	IGalleryExtension, IExtensionIdentity, IExtensionManifest, IGalleryMetadata,
	InstallExtensionEvent, DidInstallExtensionEvent, LocalExtensionType
} from 'vs/platform/extensionManagement/common/extensionManagement';
21
import { localizeManifest } from '../common/extensionNls';
J
Joao Moreno 已提交
22
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
E
Erich Gamma 已提交
23
import { Limiter } from 'vs/base/common/async';
J
Joao Moreno 已提交
24
import Event, { Emitter } from 'vs/base/common/event';
J
Joao Moreno 已提交
25
import * as semver from 'semver';
26
import { groupBy, values } from 'vs/base/common/collections';
J
João Moreno 已提交
27
import URI from 'vs/base/common/uri';
28
import { IChoiceService, Severity } from 'vs/platform/message/common/message';
E
Erich Gamma 已提交
29

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

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

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

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

67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
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 已提交
84
function getExtensionId(extension: IExtensionIdentity, version: string): string {
J
Joao Moreno 已提交
85
	return `${ extension.publisher }.${ extension.name }-${ version }`;
86 87
}

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

90
	_serviceBrand: any;
E
Erich Gamma 已提交
91 92

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

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

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

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

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

	constructor(
110
		@IEnvironmentService private environmentService: IEnvironmentService,
111
		@IChoiceService private choiceService: IChoiceService,
112
		@IExtensionGalleryService private galleryService: IExtensionGalleryService
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);
E
Erich Gamma 已提交
117 118
	}

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

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

125 126 127 128
			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 已提交
129

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

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

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

144 145 146 147 148
		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 });
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
			return this.galleryService.loadCompatibleVersion(extension)
					.then(compatibleVersion => this.installCompatibleVersion(compatibleVersion, checkDependecies))
					.then(
						local => this._onDidInstallExtension.fire({ id, local, gallery: extension }),
						error => {
							this._onDidInstallExtension.fire({ id, gallery: extension, error });
							return TPromise.wrapError(error);
						}
					);
		});
	}

	private installCompatibleVersion(extension: IGalleryExtension, checkDependecies: boolean): TPromise<ILocalExtension> {
		const dependencies = checkDependecies ? this.getDependenciesToInstall(extension) : [];
		if (!dependencies.length) {
			return this.downloadAndInstall(extension);
		}

		const message = nls.localize('installDependecies', "This extension has dependencies. Would you like to install them along with it?");
		const options = [
			nls.localize('installWithDependenices', "Install With Dependencies"),
			nls.localize('installWithoutDependenices', "Install only this"),
			nls.localize('close', "Close")
		];
		return this.choiceService.choose(Severity.Info, message, options)
			.then(value => {
				switch (value) {
					case 0:
						return this.installWithDependencies(extension);
					case 1:
						return this.downloadAndInstall(extension);
					default:
						return TPromise.wrapError(errors.canceled());
				}
		});
	}

	private getDependenciesToInstall(extension: IGalleryExtension): string[] {
		const extensionName = `${extension.publisher}.${extension.name}`;
		return extension.properties.dependencies ? extension.properties.dependencies.filter(name => name !== extensionName) : [];
	}

	private installWithDependencies(extension: IGalleryExtension): TPromise<ILocalExtension> {
		return this.galleryService.getAllDependencies(extension)
			.then(allDependencies => this.filterOutInstalled(allDependencies))
			.then(toInstall => this.bulkInstallWithDependencies(extension, toInstall));
	}

	private bulkInstallWithDependencies(extension: IGalleryExtension, dependecies: IGalleryExtension[]): TPromise<ILocalExtension> {
		return this.downloadAndInstall(extension)
				.then(localExtension => TPromise.join(dependecies.map((dep) => this.installFromGallery(dep, false)))
					.then(() => localExtension, error => this.rollback(localExtension, dependecies).then(() => error)));
	}

	private rollback(localExtension: ILocalExtension, dependecies: IGalleryExtension[]): TPromise<void> {
		return this.uninstall(localExtension)
					.then(() => this.filterOutUnInstalled(dependecies))
					.then(installed => TPromise.join(installed.map((i) => this.uninstall(i))))
					.then(() => null);
	}

	private filterOutInstalled(extensions: IGalleryExtension[]): TPromise<IGalleryExtension[]> {
		return this.getInstalled().then(local => {
212 213 214 215
			return extensions.filter(extension => {
				const extensionId = getExtensionId(extension, extension.version);
				return local.every(local => local.id !== extensionId);
			});
216 217 218 219 220
		});
	}

	private filterOutUnInstalled(extensions: IGalleryExtension[]): TPromise<ILocalExtension[]> {
		return this.getInstalled().then(installed => {
221 222 223
			return installed.filter(local => {
				return extensions.every(extension => getExtensionId(extension, extension.version) === local.id);
			});
224 225
		});
	}
226

227 228 229
	private downloadAndInstall(extension: IGalleryExtension): TPromise<ILocalExtension> {
		const id = getExtensionId(extension, extension.version);
		const metadata = {
230 231 232 233
				id: extension.id,
				publisherId: extension.publisherId,
				publisherDisplayName: extension.publisherDisplayName
			};
234 235 236
		return this.galleryService.download(extension)
			.then(zipPath => validate(zipPath).then(() => zipPath))
			.then(zipPath => this.installExtension(zipPath, id, metadata));
237
	}
J
Joao Moreno 已提交
238

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

		return extract(zipPath, extensionPath, { sourcePath: 'extension', overwrite: true })
243
			.then(() => readManifest(extensionPath))
J
Joao Moreno 已提交
244
			.then(({ manifest }) => {
J
Joao Moreno 已提交
245 246
				return pfs.readdir(extensionPath).then(children => {
					const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0];
J
João Moreno 已提交
247
					const readmeUrl = readme ? URI.file(path.join(extensionPath, readme)).toString() : null;
248 249
					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 已提交
250
					const type = LocalExtensionType.User;
251

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

255 256 257 258
					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')))
259
						.then(() => local);
J
Joao Moreno 已提交
260
				});
261
			});
E
Erich Gamma 已提交
262 263
	}

J
Joao Moreno 已提交
264
	uninstall(extension: ILocalExtension): TPromise<void> {
J
Joao Moreno 已提交
265
		return this.scanUserExtensions().then<void>(installed => {
J
Joao Moreno 已提交
266 267 268 269 270 271 272 273 274
			const promises = installed
				.filter(e => e.manifest.publisher === extension.manifest.publisher && e.manifest.name === extension.manifest.name)
				.map(({ id }) => this.uninstallExtension(id));

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

	private uninstallExtension(id: string): TPromise<void> {
J
Joao Moreno 已提交
275
		const extensionPath = path.join(this.extensionsPath, id);
E
Erich Gamma 已提交
276 277 278

		return pfs.exists(extensionPath)
			.then(exists => exists ? null : Promise.wrapError(new Error(nls.localize('notExists', "Could not find extension"))))
J
Joao Moreno 已提交
279
			.then(() => this._onUninstallExtension.fire(id))
J
Joao Moreno 已提交
280
			.then(() => this.setObsolete(id))
E
Erich Gamma 已提交
281
			.then(() => pfs.rimraf(extensionPath))
J
Joao Moreno 已提交
282
			.then(() => this.unsetObsolete(id))
J
Joao Moreno 已提交
283
			.then(() => this._onDidUninstallExtension.fire(id));
E
Erich Gamma 已提交
284 285
	}

J
Joao Moreno 已提交
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
	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
Joao Moreno 已提交
306 307
			const byId = values(groupBy(extensions, p => `${ p.manifest.publisher }.${ p.manifest.name }`));
			return byId.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]);
J
Joao Moreno 已提交
308 309 310
		});
	}

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

314 315
		return this.getObsoleteExtensions()
			.then(obsolete => {
J
Joao Moreno 已提交
316
				return pfs.readdir(root)
J
Joao Moreno 已提交
317
					.then(extensions => extensions.filter(id => !obsolete[id]))
J
Joao Moreno 已提交
318
					.then<ILocalExtension[]>(extensionIds => Promise.join(extensionIds.map(id => {
J
Joao Moreno 已提交
319
						const extensionPath = path.join(root, id);
320

J
Joao Moreno 已提交
321 322
						const each = () => pfs.readdir(extensionPath).then(children => {
							const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0];
J
João Moreno 已提交
323
							const readmeUrl = readme ? URI.file(path.join(extensionPath, readme)).toString() : null;
324 325 326
							const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0];
							const changelogUrl = changelog ? URI.file(path.join(extensionPath, changelog)).toString() : null;

327
							return readManifest(extensionPath)
J
Joao Moreno 已提交
328
								.then<ILocalExtension>(({ manifest, metadata }) => ({ type, id, manifest, metadata, path: extensionPath, readmeUrl, changelogUrl }));
J
Joao Moreno 已提交
329 330 331
						}).then(null, () => null);

						return limiter.queue(each);
332 333 334
					})))
					.then(result => result.filter(a => !!a));
			});
E
Erich Gamma 已提交
335 336
	}

J
Joao Moreno 已提交
337
	removeDeprecatedExtensions(): TPromise<void> {
J
Joao Moreno 已提交
338
		const outdated = this.getOutdatedExtensionIds()
J
Joao Moreno 已提交
339
			.then(extensions => extensions.map(e => getExtensionId(e.manifest, e.manifest.version)));
340 341 342 343 344 345 346 347 348

		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 已提交
349
						.then(() => this.withObsoleteExtensions(obsolete => delete obsolete[id]));
350 351 352 353
				}));
			});
	}

J
Joao Moreno 已提交
354
	private getOutdatedExtensionIds(): TPromise<ILocalExtension[]> {
J
Joao Moreno 已提交
355
		return this.scanUserExtensions()
J
Joao Moreno 已提交
356 357
			.then(extensions => values(groupBy(extensions, p => `${ p.manifest.publisher }.${ p.manifest.name }`)))
			.then(versions => flatten(versions.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version)).slice(1))));
J
Joao Moreno 已提交
358 359
	}

J
Joao Moreno 已提交
360
	private isObsolete(id: string): TPromise<boolean> {
361 362 363
		return this.withObsoleteExtensions(obsolete => !!obsolete[id]);
	}

J
Joao Moreno 已提交
364
	private setObsolete(id: string): TPromise<void> {
J
Joao Moreno 已提交
365
		return this.withObsoleteExtensions(obsolete => assign(obsolete, { [id]: true }));
366 367
	}

J
Joao Moreno 已提交
368
	private unsetObsolete(id: string): TPromise<void> {
J
Joao Moreno 已提交
369
		return this.withObsoleteExtensions<void>(obsolete => delete obsolete[id]);
370 371 372
	}

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

J
Joao Moreno 已提交
376
	private withObsoleteExtensions<T>(fn: (obsolete: { [id:string]: boolean; }) => T): TPromise<T> {
377 378 379 380
		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))
J
Joao Moreno 已提交
381
				.then<{ [id: string]: boolean }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; }})
382 383 384 385 386 387 388 389 390 391 392 393
				.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);
		});
	}
394

395 396 397
	dispose() {
		this.disposables = dispose(this.disposables);
	}
E
Erich Gamma 已提交
398
}