globalStateSync.ts 9.0 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 107 108 109 110 111 112 113
	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 });

			this.logService.info('UI State: Finished pulling UI State.');
		} 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);
S
Sandeep Somavarapu 已提交
131
			this.logService.trace('UI State: Finised 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 153
	async hasPreviouslySynced(): Promise<boolean> {
		const lastSyncData = await this.getLastSyncUserData();
		return !!lastSyncData;
	}

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

158
	private async getPreview(): Promise<ISyncPreviewResult> {
159 160 161
		const lastSyncData = await this.getLastSyncUserData();
		const lastSyncGlobalState = lastSyncData && lastSyncData.content ? JSON.parse(lastSyncData.content) : null;

162 163
		const remoteUserData = await this.getRemoteUserData();
		const remoteGlobalState: IGlobalState = remoteUserData.content ? JSON.parse(remoteUserData.content) : null;
164 165 166 167 168

		const localGloablState = await this.getLocalGlobalState();

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

169 170 171 172
		return { local, remote, remoteUserData };
	}

	private async apply({ local, remote, remoteUserData }: ISyncPreviewResult): Promise<void> {
173 174
		if (local) {
			// update local
S
Sandeep Somavarapu 已提交
175
			this.logService.info('UI State: Updating local ui state...');
176 177 178 179 180
			await this.writeLocalGlobalState(local);
		}

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

185
		if (remoteUserData?.content) {
186
			// update last sync
S
Sandeep Somavarapu 已提交
187
			this.logService.info('UI State: Updating last synchronised ui state...');
188
			await this.updateLastSyncValue(remoteUserData);
189 190 191 192 193 194 195 196
		}
	}

	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 已提交
197
			const argvValue: IStringDictionary<any> = parse(content.value.toString());
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
			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)));
	}

231 232 233 234
	private getRemoteUserData(lastSyncData?: IUserData | null): Promise<IUserData> {
		return this.userDataSyncStoreService.read(GlobalStateSynchroniser.EXTERNAL_USER_DATA_GLOBAL_STATE_KEY, lastSyncData || null);
	}

235 236 237 238 239 240 241
	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 };
	}

}