globalStateSync.ts 14.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 { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, ISyncPreviewResult, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync';
7
import { VSBuffer } from 'vs/base/common/buffer';
S
Sandeep Somavarapu 已提交
8
import { Event } from 'vs/base/common/event';
9
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
10
import { dirname, joinPath, basename, isEqual } from 'vs/base/common/resources';
11 12 13 14
import { IFileService } from 'vs/platform/files/common/files';
import { IStringDictionary } from 'vs/base/common/collections';
import { edit } from 'vs/platform/userDataSync/common/content';
import { merge } from 'vs/platform/userDataSync/common/globalStateMerge';
S
Sandeep Somavarapu 已提交
15
import { parse } from 'vs/base/common/json';
S
Sandeep Somavarapu 已提交
16
import { AbstractSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
17
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
S
Sandeep Somavarapu 已提交
18
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
19
import { URI } from 'vs/base/common/uri';
20 21
import { format } from 'vs/base/common/jsonFormatter';
import { applyEdits } from 'vs/base/common/jsonEdit';
S
Sandeep Somavarapu 已提交
22
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
S
Sandeep Somavarapu 已提交
23
import { IStorageKeysSyncRegistryService, IStorageKey } from 'vs/platform/userDataSync/common/storageKeys';
24
import { equals } from 'vs/base/common/arrays';
25

S
Sandeep Somavarapu 已提交
26
const argvStoragePrefx = 'globalState.argv.';
27 28
const argvProperties: string[] = ['locale'];

S
Sandeep Somavarapu 已提交
29
interface IGlobalSyncPreviewResult extends ISyncPreviewResult {
S
Sandeep Somavarapu 已提交
30 31
	readonly local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> };
	readonly remote: IStringDictionary<IStorageValue> | null;
32
	readonly skippedStorageKeys: string[];
33
	readonly localUserData: IGlobalState;
S
Sandeep Somavarapu 已提交
34
	readonly remoteUserData: IRemoteUserData;
35 36 37 38 39
	readonly lastSyncUserData: ILastSyncUserData | null;
}

interface ILastSyncUserData extends IRemoteUserData {
	skippedStorageKeys: string[] | undefined;
40 41
}

42
export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
43

44
	private static readonly GLOBAL_STATE_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'globalState', path: `/current.json` });
S
Sandeep Somavarapu 已提交
45
	protected readonly version: number = 1;
46

47
	constructor(
48
		@IFileService fileService: IFileService,
S
Sandeep Somavarapu 已提交
49
		@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
S
Sandeep Somavarapu 已提交
50
		@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
51
		@IUserDataSyncLogService logService: IUserDataSyncLogService,
52
		@IEnvironmentService private readonly environmentService: IEnvironmentService,
S
Sandeep Somavarapu 已提交
53
		@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
54
		@ITelemetryService telemetryService: ITelemetryService,
S
Sandeep Somavarapu 已提交
55
		@IConfigurationService configurationService: IConfigurationService,
56 57
		@IStorageService private readonly storageService: IStorageService,
		@IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
58
	) {
S
Sandeep Somavarapu 已提交
59
		super(SyncResource.GlobalState, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
60
		this._register(this.fileService.watch(dirname(this.environmentService.argvResource)));
S
Sandeep Somavarapu 已提交
61 62 63 64 65 66 67 68 69 70
		this._register(
			Event.any(
				/* Locale change */
				Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource)),
				/* Storage change */
				Event.filter(this.storageService.onDidChangeStorage, e => storageKeysSyncRegistryService.storageKeys.some(({ key }) => e.key === key)),
				/* Storage key registered */
				this.storageKeysSyncRegistryService.onDidChangeStorageKeys
			)((() => this._onDidChangeLocal.fire()))
		);
71 72
	}

73
	async pull(): Promise<void> {
S
Sandeep Somavarapu 已提交
74
		if (!this.isEnabled()) {
S
Sandeep Somavarapu 已提交
75
			this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling ui state as it is disabled.`);
76 77 78 79 80 81
			return;
		}

		this.stop();

		try {
S
Sandeep Somavarapu 已提交
82
			this.logService.info(`${this.syncResourceLogLabel}: Started pulling ui state...`);
83 84
			this.setStatus(SyncStatus.Syncing);

85
			const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
86
			const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
87

S
Sandeep Somavarapu 已提交
88
			if (remoteUserData.syncData !== null) {
S
Sandeep Somavarapu 已提交
89 90
				const localGlobalState = await this.getLocalGlobalState();
				const remoteGlobalState: IGlobalState = JSON.parse(remoteUserData.syncData.content);
91
				const { local, remote, skipped } = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
S
Sandeep Somavarapu 已提交
92 93
				await this.apply({
					local, remote, remoteUserData, localUserData: localGlobalState, lastSyncUserData,
94
					skippedStorageKeys: skipped,
S
Sandeep Somavarapu 已提交
95 96 97
					hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0,
					hasRemoteChanged: remote !== null
				});
98 99 100 101
			}

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

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

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

		this.stop();

		try {
S
Sandeep Somavarapu 已提交
120
			this.logService.info(`${this.syncResourceLogLabel}: Started pushing UI State...`);
121 122
			this.setStatus(SyncStatus.Syncing);

123
			const localUserData = await this.getLocalGlobalState();
124
			const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
125
			const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
S
Sandeep Somavarapu 已提交
126 127
			await this.apply({
				local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, remoteUserData, localUserData, lastSyncUserData,
128
				skippedStorageKeys: [],
S
Sandeep Somavarapu 已提交
129 130 131
				hasLocalChanged: false,
				hasRemoteChanged: true
			}, true);
132

S
Sandeep Somavarapu 已提交
133
			this.logService.info(`${this.syncResourceLogLabel}: Finished pushing UI State.`);
134 135 136 137 138 139
		} finally {
			this.setStatus(SyncStatus.Idle);
		}

	}

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

142
	async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
143
		return [{ resource: joinPath(uri, 'globalState.json'), comparableResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI }];
144 145
	}

146
	async resolveContent(uri: URI): Promise<string | null> {
147 148 149 150 151
		if (isEqual(uri, GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI)) {
			const localGlobalState = await this.getLocalGlobalState();
			return this.format(localGlobalState);
		}

152 153 154
		let content = await super.resolveContent(uri);
		if (content) {
			return content;
155
		}
156

157 158 159 160 161 162
		content = await super.resolveContent(dirname(uri));
		if (content) {
			const syncData = this.parseSyncData(content);
			if (syncData) {
				switch (basename(uri)) {
					case 'globalState.json':
163
						return this.format(JSON.parse(syncData.content));
164
				}
165 166
			}
		}
167

168 169 170
		return null;
	}

171 172 173 174 175 176 177 178 179 180
	private format(globalState: IGlobalState): string {
		const storageKeys = Object.keys(globalState.storage).sort();
		const storage: IStringDictionary<IStorageValue> = {};
		storageKeys.forEach(key => storage[key] = globalState.storage[key]);
		globalState.storage = storage;
		const content = JSON.stringify(globalState);
		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
	}
184

185 186
	async hasLocalData(): Promise<boolean> {
		try {
S
Sandeep Somavarapu 已提交
187 188
			const { storage } = await this.getLocalGlobalState();
			if (Object.keys(storage).length > 1 || storage[`${argvStoragePrefx}.locale`]?.value !== 'en') {
189 190 191 192 193 194 195 196
				return true;
			}
		} catch (error) {
			/* ignore error */
		}
		return false;
	}

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

203
	protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IGlobalSyncPreviewResult> {
S
Sandeep Somavarapu 已提交
204
		const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
S
Sandeep Somavarapu 已提交
205
		const lastSyncGlobalState: IGlobalState = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;
206 207 208

		const localGloablState = await this.getLocalGlobalState();

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

215
		const { local, remote, skipped } = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
216

S
Sandeep Somavarapu 已提交
217 218
		return {
			local, remote, remoteUserData, localUserData: localGloablState, lastSyncUserData,
219
			skippedStorageKeys: skipped,
S
Sandeep Somavarapu 已提交
220 221 222
			hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0,
			hasRemoteChanged: remote !== null
		};
223 224
	}

225
	private async apply({ local, remote, remoteUserData, lastSyncUserData, localUserData, hasLocalChanged, hasRemoteChanged, skippedStorageKeys }: IGlobalSyncPreviewResult, forcePush?: boolean): Promise<void> {
S
Sandeep Somavarapu 已提交
226

S
Sandeep Somavarapu 已提交
227
		if (!hasLocalChanged && !hasRemoteChanged) {
S
Sandeep Somavarapu 已提交
228
			this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ui state.`);
S
Sandeep Somavarapu 已提交
229 230
		}

S
Sandeep Somavarapu 已提交
231
		if (hasLocalChanged) {
232
			// update local
S
Sandeep Somavarapu 已提交
233
			this.logService.trace(`${this.syncResourceLogLabel}: Updating local ui state...`);
S
Sandeep Somavarapu 已提交
234
			await this.backupLocal(JSON.stringify(localUserData));
235
			await this.writeLocalGlobalState(local);
S
Sandeep Somavarapu 已提交
236
			this.logService.info(`${this.syncResourceLogLabel}: Updated local ui state`);
237 238
		}

S
Sandeep Somavarapu 已提交
239
		if (hasRemoteChanged) {
240
			// update remote
S
Sandeep Somavarapu 已提交
241
			this.logService.trace(`${this.syncResourceLogLabel}: Updating remote ui state...`);
S
Sandeep Somavarapu 已提交
242
			const content = JSON.stringify(<IGlobalState>{ storage: remote });
S
Sandeep Somavarapu 已提交
243
			remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
S
Sandeep Somavarapu 已提交
244
			this.logService.info(`${this.syncResourceLogLabel}: Updated remote ui state`);
245 246
		}

247
		if (lastSyncUserData?.ref !== remoteUserData.ref || !equals(lastSyncUserData.skippedStorageKeys, skippedStorageKeys)) {
248
			// update last sync
S
Sandeep Somavarapu 已提交
249
			this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized ui state...`);
250
			await this.updateLastSyncUserData(remoteUserData, { skippedStorageKeys });
S
Sandeep Somavarapu 已提交
251
			this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized ui state`);
252 253 254 255
		}
	}

	private async getLocalGlobalState(): Promise<IGlobalState> {
256
		const storage: IStringDictionary<IStorageValue> = {};
S
Sandeep Somavarapu 已提交
257 258 259 260
		const argvContent: string = await this.getLocalArgvContent();
		const argvValue: IStringDictionary<any> = parse(argvContent);
		for (const argvProperty of argvProperties) {
			if (argvValue[argvProperty] !== undefined) {
S
Sandeep Somavarapu 已提交
261
				storage[`${argvStoragePrefx}${argvProperty}`] = { version: 1, value: argvValue[argvProperty] };
S
Sandeep Somavarapu 已提交
262 263
			}
		}
264 265 266 267 268 269
		for (const { key, version } of this.storageKeysSyncRegistryService.storageKeys) {
			const value = this.storageService.get(key, StorageScope.GLOBAL);
			if (value) {
				storage[key] = { version, value };
			}
		}
S
Sandeep Somavarapu 已提交
270
		return { storage };
S
Sandeep Somavarapu 已提交
271 272 273
	}

	private async getLocalArgvContent(): Promise<string> {
274 275
		try {
			const content = await this.fileService.readFile(this.environmentService.argvResource);
S
Sandeep Somavarapu 已提交
276
			return content.value.toString();
277
		} catch (error) { }
S
Sandeep Somavarapu 已提交
278
		return '{}';
279 280
	}

S
Sandeep Somavarapu 已提交
281 282
	private async writeLocalGlobalState({ added, removed, updated }: { added: IStringDictionary<IStorageValue>, updated: IStringDictionary<IStorageValue>, removed: string[] }): Promise<void> {
		const argv: IStringDictionary<any> = {};
283
		const updatedStorage: IStringDictionary<any> = {};
284 285
		const handleUpdatedStorage = (keys: string[], storage?: IStringDictionary<IStorageValue>): void => {
			for (const key of keys) {
S
Sandeep Somavarapu 已提交
286
				if (key.startsWith(argvStoragePrefx)) {
287
					argv[key.substring(argvStoragePrefx.length)] = storage ? storage[key].value : undefined;
S
Sandeep Somavarapu 已提交
288 289
					continue;
				}
290 291 292 293 294 295 296 297 298
				if (storage) {
					const storageValue = storage[key];
					if (storageValue.value !== String(this.storageService.get(key, StorageScope.GLOBAL))) {
						updatedStorage[key] = storageValue.value;
					}
				} else {
					if (this.storageService.get(key, StorageScope.GLOBAL) !== undefined) {
						updatedStorage[key] = undefined;
					}
S
Sandeep Somavarapu 已提交
299 300 301
				}
			}
		};
302 303 304
		handleUpdatedStorage(Object.keys(added), added);
		handleUpdatedStorage(Object.keys(updated), updated);
		handleUpdatedStorage(removed);
S
Sandeep Somavarapu 已提交
305 306 307 308 309
		if (Object.keys(argv).length) {
			this.logService.trace(`${this.syncResourceLogLabel}: Updating locale...`);
			await this.updateArgv(argv);
			this.logService.info(`${this.syncResourceLogLabel}: Updated locale`);
		}
310 311 312 313 314 315 316
		const updatedStorageKeys: string[] = Object.keys(updatedStorage);
		if (updatedStorageKeys.length) {
			this.logService.trace(`${this.syncResourceLogLabel}: Updating global state...`);
			for (const key of Object.keys(updatedStorage)) {
				this.storageService.store(key, updatedStorage[key], StorageScope.GLOBAL);
			}
			this.logService.info(`${this.syncResourceLogLabel}: Updated global state`, Object.keys(updatedStorage));
317 318 319
		}
	}

S
Sandeep Somavarapu 已提交
320 321 322 323 324 325 326 327 328 329 330 331 332
	private async updateArgv(argv: IStringDictionary<any>): Promise<void> {
		const argvContent = await this.getLocalArgvContent();
		let content = argvContent;
		for (const argvProperty of Object.keys(argv)) {
			content = edit(content, [argvProperty], argv[argvProperty], {});
		}
		if (argvContent !== content) {
			this.logService.trace(`${this.syncResourceLogLabel}: Updating locale...`);
			await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(content));
			this.logService.info(`${this.syncResourceLogLabel}: Updated locale.`);
		}
	}

S
Sandeep Somavarapu 已提交
333 334 335
	private getSyncStorageKeys(): IStorageKey[] {
		return [...this.storageKeysSyncRegistryService.storageKeys, ...argvProperties.map(argvProprety => (<IStorageKey>{ key: `${argvStoragePrefx}${argvProprety}`, version: 1 }))];
	}
336
}