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

'use strict';

import nls = require('vs/nls');
import { tmpdir } from 'os';
import * as path from 'path';
import types = require('vs/base/common/types');
import * as pfs from 'vs/base/node/pfs';
import { assign } from 'vs/base/common/objects';
14
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
J
Joao Moreno 已提交
15
import { flatten } from 'vs/base/common/arrays';
E
Erich Gamma 已提交
16 17
import { extract, buffer } from 'vs/base/node/zip';
import { Promise, TPromise } from 'vs/base/common/winjs.base';
J
Joao Moreno 已提交
18
import { IExtensionManagementService, ILocalExtension, IGalleryExtension, IExtensionIdentity, IExtensionManifest, IGalleryVersion, IGalleryMetadata, InstallExtensionEvent, DidInstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement';
19
import { download, json, IRequestOptions } from 'vs/base/node/request';
20
import { getProxyAgent } from 'vs/base/node/proxy';
J
Joao Moreno 已提交
21
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
E
Erich Gamma 已提交
22
import { Limiter } from 'vs/base/common/async';
J
Joao Moreno 已提交
23
import Event, { Emitter } from 'vs/base/common/event';
J
Joao Moreno 已提交
24
import { UserSettings } from 'vs/base/node/userSettings';
J
Joao Moreno 已提交
25
import * as semver from 'semver';
26
import { groupBy, values } from 'vs/base/common/collections';
27
import { isValidExtensionVersion } from 'vs/platform/extensions/node/extensionValidator';
J
Joao Moreno 已提交
28
import pkg from 'vs/platform/package';
E
Erich Gamma 已提交
29

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

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

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

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

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

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

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

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

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

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

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

	constructor(
91
		@IEnvironmentService private environmentService: IEnvironmentService
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
	}

J
Joao Moreno 已提交
98
	install(extension: IGalleryExtension): TPromise<void>;
J
Joao Moreno 已提交
99 100
	install(zipPath: string): TPromise<void>;
	install(arg: any): TPromise<void> {
101 102 103
		let id: string;
		let result: TPromise<ILocalExtension>;

E
Erich Gamma 已提交
104
		if (types.isString(arg)) {
J
Joao Moreno 已提交
105 106
			const zipPath = arg as string;

107 108
			result = validate(zipPath).then(manifest => {
				id = getExtensionId(manifest, manifest.version);
J
Joao Moreno 已提交
109 110 111 112
				this._onInstallExtension.fire({ id });

				return this.installValidExtension(zipPath, id);
			});
113 114 115 116 117 118 119 120 121
		} else {
			const extension = arg as IGalleryExtension;
			id = getExtensionId(extension, extension.versions[0].version);
			this._onInstallExtension.fire({ id, gallery: extension });

			result = this.isObsolete(id).then(obsolete => {
				if (obsolete) {
					return TPromise.wrapError<ILocalExtension>(new Error(nls.localize('restartCode', "Please restart Code before reinstalling {0}.", extension.displayName || extension.name)));
				}
J
Joao Moreno 已提交
122

123 124 125
				return this.installFromGallery(arg);
			});
		}
126

127 128 129 130
		return result.then(
			local => this._onDidInstallExtension.fire({ id, local }),
			error => { this._onDidInstallExtension.fire({ id, error }); return TPromise.wrapError(error); }
		);
E
Erich Gamma 已提交
131 132
	}

133
	private installFromGallery(extension: IGalleryExtension): TPromise<ILocalExtension> {
J
Joao Moreno 已提交
134
		return this.getLastValidExtensionVersion(extension).then(versionInfo => {
J
Joao Moreno 已提交
135 136 137
				const version = versionInfo.version;
				const url = versionInfo.downloadUrl;
				const headers = versionInfo.downloadHeaders;
J
Joao Moreno 已提交
138
				const zipPath = path.join(tmpdir(), extension.id);
J
Joao Moreno 已提交
139
				const id = getExtensionId(extension, version);
140 141 142 143 144
				const metadata = {
					id: extension.id,
					publisherId: extension.publisherId,
					publisherDisplayName: extension.publisherDisplayName
				};
J
Joao Moreno 已提交
145 146 147 148

				return this.request(url)
					.then(opts => assign(opts, { headers }))
					.then(opts => download(zipPath, opts))
J
Joao Moreno 已提交
149
					.then(() => validate(zipPath, extension, version))
150
					.then(() => this.installValidExtension(zipPath, id, metadata));
151
		});
E
Erich Gamma 已提交
152 153
	}

J
Joao Moreno 已提交
154
	private getLastValidExtensionVersion(extension: IGalleryExtension): TPromise<IGalleryVersion> {
J
Joao Moreno 已提交
155
		return this._getLastValidExtensionVersion(extension, extension.versions);
J
Joao Moreno 已提交
156 157 158
	}

	private _getLastValidExtensionVersion(extension: IGalleryExtension, versions: IGalleryVersion[]): TPromise<IGalleryVersion> {
159
		if (!versions.length) {
J
Joao Moreno 已提交
160
			return TPromise.wrapError(new Error(nls.localize('noCompatible', "Couldn't find a compatible version of {0} with this version of Code.", extension.displayName || extension.name)));
161 162
		}

J
Joao Moreno 已提交
163
		const headers = { 'accept-encoding': 'gzip' };
164
		const version = versions[0];
J
Joao Moreno 已提交
165

166
		return this.request(version.manifestUrl)
J
Joao Moreno 已提交
167
			.then(opts => assign(opts, { headers }))
168 169 170 171
			.then(opts => json<IExtensionManifest>(opts))
			.then(manifest => {
				const desc = {
					isBuiltin: false,
J
Joao Moreno 已提交
172 173
					engines: { vscode: manifest.engines.vscode },
					main: manifest.main
174 175
				};

176
				if (!isValidExtensionVersion(pkg.version, desc, [])) {
J
Joao Moreno 已提交
177
					return this._getLastValidExtensionVersion(extension, versions.slice(1));
178 179 180 181 182
				}

				return version;
			});
	}
J
Joao Moreno 已提交
183

184
	private installValidExtension(zipPath: string, id: string, metadata: IGalleryMetadata = null): TPromise<ILocalExtension> {
J
Joao Moreno 已提交
185 186 187 188 189 190
		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 已提交
191
			.then(({ manifest }) => {
J
Joao Moreno 已提交
192 193 194
				return pfs.readdir(extensionPath).then(children => {
					const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0];
					const readmeUrl = readme ? `file://${ extensionPath }/${ readme }` : null;
J
Joao Moreno 已提交
195

J
Joao Moreno 已提交
196 197 198 199
					const local: ILocalExtension = { id, manifest, metadata, path: extensionPath, readmeUrl };
					const rawManifest = assign(manifest, { __metadata: metadata });

					return pfs.writeFile(manifestPath, JSON.stringify(rawManifest, null, '\t'))
200
						.then(() => local);
J
Joao Moreno 已提交
201
				});
202
			});
E
Erich Gamma 已提交
203 204
	}

J
Joao Moreno 已提交
205
	uninstall(extension: ILocalExtension): TPromise<void> {
J
Joao Moreno 已提交
206 207 208 209 210 211 212 213 214 215
		return this.getAllInstalled().then<void>(installed => {
			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 已提交
216
		const extensionPath = path.join(this.extensionsPath, id);
E
Erich Gamma 已提交
217 218 219

		return pfs.exists(extensionPath)
			.then(exists => exists ? null : Promise.wrapError(new Error(nls.localize('notExists', "Could not find extension"))))
J
Joao Moreno 已提交
220
			.then(() => this._onUninstallExtension.fire(id))
J
Joao Moreno 已提交
221
			.then(() => this.setObsolete(id))
E
Erich Gamma 已提交
222
			.then(() => pfs.rimraf(extensionPath))
J
Joao Moreno 已提交
223
			.then(() => this.unsetObsolete(id))
J
Joao Moreno 已提交
224
			.then(() => this._onDidUninstallExtension.fire(id));
E
Erich Gamma 已提交
225 226
	}

J
Joao Moreno 已提交
227 228
	getInstalled(): TPromise<ILocalExtension[]> {
		return this.getAllInstalled().then(extensions => {
J
Joao Moreno 已提交
229 230
			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 已提交
231 232 233
		});
	}

J
Joao Moreno 已提交
234
	private getAllInstalled(): TPromise<ILocalExtension[]> {
E
Erich Gamma 已提交
235 236
		const limiter = new Limiter(10);

237 238 239
		return this.getObsoleteExtensions()
			.then(obsolete => {
				return pfs.readdir(this.extensionsPath)
J
Joao Moreno 已提交
240
					.then(extensions => extensions.filter(id => !obsolete[id]))
J
Joao Moreno 已提交
241
					.then<ILocalExtension[]>(extensionIds => Promise.join(extensionIds.map(id => {
J
Joao Moreno 已提交
242
						const extensionPath = path.join(this.extensionsPath, id);
243

J
Joao Moreno 已提交
244 245 246 247 248
						const each = () => pfs.readdir(extensionPath).then(children => {
							const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0];
							const readmeUrl = readme ? `file://${ extensionPath }/${ readme }` : null;

							return pfs.readFile(path.join(extensionPath, 'package.json'), 'utf8')
249
								.then(raw => parseManifest(raw))
J
Joao Moreno 已提交
250 251 252 253
								.then<ILocalExtension>(({ manifest, metadata }) => ({ id, manifest, metadata, path: extensionPath, readmeUrl }));
						}).then(null, () => null);

						return limiter.queue(each);
254 255 256
					})))
					.then(result => result.filter(a => !!a));
			});
E
Erich Gamma 已提交
257 258
	}

J
Joao Moreno 已提交
259
	removeDeprecatedExtensions(): TPromise<void> {
J
Joao Moreno 已提交
260
		const outdated = this.getOutdatedExtensionIds()
J
Joao Moreno 已提交
261
			.then(extensions => extensions.map(e => getExtensionId(e.manifest, e.manifest.version)));
262 263 264 265 266 267 268 269 270

		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 已提交
271
						.then(() => this.withObsoleteExtensions(obsolete => delete obsolete[id]));
272 273 274 275
				}));
			});
	}

J
Joao Moreno 已提交
276
	private getOutdatedExtensionIds(): TPromise<ILocalExtension[]> {
J
Joao Moreno 已提交
277 278 279
		return this.getAllInstalled()
			.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 已提交
280 281
	}

J
Joao Moreno 已提交
282
	private isObsolete(id: string): TPromise<boolean> {
283 284 285
		return this.withObsoleteExtensions(obsolete => !!obsolete[id]);
	}

J
Joao Moreno 已提交
286
	private setObsolete(id: string): TPromise<void> {
J
Joao Moreno 已提交
287
		return this.withObsoleteExtensions(obsolete => assign(obsolete, { [id]: true }));
288 289
	}

J
Joao Moreno 已提交
290
	private unsetObsolete(id: string): TPromise<void> {
J
Joao Moreno 已提交
291
		return this.withObsoleteExtensions<void>(obsolete => delete obsolete[id]);
292 293 294
	}

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

J
Joao Moreno 已提交
298
	private withObsoleteExtensions<T>(fn: (obsolete: { [id:string]: boolean; }) => T): TPromise<T> {
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
		return this.obsoleteFileLimiter.queue(() => {
			let result: T = null;
			return pfs.readFile(this.obsoletePath, 'utf8')
				.then(null, err => err.code === 'ENOENT' ? TPromise.as('{}') : TPromise.wrapError(err))
				.then<{ [id: string]: boolean }>(raw => JSON.parse(raw))
				.then(obsolete => { result = fn(obsolete); return obsolete; })
				.then(obsolete => {
					if (Object.keys(obsolete).length === 0) {
						return pfs.rimraf(this.obsoletePath);
					} else {
						const raw = JSON.stringify(obsolete);
						return pfs.writeFile(this.obsoletePath, raw);
					}
				})
				.then(() => result);
		});
	}
316 317 318 319 320

	// Helper for proxy business... shameful.
	// This should be pushed down and not rely on the context service
	private request(url: string): TPromise<IRequestOptions> {
		const settings = TPromise.join([
J
Joao Moreno 已提交
321 322 323
			// TODO@Joao we need a nice configuration service here!
			UserSettings.getValue(this.environmentService.userDataPath, 'http.proxy'),
			UserSettings.getValue(this.environmentService.userDataPath, 'http.proxyStrictSSL')
324 325 326 327 328 329 330 331 332 333
		]);

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

			return { url, agent, strictSSL };
		});
	}
334 335 336 337

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