Merge branch 'sandy081/syncLocale'

This commit is contained in:
Sandeep Somavarapu
2020-01-07 13:59:23 +01:00
5 changed files with 275 additions and 7 deletions
@@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as objects from 'vs/base/common/objects';
import { IGlobalState } from 'vs/platform/userDataSync/common/userDataSync';
import { IStringDictionary } from 'vs/base/common/collections';
import { values } from 'vs/base/common/map';
export function merge(localGloablState: IGlobalState, remoteGlobalState: IGlobalState | null, lastSyncGlobalState: IGlobalState | null): { local?: IGlobalState, remote?: IGlobalState } {
if (!remoteGlobalState) {
return { remote: localGloablState };
}
const { local: localArgv, remote: remoteArgv } = doMerge(localGloablState.argv, remoteGlobalState.argv, lastSyncGlobalState ? lastSyncGlobalState.argv : null);
const { local: localStorage, remote: remoteStorage } = doMerge(localGloablState.storage, remoteGlobalState.storage, lastSyncGlobalState ? lastSyncGlobalState.storage : null);
const local: IGlobalState | undefined = localArgv || localStorage ? { argv: localArgv || localGloablState.argv, storage: localStorage || localGloablState.storage } : undefined;
const remote: IGlobalState | undefined = remoteArgv || remoteStorage ? { argv: remoteArgv || remoteGlobalState.argv, storage: remoteStorage || remoteGlobalState.storage } : undefined;
return { local, remote };
}
function doMerge(local: IStringDictionary<any>, remote: IStringDictionary<any>, base: IStringDictionary<any> | null): { local?: IStringDictionary<any>, remote?: IStringDictionary<any> } {
const localToRemote = compare(local, remote);
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
// No changes found between local and remote.
return {};
}
const baseToRemote = base ? compare(base, remote) : { added: Object.keys(remote).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
if (baseToRemote.added.size === 0 && baseToRemote.removed.size === 0 && baseToRemote.updated.size === 0) {
// No changes found between base and remote.
return { remote: local };
}
const baseToLocal = base ? compare(base, local) : { added: Object.keys(local).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
if (baseToLocal.added.size === 0 && baseToLocal.removed.size === 0 && baseToLocal.updated.size === 0) {
// No changes found between base and local.
return { local: remote };
}
const merged = objects.deepClone(local);
// Added in remote
for (const key of values(baseToRemote.added)) {
merged[key] = remote[key];
}
// Updated in Remote
for (const key of values(baseToRemote.updated)) {
merged[key] = remote[key];
}
// Removed in remote & local
for (const key of values(baseToRemote.removed)) {
// Got removed in local
if (baseToLocal.removed.has(key)) {
delete merged[key];
}
}
return { local: merged, remote: merged };
}
function compare(from: IStringDictionary<any>, to: IStringDictionary<any>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
const fromKeys = Object.keys(from);
const toKeys = Object.keys(to);
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const updated: Set<string> = new Set<string>();
for (const key of fromKeys) {
if (removed.has(key)) {
continue;
}
const value1 = from[key];
const value2 = to[key];
if (!objects.equals(value1, value2)) {
updated.add(key);
}
}
return { added, removed, updated };
}
@@ -0,0 +1,165 @@
/*---------------------------------------------------------------------------------------------
* 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';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { parse } from 'vs/base/common/json';
const argvProperties: string[] = ['locale'];
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,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
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);
}
}
async sync(): Promise<boolean> {
if (!this.configurationService.getValue<boolean>('sync.enableUIState')) {
this.logService.trace('UI State: Skipping synchronizing UI state as it is disabled.');
return false;
}
if (this.status !== SyncStatus.Idle) {
this.logService.trace('UI State: Skipping synchronizing ui state as it is running already.');
return false;
}
this.logService.trace('UI State: Started synchronizing ui state...');
this.setStatus(SyncStatus.Syncing);
try {
await this.doSync();
this.logService.trace('UI State: Finised synchronizing ui state.');
this.setStatus(SyncStatus.Idle);
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,
this.logService.info('UI State: Failed to synchronise ui state as there is a new remote version available. Synchronizing again...');
return this.sync();
}
throw e;
}
}
stop(): void { }
private async doSync(): Promise<void> {
const lastSyncData = await this.getLastSyncUserData();
const lastSyncGlobalState = lastSyncData && lastSyncData.content ? JSON.parse(lastSyncData.content) : null;
let remoteData = await this.userDataSyncStoreService.read(GlobalStateSynchroniser.EXTERNAL_USER_DATA_GLOBAL_STATE_KEY, lastSyncData);
const remoteGlobalState: IGlobalState = remoteData.content ? JSON.parse(remoteData.content) : null;
const localGloablState = await this.getLocalGlobalState();
const { local, remote } = merge(localGloablState, remoteGlobalState, lastSyncGlobalState);
if (local) {
// update local
this.logService.info('UI State: Updating local ui state...');
await this.writeLocalGlobalState(local);
}
if (remote) {
// update remote
this.logService.info('UI State: Updating remote ui state...');
remoteData = await this.writeToRemote(remote, remoteData.ref);
}
if (remoteData.content
&& (!lastSyncData || lastSyncData.ref !== remoteData.ref)
) {
// update last sync
this.logService.info('UI State: Updating last synchronised ui state...');
await this.updateLastSyncValue(remoteData);
}
}
private async getLocalGlobalState(): Promise<IGlobalState> {
const argv: IStringDictionary<any> = {};
const storage: IStringDictionary<any> = {};
try {
const content = await this.fileService.readFile(this.environmentService.argvResource);
const argvValue: IStringDictionary<any> = parse(content.value.toString());
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)));
}
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 };
}
}
@@ -49,18 +49,24 @@ export function registerConfiguration(): IDisposable {
default: true,
scope: ConfigurationScope.APPLICATION,
},
'sync.enableExtensions': {
type: 'boolean',
description: localize('sync.enableExtensions', "Enable synchronizing extensions."),
default: true,
scope: ConfigurationScope.APPLICATION,
},
'sync.enableKeybindings': {
type: 'boolean',
description: localize('sync.enableKeybindings', "Enable synchronizing keybindings."),
default: true,
scope: ConfigurationScope.APPLICATION,
},
'sync.enableUIState': {
type: 'boolean',
description: localize('sync.enableUIState', "Enable synchronizing UI state."),
default: true,
scope: ConfigurationScope.APPLICATION,
},
'sync.enableExtensions': {
type: 'boolean',
description: localize('sync.enableExtensions', "Enable synchronizing extensions."),
default: true,
scope: ConfigurationScope.APPLICATION,
},
'sync.keybindingsPerPlatform': {
type: 'boolean',
description: localize('sync.keybindingsPerPlatform', "Synchronize keybindings per platform."),
@@ -145,6 +151,11 @@ export interface ISyncExtension {
enabled: boolean;
}
export interface IGlobalState {
argv: IStringDictionary<any>;
storage: IStringDictionary<any>;
}
export const enum SyncSource {
Settings = 1,
Keybindings,
@@ -12,6 +12,7 @@ import { ExtensionsSynchroniser } from 'vs/platform/userDataSync/common/extensio
import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth';
import { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync';
import { GlobalStateSynchroniser } from 'vs/platform/userDataSync/common/globalStateSync';
export class UserDataSyncService extends Disposable implements IUserDataSyncService {
@@ -32,6 +33,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
private readonly settingsSynchroniser: SettingsSynchroniser;
private readonly keybindingsSynchroniser: KeybindingsSynchroniser;
private readonly extensionsSynchroniser: ExtensionsSynchroniser;
private readonly globalStateSynchroniser: GlobalStateSynchroniser;
constructor(
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@@ -41,8 +43,9 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
super();
this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser));
this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser));
this.globalStateSynchroniser = this._register(this.instantiationService.createInstance(GlobalStateSynchroniser));
this.extensionsSynchroniser = this._register(this.instantiationService.createInstance(ExtensionsSynchroniser));
this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.extensionsSynchroniser];
this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.globalStateSynchroniser, this.extensionsSynchroniser];
this.updateStatus();
if (this.userDataSyncStoreService.userDataSyncStore) {
@@ -212,6 +212,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}, {
id: 'sync.enableKeybindings',
label: localize('user keybindings', "User Keybindings")
}, {
id: 'sync.enableUIState',
label: localize('ui state', "UI State")
}, {
id: 'sync.enableExtensions',
label: localize('extensions', "Extensions")