settingsSync.ts 16.8 KB
Newer Older
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 { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
7
import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, CONFIGURATION_SYNC_STORE_KEY, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
8 9
import { VSBuffer } from 'vs/base/common/buffer';
import { localize } from 'vs/nls';
10
import { Event } from 'vs/base/common/event';
11
import { createCancelablePromise } from 'vs/base/common/async';
12
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
13
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
S
Sandeep Somavarapu 已提交
14
import { CancellationToken } from 'vs/base/common/cancellation';
S
Sandeep Somavarapu 已提交
15
import { updateIgnoredSettings, merge, getIgnoredSettings, isEmpty } from 'vs/platform/userDataSync/common/settingsMerge';
16
import { edit } from 'vs/platform/userDataSync/common/content';
S
Sandeep Somavarapu 已提交
17
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
18
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
19
import { URI } from 'vs/base/common/uri';
S
Sandeep Somavarapu 已提交
20
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
21
import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources';
22

S
Sandeep Somavarapu 已提交
23
export interface ISettingsSyncContent {
S
Sandeep Somavarapu 已提交
24 25 26 27 28 29 30 31 32
	settings: string;
}

function isSettingsSyncContent(thing: any): thing is ISettingsSyncContent {
	return thing
		&& (thing.settings && typeof thing.settings === 'string')
		&& Object.keys(thing).length === 1;
}

33
export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
S
Sandeep Somavarapu 已提交
34 35

	_serviceBrand: any;
36

S
Sandeep Somavarapu 已提交
37
	protected readonly version: number = 1;
38 39
	protected readonly localPreviewResource: URI = joinPath(this.syncFolder, PREVIEW_DIR_NAME, 'settings.json');
	protected readonly remotePreviewResource: URI = this.localPreviewResource.with({ scheme: USER_DATA_SYNC_SCHEME });
40

41
	constructor(
S
Sandeep Somavarapu 已提交
42
		@IFileService fileService: IFileService,
43
		@IEnvironmentService environmentService: IEnvironmentService,
S
Sandeep Somavarapu 已提交
44
		@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
S
Sandeep Somavarapu 已提交
45
		@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
46 47
		@IUserDataSyncLogService logService: IUserDataSyncLogService,
		@IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService,
S
Sandeep Somavarapu 已提交
48
		@IConfigurationService configurationService: IConfigurationService,
S
Sandeep Somavarapu 已提交
49
		@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
50
		@ITelemetryService telemetryService: ITelemetryService,
S
Sandeep Somavarapu 已提交
51
		@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
52
	) {
S
Sandeep Somavarapu 已提交
53
		super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
54 55
	}

S
Sandeep Somavarapu 已提交
56 57 58
	protected setStatus(status: SyncStatus): void {
		super.setStatus(status);
		if (this.status !== SyncStatus.HasConflicts) {
59 60 61 62
			this.setConflicts([]);
		}
	}

63
	async pull(): Promise<void> {
S
Sandeep Somavarapu 已提交
64
		if (!this.isEnabled()) {
S
Sandeep Somavarapu 已提交
65
			this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling settings as it is disabled.`);
66 67 68 69 70 71
			return;
		}

		this.stop();

		try {
S
Sandeep Somavarapu 已提交
72
			this.logService.info(`${this.syncResourceLogLabel}: Started pulling settings...`);
73 74
			this.setStatus(SyncStatus.Syncing);

S
Sandeep Somavarapu 已提交
75 76
			const lastSyncUserData = await this.getLastSyncUserData();
			const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
S
Sandeep Somavarapu 已提交
77
			const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
78

S
Sandeep Somavarapu 已提交
79
			if (remoteSettingsSyncContent !== null) {
80 81
				const fileContent = await this.getLocalFileContent();
				const formatUtils = await this.getFormattingOptions();
S
Sandeep Somavarapu 已提交
82
				// Update ignored settings from local file content
S
Sandeep Somavarapu 已提交
83 84
				const ignoredSettings = await this.getIgnoredSettings();
				const content = updateIgnoredSettings(remoteSettingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
85
				this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<IFileSyncPreviewResult>({
86
					fileContent,
S
Sandeep Somavarapu 已提交
87
					remoteUserData,
S
Sandeep Somavarapu 已提交
88
					lastSyncUserData,
S
Sandeep Somavarapu 已提交
89 90 91 92
					content,
					hasLocalChanged: true,
					hasRemoteChanged: false,
					hasConflicts: false,
93 94 95 96 97 98 99
				}));

				await this.apply();
			}

			// No remote exists to pull
			else {
S
Sandeep Somavarapu 已提交
100
				this.logService.info(`${this.syncResourceLogLabel}: Remote settings does not exist.`);
101
			}
S
Sandeep Somavarapu 已提交
102

S
Sandeep Somavarapu 已提交
103
			this.logService.info(`${this.syncResourceLogLabel}: Finished pulling settings.`);
104 105 106 107 108 109
		} finally {
			this.setStatus(SyncStatus.Idle);
		}
	}

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

		this.stop();

		try {
S
Sandeep Somavarapu 已提交
118
			this.logService.info(`${this.syncResourceLogLabel}: Started pushing settings...`);
119 120 121 122 123 124 125
			this.setStatus(SyncStatus.Syncing);

			const fileContent = await this.getLocalFileContent();

			if (fileContent !== null) {
				const formatUtils = await this.getFormattingOptions();
				// Remove ignored settings
S
Sandeep Somavarapu 已提交
126 127
				const ignoredSettings = await this.getIgnoredSettings();
				const content = updateIgnoredSettings(fileContent.value.toString(), '{}', ignoredSettings, formatUtils);
S
Sandeep Somavarapu 已提交
128 129
				const lastSyncUserData = await this.getLastSyncUserData();
				const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
130

131
				this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<IFileSyncPreviewResult>({
132
					fileContent,
S
Sandeep Somavarapu 已提交
133
					remoteUserData,
S
Sandeep Somavarapu 已提交
134
					lastSyncUserData,
S
Sandeep Somavarapu 已提交
135 136 137 138
					content,
					hasRemoteChanged: true,
					hasLocalChanged: false,
					hasConflicts: false,
139 140
				}));

S
Sandeep Somavarapu 已提交
141
				await this.apply(true);
142 143 144 145
			}

			// No local exists to push
			else {
S
Sandeep Somavarapu 已提交
146
				this.logService.info(`${this.syncResourceLogLabel}: Local settings does not exist.`);
147
			}
S
Sandeep Somavarapu 已提交
148

S
Sandeep Somavarapu 已提交
149
			this.logService.info(`${this.syncResourceLogLabel}: Finished pushing settings.`);
150 151 152 153 154
		} finally {
			this.setStatus(SyncStatus.Idle);
		}
	}

155 156 157 158 159 160
	async hasLocalData(): Promise<boolean> {
		try {
			const localFileContent = await this.getLocalFileContent();
			if (localFileContent) {
				const formatUtils = await this.getFormattingOptions();
				const content = edit(localFileContent.value.toString(), [CONFIGURATION_SYNC_STORE_KEY], undefined, formatUtils);
S
Sandeep Somavarapu 已提交
161
				return !isEmpty(content);
162 163 164 165 166 167 168 169 170
			}
		} catch (error) {
			if ((<FileOperationError>error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) {
				return true;
			}
		}
		return false;
	}

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
	async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
		return [{ resource: joinPath(uri, 'settings.json'), comparableResource: this.file }];
	}

	async resolveContent(uri: URI): Promise<string | null> {
		if (isEqual(this.remotePreviewResource, uri)) {
			return this.getConflictContent(uri);
		}
		let content = await super.resolveContent(uri);
		if (content) {
			return content;
		}
		content = await super.resolveContent(dirname(uri));
		if (content) {
			const syncData = this.parseSyncData(content);
			if (syncData) {
				const settingsSyncContent = this.parseSettingsSyncContent(syncData.content);
				if (settingsSyncContent) {
					switch (basename(uri)) {
						case 'settings.json':
							return settingsSyncContent.settings;
					}
				}
			}
		}
		return null;
	}

	protected async getConflictContent(conflictResource: URI): Promise<string | null> {
200
		let content = await super.getConflictContent(conflictResource);
S
Sandeep Somavarapu 已提交
201 202 203 204
		if (content !== null) {
			const settingsSyncContent = this.parseSettingsSyncContent(content);
			content = settingsSyncContent ? settingsSyncContent.settings : null;
		}
205
		if (content !== null) {
S
Sandeep Somavarapu 已提交
206 207
			const formatUtils = await this.getFormattingOptions();
			// remove ignored settings from the remote content for preview
S
Sandeep Somavarapu 已提交
208 209
			const ignoredSettings = await this.getIgnoredSettings();
			content = updateIgnoredSettings(content, '{}', ignoredSettings, formatUtils);
S
Sandeep Somavarapu 已提交
210
		}
211
		return content;
S
Sandeep Somavarapu 已提交
212 213
	}

214 215 216 217
	async acceptConflict(conflict: URI, content: string): Promise<void> {
		if (this.status === SyncStatus.HasConflicts
			&& (isEqual(this.localPreviewResource, conflict) || isEqual(this.remotePreviewResource, conflict))
		) {
218
			const preview = await this.syncPreviewResultPromise!;
219
			this.cancel();
220 221
			const formatUtils = await this.getFormattingOptions();
			// Add ignored settings from local file content
S
Sandeep Somavarapu 已提交
222 223
			const ignoredSettings = await this.getIgnoredSettings();
			content = updateIgnoredSettings(content, preview.fileContent ? preview.fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
224 225 226
			this.syncPreviewResultPromise = createCancelablePromise(async () => ({ ...preview, content }));
			await this.apply(true);
			this.setStatus(SyncStatus.Idle);
S
Sandeep Somavarapu 已提交
227 228 229 230
		}
	}

	async resolveSettingsConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<void> {
S
Sandeep Somavarapu 已提交
231
		if (this.status === SyncStatus.HasConflicts) {
S
Sandeep Somavarapu 已提交
232
			const preview = await this.syncPreviewResultPromise!;
233
			this.cancel();
234
			await this.performSync(preview.remoteUserData, preview.lastSyncUserData, resolvedConflicts);
S
Sandeep Somavarapu 已提交
235 236 237
		}
	}

238
	protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any | undefined }[] = []): Promise<SyncStatus> {
S
Sandeep Somavarapu 已提交
239
		try {
S
Sandeep Somavarapu 已提交
240
			const result = await this.getPreview(remoteUserData, lastSyncUserData, resolvedConflicts);
S
Sandeep Somavarapu 已提交
241
			if (result.hasConflicts) {
242
				return SyncStatus.HasConflicts;
S
Sandeep Somavarapu 已提交
243
			}
244 245
			await this.apply();
			return SyncStatus.Idle;
S
Sandeep Somavarapu 已提交
246
		} catch (e) {
S
Sandeep Somavarapu 已提交
247
			this.syncPreviewResultPromise = null;
248 249
			if (e instanceof UserDataSyncError) {
				switch (e.code) {
250
					case UserDataSyncErrorCode.LocalPreconditionFailed:
251
						// Rejected as there is a new local version. Syncing again.
S
Sandeep Somavarapu 已提交
252
						this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize settings as there is a new local version available. Synchronizing again...`);
253
						return this.performSync(remoteUserData, lastSyncUserData, resolvedConflicts);
254
				}
S
Sandeep Somavarapu 已提交
255
			}
S
Sandeep Somavarapu 已提交
256
			throw e;
257
		}
258
	}
259

S
Sandeep Somavarapu 已提交
260
	private async apply(forcePush?: boolean): Promise<void> {
261 262 263
		if (!this.syncPreviewResultPromise) {
			return;
		}
S
Sandeep Somavarapu 已提交
264

S
Sandeep Somavarapu 已提交
265
		let { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise;
S
Sandeep Somavarapu 已提交
266

S
Sandeep Somavarapu 已提交
267
		if (content !== null) {
S
Sandeep Somavarapu 已提交
268

269
			this.validateContent(content);
S
Sandeep Somavarapu 已提交
270

S
Sandeep Somavarapu 已提交
271
			if (hasLocalChanged) {
S
Sandeep Somavarapu 已提交
272
				this.logService.trace(`${this.syncResourceLogLabel}: Updating local settings...`);
273 274 275
				if (fileContent) {
					await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(fileContent.value.toString())));
				}
S
Sandeep Somavarapu 已提交
276
				await this.updateLocalFileContent(content, fileContent);
S
Sandeep Somavarapu 已提交
277
				this.logService.info(`${this.syncResourceLogLabel}: Updated local settings`);
278
			}
S
Sandeep Somavarapu 已提交
279
			if (hasRemoteChanged) {
S
Sandeep Somavarapu 已提交
280
				const formatUtils = await this.getFormattingOptions();
S
Sandeep Somavarapu 已提交
281
				// Update ignored settings from remote
S
Sandeep Somavarapu 已提交
282
				const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
S
Sandeep Somavarapu 已提交
283 284
				const ignoredSettings = await this.getIgnoredSettings(content);
				content = updateIgnoredSettings(content, remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : '{}', ignoredSettings, formatUtils);
S
Sandeep Somavarapu 已提交
285
				this.logService.trace(`${this.syncResourceLogLabel}: Updating remote settings...`);
S
Sandeep Somavarapu 已提交
286
				remoteUserData = await this.updateRemoteUserData(JSON.stringify(this.toSettingsSyncContent(content)), forcePush ? null : remoteUserData.ref);
S
Sandeep Somavarapu 已提交
287
				this.logService.info(`${this.syncResourceLogLabel}: Updated remote settings`);
S
Sandeep Somavarapu 已提交
288
			}
S
Sandeep Somavarapu 已提交
289 290

			// Delete the preview
S
Sandeep Somavarapu 已提交
291
			try {
292
				await this.fileService.del(this.localPreviewResource);
S
Sandeep Somavarapu 已提交
293
			} catch (e) { /* ignore */ }
S
Sandeep Somavarapu 已提交
294
		} else {
S
Sandeep Somavarapu 已提交
295
			this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing settings.`);
296
		}
S
Sandeep Somavarapu 已提交
297

S
Sandeep Somavarapu 已提交
298
		if (lastSyncUserData?.ref !== remoteUserData.ref) {
S
Sandeep Somavarapu 已提交
299
			this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized settings...`);
S
Sandeep Somavarapu 已提交
300
			await this.updateLastSyncUserData(remoteUserData);
S
Sandeep Somavarapu 已提交
301
			this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized settings`);
S
Sandeep Somavarapu 已提交
302 303
		}

304
		this.syncPreviewResultPromise = null;
305 306
	}

307
	private getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any }[] = []): Promise<IFileSyncPreviewResult> {
308
		if (!this.syncPreviewResultPromise) {
S
Sandeep Somavarapu 已提交
309
			this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, resolvedConflicts, token));
310 311 312 313
		}
		return this.syncPreviewResultPromise;
	}

314
	protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any }[] = [], token: CancellationToken = CancellationToken.None): Promise<IFileSyncPreviewResult> {
315
		const fileContent = await this.getLocalFileContent();
316
		const formattingOptions = await this.getFormattingOptions();
S
Sandeep Somavarapu 已提交
317 318
		const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
		const lastSettingsSyncContent = lastSyncUserData ? this.getSettingsSyncContent(lastSyncUserData) : null;
S
Sandeep Somavarapu 已提交
319 320 321 322

		let content: string | null = null;
		let hasLocalChanged: boolean = false;
		let hasRemoteChanged: boolean = false;
S
Sandeep Somavarapu 已提交
323
		let hasConflicts: boolean = false;
324

S
Sandeep Somavarapu 已提交
325
		if (remoteSettingsSyncContent) {
S
Sandeep Somavarapu 已提交
326
			const localContent: string = fileContent ? fileContent.value.toString() : '{}';
327
			this.validateContent(localContent);
S
Sandeep Somavarapu 已提交
328
			this.logService.trace(`${this.syncResourceLogLabel}: Merging remote settings with local settings...`);
S
Sandeep Somavarapu 已提交
329 330
			const ignoredSettings = await this.getIgnoredSettings();
			const result = merge(localContent, remoteSettingsSyncContent.settings, lastSettingsSyncContent ? lastSettingsSyncContent.settings : null, ignoredSettings, resolvedConflicts, formattingOptions);
331 332 333 334
			content = result.localContent || result.remoteContent;
			hasLocalChanged = result.localContent !== null;
			hasRemoteChanged = result.remoteContent !== null;
			hasConflicts = result.hasConflicts;
335 336
		}

337
		// First time syncing to remote
S
Sandeep Somavarapu 已提交
338
		else if (fileContent) {
S
Sandeep Somavarapu 已提交
339
			this.logService.trace(`${this.syncResourceLogLabel}: Remote settings does not exist. Synchronizing settings for the first time.`);
S
Sandeep Somavarapu 已提交
340 341
			content = fileContent.value.toString();
			hasRemoteChanged = true;
S
Sandeep Somavarapu 已提交
342 343
		}

S
Sandeep Somavarapu 已提交
344 345
		if (content && !token.isCancellationRequested) {
			// Remove the ignored settings from the preview.
S
Sandeep Somavarapu 已提交
346 347
			const ignoredSettings = await this.getIgnoredSettings();
			const previewContent = updateIgnoredSettings(content, '{}', ignoredSettings, formattingOptions);
348
			await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(previewContent));
349 350
		}

351 352
		this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []);

353
		return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts };
S
Sandeep Somavarapu 已提交
354 355
	}

S
Sandeep Somavarapu 已提交
356 357 358 359
	private getSettingsSyncContent(remoteUserData: IRemoteUserData): ISettingsSyncContent | null {
		return remoteUserData.syncData ? this.parseSettingsSyncContent(remoteUserData.syncData.content) : null;
	}

S
Sandeep Somavarapu 已提交
360
	parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null {
S
Sandeep Somavarapu 已提交
361 362 363 364 365 366 367 368
		try {
			const parsed = <ISettingsSyncContent>JSON.parse(syncContent);
			return isSettingsSyncContent(parsed) ? parsed : /* migrate */ { settings: syncContent };
		} catch (e) {
			this.logService.error(e);
		}
		return null;
	}
369

S
Sandeep Somavarapu 已提交
370 371 372 373
	private toSettingsSyncContent(settings: string): ISettingsSyncContent {
		return { settings };
	}

S
Sandeep Somavarapu 已提交
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
	private _defaultIgnoredSettings: Promise<string[]> | undefined = undefined;
	protected async getIgnoredSettings(content?: string): Promise<string[]> {
		if (!this._defaultIgnoredSettings) {
			this._defaultIgnoredSettings = this.userDataSyncUtilService.resolveDefaultIgnoredSettings();
			const disposable = Event.any<any>(
				Event.filter(this.extensionManagementService.onDidInstallExtension, (e => !!e.gallery)),
				Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error)))(() => {
					disposable.dispose();
					this._defaultIgnoredSettings = undefined;
				});
		}
		const defaultIgnoredSettings = await this._defaultIgnoredSettings;
		return getIgnoredSettings(defaultIgnoredSettings, this.configurationService, content);
	}

389 390
	private validateContent(content: string): void {
		if (this.hasErrors(content)) {
S
Sandeep Somavarapu 已提交
391
			throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync settings as there are errors/warning in settings file."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
392 393
		}
	}
394
}