globalStateSync.ts 9.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/*---------------------------------------------------------------------------------------------
 *  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 { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState } from 'vs/platform/userDataSync/common/userDataSync';
import { VSBuffer } from 'vs/base/common/buffer';
import { Emitter, Event } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { URI } from 'vs/base/common/uri';
import { joinPath, dirname } from 'vs/base/common/resources';
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 已提交
17
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
S
Sandeep Somavarapu 已提交
18
import { parse } from 'vs/base/common/json';
19 20 21

const argvProperties: string[] = ['locale'];

22 23 24 25 26 27
interface ISyncPreviewResult {
	readonly local: IGlobalState | undefined;
	readonly remote: IGlobalState | undefined;
	readonly remoteUserData: IUserData | null;
}

28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
export class GlobalStateSynchroniser extends Disposable implements ISynchroniser {

	private static EXTERNAL_USER_DATA_GLOBAL_STATE_KEY: string = 'globalState';

	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;

	private _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
	readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;

	private readonly lastSyncGlobalStateResource: URI;

	constructor(
		@IFileService private readonly fileService: IFileService,
		@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
		@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
		@IEnvironmentService private readonly environmentService: IEnvironmentService,
S
Sandeep Somavarapu 已提交
47
		@IConfigurationService private readonly configurationService: IConfigurationService,
48 49 50 51 52 53 54 55 56 57 58 59 60 61
	) {
		super();
		this.lastSyncGlobalStateResource = joinPath(environmentService.userRoamingDataHome, '.lastSyncGlobalState');
		this._register(this.fileService.watch(dirname(this.environmentService.argvResource)));
		this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.argvResource))(() => this._onDidChangeLocal.fire()));
	}

	private setStatus(status: SyncStatus): void {
		if (this._status !== status) {
			this._status = status;
			this._onDidChangStatus.fire(status);
		}
	}

62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
	async pull(): Promise<void> {
		if (!this.configurationService.getValue<boolean>('sync.enableUIState')) {
			this.logService.info('UI State: Skipped pulling ui state as it is disabled.');
			return;
		}

		this.stop();

		try {
			this.logService.info('UI State: Started pulling ui state...');
			this.setStatus(SyncStatus.Syncing);

			const remoteUserData = await this.getRemoteUserData();

			if (remoteUserData.content !== null) {
				const local: IGlobalState = JSON.parse(remoteUserData.content);
				await this.apply({ local, remote: undefined, remoteUserData });
			}

			// No remote exists to pull
			else {
				this.logService.info('UI State: Remote UI state does not exist.');
			}

			this.logService.info('UI State: Finished pulling UI state.');
		} finally {
			this.setStatus(SyncStatus.Idle);
		}
	}

	async push(): Promise<void> {
		if (!this.configurationService.getValue<boolean>('sync.enableUIState')) {
			this.logService.info('UI State: Skipped pushing UI State as it is disabled.');
			return;
		}

		this.stop();

		try {
			this.logService.info('UI State: Started pushing UI State...');
			this.setStatus(SyncStatus.Syncing);

			const remote = await this.getLocalGlobalState();
			await this.apply({ local: undefined, remote, remoteUserData: null });

S
Sandeep Somavarapu 已提交
107
			this.logService.info('UI State: Finished pushing UI State.');
108 109 110 111 112 113
		} finally {
			this.setStatus(SyncStatus.Idle);
		}

	}

114
	async sync(): Promise<boolean> {
S
Sandeep Somavarapu 已提交
115 116 117 118 119
		if (!this.configurationService.getValue<boolean>('sync.enableUIState')) {
			this.logService.trace('UI State: Skipping synchronizing UI state as it is disabled.');
			return false;
		}

120
		if (this.status !== SyncStatus.Idle) {
S
Sandeep Somavarapu 已提交
121
			this.logService.trace('UI State: Skipping synchronizing ui state as it is running already.');
122 123 124
			return false;
		}

S
Sandeep Somavarapu 已提交
125
		this.logService.trace('UI State: Started synchronizing ui state...');
126 127 128
		this.setStatus(SyncStatus.Syncing);

		try {
129 130
			const result = await this.getPreview();
			await this.apply(result);
D
Daniel Imms 已提交
131
			this.logService.trace('UI State: Finished synchronizing ui state.');
132 133 134 135 136
			return true;
		} catch (e) {
			this.setStatus(SyncStatus.Idle);
			if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Rejected) {
				// Rejected as there is a new remote version. Syncing again,
S
Sandeep Somavarapu 已提交
137
				this.logService.info('UI State: Failed to synchronise ui state as there is a new remote version available. Synchronizing again...');
138 139 140
				return this.sync();
			}
			throw e;
141 142
		} finally {
			this.setStatus(SyncStatus.Idle);
143 144 145 146 147
		}
	}

	stop(): void { }

148 149 150 151 152
	async hasPreviouslySynced(): Promise<boolean> {
		const lastSyncData = await this.getLastSyncUserData();
		return !!lastSyncData;
	}

153
	async hasRemoteData(): Promise<boolean> {
154
		const remoteUserData = await this.getRemoteUserData();
155 156 157
		return remoteUserData.content !== null;
	}

158 159 160 161 162 163 164 165 166 167 168 169
	async hasLocalData(): Promise<boolean> {
		try {
			const localGloablState = await this.getLocalGlobalState();
			if (localGloablState.argv['locale'] !== 'en') {
				return true;
			}
		} catch (error) {
			/* ignore error */
		}
		return false;
	}

S
Sandeep Somavarapu 已提交
170 171 172 173 174 175
	async resetLocal(): Promise<void> {
		try {
			await this.fileService.del(this.lastSyncGlobalStateResource);
		} catch (e) { /* ignore */ }
	}

176
	private async getPreview(): Promise<ISyncPreviewResult> {
177 178 179
		const lastSyncData = await this.getLastSyncUserData();
		const lastSyncGlobalState = lastSyncData && lastSyncData.content ? JSON.parse(lastSyncData.content) : null;

180 181
		const remoteUserData = await this.getRemoteUserData();
		const remoteGlobalState: IGlobalState = remoteUserData.content ? JSON.parse(remoteUserData.content) : null;
182 183 184 185 186

		const localGloablState = await this.getLocalGlobalState();

		const { local, remote } = merge(localGloablState, remoteGlobalState, lastSyncGlobalState);

187 188 189 190
		return { local, remote, remoteUserData };
	}

	private async apply({ local, remote, remoteUserData }: ISyncPreviewResult): Promise<void> {
191 192
		if (local) {
			// update local
S
Sandeep Somavarapu 已提交
193
			this.logService.info('UI State: Updating local ui state...');
194 195 196 197 198
			await this.writeLocalGlobalState(local);
		}

		if (remote) {
			// update remote
S
Sandeep Somavarapu 已提交
199
			this.logService.info('UI State: Updating remote ui state...');
200
			remoteUserData = await this.writeToRemote(remote, remoteUserData ? remoteUserData.ref : null);
201 202
		}

203
		if (remoteUserData?.content) {
204
			// update last sync
S
Sandeep Somavarapu 已提交
205
			this.logService.info('UI State: Updating last synchronised ui state...');
206
			await this.updateLastSyncValue(remoteUserData);
207 208 209 210 211 212 213 214
		}
	}

	private async getLocalGlobalState(): Promise<IGlobalState> {
		const argv: IStringDictionary<any> = {};
		const storage: IStringDictionary<any> = {};
		try {
			const content = await this.fileService.readFile(this.environmentService.argvResource);
S
Sandeep Somavarapu 已提交
215
			const argvValue: IStringDictionary<any> = parse(content.value.toString());
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
			for (const argvProperty of argvProperties) {
				if (argvValue[argvProperty] !== undefined) {
					argv[argvProperty] = argvValue[argvProperty];
				}
			}
		} catch (error) { }
		return { argv, storage };
	}

	private async writeLocalGlobalState(globalState: IGlobalState): Promise<void> {
		const content = await this.fileService.readFile(this.environmentService.argvResource);
		let argvContent = content.value.toString();
		for (const argvProperty of Object.keys(globalState.argv)) {
			argvContent = edit(argvContent, [argvProperty], globalState.argv[argvProperty], {});
		}
		if (argvContent !== content.value.toString()) {
			await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(argvContent));
		}
	}

	private async getLastSyncUserData(): Promise<IUserData | null> {
		try {
			const content = await this.fileService.readFile(this.lastSyncGlobalStateResource);
			return JSON.parse(content.value.toString());
		} catch (error) {
			return null;
		}
	}

	private async updateLastSyncValue(remoteUserData: IUserData): Promise<void> {
		await this.fileService.writeFile(this.lastSyncGlobalStateResource, VSBuffer.fromString(JSON.stringify(remoteUserData)));
	}

249 250 251 252
	private getRemoteUserData(lastSyncData?: IUserData | null): Promise<IUserData> {
		return this.userDataSyncStoreService.read(GlobalStateSynchroniser.EXTERNAL_USER_DATA_GLOBAL_STATE_KEY, lastSyncData || null);
	}

253 254 255 256 257 258 259
	private async writeToRemote(globalState: IGlobalState, ref: string | null): Promise<IUserData> {
		const content = JSON.stringify(globalState);
		ref = await this.userDataSyncStoreService.write(GlobalStateSynchroniser.EXTERNAL_USER_DATA_GLOBAL_STATE_KEY, content, ref);
		return { content, ref };
	}

}