settingsSync.ts 10.3 KB
Newer Older
1 2 3 4 5 6 7 8
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent } from 'vs/platform/files/common/files';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
9
import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, ISettingsMergeService, IUserDataSyncStoreService, SETTINGS_PREVIEW_RESOURCE } from 'vs/platform/userDataSync/common/userDataSync';
10
import { VSBuffer } from 'vs/base/common/buffer';
11
import { parse, ParseError } from 'vs/base/common/json';
12
import { localize } from 'vs/nls';
13
import { Emitter, Event } from 'vs/base/common/event';
14
import { ILogService } from 'vs/platform/log/common/log';
S
Sandeep Somavarapu 已提交
15
import { CancelablePromise, createCancelablePromise, ThrottledDelayer } from 'vs/base/common/async';
16
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
17
import { URI } from 'vs/base/common/uri';
18 19 20 21

interface ISyncPreviewResult {
	readonly fileContent: IFileContent | null;
	readonly remoteUserData: IUserData | null;
22 23
	readonly hasLocalChanged: boolean;
	readonly hasRemoteChanged: boolean;
24
	readonly hasConflicts: boolean;
25 26
}

S
Sandeep Somavarapu 已提交
27
export class SettingsSynchroniser extends Disposable implements ISynchroniser {
28 29 30 31

	private static LAST_SYNC_SETTINGS_STORAGE_KEY: string = 'LAST_SYNC_SETTINGS_CONTENTS';
	private static EXTERNAL_USER_DATA_SETTINGS_KEY: string = 'settings';

32
	private syncPreviewResultPromise: CancelablePromise<ISyncPreviewResult> | null = null;
33

34 35 36 37 38
	private _status: SyncStatus = SyncStatus.Idle;
	get status(): SyncStatus { return this._status; }
	private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
	readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;

S
Sandeep Somavarapu 已提交
39 40 41 42
	private readonly throttledDelayer: ThrottledDelayer<void>;
	private _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
	readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;

43
	readonly conflicts: URI = SETTINGS_PREVIEW_RESOURCE;
44

45 46
	constructor(
		@IFileService private readonly fileService: IFileService,
47
		@IEnvironmentService private readonly environmentService: IEnvironmentService,
48
		@IStorageService private readonly storageService: IStorageService,
S
Sandeep Somavarapu 已提交
49
		@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
50
		@ISettingsMergeService private readonly settingsMergeService: ISettingsMergeService,
51
		@ILogService private readonly logService: ILogService,
52 53
	) {
		super();
S
Sandeep Somavarapu 已提交
54
		this.throttledDelayer = this._register(new ThrottledDelayer<void>(500));
55
		this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.settingsResource))(() => this.throttledDelayer.trigger(() => this.onDidChangeSettings())));
S
Sandeep Somavarapu 已提交
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
	}

	private async onDidChangeSettings(): Promise<void> {
		const localFileContent = await this.getLocalFileContent();
		const lastSyncData = this.getLastSyncUserData();
		if (localFileContent && lastSyncData) {
			if (localFileContent.value.toString() !== lastSyncData.content) {
				this._onDidChangeLocal.fire();
				return;
			}
		}
		if (!localFileContent || !lastSyncData) {
			this._onDidChangeLocal.fire();
			return;
		}
71 72
	}

73 74 75 76 77 78 79 80 81 82 83 84 85 86
	private setStatus(status: SyncStatus): void {
		if (this._status !== status) {
			this._status = status;
			this._onDidChangStatus.fire(status);
		}
	}

	async sync(): Promise<boolean> {

		if (this.status !== SyncStatus.Idle) {
			return false;
		}

		this.setStatus(SyncStatus.Syncing);
87

S
Sandeep Somavarapu 已提交
88 89 90 91 92 93
		try {
			const result = await this.getPreview();
			if (result.hasConflicts) {
				this.setStatus(SyncStatus.HasConflicts);
				return false;
			}
94
			await this.apply();
S
Sandeep Somavarapu 已提交
95 96
			return true;
		} catch (e) {
S
Sandeep Somavarapu 已提交
97
			this.syncPreviewResultPromise = null;
S
Sandeep Somavarapu 已提交
98
			this.setStatus(SyncStatus.Idle);
S
Sandeep Somavarapu 已提交
99
			if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Rejected) {
S
Sandeep Somavarapu 已提交
100 101 102 103 104 105 106 107 108
				// Rejected as there is a new remote version. Syncing again,
				this.logService.info('Failed to Synchronise settings as there is a new remote version available. Synchronising again...');
				return this.sync();
			}
			if (e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) {
				// Rejected as there is a new local version. Syncing again.
				this.logService.info('Failed to Synchronise settings as there is a new local version available. Synchronising again...');
				return this.sync();
			}
S
Sandeep Somavarapu 已提交
109
			throw e;
110
		}
111
	}
112

113 114
	async continueSync(): Promise<boolean> {
		if (this.status !== SyncStatus.HasConflicts) {
S
Sandeep Somavarapu 已提交
115 116
			return false;
		}
117 118 119 120 121 122 123 124
		await this.apply();
		return true;
	}

	private async apply(): Promise<void> {
		if (!this.syncPreviewResultPromise) {
			return;
		}
S
Sandeep Somavarapu 已提交
125

126 127
		if (await this.fileService.exists(this.conflicts)) {
			const settingsPreivew = await this.fileService.readFile(this.conflicts);
S
Sandeep Somavarapu 已提交
128 129 130 131 132
			const content = settingsPreivew.value.toString();
			if (this.hasErrors(content)) {
				return Promise.reject(localize('errorInvalidSettings', "Unable to sync settings. Please resolve conflicts without any errors/warnings and try again."));
			}

S
Sandeep Somavarapu 已提交
133 134
			let { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise;
			if (hasRemoteChanged) {
S
Sandeep Somavarapu 已提交
135 136 137
				const ref = await this.writeToRemote(content, remoteUserData ? remoteUserData.ref : null);
				remoteUserData = { ref, content };
			}
S
Sandeep Somavarapu 已提交
138 139
			if (hasLocalChanged) {
				await this.writeToLocal(content, fileContent);
S
Sandeep Somavarapu 已提交
140 141 142 143
			}
			if (remoteUserData) {
				this.updateLastSyncValue(remoteUserData);
			}
S
Sandeep Somavarapu 已提交
144 145

			// Delete the preview
146
			await this.fileService.del(this.conflicts);
147
		}
S
Sandeep Somavarapu 已提交
148

149 150
		this.syncPreviewResultPromise = null;
		this.setStatus(SyncStatus.Idle);
151 152
	}

S
Sandeep Somavarapu 已提交
153 154 155 156 157 158
	private hasErrors(content: string): boolean {
		const parseErrors: ParseError[] = [];
		parse(content, parseErrors);
		return parseErrors.length > 0;
	}

159 160
	private getPreview(): Promise<ISyncPreviewResult> {
		if (!this.syncPreviewResultPromise) {
161
			this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview());
162 163 164 165 166
		}
		return this.syncPreviewResultPromise;
	}

	private async generatePreview(): Promise<ISyncPreviewResult> {
S
Sandeep Somavarapu 已提交
167
		const remoteUserData = await this.userDataSyncStoreService.read(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY);
168
		// Get file content last to get the latest
169
		const fileContent = await this.getLocalFileContent();
170 171 172
		let hasLocalChanged: boolean = false;
		let hasRemoteChanged: boolean = false;
		let hasConflicts: boolean = false;
173

174
		// First time sync to remote
175
		if (fileContent && !remoteUserData) {
176 177
			this.logService.trace('Settings Sync: Remote contents does not exist. So sync with settings file.');
			hasRemoteChanged = true;
178
			await this.fileService.writeFile(this.conflicts, VSBuffer.fromString(fileContent.value.toString()));
179
			return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
180 181
		}

182
		// Settings file does not exist, so sync with remote contents.
183
		if (remoteUserData && !fileContent) {
184 185
			this.logService.trace('Settings Sync: Settings file does not exist. So sync with remote contents');
			hasLocalChanged = true;
186
			await this.fileService.writeFile(this.conflicts, VSBuffer.fromString(remoteUserData.content));
187
			return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
188 189 190 191 192 193
		}

		if (fileContent && remoteUserData) {
			const localContent: string = fileContent.value.toString();
			const remoteContent: string = remoteUserData.content;
			const lastSyncData = this.getLastSyncUserData();
194 195 196 197 198
			if (!lastSyncData // First time sync
				|| lastSyncData.content !== localContent // Local has moved forwarded
				|| lastSyncData.content !== remoteContent // Remote has moved forwarded
			) {
				this.logService.trace('Settings Sync: Merging remote contents with settings file.');
199 200 201 202
				const mergeContent = await this.settingsMergeService.merge(localContent, remoteContent, lastSyncData ? lastSyncData.content : null);
				hasLocalChanged = mergeContent !== localContent;
				hasRemoteChanged = mergeContent !== remoteContent;
				if (hasLocalChanged || hasRemoteChanged) {
203
					// Sync only if there are changes
204
					hasConflicts = this.hasErrors(mergeContent);
205
					await this.fileService.writeFile(this.conflicts, VSBuffer.fromString(mergeContent));
206
					return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
207 208 209 210
				}
			}
		}

211
		this.logService.trace('Settings Sync: No changes.');
212
		return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
213 214
	}

S
Sandeep Somavarapu 已提交
215 216 217 218 219 220 221 222 223 224
	private getLastSyncUserData(): IUserData | null {
		const lastSyncStorageContents = this.storageService.get(SettingsSynchroniser.LAST_SYNC_SETTINGS_STORAGE_KEY, StorageScope.GLOBAL, undefined);
		if (lastSyncStorageContents) {
			return JSON.parse(lastSyncStorageContents);
		}
		return null;
	}

	private async getLocalFileContent(): Promise<IFileContent | null> {
		try {
225
			return await this.fileService.readFile(this.environmentService.settingsResource);
S
Sandeep Somavarapu 已提交
226 227 228 229 230 231 232 233
		} catch (error) {
			if (error instanceof FileSystemProviderError && error.code !== FileSystemProviderErrorCode.FileNotFound) {
				return null;
			}
			throw error;
		}
	}

S
Sandeep Somavarapu 已提交
234
	private async writeToRemote(content: string, ref: string | null): Promise<string> {
S
Sandeep Somavarapu 已提交
235
		return this.userDataSyncStoreService.write(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY, content, ref);
236 237
	}

238
	private async writeToLocal(newContent: string, oldContent: IFileContent | null): Promise<void> {
239
		if (oldContent) {
S
Sandeep Somavarapu 已提交
240
			// file exists already
241
			await this.fileService.writeFile(this.environmentService.settingsResource, VSBuffer.fromString(newContent), oldContent);
242
		} else {
S
Sandeep Somavarapu 已提交
243
			// file does not exist
244
			await this.fileService.createFile(this.environmentService.settingsResource, VSBuffer.fromString(newContent), { overwrite: false });
245
		}
246 247 248
	}

	private updateLastSyncValue(remoteUserData: IUserData): void {
S
Sandeep Somavarapu 已提交
249 250 251 252
		const lastSyncUserData = this.getLastSyncUserData();
		if (lastSyncUserData && lastSyncUserData.ref === remoteUserData.ref) {
			return;
		}
S
Sandeep Somavarapu 已提交
253
		this.storageService.store(SettingsSynchroniser.LAST_SYNC_SETTINGS_STORAGE_KEY, JSON.stringify(remoteUserData), StorageScope.GLOBAL);
254 255 256
	}

}