From 60c7478046f6eb46bd52f82e5399b650c0f8946a Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 6 Jan 2020 09:03:42 +0100 Subject: [PATCH 1/4] first cut --- .../userDataSync/common/localizationSync.ts | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/vs/platform/userDataSync/common/localizationSync.ts diff --git a/src/vs/platform/userDataSync/common/localizationSync.ts b/src/vs/platform/userDataSync/common/localizationSync.ts new file mode 100644 index 00000000000..724eac4dee5 --- /dev/null +++ b/src/vs/platform/userDataSync/common/localizationSync.ts @@ -0,0 +1,160 @@ +/*--------------------------------------------------------------------------------------------- + * 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, ISyncExtension, IUserDataSyncLogService } 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 } from 'vs/base/common/resources'; +import { IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IFileService } from 'vs/platform/files/common/files'; +import { Queue } from 'vs/base/common/async'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { localize } from 'vs/nls'; +import { merge } from 'vs/platform/userDataSync/common/extensionsMerge'; + +export class LocalizationSynchroniser extends Disposable implements ISynchroniser { + + private static EXTERNAL_USER_DATA_LOCALIZATION_KEY: string = 'localization'; + + private _status: SyncStatus = SyncStatus.Idle; + get status(): SyncStatus { return this._status; } + private _onDidChangStatus: Emitter = this._register(new Emitter()); + readonly onDidChangeStatus: Event = this._onDidChangStatus.event; + + private _onDidChangeLocal: Emitter = this._register(new Emitter()); + readonly onDidChangeLocal: Event = this._onDidChangeLocal.event; + + private readonly lastSyncExtensionsResource: URI; + + constructor( + @IEnvironmentService environmentService: IEnvironmentService, + @IFileService private readonly fileService: IFileService, + @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { + super(); + this.lastSyncExtensionsResource = joinPath(environmentService.userRoamingDataHome, '.lastSyncLocalization'); + this._register( + Event.debounce( + Event.any( + Event.filter(this.extensionManagementService.onDidInstallExtension, (e => !!e.gallery)), + Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error))), + () => undefined, 500)(() => this._onDidChangeLocal.fire())); + } + + private setStatus(status: SyncStatus): void { + if (this._status !== status) { + this._status = status; + this._onDidChangStatus.fire(status); + } + } + + async sync(): Promise { + if (!this.configurationService.getValue('sync.enableLocale')) { + this.logService.trace('Extensions: Skipping synchronizing locale as it is disabled.'); + return false; + } + if (this.status !== SyncStatus.Idle) { + this.logService.trace('Extensions: Skipping synchronizing locale as it is running already.'); + return false; + } + + this.logService.trace('Extensions: Started synchronizing locale...'); + this.setStatus(SyncStatus.Syncing); + + try { + await this.doSync(); + } 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('Extensions: Failed to synchronise extensions as there is a new remote version available. Synchronizing again...'); + return this.sync(); + } + throw e; + } + + this.logService.trace('Extensions: Finised synchronizing extensions.'); + this.setStatus(SyncStatus.Idle); + return true; + } + + stop(): void { } + + private async doSync(): Promise { + const lastSyncData = await this.getLastSyncUserData(); + const lastSyncExtensions: ISyncExtension[] | null = lastSyncData ? JSON.parse(lastSyncData.content!) : null; + let skippedExtensions: ISyncExtension[] = lastSyncData ? lastSyncData.skippedExtensions || [] : []; + + let remoteData = await this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_LOCALIZATION_KEY, lastSyncData); + const remoteExtensions: ISyncExtension[] = remoteData.content ? JSON.parse(remoteData.content) : null; + + const localExtensions = await this.getLocalExtensions(); + + if (remoteExtensions) { + this.logService.trace('Extensions: Merging remote extensions with local extensions...'); + } else { + this.logService.info('Extensions: Remote extensions does not exist. Synchronizing extensions for the first time.'); + } + + const ignoredExtensions = this.configurationService.getValue('sync.ignoredExtensions') || []; + const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions); + + if (!added.length && !removed.length && !updated.length && !remote) { + this.logService.trace('Extensions: No changes found during synchronizing extensions.'); + } + + if (added.length || removed.length || updated.length) { + this.logService.info('Extensions: Updating local extensions...'); + skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions); + } + + if (remote) { + // update remote + this.logService.info('Extensions: Updating remote extensions...'); + remoteData = await this.writeToRemote(remote, remoteData.ref); + } + + if (remoteData.content + && (!lastSyncData || lastSyncData.ref !== remoteData.ref) + ) { + // update last sync + this.logService.info('Extensions: Updating last synchronised extensions...'); + await this.updateLastSyncValue({ ...remoteData, skippedExtensions }); + } + } + + private async getLocalExtensions(): Promise { + const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User); + return installedExtensions + .map(({ identifier }) => ({ identifier, enabled: true })); + } + + private async getLastSyncUserData(): Promise { + try { + const content = await this.fileService.readFile(this.lastSyncExtensionsResource); + return JSON.parse(content.value.toString()); + } catch (error) { + return null; + } + } + + private async updateLastSyncValue(lastSyncUserData: ILastSyncUserData): Promise { + await this.fileService.writeFile(this.lastSyncExtensionsResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData))); + } + + private async writeToRemote(extensions: ISyncExtension[], ref: string | null): Promise { + const content = JSON.stringify(extensions); + ref = await this.userDataSyncStoreService.write(ExtensionsSynchroniser.EXTERNAL_USER_DATA_LOCALIZATION_KEY, content, ref); + return { content, ref }; + } + +} From c3b939bfeaa1b84ff7868090c2561ec23125a355 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 6 Jan 2020 16:03:40 +0100 Subject: [PATCH 2/4] Sync locale using global state --- .../userDataSync/common/globalStateMerge.ts | 86 ++++++++++ .../userDataSync/common/globalStateSync.ts | 157 +++++++++++++++++ .../userDataSync/common/localizationSync.ts | 160 ------------------ .../userDataSync/common/userDataSync.ts | 5 + 4 files changed, 248 insertions(+), 160 deletions(-) create mode 100644 src/vs/platform/userDataSync/common/globalStateMerge.ts create mode 100644 src/vs/platform/userDataSync/common/globalStateSync.ts delete mode 100644 src/vs/platform/userDataSync/common/localizationSync.ts diff --git a/src/vs/platform/userDataSync/common/globalStateMerge.ts b/src/vs/platform/userDataSync/common/globalStateMerge.ts new file mode 100644 index 00000000000..f4520a76a38 --- /dev/null +++ b/src/vs/platform/userDataSync/common/globalStateMerge.ts @@ -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, remote: IStringDictionary, base: IStringDictionary | null): { local?: IStringDictionary, remote?: IStringDictionary } { + 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()), removed: new Set(), updated: new Set() }; + 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()), removed: new Set(), updated: new Set() }; + 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, to: IStringDictionary): { added: Set, removed: Set, updated: Set } { + 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()); + const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set()); + const updated: Set = new Set(); + + 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 }; +} + diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts new file mode 100644 index 00000000000..d5e218a964b --- /dev/null +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -0,0 +1,157 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; + +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 = this._register(new Emitter()); + readonly onDidChangeStatus: Event = this._onDidChangStatus.event; + + private _onDidChangeLocal: Emitter = this._register(new Emitter()); + readonly onDidChangeLocal: Event = 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, + ) { + 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 { + if (this.status !== SyncStatus.Idle) { + this.logService.trace('Global State: Skipping synchronizing global state as it is running already.'); + return false; + } + + this.logService.trace('Global State: Started synchronizing global state...'); + this.setStatus(SyncStatus.Syncing); + + try { + await this.doSync(); + this.logService.trace('Global State: Finised synchronizing global 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('Global State: Failed to synchronise global state as there is a new remote version available. Synchronizing again...'); + return this.sync(); + } + throw e; + } + } + + stop(): void { } + + private async doSync(): Promise { + 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('Global State: Updating local global state...'); + await this.writeLocalGlobalState(local); + } + + if (remote) { + // update remote + this.logService.info('Global State: Updating remote global state...'); + remoteData = await this.writeToRemote(remote, remoteData.ref); + } + + if (remoteData.content + && (!lastSyncData || lastSyncData.ref !== remoteData.ref) + ) { + // update last sync + this.logService.info('Global State: Updating last synchronised global state...'); + await this.updateLastSyncValue(remoteData); + } + } + + private async getLocalGlobalState(): Promise { + const argv: IStringDictionary = {}; + const storage: IStringDictionary = {}; + try { + const content = await this.fileService.readFile(this.environmentService.argvResource); + const argvValue: IStringDictionary = JSON.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 { + 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 { + 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 { + await this.fileService.writeFile(this.lastSyncGlobalStateResource, VSBuffer.fromString(JSON.stringify(remoteUserData))); + } + + private async writeToRemote(globalState: IGlobalState, ref: string | null): Promise { + const content = JSON.stringify(globalState); + ref = await this.userDataSyncStoreService.write(GlobalStateSynchroniser.EXTERNAL_USER_DATA_GLOBAL_STATE_KEY, content, ref); + return { content, ref }; + } + +} diff --git a/src/vs/platform/userDataSync/common/localizationSync.ts b/src/vs/platform/userDataSync/common/localizationSync.ts deleted file mode 100644 index 724eac4dee5..00000000000 --- a/src/vs/platform/userDataSync/common/localizationSync.ts +++ /dev/null @@ -1,160 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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, ISyncExtension, IUserDataSyncLogService } 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 } from 'vs/base/common/resources'; -import { IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { IFileService } from 'vs/platform/files/common/files'; -import { Queue } from 'vs/base/common/async'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { localize } from 'vs/nls'; -import { merge } from 'vs/platform/userDataSync/common/extensionsMerge'; - -export class LocalizationSynchroniser extends Disposable implements ISynchroniser { - - private static EXTERNAL_USER_DATA_LOCALIZATION_KEY: string = 'localization'; - - private _status: SyncStatus = SyncStatus.Idle; - get status(): SyncStatus { return this._status; } - private _onDidChangStatus: Emitter = this._register(new Emitter()); - readonly onDidChangeStatus: Event = this._onDidChangStatus.event; - - private _onDidChangeLocal: Emitter = this._register(new Emitter()); - readonly onDidChangeLocal: Event = this._onDidChangeLocal.event; - - private readonly lastSyncExtensionsResource: URI; - - constructor( - @IEnvironmentService environmentService: IEnvironmentService, - @IFileService private readonly fileService: IFileService, - @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, - @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, - @IConfigurationService private readonly configurationService: IConfigurationService, - ) { - super(); - this.lastSyncExtensionsResource = joinPath(environmentService.userRoamingDataHome, '.lastSyncLocalization'); - this._register( - Event.debounce( - Event.any( - Event.filter(this.extensionManagementService.onDidInstallExtension, (e => !!e.gallery)), - Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error))), - () => undefined, 500)(() => this._onDidChangeLocal.fire())); - } - - private setStatus(status: SyncStatus): void { - if (this._status !== status) { - this._status = status; - this._onDidChangStatus.fire(status); - } - } - - async sync(): Promise { - if (!this.configurationService.getValue('sync.enableLocale')) { - this.logService.trace('Extensions: Skipping synchronizing locale as it is disabled.'); - return false; - } - if (this.status !== SyncStatus.Idle) { - this.logService.trace('Extensions: Skipping synchronizing locale as it is running already.'); - return false; - } - - this.logService.trace('Extensions: Started synchronizing locale...'); - this.setStatus(SyncStatus.Syncing); - - try { - await this.doSync(); - } 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('Extensions: Failed to synchronise extensions as there is a new remote version available. Synchronizing again...'); - return this.sync(); - } - throw e; - } - - this.logService.trace('Extensions: Finised synchronizing extensions.'); - this.setStatus(SyncStatus.Idle); - return true; - } - - stop(): void { } - - private async doSync(): Promise { - const lastSyncData = await this.getLastSyncUserData(); - const lastSyncExtensions: ISyncExtension[] | null = lastSyncData ? JSON.parse(lastSyncData.content!) : null; - let skippedExtensions: ISyncExtension[] = lastSyncData ? lastSyncData.skippedExtensions || [] : []; - - let remoteData = await this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_LOCALIZATION_KEY, lastSyncData); - const remoteExtensions: ISyncExtension[] = remoteData.content ? JSON.parse(remoteData.content) : null; - - const localExtensions = await this.getLocalExtensions(); - - if (remoteExtensions) { - this.logService.trace('Extensions: Merging remote extensions with local extensions...'); - } else { - this.logService.info('Extensions: Remote extensions does not exist. Synchronizing extensions for the first time.'); - } - - const ignoredExtensions = this.configurationService.getValue('sync.ignoredExtensions') || []; - const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions); - - if (!added.length && !removed.length && !updated.length && !remote) { - this.logService.trace('Extensions: No changes found during synchronizing extensions.'); - } - - if (added.length || removed.length || updated.length) { - this.logService.info('Extensions: Updating local extensions...'); - skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions); - } - - if (remote) { - // update remote - this.logService.info('Extensions: Updating remote extensions...'); - remoteData = await this.writeToRemote(remote, remoteData.ref); - } - - if (remoteData.content - && (!lastSyncData || lastSyncData.ref !== remoteData.ref) - ) { - // update last sync - this.logService.info('Extensions: Updating last synchronised extensions...'); - await this.updateLastSyncValue({ ...remoteData, skippedExtensions }); - } - } - - private async getLocalExtensions(): Promise { - const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User); - return installedExtensions - .map(({ identifier }) => ({ identifier, enabled: true })); - } - - private async getLastSyncUserData(): Promise { - try { - const content = await this.fileService.readFile(this.lastSyncExtensionsResource); - return JSON.parse(content.value.toString()); - } catch (error) { - return null; - } - } - - private async updateLastSyncValue(lastSyncUserData: ILastSyncUserData): Promise { - await this.fileService.writeFile(this.lastSyncExtensionsResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData))); - } - - private async writeToRemote(extensions: ISyncExtension[], ref: string | null): Promise { - const content = JSON.stringify(extensions); - ref = await this.userDataSyncStoreService.write(ExtensionsSynchroniser.EXTERNAL_USER_DATA_LOCALIZATION_KEY, content, ref); - return { content, ref }; - } - -} diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 21f92cacb8e..e660f31e2c6 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -145,6 +145,11 @@ export interface ISyncExtension { enabled: boolean; } +export interface IGlobalState { + argv: IStringDictionary; + storage: IStringDictionary; +} + export const enum SyncSource { Settings = 1, Keybindings, From e8da35fe13069eb74a2234f0d0a0432be84f9803 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 6 Jan 2020 17:51:41 +0100 Subject: [PATCH 3/4] Configure syncing ui state --- .../userDataSync/common/globalStateSync.ts | 23 ++++++++++++------- .../userDataSync/common/userDataSync.ts | 18 ++++++++++----- .../common/userDataSyncService.ts | 5 +++- .../userDataSync/browser/userDataSync.ts | 3 +++ 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index d5e218a964b..0e711eec298 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -14,6 +14,7 @@ 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'; const argvProperties: string[] = ['locale']; @@ -36,6 +37,7 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser @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'); @@ -51,24 +53,29 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser } async sync(): Promise { - if (this.status !== SyncStatus.Idle) { - this.logService.trace('Global State: Skipping synchronizing global state as it is running already.'); + if (!this.configurationService.getValue('sync.enableUIState')) { + this.logService.trace('UI State: Skipping synchronizing UI state as it is disabled.'); return false; } - this.logService.trace('Global State: Started synchronizing global state...'); + 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('Global State: Finised synchronizing global state.'); + 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('Global State: Failed to synchronise global state as there is a new remote version available. Synchronizing 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; @@ -90,13 +97,13 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser if (local) { // update local - this.logService.info('Global State: Updating local global state...'); + this.logService.info('UI State: Updating local ui state...'); await this.writeLocalGlobalState(local); } if (remote) { // update remote - this.logService.info('Global State: Updating remote global state...'); + this.logService.info('UI State: Updating remote ui state...'); remoteData = await this.writeToRemote(remote, remoteData.ref); } @@ -104,7 +111,7 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser && (!lastSyncData || lastSyncData.ref !== remoteData.ref) ) { // update last sync - this.logService.info('Global State: Updating last synchronised global state...'); + this.logService.info('UI State: Updating last synchronised ui state...'); await this.updateLastSyncValue(remoteData); } } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index e660f31e2c6..e273fdde569 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -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."), diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index b906b66aafa..7619d616ddb 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -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) { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 8fd8c739cf4..69eac6711ac 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -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") From 4fa0c5481ae0c9df1083b4e4ee5f40d4db3f3fab Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 7 Jan 2020 13:48:34 +0100 Subject: [PATCH 4/4] parse using jsonc parser --- src/vs/platform/userDataSync/common/globalStateSync.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 0e711eec298..ff5ce0d5ae9 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -15,6 +15,7 @@ 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']; @@ -121,7 +122,7 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser const storage: IStringDictionary = {}; try { const content = await this.fileService.readFile(this.environmentService.argvResource); - const argvValue: IStringDictionary = JSON.parse(content.value.toString()); + const argvValue: IStringDictionary = parse(content.value.toString()); for (const argvProperty of argvProperties) { if (argvValue[argvProperty] !== undefined) { argv[argvProperty] = argvValue[argvProperty];