extensionsSync.ts 16.3 KB
Newer Older
S
Sandeep Somavarapu 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, ISyncPreviewResult, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync';
S
Sandeep Somavarapu 已提交
7
import { Event } from 'vs/base/common/event';
S
Sandeep Somavarapu 已提交
8
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
9
import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
S
Sandeep Somavarapu 已提交
10 11 12
import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IFileService } from 'vs/platform/files/common/files';
13
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
14
import { merge } from 'vs/platform/userDataSync/common/extensionsMerge';
15
import { isNonEmptyArray } from 'vs/base/common/arrays';
S
Sandeep Somavarapu 已提交
16
import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
17
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
18
import { URI } from 'vs/base/common/uri';
19
import { joinPath, dirname, basename, isEqual } from 'vs/base/common/resources';
20 21
import { format } from 'vs/base/common/jsonFormatter';
import { applyEdits } from 'vs/base/common/jsonEdit';
22
import { compare } from 'vs/base/common/strings';
S
Sandeep Somavarapu 已提交
23

S
Sandeep Somavarapu 已提交
24
interface IExtensionsSyncPreviewResult extends ISyncPreviewResult {
25 26 27
	readonly localExtensions: ISyncExtension[];
	readonly remoteUserData: IRemoteUserData;
	readonly lastSyncUserData: ILastSyncUserData | null;
S
Sandeep Somavarapu 已提交
28
	readonly added: ISyncExtension[];
29
	readonly removed: IExtensionIdentifier[];
S
Sandeep Somavarapu 已提交
30 31
	readonly updated: ISyncExtension[];
	readonly remote: ISyncExtension[] | null;
32
	readonly skippedExtensions: ISyncExtension[];
S
Sandeep Somavarapu 已提交
33
}
S
Sandeep Somavarapu 已提交
34

S
Sandeep Somavarapu 已提交
35
interface ILastSyncUserData extends IRemoteUserData {
S
Sandeep Somavarapu 已提交
36 37 38
	skippedExtensions: ISyncExtension[] | undefined;
}

39

40
export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
S
Sandeep Somavarapu 已提交
41

42
	private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/current.json` });
S
Sandeep Somavarapu 已提交
43
	protected readonly version: number = 2;
S
Sandeep Somavarapu 已提交
44
	protected isEnabled(): boolean { return super.isEnabled() && this.extensionGalleryService.isEnabled(); }
45

S
Sandeep Somavarapu 已提交
46
	constructor(
S
Sandeep Somavarapu 已提交
47
		@IEnvironmentService environmentService: IEnvironmentService,
48
		@IFileService fileService: IFileService,
S
Sandeep Somavarapu 已提交
49
		@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
S
Sandeep Somavarapu 已提交
50
		@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
S
Sandeep Somavarapu 已提交
51
		@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
52
		@IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService,
53
		@IUserDataSyncLogService logService: IUserDataSyncLogService,
S
Sandeep Somavarapu 已提交
54
		@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
S
Sandeep Somavarapu 已提交
55
		@IConfigurationService configurationService: IConfigurationService,
S
Sandeep Somavarapu 已提交
56
		@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
57
		@ITelemetryService telemetryService: ITelemetryService,
S
Sandeep Somavarapu 已提交
58
	) {
S
Sandeep Somavarapu 已提交
59
		super(SyncResource.Extensions, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
60 61
		this._register(
			Event.debounce(
S
Sandeep Somavarapu 已提交
62
				Event.any<any>(
63
					Event.filter(this.extensionManagementService.onDidInstallExtension, (e => !!e.gallery)),
S
Sandeep Somavarapu 已提交
64 65
					Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error)),
					this.extensionEnablementService.onDidChangeEnablement),
66
				() => undefined, 500)(() => this._onDidChangeLocal.fire()));
S
Sandeep Somavarapu 已提交
67 68
	}

69
	async pull(): Promise<void> {
S
Sandeep Somavarapu 已提交
70
		if (!this.isEnabled()) {
S
Sandeep Somavarapu 已提交
71
			this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling extensions as it is disabled.`);
72 73 74 75 76 77
			return;
		}

		this.stop();

		try {
S
Sandeep Somavarapu 已提交
78
			this.logService.info(`${this.syncResourceLogLabel}: Started pulling extensions...`);
79 80
			this.setStatus(SyncStatus.Syncing);

81 82
			const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
			const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
83

S
Sandeep Somavarapu 已提交
84
			if (remoteUserData.syncData !== null) {
85
				const localExtensions = await this.getLocalExtensions();
S
Sandeep Somavarapu 已提交
86
				const remoteExtensions = this.parseExtensions(remoteUserData.syncData);
S
Sandeep Somavarapu 已提交
87
				const { added, updated, remote, removed } = merge(localExtensions, remoteExtensions, localExtensions, [], this.getIgnoredExtensions());
S
Sandeep Somavarapu 已提交
88 89 90 91 92
				await this.apply({
					added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
					hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
					hasRemoteChanged: remote !== null
				});
93 94 95 96
			}

			// No remote exists to pull
			else {
S
Sandeep Somavarapu 已提交
97
				this.logService.info(`${this.syncResourceLogLabel}: Remote extensions does not exist.`);
98 99
			}

S
Sandeep Somavarapu 已提交
100
			this.logService.info(`${this.syncResourceLogLabel}: Finished pulling extensions.`);
101 102 103 104 105 106
		} finally {
			this.setStatus(SyncStatus.Idle);
		}
	}

	async push(): Promise<void> {
S
Sandeep Somavarapu 已提交
107
		if (!this.isEnabled()) {
S
Sandeep Somavarapu 已提交
108
			this.logService.info(`${this.syncResourceLogLabel}: Skipped pushing extensions as it is disabled.`);
109 110 111 112 113 114
			return;
		}

		this.stop();

		try {
S
Sandeep Somavarapu 已提交
115
			this.logService.info(`${this.syncResourceLogLabel}: Started pushing extensions...`);
116 117 118 119
			this.setStatus(SyncStatus.Syncing);

			const localExtensions = await this.getLocalExtensions();
			const { added, removed, updated, remote } = merge(localExtensions, null, null, [], this.getIgnoredExtensions());
120 121
			const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
			const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
S
Sandeep Somavarapu 已提交
122 123 124 125 126
			await this.apply({
				added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
				hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
				hasRemoteChanged: remote !== null
			}, true);
127

S
Sandeep Somavarapu 已提交
128
			this.logService.info(`${this.syncResourceLogLabel}: Finished pushing extensions.`);
129 130 131 132 133 134
		} finally {
			this.setStatus(SyncStatus.Idle);
		}

	}

S
Sandeep Somavarapu 已提交
135 136
	async stop(): Promise<void> { }

137
	async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
138
		return [{ resource: joinPath(uri, 'extensions.json'), comparableResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI }];
139 140
	}

141
	async resolveContent(uri: URI): Promise<string | null> {
142 143 144 145 146
		if (isEqual(uri, ExtensionsSynchroniser.EXTENSIONS_DATA_URI)) {
			const localExtensions = await this.getLocalExtensions();
			return this.format(localExtensions);
		}

147 148 149
		let content = await super.resolveContent(uri);
		if (content) {
			return content;
150
		}
151

152 153 154 155 156 157
		content = await super.resolveContent(dirname(uri));
		if (content) {
			const syncData = this.parseSyncData(content);
			if (syncData) {
				switch (basename(uri)) {
					case 'extensions.json':
158
						return this.format(this.parseExtensions(syncData));
159
				}
160 161
			}
		}
162

163 164 165
		return null;
	}

166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
	private format(extensions: ISyncExtension[]): string {
		extensions.sort((e1, e2) => {
			if (!e1.identifier.uuid && e2.identifier.uuid) {
				return -1;
			}
			if (e1.identifier.uuid && !e2.identifier.uuid) {
				return 1;
			}
			return compare(e1.identifier.id, e2.identifier.id);
		});
		const content = JSON.stringify(extensions);
		const edits = format(content, undefined, {});
		return applyEdits(content, edits);
	}

181
	async acceptConflict(conflict: URI, content: string): Promise<void> {
S
Sandeep Somavarapu 已提交
182
		throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`);
S
Sandeep Somavarapu 已提交
183
	}
S
Sandeep Somavarapu 已提交
184

185 186 187 188 189 190 191 192 193 194 195 196
	async hasLocalData(): Promise<boolean> {
		try {
			const localExtensions = await this.getLocalExtensions();
			if (isNonEmptyArray(localExtensions)) {
				return true;
			}
		} catch (error) {
			/* ignore error */
		}
		return false;
	}

197
	protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<SyncStatus> {
198
		const previewResult = await this.generatePreview(remoteUserData, lastSyncUserData);
199 200
		await this.apply(previewResult);
		return SyncStatus.Idle;
201 202
	}

203
	protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionsSyncPreviewResult> {
S
Sandeep Somavarapu 已提交
204 205
		const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? this.parseExtensions(remoteUserData.syncData) : null;
		const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? this.parseExtensions(lastSyncUserData.syncData!) : null;
206
		const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : [];
S
Sandeep Somavarapu 已提交
207 208 209

		const localExtensions = await this.getLocalExtensions();

210
		if (remoteExtensions) {
S
Sandeep Somavarapu 已提交
211
			this.logService.trace(`${this.syncResourceLogLabel}: Merging remote extensions with local extensions...`);
212
		} else {
S
Sandeep Somavarapu 已提交
213
			this.logService.trace(`${this.syncResourceLogLabel}: Remote extensions does not exist. Synchronizing extensions for the first time.`);
214 215
		}

216 217
		const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, this.getIgnoredExtensions());

S
Sandeep Somavarapu 已提交
218 219 220 221 222 223 224 225 226 227 228 229
		return {
			added,
			removed,
			updated,
			remote,
			skippedExtensions,
			remoteUserData,
			localExtensions,
			lastSyncUserData,
			hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
			hasRemoteChanged: remote !== null
		};
230 231 232 233 234
	}

	private getIgnoredExtensions() {
		return this.configurationService.getValue<string[]>('sync.ignoredExtensions') || [];
	}
S
Sandeep Somavarapu 已提交
235

S
Sandeep Somavarapu 已提交
236
	private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions, lastSyncUserData, localExtensions, hasLocalChanged, hasRemoteChanged }: IExtensionsSyncPreviewResult, forcePush?: boolean): Promise<void> {
S
Sandeep Somavarapu 已提交
237

S
Sandeep Somavarapu 已提交
238
		if (!hasLocalChanged && !hasRemoteChanged) {
S
Sandeep Somavarapu 已提交
239
			this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing extensions.`);
240 241
		}

S
Sandeep Somavarapu 已提交
242
		if (hasLocalChanged) {
243
			await this.backupLocal(JSON.stringify(localExtensions));
S
Sandeep Somavarapu 已提交
244
			skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions);
245
		}
S
Sandeep Somavarapu 已提交
246

S
Sandeep Somavarapu 已提交
247 248
		if (remote) {
			// update remote
S
Sandeep Somavarapu 已提交
249
			this.logService.trace(`${this.syncResourceLogLabel}: Updating remote extensions...`);
S
Sandeep Somavarapu 已提交
250
			const content = JSON.stringify(remote);
S
Sandeep Somavarapu 已提交
251
			remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
S
Sandeep Somavarapu 已提交
252
			this.logService.info(`${this.syncResourceLogLabel}: Updated remote extensions`);
S
Sandeep Somavarapu 已提交
253
		}
S
Sandeep Somavarapu 已提交
254

S
Sandeep Somavarapu 已提交
255
		if (lastSyncUserData?.ref !== remoteUserData.ref) {
256
			// update last sync
S
Sandeep Somavarapu 已提交
257
			this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized extensions...`);
S
Sandeep Somavarapu 已提交
258
			await this.updateLastSyncUserData(remoteUserData, { skippedExtensions });
S
Sandeep Somavarapu 已提交
259
			this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized extensions`);
260
		}
S
Sandeep Somavarapu 已提交
261 262
	}

S
Sandeep Somavarapu 已提交
263 264 265 266
	private async updateLocalExtensions(added: ISyncExtension[], removed: IExtensionIdentifier[], updated: ISyncExtension[], skippedExtensions: ISyncExtension[]): Promise<ISyncExtension[]> {
		const removeFromSkipped: IExtensionIdentifier[] = [];
		const addToSkipped: ISyncExtension[] = [];

S
Sandeep Somavarapu 已提交
267 268 269
		if (removed.length) {
			const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User);
			const extensionsToRemove = installedExtensions.filter(({ identifier }) => removed.some(r => areSameExtensions(identifier, r)));
S
Sandeep Somavarapu 已提交
270
			await Promise.all(extensionsToRemove.map(async extensionToRemove => {
271
				this.logService.trace(`${this.syncResourceLogLabel}: Uninstalling local extension...`, extensionToRemove.identifier.id);
S
Sandeep Somavarapu 已提交
272
				await this.extensionManagementService.uninstall(extensionToRemove);
273
				this.logService.info(`${this.syncResourceLogLabel}: Uninstalled local extension.`, extensionToRemove.identifier.id);
S
Sandeep Somavarapu 已提交
274
				removeFromSkipped.push(extensionToRemove.identifier);
275
			}));
S
Sandeep Somavarapu 已提交
276 277 278 279
		}

		if (added.length || updated.length) {
			await Promise.all([...added, ...updated].map(async e => {
S
Sandeep Somavarapu 已提交
280 281 282 283 284
				const installedExtensions = await this.extensionManagementService.getInstalled();
				const installedExtension = installedExtensions.filter(installed => areSameExtensions(installed.identifier, e.identifier))[0];

				// Builtin Extension: Sync only enablement state
				if (installedExtension && installedExtension.type === ExtensionType.System) {
S
Sandeep Somavarapu 已提交
285
					if (e.disabled) {
286
						this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...`, e.identifier.id);
S
Sandeep Somavarapu 已提交
287
						await this.extensionEnablementService.disableExtension(e.identifier);
288
						this.logService.info(`${this.syncResourceLogLabel}: Disabled extension`, e.identifier.id);
S
Sandeep Somavarapu 已提交
289
					} else {
290
						this.logService.trace(`${this.syncResourceLogLabel}: Enabling extension...`, e.identifier.id);
S
Sandeep Somavarapu 已提交
291
						await this.extensionEnablementService.enableExtension(e.identifier);
292
						this.logService.info(`${this.syncResourceLogLabel}: Enabled extension`, e.identifier.id);
S
Sandeep Somavarapu 已提交
293 294 295 296 297
					}
					removeFromSkipped.push(e.identifier);
					return;
				}

S
Sandeep Somavarapu 已提交
298 299
				const extension = await this.extensionGalleryService.getCompatibleExtension(e.identifier, e.version);
				if (extension) {
S
Sandeep Somavarapu 已提交
300
					try {
S
Sandeep Somavarapu 已提交
301
						if (e.disabled) {
302
							this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...`, e.identifier.id, extension.version);
303
							await this.extensionEnablementService.disableExtension(extension.identifier);
304
							this.logService.info(`${this.syncResourceLogLabel}: Disabled extension`, e.identifier.id, extension.version);
S
Sandeep Somavarapu 已提交
305
						} else {
306
							this.logService.trace(`${this.syncResourceLogLabel}: Enabling extension...`, e.identifier.id, extension.version);
S
Sandeep Somavarapu 已提交
307
							await this.extensionEnablementService.enableExtension(extension.identifier);
308
							this.logService.info(`${this.syncResourceLogLabel}: Enabled extension`, e.identifier.id, extension.version);
309
						}
S
Sandeep Somavarapu 已提交
310
						// Install only if the extension does not exist
S
Sandeep Somavarapu 已提交
311
						if (!installedExtension || installedExtension.manifest.version !== extension.version) {
312
							this.logService.trace(`${this.syncResourceLogLabel}: Installing extension...`, e.identifier.id, extension.version);
S
Sandeep Somavarapu 已提交
313
							await this.extensionManagementService.installFromGallery(extension);
314
							this.logService.info(`${this.syncResourceLogLabel}: Installed extension.`, e.identifier.id, extension.version);
S
Sandeep Somavarapu 已提交
315 316
							removeFromSkipped.push(extension.identifier);
						}
S
Sandeep Somavarapu 已提交
317 318 319
					} catch (error) {
						addToSkipped.push(e);
						this.logService.error(error);
320
						this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing extension`, extension.displayName || extension.identifier.id);
S
Sandeep Somavarapu 已提交
321
					}
S
Sandeep Somavarapu 已提交
322 323
				} else {
					addToSkipped.push(e);
S
Sandeep Somavarapu 已提交
324 325 326
				}
			}));
		}
S
Sandeep Somavarapu 已提交
327 328 329 330 331 332 333 334 335 336 337 338 339

		const newSkippedExtensions: ISyncExtension[] = [];
		for (const skippedExtension of skippedExtensions) {
			if (!removeFromSkipped.some(e => areSameExtensions(e, skippedExtension.identifier))) {
				newSkippedExtensions.push(skippedExtension);
			}
		}
		for (const skippedExtension of addToSkipped) {
			if (!newSkippedExtensions.some(e => areSameExtensions(e.identifier, skippedExtension.identifier))) {
				newSkippedExtensions.push(skippedExtension);
			}
		}
		return newSkippedExtensions;
S
Sandeep Somavarapu 已提交
340
	}
S
Sandeep Somavarapu 已提交
341

S
Sandeep Somavarapu 已提交
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
	private parseExtensions(syncData: ISyncData): ISyncExtension[] {
		let extensions: ISyncExtension[] = JSON.parse(syncData.content);
		if (syncData.version !== this.version) {
			extensions = extensions.map(e => {
				// #region Migration from v1 (enabled -> disabled)
				if (!(<any>e).enabled) {
					e.disabled = true;
				}
				delete (<any>e).enabled;
				// #endregion
				return e;
			});
		}
		return extensions;
	}

S
Sandeep Somavarapu 已提交
358
	private async getLocalExtensions(): Promise<ISyncExtension[]> {
S
Sandeep Somavarapu 已提交
359
		const installedExtensions = await this.extensionManagementService.getInstalled();
S
Sandeep Somavarapu 已提交
360
		const disabledExtensions = this.extensionEnablementService.getDisabledExtensions();
S
Sandeep Somavarapu 已提交
361
		return installedExtensions
S
Sandeep Somavarapu 已提交
362 363 364 365 366 367 368
			.map(({ identifier }) => {
				const syncExntesion: ISyncExtension = { identifier };
				if (disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier))) {
					syncExntesion.disabled = true;
				}
				return syncExntesion;
			});
S
Sandeep Somavarapu 已提交
369 370 371
	}

}