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

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

J
Joao Moreno 已提交
29
function parseManifest(raw: string): TPromise<{ manifest: IExtensionManifest; metadata: IGalleryMetadata; }> {
E
Erich Gamma 已提交
30 31
	return new Promise((c, e) => {
		try {
J
Joao Moreno 已提交
32 33 34 35
			const manifest = JSON.parse(raw);
			const metadata = manifest.__metadata || null;
			delete manifest.__metadata;
			c({ manifest, metadata });
E
Erich Gamma 已提交
36 37 38 39 40 41
		} 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?: IExtensionIdentity, version?: string): TPromise<IExtensionManifest> {
E
Erich Gamma 已提交
43 44
	return buffer(zipPath, 'extension/package.json')
		.then(buffer => parseManifest(buffer.toString('utf8')))
J
Joao Moreno 已提交
45
		.then(({ manifest }) => {
E
Erich Gamma 已提交
46 47 48 49 50 51 52 53 54
			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
		});
}

J
Joao Moreno 已提交
64
function getExtensionId(extension: IExtensionIdentity, version: string): string {
J
Joao Moreno 已提交
65
	return `${ extension.publisher }.${ extension.name }-${ version }`;
66 67
}

J
Joao Moreno 已提交
68
export class ExtensionManagementService implements IExtensionManagementService {
E
Erich Gamma 已提交
69

70
	_serviceBrand: any;
E
Erich Gamma 已提交
71 72

	private extensionsPath: string;
73 74
	private obsoletePath: string;
	private obsoleteFileLimiter: Limiter<void>;
75
	private disposables: IDisposable[];
E
Erich Gamma 已提交
76

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

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

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

J
Joao Moreno 已提交
86 87
	private _onDidUninstallExtension = new Emitter<string>();
	onDidUninstallExtension: Event<string> = this._onDidUninstallExtension.event;
E
Erich Gamma 已提交
88 89

	constructor(
90 91
		@IEnvironmentService private environmentService: IEnvironmentService,
		@IExtensionGalleryService private galleryService: IExtensionGalleryService
E
Erich Gamma 已提交
92
	) {
J
Joao Moreno 已提交
93
		this.extensionsPath = environmentService.extensionsPath;
94 95
		this.obsoletePath = path.join(this.extensionsPath, '.obsolete');
		this.obsoleteFileLimiter = new Limiter(1);
E
Erich Gamma 已提交
96 97
	}

98
	install(zipPath: string): TPromise<void> {
99 100
		zipPath = path.resolve(zipPath);

101 102
		return validate(zipPath).then<void>(manifest => {
			const id = getExtensionId(manifest, manifest.version);
103

104 105 106 107
			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 已提交
108

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

111 112
				return this.installExtension(zipPath, id)
					.then(
113 114
						local => this._onDidInstallExtension.fire({ id, zipPath, local }),
						error => { this._onDidInstallExtension.fire({ id, zipPath, error }); return TPromise.wrapError(error); }
115
					);
116
			});
117
		});
E
Erich Gamma 已提交
118 119
	}

120 121
	installFromGallery(extension: IGalleryExtension): TPromise<void> {
		const id = getExtensionId(extension, extension.version);
J
Joao Moreno 已提交
122

123 124 125 126
		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)));
			}
127

128
			this._onInstallExtension.fire({ id, gallery: extension });
129

130 131 132 133 134 135 136
			const metadata = {
				id: extension.id,
				publisherId: extension.publisherId,
				publisherDisplayName: extension.publisherDisplayName
			};

			return this.galleryService.download(extension)
137
				.then(zipPath => validate(zipPath).then(() => zipPath))
138 139
				.then(zipPath => this.installExtension(zipPath, id, metadata))
				.then(
140 141
					local => this._onDidInstallExtension.fire({ id, local, gallery: extension }),
					error => { this._onDidInstallExtension.fire({ id, gallery: extension, error }); return TPromise.wrapError(error); }
142 143
				);
		});
144
	}
J
Joao Moreno 已提交
145

146
	private installExtension(zipPath: string, id: string, metadata: IGalleryMetadata = null): TPromise<ILocalExtension> {
J
Joao Moreno 已提交
147 148 149 150 151 152
		const extensionPath = path.join(this.extensionsPath, id);
		const manifestPath = path.join(extensionPath, 'package.json');

		return extract(zipPath, extensionPath, { sourcePath: 'extension', overwrite: true })
			.then(() => pfs.readFile(manifestPath, 'utf8'))
			.then(raw => parseManifest(raw))
J
Joao Moreno 已提交
153
			.then(({ manifest }) => {
J
Joao Moreno 已提交
154 155
				return pfs.readdir(extensionPath).then(children => {
					const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0];
J
João Moreno 已提交
156
					const readmeUrl = readme ? URI.file(path.join(extensionPath, readme)).toString() : null;
157 158
					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 已提交
159
					const type = LocalExtensionType.User;
160

J
Joao Moreno 已提交
161
					const local: ILocalExtension = { type, id, manifest, metadata, path: extensionPath, readmeUrl, changelogUrl };
J
Joao Moreno 已提交
162 163 164
					const rawManifest = assign(manifest, { __metadata: metadata });

					return pfs.writeFile(manifestPath, JSON.stringify(rawManifest, null, '\t'))
165
						.then(() => local);
J
Joao Moreno 已提交
166
				});
167
			});
E
Erich Gamma 已提交
168 169
	}

J
Joao Moreno 已提交
170
	uninstall(extension: ILocalExtension): TPromise<void> {
J
Joao Moreno 已提交
171
		return this.scanUserExtensions().then<void>(installed => {
J
Joao Moreno 已提交
172 173 174 175 176 177 178 179 180
			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 已提交
181
		const extensionPath = path.join(this.extensionsPath, id);
E
Erich Gamma 已提交
182 183 184

		return pfs.exists(extensionPath)
			.then(exists => exists ? null : Promise.wrapError(new Error(nls.localize('notExists', "Could not find extension"))))
J
Joao Moreno 已提交
185
			.then(() => this._onUninstallExtension.fire(id))
J
Joao Moreno 已提交
186
			.then(() => this.setObsolete(id))
E
Erich Gamma 已提交
187
			.then(() => pfs.rimraf(extensionPath))
J
Joao Moreno 已提交
188
			.then(() => this.unsetObsolete(id))
J
Joao Moreno 已提交
189
			.then(() => this._onDidUninstallExtension.fire(id));
E
Erich Gamma 已提交
190 191
	}

J
Joao Moreno 已提交
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
	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 已提交
212 213
			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 已提交
214 215 216
		});
	}

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

220 221
		return this.getObsoleteExtensions()
			.then(obsolete => {
J
Joao Moreno 已提交
222
				return pfs.readdir(root)
J
Joao Moreno 已提交
223
					.then(extensions => extensions.filter(id => !obsolete[id]))
J
Joao Moreno 已提交
224
					.then<ILocalExtension[]>(extensionIds => Promise.join(extensionIds.map(id => {
J
Joao Moreno 已提交
225
						const extensionPath = path.join(root, id);
226

J
Joao Moreno 已提交
227 228
						const each = () => pfs.readdir(extensionPath).then(children => {
							const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0];
J
João Moreno 已提交
229
							const readmeUrl = readme ? URI.file(path.join(extensionPath, readme)).toString() : null;
230 231 232
							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 已提交
233
							return pfs.readFile(path.join(extensionPath, 'package.json'), 'utf8')
234
								.then(raw => parseManifest(raw))
J
Joao Moreno 已提交
235
								.then<ILocalExtension>(({ manifest, metadata }) => ({ type, id, manifest, metadata, path: extensionPath, readmeUrl, changelogUrl }));
J
Joao Moreno 已提交
236 237 238
						}).then(null, () => null);

						return limiter.queue(each);
239 240 241
					})))
					.then(result => result.filter(a => !!a));
			});
E
Erich Gamma 已提交
242 243
	}

J
Joao Moreno 已提交
244
	removeDeprecatedExtensions(): TPromise<void> {
J
Joao Moreno 已提交
245
		const outdated = this.getOutdatedExtensionIds()
J
Joao Moreno 已提交
246
			.then(extensions => extensions.map(e => getExtensionId(e.manifest, e.manifest.version)));
247 248 249 250 251 252 253 254 255

		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 已提交
256
						.then(() => this.withObsoleteExtensions(obsolete => delete obsolete[id]));
257 258 259 260
				}));
			});
	}

J
Joao Moreno 已提交
261
	private getOutdatedExtensionIds(): TPromise<ILocalExtension[]> {
J
Joao Moreno 已提交
262
		return this.scanUserExtensions()
J
Joao Moreno 已提交
263 264
			.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 已提交
265 266
	}

J
Joao Moreno 已提交
267
	private isObsolete(id: string): TPromise<boolean> {
268 269 270
		return this.withObsoleteExtensions(obsolete => !!obsolete[id]);
	}

J
Joao Moreno 已提交
271
	private setObsolete(id: string): TPromise<void> {
J
Joao Moreno 已提交
272
		return this.withObsoleteExtensions(obsolete => assign(obsolete, { [id]: true }));
273 274
	}

J
Joao Moreno 已提交
275
	private unsetObsolete(id: string): TPromise<void> {
J
Joao Moreno 已提交
276
		return this.withObsoleteExtensions<void>(obsolete => delete obsolete[id]);
277 278 279
	}

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

J
Joao Moreno 已提交
283
	private withObsoleteExtensions<T>(fn: (obsolete: { [id:string]: boolean; }) => T): TPromise<T> {
284 285 286 287
		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 已提交
288
				.then<{ [id: string]: boolean }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; }})
289 290 291 292 293 294 295 296 297 298 299 300
				.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);
		});
	}
301

302 303 304
	dispose() {
		this.disposables = dispose(this.disposables);
	}
E
Erich Gamma 已提交
305
}