Merge from vscode a234f13c45b40a0929777cb440ee011b7549eed2 (#8911)

* Merge from vscode a234f13c45b40a0929777cb440ee011b7549eed2

* update distro

* fix layering

* update distro

* fix tests
This commit is contained in:
Anthony Dresser
2020-01-22 13:42:37 -08:00
committed by GitHub
parent 977111eb21
commit bd7aac8ee0
895 changed files with 24651 additions and 14520 deletions

View File

@@ -0,0 +1,46 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService } from 'vs/platform/files/common/files';
import { VSBuffer } from 'vs/base/common/buffer';
import { URI } from 'vs/base/common/uri';
import { SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { joinPath } from 'vs/base/common/resources';
import { toLocalISOString } from 'vs/base/common/date';
import { ThrottledDelayer } from 'vs/base/common/async';
export abstract class AbstractSynchroniser extends Disposable {
private readonly syncFolder: URI;
private cleanUpDelayer: ThrottledDelayer<void>;
constructor(
syncSource: SyncSource,
@IFileService protected readonly fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService
) {
super();
this.syncFolder = joinPath(environmentService.userRoamingDataHome, '.sync', syncSource);
this.cleanUpDelayer = new ThrottledDelayer(50);
}
protected async backupLocal(content: VSBuffer): Promise<void> {
const resource = joinPath(this.syncFolder, toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, ''));
await this.fileService.writeFile(resource, content);
this.cleanUpDelayer.trigger(() => this.cleanUpBackup());
}
private async cleanUpBackup(): Promise<void> {
const stat = await this.fileService.resolve(this.syncFolder);
if (stat.children) {
const all = stat.children.filter(stat => stat.isFile && /^\d{8}T\d{6}$/.test(stat.name)).sort();
const toDelete = all.slice(0, Math.max(0, all.length - 9));
await Promise.all(toDelete.map(stat => this.fileService.del(stat.resource)));
}
}
}

View File

@@ -18,12 +18,15 @@ 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';
import { isNonEmptyArray } from 'vs/base/common/arrays';
export interface ISyncPreviewResult {
interface ISyncPreviewResult {
readonly added: ISyncExtension[];
readonly removed: ISyncExtension[];
readonly removed: IExtensionIdentifier[];
readonly updated: ISyncExtension[];
readonly remote: ISyncExtension[] | null;
readonly remoteUserData: IUserData | null;
readonly skippedExtensions: ISyncExtension[];
}
interface ILastSyncUserData extends IUserData {
@@ -72,6 +75,61 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
}
}
async pull(): Promise<void> {
if (!this.configurationService.getValue<boolean>('sync.enableExtensions')) {
this.logService.info('Extensions: Skipped pulling extensions as it is disabled.');
return;
}
this.stop();
try {
this.logService.info('Extensions: Started pulling extensions...');
this.setStatus(SyncStatus.Syncing);
const remoteUserData = await this.getRemoteUserData();
if (remoteUserData.content !== null) {
const localExtensions = await this.getLocalExtensions();
const remoteExtensions: ISyncExtension[] = JSON.parse(remoteUserData.content);
const { added, updated, remote } = merge(localExtensions, remoteExtensions, [], [], this.getIgnoredExtensions());
await this.apply({ added, removed: [], updated, remote, remoteUserData, skippedExtensions: [] });
}
// No remote exists to pull
else {
this.logService.info('Extensions: Remote extensions does not exist.');
}
this.logService.info('Extensions: Finished pulling extensions.');
} finally {
this.setStatus(SyncStatus.Idle);
}
}
async push(): Promise<void> {
if (!this.configurationService.getValue<boolean>('sync.enableExtensions')) {
this.logService.info('Extensions: Skipped pushing extensions as it is disabled.');
return;
}
this.stop();
try {
this.logService.info('Extensions: Started pushing extensions...');
this.setStatus(SyncStatus.Syncing);
const localExtensions = await this.getLocalExtensions();
const { added, removed, updated, remote } = merge(localExtensions, null, null, [], this.getIgnoredExtensions());
await this.apply({ added, removed, updated, remote, remoteUserData: null, skippedExtensions: [] });
this.logService.info('Extensions: Finished pushing extensions.');
} finally {
this.setStatus(SyncStatus.Idle);
}
}
async sync(): Promise<boolean> {
if (!this.configurationService.getValue<boolean>('sync.enableExtensions')) {
this.logService.trace('Extensions: Skipping synchronizing extensions as it is disabled.');
@@ -90,7 +148,8 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
this.setStatus(SyncStatus.Syncing);
try {
await this.doSync();
const previewResult = await this.getPreview();
await this.apply(previewResult);
} catch (e) {
this.setStatus(SyncStatus.Idle);
if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Rejected) {
@@ -108,6 +167,28 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
stop(): void { }
async hasPreviouslySynced(): Promise<boolean> {
const lastSyncData = await this.getLastSyncUserData();
return !!lastSyncData;
}
async hasRemoteData(): Promise<boolean> {
const remoteUserData = await this.getRemoteUserData();
return remoteUserData.content !== null;
}
async hasLocalData(): Promise<boolean> {
try {
const localExtensions = await this.getLocalExtensions();
if (isNonEmptyArray(localExtensions)) {
return true;
}
} catch (error) {
/* ignore error */
}
return false;
}
removeExtension(identifier: IExtensionIdentifier): Promise<void> {
return this.replaceQueue.queue(async () => {
const remoteData = await this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, null);
@@ -124,13 +205,19 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
});
}
private async doSync(): Promise<void> {
async resetLocal(): Promise<void> {
try {
await this.fileService.del(this.lastSyncExtensionsResource);
} catch (e) { /* ignore */ }
}
private async getPreview(): Promise<ISyncPreviewResult> {
const lastSyncData = await this.getLastSyncUserData();
const lastSyncExtensions: ISyncExtension[] | null = lastSyncData ? JSON.parse(lastSyncData.content!) : null;
let skippedExtensions: ISyncExtension[] = lastSyncData ? lastSyncData.skippedExtensions || [] : [];
const skippedExtensions: ISyncExtension[] = lastSyncData ? lastSyncData.skippedExtensions || [] : [];
let remoteData = await this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, lastSyncData);
const remoteExtensions: ISyncExtension[] = remoteData.content ? JSON.parse(remoteData.content) : null;
const remoteUserData = await this.getRemoteUserData(lastSyncData);
const remoteExtensions: ISyncExtension[] = remoteUserData.content ? JSON.parse(remoteUserData.content) : null;
const localExtensions = await this.getLocalExtensions();
@@ -140,9 +227,16 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
this.logService.info('Extensions: Remote extensions does not exist. Synchronizing extensions for the first time.');
}
const ignoredExtensions = this.configurationService.getValue<string[]>('sync.ignoredExtensions') || [];
const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions);
const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, this.getIgnoredExtensions());
return { added, removed, updated, remote, skippedExtensions, remoteUserData };
}
private getIgnoredExtensions() {
return this.configurationService.getValue<string[]>('sync.ignoredExtensions') || [];
}
private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions }: ISyncPreviewResult): Promise<void> {
if (!added.length && !removed.length && !updated.length && !remote) {
this.logService.trace('Extensions: No changes found during synchronizing extensions.');
}
@@ -155,15 +249,13 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
if (remote) {
// update remote
this.logService.info('Extensions: Updating remote extensions...');
remoteData = await this.writeToRemote(remote, remoteData.ref);
remoteUserData = await this.writeToRemote(remote, remoteUserData ? remoteUserData.ref : null);
}
if (remoteData.content
&& (!lastSyncData || lastSyncData.ref !== remoteData.ref)
) {
if (remoteUserData?.content) {
// update last sync
this.logService.info('Extensions: Updating last synchronised extensions...');
await this.updateLastSyncValue({ ...remoteData, skippedExtensions });
await this.updateLastSyncValue({ ...remoteUserData, skippedExtensions });
}
}
@@ -233,6 +325,10 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
await this.fileService.writeFile(this.lastSyncExtensionsResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData)));
}
private getRemoteUserData(lastSyncData?: IUserData | null): Promise<IUserData> {
return this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, lastSyncData || null);
}
private async writeToRemote(extensions: ISyncExtension[], ref: string | null): Promise<IUserData> {
const content = JSON.stringify(extensions);
ref = await this.userDataSyncStoreService.write(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, content, ref);

View File

@@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. 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 };
}

View File

@@ -0,0 +1,259 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. 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'];
interface ISyncPreviewResult {
readonly local: IGlobalState | undefined;
readonly remote: IGlobalState | undefined;
readonly remoteUserData: IUserData | null;
}
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 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 pushing UI State.');
} finally {
this.setStatus(SyncStatus.Idle);
}
}
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 {
const result = await this.getPreview();
await this.apply(result);
this.logService.trace('UI State: Finised synchronizing ui state.');
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;
} finally {
this.setStatus(SyncStatus.Idle);
}
}
stop(): void { }
async hasPreviouslySynced(): Promise<boolean> {
const lastSyncData = await this.getLastSyncUserData();
return !!lastSyncData;
}
async hasRemoteData(): Promise<boolean> {
const remoteUserData = await this.getRemoteUserData();
return remoteUserData.content !== null;
}
async hasLocalData(): Promise<boolean> {
try {
const localGloablState = await this.getLocalGlobalState();
if (localGloablState.argv['locale'] !== 'en') {
return true;
}
} catch (error) {
/* ignore error */
}
return false;
}
async resetLocal(): Promise<void> {
try {
await this.fileService.del(this.lastSyncGlobalStateResource);
} catch (e) { /* ignore */ }
}
private async getPreview(): Promise<ISyncPreviewResult> {
const lastSyncData = await this.getLastSyncUserData();
const lastSyncGlobalState = lastSyncData && lastSyncData.content ? JSON.parse(lastSyncData.content) : null;
const remoteUserData = await this.getRemoteUserData();
const remoteGlobalState: IGlobalState = remoteUserData.content ? JSON.parse(remoteUserData.content) : null;
const localGloablState = await this.getLocalGlobalState();
const { local, remote } = merge(localGloablState, remoteGlobalState, lastSyncGlobalState);
return { local, remote, remoteUserData };
}
private async apply({ local, remote, remoteUserData }: ISyncPreviewResult): Promise<void> {
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...');
remoteUserData = await this.writeToRemote(remote, remoteUserData ? remoteUserData.ref : null);
}
if (remoteUserData?.content) {
// update last sync
this.logService.info('UI State: Updating last synchronised ui state...');
await this.updateLastSyncValue(remoteUserData);
}
}
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 getRemoteUserData(lastSyncData?: IUserData | null): Promise<IUserData> {
return this.userDataSyncStoreService.read(GlobalStateSynchroniser.EXTERNAL_USER_DATA_GLOBAL_STATE_KEY, lastSyncData || null);
}
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 };
}
}

View File

@@ -3,9 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent } from 'vs/platform/files/common/files';
import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync';
import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge';
import { VSBuffer } from 'vs/base/common/buffer';
import { parse, ParseError } from 'vs/base/common/json';
@@ -20,6 +19,8 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { OS, OperatingSystem } from 'vs/base/common/platform';
import { isUndefined } from 'vs/base/common/types';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { AbstractSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
interface ISyncContent {
mac?: string;
@@ -30,13 +31,13 @@ interface ISyncContent {
interface ISyncPreviewResult {
readonly fileContent: IFileContent | null;
readonly remoteUserData: IUserData;
readonly remoteUserData: IUserData | null;
readonly hasLocalChanged: boolean;
readonly hasRemoteChanged: boolean;
readonly hasConflicts: boolean;
}
export class KeybindingsSynchroniser extends Disposable implements ISynchroniser {
export class KeybindingsSynchroniser extends AbstractSynchroniser implements ISynchroniser {
private static EXTERNAL_USER_DATA_KEYBINDINGS_KEY: string = 'keybindings';
@@ -56,11 +57,11 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IFileService private readonly fileService: IFileService,
@IFileService fileService: IFileService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService,
) {
super();
super(SyncSource.Keybindings, fileService, environmentService);
this.lastSyncKeybindingsResource = joinPath(this.environmentService.userRoamingDataHome, '.lastSyncKeybindings.json');
this._register(this.fileService.watch(dirname(this.environmentService.keybindingsResource)));
this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.keybindingsResource))(() => this._onDidChangeLocal.fire()));
@@ -73,6 +74,84 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser
}
}
async pull(): Promise<void> {
if (!this.configurationService.getValue<boolean>('sync.enableKeybindings')) {
this.logService.info('Keybindings: Skipped pulling keybindings as it is disabled.');
return;
}
this.stop();
try {
this.logService.info('Keybindings: Started pulling keybindings...');
this.setStatus(SyncStatus.Syncing);
const remoteUserData = await this.getRemoteUserData();
const remoteContent = remoteUserData.content !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.content) : null;
if (remoteContent !== null) {
await this.fileService.writeFile(this.environmentService.keybindingsSyncPreviewResource, VSBuffer.fromString(remoteContent));
const fileContent = await this.getLocalFileContent();
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
fileContent,
hasConflicts: false,
hasLocalChanged: true,
hasRemoteChanged: false,
remoteUserData
}));
await this.apply();
}
// No remote exists to pull
else {
this.logService.info('Keybindings: Remote keybindings does not exist.');
}
this.logService.info('Keybindings: Finished pulling keybindings.');
} finally {
this.setStatus(SyncStatus.Idle);
}
}
async push(): Promise<void> {
if (!this.configurationService.getValue<boolean>('sync.enableKeybindings')) {
this.logService.info('Keybindings: Skipped pushing keybindings as it is disabled.');
return;
}
this.stop();
try {
this.logService.info('Keybindings: Started pushing keybindings...');
this.setStatus(SyncStatus.Syncing);
const fileContent = await this.getLocalFileContent();
if (fileContent !== null) {
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, fileContent.value);
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
fileContent,
hasConflicts: false,
hasLocalChanged: false,
hasRemoteChanged: true,
remoteUserData: null
}));
await this.apply();
}
// No local exists to push
else {
this.logService.info('Keybindings: Local keybindings does not exist.');
}
this.logService.info('Keybindings: Finished pushing keybindings.');
} finally {
this.setStatus(SyncStatus.Idle);
}
}
async sync(_continue?: boolean): Promise<boolean> {
if (!this.configurationService.getValue<boolean>('sync.enableKeybindings')) {
this.logService.trace('Keybindings: Skipping synchronizing keybindings as it is disabled.');
@@ -99,8 +178,13 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser
this.setStatus(SyncStatus.HasConflicts);
return false;
}
await this.apply();
return true;
try {
await this.apply();
this.logService.trace('Keybindings: Finished synchronizing keybindings...');
return true;
} finally {
this.setStatus(SyncStatus.Idle);
}
} catch (e) {
this.syncPreviewResultPromise = null;
this.setStatus(SyncStatus.Idle);
@@ -128,11 +212,45 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser
this.setStatus(SyncStatus.Idle);
}
async hasPreviouslySynced(): Promise<boolean> {
const lastSyncData = await this.getLastSyncUserData();
return !!lastSyncData;
}
async hasRemoteData(): Promise<boolean> {
const remoteUserData = await this.getRemoteUserData();
return remoteUserData.content !== null;
}
async hasLocalData(): Promise<boolean> {
try {
const localFileContent = await this.getLocalFileContent();
if (localFileContent) {
const keybindings = parse(localFileContent.value.toString());
if (isNonEmptyArray(keybindings)) {
return true;
}
}
} catch (error) {
if ((<FileOperationError>error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) {
return true;
}
}
return false;
}
async resetLocal(): Promise<void> {
try {
await this.fileService.del(this.lastSyncKeybindingsResource);
} catch (e) { /* ignore */ }
}
private async continueSync(): Promise<boolean> {
if (this.status !== SyncStatus.HasConflicts) {
return false;
}
await this.apply();
this.setStatus(SyncStatus.Idle);
return true;
}
@@ -160,11 +278,12 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser
}
if (hasRemoteChanged) {
this.logService.info('Keybindings: Updating remote keybindings');
const remoteContents = this.updateSyncContent(content, remoteUserData.content);
const ref = await this.updateRemoteUserData(remoteContents, remoteUserData.ref);
let remoteContents = remoteUserData ? remoteUserData.content : (await this.getRemoteUserData()).content;
remoteContents = this.updateSyncContent(content, remoteContents);
const ref = await this.updateRemoteUserData(remoteContents, remoteUserData ? remoteUserData.ref : null);
remoteUserData = { ref, content: remoteContents };
}
if (remoteUserData.content) {
if (remoteUserData?.content) {
this.logService.info('Keybindings: Updating last synchronised keybindings');
const lastSyncContent = this.updateSyncContent(content, null);
await this.updateLastSyncUserData({ ref: remoteUserData.ref, content: lastSyncContent });
@@ -176,9 +295,7 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser
this.logService.trace('Keybindings: No changes found during synchronizing keybindings.');
}
this.logService.trace('Keybindings: Finised synchronizing keybindings.');
this.syncPreviewResultPromise = null;
this.setStatus(SyncStatus.Idle);
}
private hasErrors(content: string): boolean {
@@ -200,7 +317,7 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser
const remoteUserData = await this.getRemoteUserData(lastSyncData);
const remoteContent = remoteUserData.content ? this.getKeybindingsContentFromSyncContent(remoteUserData.content) : null;
// Get file content last to get the latest
const fileContent = await this.getLocalContent();
const fileContent = await this.getLocalFileContent();
let hasLocalChanged: boolean = false;
let hasRemoteChanged: boolean = false;
let hasConflicts: boolean = false;
@@ -252,7 +369,7 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser
return this._formattingOptions;
}
private async getLocalContent(): Promise<IFileContent | null> {
private async getLocalFileContent(): Promise<IFileContent | null> {
try {
return await this.fileService.readFile(this.environmentService.keybindingsResource);
} catch (error) {
@@ -263,6 +380,7 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser
private async updateLocalContent(newContent: string, oldContent: IFileContent | null): Promise<void> {
if (oldContent) {
// file exists already
await this.backupLocal(oldContent.value);
await this.fileService.writeFile(this.environmentService.keybindingsResource, VSBuffer.fromString(newContent), oldContent);
} else {
// file does not exist
@@ -283,8 +401,8 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser
await this.fileService.writeFile(this.lastSyncKeybindingsResource, VSBuffer.fromString(JSON.stringify(remoteUserData)));
}
private async getRemoteUserData(lastSyncData: IUserData | null): Promise<IUserData> {
return this.userDataSyncStoreService.read(KeybindingsSynchroniser.EXTERNAL_USER_DATA_KEYBINDINGS_KEY, lastSyncData);
private async getRemoteUserData(lastSyncData?: IUserData | null): Promise<IUserData> {
return this.userDataSyncStoreService.read(KeybindingsSynchroniser.EXTERNAL_USER_DATA_KEYBINDINGS_KEY, lastSyncData || null);
}
private async updateRemoteUserData(content: string, ref: string | null): Promise<string> {

View File

@@ -10,21 +10,19 @@ import { values } from 'vs/base/common/map';
import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import * as contentUtil from 'vs/platform/userDataSync/common/content';
import { IConflictSetting } from 'vs/platform/userDataSync/common/userDataSync';
export function computeRemoteContent(localContent: string, remoteContent: string, ignoredSettings: string[], formattingOptions: FormattingOptions): string {
export function updateIgnoredSettings(targetContent: string, sourceContent: string, ignoredSettings: string[], formattingOptions: FormattingOptions): string {
if (ignoredSettings.length) {
const remote = parse(remoteContent);
const ignored = ignoredSettings.reduce((set, key) => { set.add(key); return set; }, new Set<string>());
const source = parse(sourceContent);
for (const key of ignoredSettings) {
if (ignored.has(key)) {
localContent = contentUtil.edit(localContent, [key], remote[key], formattingOptions);
}
targetContent = contentUtil.edit(targetContent, [key], source[key], formattingOptions);
}
}
return localContent;
return targetContent;
}
export function merge(localContent: string, remoteContent: string, baseContent: string | null, ignoredSettings: string[], formattingOptions: FormattingOptions): { mergeContent: string, hasChanges: boolean, hasConflicts: boolean } {
export function merge(localContent: string, remoteContent: string, baseContent: string | null, ignoredSettings: string[], resolvedConflicts: { key: string, value: any | undefined }[], formattingOptions: FormattingOptions): { mergeContent: string, hasChanges: boolean, conflicts: IConflictSetting[] } {
const local = parse(localContent);
const remote = parse(remoteContent);
const base = baseContent ? parse(baseContent) : null;
@@ -33,30 +31,41 @@ export function merge(localContent: string, remoteContent: string, baseContent:
const localToRemote = compare(local, remote, ignored);
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
// No changes found between local and remote.
return { mergeContent: localContent, hasChanges: false, hasConflicts: false };
return { mergeContent: localContent, hasChanges: false, conflicts: [] };
}
const conflicts: Set<string> = new Set<string>();
const conflicts: Map<string, IConflictSetting> = new Map<string, IConflictSetting>();
const handledConflicts: Set<string> = new Set<string>();
const baseToLocal = base ? compare(base, local, ignored) : { added: Object.keys(local).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const baseToRemote = base ? compare(base, remote, ignored) : { added: Object.keys(remote).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
let mergeContent = localContent;
const handleConflict = (conflictKey: string): void => {
handledConflicts.add(conflictKey);
const resolvedConflict = resolvedConflicts.filter(({ key }) => key === conflictKey)[0];
if (resolvedConflict) {
mergeContent = contentUtil.edit(mergeContent, [conflictKey], resolvedConflict.value, formattingOptions);
} else {
conflicts.set(conflictKey, { key: conflictKey, localValue: local[conflictKey], remoteValue: remote[conflictKey] });
}
};
// Removed settings in Local
for (const key of values(baseToLocal.removed)) {
// Got updated in remote
if (baseToRemote.updated.has(key)) {
conflicts.add(key);
handleConflict(key);
}
}
// Removed settings in Remote
for (const key of values(baseToRemote.removed)) {
if (conflicts.has(key)) {
if (handledConflicts.has(key)) {
continue;
}
// Got updated in local
if (baseToLocal.updated.has(key)) {
conflicts.add(key);
handleConflict(key);
} else {
mergeContent = contentUtil.edit(mergeContent, [key], undefined, formattingOptions);
}
@@ -64,28 +73,28 @@ export function merge(localContent: string, remoteContent: string, baseContent:
// Added settings in Local
for (const key of values(baseToLocal.added)) {
if (conflicts.has(key)) {
if (handledConflicts.has(key)) {
continue;
}
// Got added in remote
if (baseToRemote.added.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
conflicts.add(key);
handleConflict(key);
}
}
}
// Added settings in remote
for (const key of values(baseToRemote.added)) {
if (conflicts.has(key)) {
if (handledConflicts.has(key)) {
continue;
}
// Got added in local
if (baseToLocal.added.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
conflicts.add(key);
handleConflict(key);
}
} else {
mergeContent = contentUtil.edit(mergeContent, [key], remote[key], formattingOptions);
@@ -94,28 +103,28 @@ export function merge(localContent: string, remoteContent: string, baseContent:
// Updated settings in Local
for (const key of values(baseToLocal.updated)) {
if (conflicts.has(key)) {
if (handledConflicts.has(key)) {
continue;
}
// Got updated in remote
if (baseToRemote.updated.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
conflicts.add(key);
handleConflict(key);
}
}
}
// Updated settings in Remote
for (const key of values(baseToRemote.updated)) {
if (conflicts.has(key)) {
if (handledConflicts.has(key)) {
continue;
}
// Got updated in local
if (baseToLocal.updated.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
conflicts.add(key);
handleConflict(key);
}
} else {
mergeContent = contentUtil.edit(mergeContent, [key], remote[key], formattingOptions);
@@ -126,7 +135,7 @@ export function merge(localContent: string, remoteContent: string, baseContent:
const conflictNodes: { key: string, node: Node | undefined }[] = [];
const tree = parseTree(mergeContent);
const eol = formattingOptions.eol!;
for (const key of values(conflicts)) {
for (const { key } of values(conflicts)) {
const node = findNodeAtLocation(tree, [key]);
conflictNodes.push({ key, node });
}
@@ -166,7 +175,7 @@ export function merge(localContent: string, remoteContent: string, baseContent:
}
}
return { mergeContent, hasChanges: true, hasConflicts: conflicts.size > 0 };
return { mergeContent, hasChanges: true, conflicts: values(conflicts) };
}
function compare(from: IStringDictionary<any>, to: IStringDictionary<any>, ignored: Set<string>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {

View File

@@ -3,9 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent } from 'vs/platform/files/common/files';
import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, DEFAULT_IGNORED_SETTINGS, IUserDataSyncLogService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync';
import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, SyncStatus, IUserDataSyncStoreService, DEFAULT_IGNORED_SETTINGS, IUserDataSyncLogService, IUserDataSyncUtilService, IConflictSetting, ISettingsSyncService, CONFIGURATION_SYNC_STORE_KEY, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
import { VSBuffer } from 'vs/base/common/buffer';
import { parse, ParseError } from 'vs/base/common/json';
import { localize } from 'vs/nls';
@@ -17,18 +16,25 @@ import { joinPath, dirname } from 'vs/base/common/resources';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { startsWith } from 'vs/base/common/strings';
import { CancellationToken } from 'vs/base/common/cancellation';
import { computeRemoteContent, merge } from 'vs/platform/userDataSync/common/settingsMerge';
import { updateIgnoredSettings, merge } from 'vs/platform/userDataSync/common/settingsMerge';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import * as arrays from 'vs/base/common/arrays';
import * as objects from 'vs/base/common/objects';
import { isEmptyObject } from 'vs/base/common/types';
import { edit } from 'vs/platform/userDataSync/common/content';
import { AbstractSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
interface ISyncPreviewResult {
readonly fileContent: IFileContent | null;
readonly remoteUserData: IUserData;
readonly remoteUserData: IUserData | null;
readonly hasLocalChanged: boolean;
readonly hasRemoteChanged: boolean;
readonly hasConflicts: boolean;
readonly conflicts: IConflictSetting[];
}
export class SettingsSynchroniser extends Disposable implements ISynchroniser {
export class SettingsSynchroniser extends AbstractSynchroniser implements ISettingsSyncService {
_serviceBrand: any;
private static EXTERNAL_USER_DATA_SETTINGS_KEY: string = 'settings';
@@ -39,20 +45,25 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;
private _conflicts: IConflictSetting[] = [];
get conflicts(): IConflictSetting[] { return this._conflicts; }
private _onDidChangeConflicts: Emitter<IConflictSetting[]> = this._register(new Emitter<IConflictSetting[]>());
readonly onDidChangeConflicts: Event<IConflictSetting[]> = this._onDidChangeConflicts.event;
private _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;
private readonly lastSyncSettingsResource: URI;
constructor(
@IFileService private readonly fileService: IFileService,
@IFileService fileService: IFileService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
super();
super(SyncSource.Settings, fileService, environmentService);
this.lastSyncSettingsResource = joinPath(this.environmentService.userRoamingDataHome, '.lastSyncSettings.json');
this._register(this.fileService.watch(dirname(this.environmentService.settingsResource)));
this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.settingsResource))(() => this._onDidChangeLocal.fire()));
@@ -63,6 +74,103 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
this._status = status;
this._onDidChangStatus.fire(status);
}
if (this._status !== SyncStatus.HasConflicts) {
this.setConflicts([]);
}
}
private setConflicts(conflicts: IConflictSetting[]): void {
if (!arrays.equals(this.conflicts, conflicts,
(a, b) => a.key === b.key && objects.equals(a.localValue, b.localValue) && objects.equals(a.remoteValue, b.remoteValue))
) {
this._conflicts = conflicts;
this._onDidChangeConflicts.fire(conflicts);
}
}
async pull(): Promise<void> {
if (!this.configurationService.getValue<boolean>('sync.enableSettings')) {
this.logService.info('Settings: Skipped pulling settings as it is disabled.');
return;
}
this.stop();
try {
this.logService.info('Settings: Started pulling settings...');
this.setStatus(SyncStatus.Syncing);
const remoteUserData = await this.getRemoteUserData();
if (remoteUserData.content !== null) {
const fileContent = await this.getLocalFileContent();
const formatUtils = await this.getFormattingOptions();
// Update ignored settings
const content = updateIgnoredSettings(remoteUserData.content, fileContent ? fileContent.value.toString() : '{}', getIgnoredSettings(this.configurationService), formatUtils);
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(content));
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
conflicts: [],
fileContent,
hasLocalChanged: true,
hasRemoteChanged: false,
remoteUserData
}));
await this.apply();
}
// No remote exists to pull
else {
this.logService.info('Settings: Remote settings does not exist.');
}
this.logService.info('Settings: Finished pulling settings.');
} finally {
this.setStatus(SyncStatus.Idle);
}
}
async push(): Promise<void> {
if (!this.configurationService.getValue<boolean>('sync.enableSettings')) {
this.logService.info('Settings: Skipped pushing settings as it is disabled.');
return;
}
this.stop();
try {
this.logService.info('Settings: Started pushing settings...');
this.setStatus(SyncStatus.Syncing);
const fileContent = await this.getLocalFileContent();
if (fileContent !== null) {
const formatUtils = await this.getFormattingOptions();
// Remove ignored settings
const content = updateIgnoredSettings(fileContent.value.toString(), '{}', getIgnoredSettings(this.configurationService), formatUtils);
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(content));
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
conflicts: [],
fileContent,
hasLocalChanged: false,
hasRemoteChanged: true,
remoteUserData: null
}));
await this.apply();
}
// No local exists to push
else {
this.logService.info('Settings: Local settings does not exist.');
}
this.logService.info('Settings: Finished pushing settings.');
} finally {
this.setStatus(SyncStatus.Idle);
}
}
async sync(_continue?: boolean): Promise<boolean> {
@@ -83,16 +191,77 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
this.logService.trace('Settings: Started synchronizing settings...');
this.setStatus(SyncStatus.Syncing);
return this.doSync([]);
}
stop(): void {
if (this.syncPreviewResultPromise) {
this.syncPreviewResultPromise.cancel();
this.syncPreviewResultPromise = null;
this.logService.info('Settings: Stopped synchronizing settings.');
}
this.fileService.del(this.environmentService.settingsSyncPreviewResource);
this.setStatus(SyncStatus.Idle);
}
async hasPreviouslySynced(): Promise<boolean> {
const lastSyncData = await this.getLastSyncUserData();
return !!lastSyncData;
}
async hasRemoteData(): Promise<boolean> {
const remoteUserData = await this.getRemoteUserData();
return remoteUserData.content !== null;
}
async hasLocalData(): Promise<boolean> {
try {
const result = await this.getPreview();
if (result.hasConflicts) {
const localFileContent = await this.getLocalFileContent();
if (localFileContent) {
const formatUtils = await this.getFormattingOptions();
const content = edit(localFileContent.value.toString(), [CONFIGURATION_SYNC_STORE_KEY], undefined, formatUtils);
const settings = parse(content);
if (!isEmptyObject(settings)) {
return true;
}
}
} catch (error) {
if ((<FileOperationError>error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) {
return true;
}
}
return false;
}
async resolveConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<void> {
if (this.status === SyncStatus.HasConflicts) {
this.syncPreviewResultPromise!.cancel();
this.syncPreviewResultPromise = null;
await this.doSync(resolvedConflicts);
}
}
async resetLocal(): Promise<void> {
try {
await this.fileService.del(this.lastSyncSettingsResource);
} catch (e) { /* ignore */ }
}
private async doSync(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<boolean> {
try {
const result = await this.getPreview(resolvedConflicts);
if (result.conflicts.length) {
this.logService.info('Settings: Detected conflicts while synchronizing settings.');
this.setStatus(SyncStatus.HasConflicts);
return false;
}
await this.apply();
return true;
try {
await this.apply();
this.logService.trace('Settings: Finished synchronizing settings.');
return true;
} finally {
this.setStatus(SyncStatus.Idle);
}
} catch (e) {
this.syncPreviewResultPromise = null;
this.setStatus(SyncStatus.Idle);
@@ -110,19 +279,10 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
}
}
stop(): void {
if (this.syncPreviewResultPromise) {
this.syncPreviewResultPromise.cancel();
this.syncPreviewResultPromise = null;
this.logService.info('Settings: Stopped synchronizing settings.');
}
this.fileService.del(this.environmentService.settingsSyncPreviewResource);
this.setStatus(SyncStatus.Idle);
}
private async continueSync(): Promise<boolean> {
if (this.status === SyncStatus.HasConflicts) {
await this.apply();
this.setStatus(SyncStatus.Idle);
}
return true;
}
@@ -151,12 +311,12 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
}
if (hasRemoteChanged) {
const formatUtils = await this.getFormattingOptions();
const remoteContent = remoteUserData.content ? computeRemoteContent(content, remoteUserData.content, this.getIgnoredSettings(content), formatUtils) : content;
const remoteContent = remoteUserData?.content ? updateIgnoredSettings(content, remoteUserData.content, getIgnoredSettings(this.configurationService, content), formatUtils) : content;
this.logService.info('Settings: Updating remote settings');
const ref = await this.writeToRemote(remoteContent, remoteUserData.ref);
const ref = await this.writeToRemote(remoteContent, remoteUserData ? remoteUserData.ref : null);
remoteUserData = { ref, content };
}
if (remoteUserData.content) {
if (remoteUserData?.content) {
this.logService.info('Settings: Updating last synchronised sttings');
await this.updateLastSyncValue(remoteUserData);
}
@@ -167,9 +327,7 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
this.logService.trace('Settings: No changes found during synchronizing settings.');
}
this.logService.trace('Settings: Finised synchronizing settings.');
this.syncPreviewResultPromise = null;
this.setStatus(SyncStatus.Idle);
}
private hasErrors(content: string): boolean {
@@ -178,43 +336,44 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
return parseErrors.length > 0;
}
private getPreview(): Promise<ISyncPreviewResult> {
private getPreview(resolvedConflicts: { key: string, value: any }[]): Promise<ISyncPreviewResult> {
if (!this.syncPreviewResultPromise) {
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(token));
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(resolvedConflicts, token));
}
return this.syncPreviewResultPromise;
}
private async generatePreview(token: CancellationToken): Promise<ISyncPreviewResult> {
private async generatePreview(resolvedConflicts: { key: string, value: any }[], token: CancellationToken): Promise<ISyncPreviewResult> {
const lastSyncData = await this.getLastSyncUserData();
const remoteUserData = await this.userDataSyncStoreService.read(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY, lastSyncData);
const remoteUserData = await this.getRemoteUserData(lastSyncData);
const remoteContent: string | null = remoteUserData.content;
// Get file content last to get the latest
const fileContent = await this.getLocalFileContent();
let hasLocalChanged: boolean = false;
let hasRemoteChanged: boolean = false;
let hasConflicts: boolean = false;
let conflicts: IConflictSetting[] = [];
let previewContent = null;
if (remoteContent) {
const localContent: string = fileContent ? fileContent.value.toString() : '{}';
// No action when there are errors
if (this.hasErrors(localContent)) {
this.logService.error('Settings: Unable to sync settings as there are errors/warning in settings file.');
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
}
if (!lastSyncData // First time sync
else if (!lastSyncData // First time sync
|| lastSyncData.content !== localContent // Local has forwarded
|| lastSyncData.content !== remoteContent // Remote has forwarded
) {
this.logService.trace('Settings: Merging remote settings with local settings...');
const formatUtils = await this.getFormattingOptions();
const result = merge(localContent, remoteContent, lastSyncData ? lastSyncData.content : null, this.getIgnoredSettings(), formatUtils);
const result = merge(localContent, remoteContent, lastSyncData ? lastSyncData.content : null, getIgnoredSettings(this.configurationService), resolvedConflicts, formatUtils);
// Sync only if there are changes
if (result.hasChanges) {
hasLocalChanged = result.mergeContent !== localContent;
hasRemoteChanged = result.mergeContent !== remoteContent;
hasConflicts = result.hasConflicts;
conflicts = result.conflicts;
previewContent = result.mergeContent;
}
}
@@ -231,7 +390,8 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(previewContent));
}
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
this.setConflicts(conflicts);
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, conflicts };
}
private _formattingOptions: Promise<FormattingOptions> | undefined = undefined;
@@ -242,29 +402,6 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
return this._formattingOptions;
}
private getIgnoredSettings(settingsContent?: string): string[] {
let value: string[] = [];
if (settingsContent) {
const setting = parse(settingsContent);
if (setting) {
value = setting['sync.ignoredSettings'];
}
} else {
value = this.configurationService.getValue<string[]>('sync.ignoredSettings');
}
const added: string[] = [], removed: string[] = [];
if (Array.isArray(value)) {
for (const key of value) {
if (startsWith(key, '-')) {
removed.push(key.substring(1));
} else {
added.push(key);
}
}
}
return [...DEFAULT_IGNORED_SETTINGS, ...added].filter(setting => removed.indexOf(setting) === -1);
}
private async getLastSyncUserData(): Promise<IUserData | null> {
try {
const content = await this.fileService.readFile(this.lastSyncSettingsResource);
@@ -282,6 +419,10 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
}
}
private getRemoteUserData(lastSyncData?: IUserData | null): Promise<IUserData> {
return this.userDataSyncStoreService.read(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY, lastSyncData || null);
}
private async writeToRemote(content: string, ref: string | null): Promise<string> {
return this.userDataSyncStoreService.write(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY, content, ref);
}
@@ -289,6 +430,7 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
private async writeToLocal(newContent: string, oldContent: IFileContent | null): Promise<void> {
if (oldContent) {
// file exists already
await this.backupLocal(oldContent.value);
await this.fileService.writeFile(this.environmentService.settingsResource, VSBuffer.fromString(newContent), oldContent);
} else {
// file does not exist
@@ -301,3 +443,26 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
}
}
export function getIgnoredSettings(configurationService: IConfigurationService, settingsContent?: string): string[] {
let value: string[] = [];
if (settingsContent) {
const setting = parse(settingsContent);
if (setting) {
value = setting['sync.ignoredSettings'];
}
} else {
value = configurationService.getValue<string[]>('sync.ignoredSettings');
}
const added: string[] = [], removed: string[] = [];
if (Array.isArray(value)) {
for (const key of value) {
if (startsWith(key, '-')) {
removed.push(key.substring(1));
} else {
added.push(key);
}
}
}
return [...DEFAULT_IGNORED_SETTINGS, ...added].filter(setting => removed.indexOf(setting) === -1);
}

View File

@@ -0,0 +1,33 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync';
export class UserDataAuthTokenService extends Disposable implements IUserDataAuthTokenService {
_serviceBrand: any;
private _onDidChangeToken: Emitter<string | undefined> = this._register(new Emitter<string | undefined>());
readonly onDidChangeToken: Event<string | undefined> = this._onDidChangeToken.event;
private _token: string | undefined;
constructor() {
super();
}
async getToken(): Promise<string | undefined> {
return this._token;
}
async setToken(token: string | undefined): Promise<void> {
if (token !== this._token) {
this._token = token;
this._onDidChangeToken.fire(token);
}
}
}

View File

@@ -3,14 +3,15 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUserDataSyncService, SyncStatus, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync';
import { Disposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { timeout } from 'vs/base/common/async';
import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth';
import { Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IUserDataSyncLogService, IUserDataSyncService, SyncStatus, IUserDataAuthTokenService, IUserDataAutoSyncService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync';
export class UserDataAutoSync extends Disposable {
export class UserDataAutoSync extends Disposable implements IUserDataAutoSyncService {
_serviceBrand: any;
private enabled: boolean = false;
@@ -18,16 +19,18 @@ export class UserDataAutoSync extends Disposable {
@IConfigurationService private readonly configurationService: IConfigurationService,
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IAuthTokenService private readonly authTokenService: IAuthTokenService,
@IUserDataAuthTokenService private readonly userDataAuthTokenService: IUserDataAuthTokenService,
@IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService,
) {
super();
this.updateEnablement(false);
this._register(Event.any<any>(authTokenService.onDidChangeStatus, userDataSyncService.onDidChangeStatus)(() => this.updateEnablement(true)));
this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('sync.enable'))(() => this.updateEnablement(true)));
this.updateEnablement(false, true);
this._register(Event.any<any>(userDataAuthTokenService.onDidChangeToken)(() => this.updateEnablement(true, true)));
this._register(Event.any<any>(userDataSyncService.onDidChangeStatus)(() => this.updateEnablement(true, true)));
this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('sync.enable'))(() => this.updateEnablement(true, false)));
}
private updateEnablement(stopIfDisabled: boolean): void {
const enabled = this.isSyncEnabled();
private async updateEnablement(stopIfDisabled: boolean, auto: boolean): Promise<void> {
const enabled = await this.isSyncEnabled();
if (this.enabled === enabled) {
return;
}
@@ -35,7 +38,7 @@ export class UserDataAutoSync extends Disposable {
this.enabled = enabled;
if (this.enabled) {
this.logService.info('Syncing configuration started');
this.sync(true);
this.sync(true, auto);
return;
} else {
if (stopIfDisabled) {
@@ -46,24 +49,42 @@ export class UserDataAutoSync extends Disposable {
}
protected async sync(loop: boolean): Promise<void> {
private async sync(loop: boolean, auto: boolean): Promise<void> {
if (this.enabled) {
try {
if (auto) {
if (await this.isTurnedOffEverywhere()) {
// Turned off everywhere. Reset & Stop Sync.
await this.userDataSyncService.resetLocal();
await this.userDataSyncUtilService.updateConfigurationValue('sync.enable', false);
return;
}
}
await this.userDataSyncService.sync();
} catch (e) {
this.logService.error(e);
}
if (loop) {
await timeout(1000 * 60 * 5); // Loop sync for every 5 min.
this.sync(loop);
this.sync(loop, true);
}
}
}
private isSyncEnabled(): boolean {
private async isTurnedOffEverywhere(): Promise<boolean> {
const hasRemote = await this.userDataSyncService.hasRemoteData();
const hasPreviouslySynced = await this.userDataSyncService.hasPreviouslySynced();
return !hasRemote && hasPreviouslySynced;
}
private async isSyncEnabled(): Promise<boolean> {
return this.configurationService.getValue<boolean>('sync.enable')
&& this.userDataSyncService.status !== SyncStatus.Uninitialized
&& this.authTokenService.status === AuthTokenStatus.SignedIn;
&& !!(await this.userDataAuthTokenService.getToken());
}
triggerAutoSync(): Promise<void> {
return this.sync(false, true);
}
}

View File

@@ -19,7 +19,7 @@ import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import { URI } from 'vs/base/common/uri';
const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store';
export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store';
export const DEFAULT_IGNORED_SETTINGS = [
CONFIGURATION_SYNC_STORE_KEY,
@@ -28,6 +28,19 @@ export const DEFAULT_IGNORED_SETTINGS = [
'sync.enableExtensions',
];
export interface ISyncConfiguration {
sync: {
enable: boolean,
enableSettings: boolean,
enableKeybindings: boolean,
enableUIState: boolean,
enableExtensions: boolean,
keybindingsPerPlatform: boolean,
ignoredExtensions: string[],
ignoredSettings: string[]
}
}
export function registerConfiguration(): IDisposable {
const ignoredSettingsSchemaId = 'vscode://schemas/ignoredSettings';
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
@@ -49,18 +62,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 (Only Display Language)."),
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."),
@@ -121,22 +140,21 @@ export interface IUserDataSyncStore {
url: string;
name: string;
account: string;
authenticationProviderId: string;
}
export function getUserDataSyncStore(configurationService: IConfigurationService): IUserDataSyncStore | undefined {
const value = configurationService.getValue<IUserDataSyncStore>(CONFIGURATION_SYNC_STORE_KEY);
return value && value.url && value.name && value.account ? value : undefined;
return value && value.url && value.name && value.account && value.authenticationProviderId ? value : undefined;
}
export const IUserDataSyncStoreService = createDecorator<IUserDataSyncStoreService>('IUserDataSyncStoreService');
export interface IUserDataSyncStoreService {
_serviceBrand: undefined;
readonly userDataSyncStore: IUserDataSyncStore | undefined;
read(key: string, oldValue: IUserData | null): Promise<IUserData>;
write(key: string, content: string, ref: string | null): Promise<string>;
clear(): Promise<void>;
}
export interface ISyncExtension {
@@ -145,10 +163,16 @@ export interface ISyncExtension {
enabled: boolean;
}
export interface IGlobalState {
argv: IStringDictionary<any>;
storage: IStringDictionary<any>;
}
export const enum SyncSource {
Settings = 1,
Keybindings,
Extensions
Settings = 'Settings',
Keybindings = 'Keybindings',
Extensions = 'Extensions',
UIState = 'UI State'
}
export const enum SyncStatus {
@@ -159,40 +183,70 @@ export const enum SyncStatus {
}
export interface ISynchroniser {
readonly status: SyncStatus;
readonly onDidChangeStatus: Event<SyncStatus>;
readonly onDidChangeLocal: Event<void>;
pull(): Promise<void>;
push(): Promise<void>;
sync(_continue?: boolean): Promise<boolean>;
stop(): void;
hasPreviouslySynced(): Promise<boolean>
hasRemoteData(): Promise<boolean>;
hasLocalData(): Promise<boolean>;
resetLocal(): Promise<void>;
}
export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUserDataSyncService');
export interface IUserDataSyncService extends ISynchroniser {
_serviceBrand: any;
readonly conflictsSource: SyncSource | null;
isFirstTimeSyncAndHasUserData(): Promise<boolean>;
reset(): Promise<void>;
resetLocal(): Promise<void>;
removeExtension(identifier: IExtensionIdentifier): Promise<void>;
}
export const IUserDataAutoSyncService = createDecorator<IUserDataAutoSyncService>('IUserDataAutoSyncService');
export interface IUserDataAutoSyncService {
_serviceBrand: any;
triggerAutoSync(): Promise<void>;
}
export const IUserDataSyncUtilService = createDecorator<IUserDataSyncUtilService>('IUserDataSyncUtilService');
export interface IUserDataSyncUtilService {
_serviceBrand: undefined;
updateConfigurationValue(key: string, value: any): Promise<void>;
resolveUserBindings(userbindings: string[]): Promise<IStringDictionary<string>>;
resolveFormattingOptions(resource: URI): Promise<FormattingOptions>;
ignoreExtensionsToSync(extensionIdentifiers: IExtensionIdentifier[]): Promise<void>;
}
export const IUserDataAuthTokenService = createDecorator<IUserDataAuthTokenService>('IUserDataAuthTokenService');
export interface IUserDataAuthTokenService {
_serviceBrand: undefined;
resolveUserBindings(userbindings: string[]): Promise<IStringDictionary<string>>;
resolveFormattingOptions(resource: URI): Promise<FormattingOptions>;
readonly onDidChangeToken: Event<string | undefined>;
getToken(): Promise<string | undefined>;
setToken(accessToken: string | undefined): Promise<void>;
}
export const IUserDataSyncLogService = createDecorator<IUserDataSyncLogService>('IUserDataSyncLogService');
export interface IUserDataSyncLogService extends ILogService { }
export interface IUserDataSyncLogService extends ILogService {
export interface IConflictSetting {
key: string;
localValue: any | undefined;
remoteValue: any | undefined;
}
export const ISettingsSyncService = createDecorator<ISettingsSyncService>('ISettingsSyncService');
export interface ISettingsSyncService extends ISynchroniser {
_serviceBrand: any;
readonly onDidChangeConflicts: Event<IConflictSetting[]>;
readonly conflicts: IConflictSetting[];
resolveConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<void>;
}
export const CONTEXT_SYNC_STATE = new RawContextKey<string>('syncStatus', SyncStatus.Uninitialized);

View File

@@ -5,10 +5,11 @@
import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event } from 'vs/base/common/event';
import { IUserDataSyncService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncService, IUserDataSyncUtilService, ISettingsSyncService, IUserDataAuthTokenService, IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync';
import { URI } from 'vs/base/common/uri';
import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import type { IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement';
export class UserDataSyncChannel implements IServerChannel {
@@ -25,10 +26,84 @@ export class UserDataSyncChannel implements IServerChannel {
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'sync': return this.service.sync(args[0]);
case 'pull': return this.service.pull();
case 'push': return this.service.push();
case '_getInitialStatus': return Promise.resolve(this.service.status);
case 'getConflictsSource': return Promise.resolve(this.service.conflictsSource);
case 'removeExtension': return this.service.removeExtension(args[0]);
case 'stop': this.service.stop(); return Promise.resolve();
case 'reset': return this.service.reset();
case 'resetLocal': return this.service.resetLocal();
case 'hasPreviouslySynced': return this.service.hasPreviouslySynced();
case 'hasRemoteData': return this.service.hasRemoteData();
case 'hasLocalData': return this.service.hasLocalData();
case 'isFirstTimeSyncAndHasUserData': return this.service.isFirstTimeSyncAndHasUserData();
}
throw new Error('Invalid call');
}
}
export class SettingsSyncChannel implements IServerChannel {
constructor(private readonly service: ISettingsSyncService) { }
listen(_: unknown, event: string): Event<any> {
switch (event) {
case 'onDidChangeStatus': return this.service.onDidChangeStatus;
case 'onDidChangeLocal': return this.service.onDidChangeLocal;
case 'onDidChangeConflicts': return this.service.onDidChangeConflicts;
}
throw new Error(`Event not found: ${event}`);
}
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'sync': return this.service.sync(args[0]);
case 'pull': return this.service.pull();
case 'push': return this.service.push();
case '_getInitialStatus': return Promise.resolve(this.service.status);
case '_getInitialConflicts': return Promise.resolve(this.service.conflicts);
case 'stop': this.service.stop(); return Promise.resolve();
case 'resetLocal': return this.service.resetLocal();
case 'hasPreviouslySynced': return this.service.hasPreviouslySynced();
case 'hasRemoteData': return this.service.hasRemoteData();
case 'hasLocalData': return this.service.hasLocalData();
case 'resolveConflicts': return this.service.resolveConflicts(args[0]);
}
throw new Error('Invalid call');
}
}
export class UserDataAutoSyncChannel implements IServerChannel {
constructor(private readonly service: IUserDataAutoSyncService) { }
listen(_: unknown, event: string): Event<any> {
throw new Error(`Event not found: ${event}`);
}
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'triggerAutoSync': return this.service.triggerAutoSync();
}
throw new Error('Invalid call');
}
}
export class UserDataAuthTokenServiceChannel implements IServerChannel {
constructor(private readonly service: IUserDataAuthTokenService) { }
listen(_: unknown, event: string): Event<any> {
switch (event) {
case 'onDidChangeToken': return this.service.onDidChangeToken;
}
throw new Error(`Event not found: ${event}`);
}
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'setToken': return this.service.setToken(args);
case 'getToken': return this.service.getToken();
}
throw new Error('Invalid call');
}
@@ -46,6 +121,8 @@ export class UserDataSycnUtilServiceChannel implements IServerChannel {
switch (command) {
case 'resolveUserKeybindings': return this.service.resolveUserBindings(args[0]);
case 'resolveFormattingOptions': return this.service.resolveFormattingOptions(URI.revive(args[0]));
case 'updateConfigurationValue': return this.service.updateConfigurationValue(args[0], args[1]);
case 'ignoreExtensionsToSync': return this.service.ignoreExtensionsToSync(args[0]);
}
throw new Error('Invalid call');
}
@@ -66,5 +143,13 @@ export class UserDataSyncUtilServiceClient implements IUserDataSyncUtilService {
return this.channel.call('resolveFormattingOptions', [file]);
}
async updateConfigurationValue(key: string, value: any): Promise<void> {
return this.channel.call('updateConfigurationValue', [key, value]);
}
async ignoreExtensionsToSync(extensionIdentifiers: IExtensionIdentifier[]): Promise<void> {
return this.channel.call('ignoreExtensionsToSync', [extensionIdentifiers]);
}
}

View File

@@ -3,15 +3,16 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource, ISettingsSyncService, IUserDataSyncLogService, IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync';
import { Disposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync';
import { Emitter, Event } from 'vs/base/common/event';
import { ExtensionsSynchroniser } from 'vs/platform/userDataSync/common/extensionsSync';
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';
import { toErrorMessage } from 'vs/base/common/errorMessage';
export class UserDataSyncService extends Disposable implements IUserDataSyncService {
@@ -29,39 +30,78 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
private _conflictsSource: SyncSource | null = null;
get conflictsSource(): SyncSource | null { return this._conflictsSource; }
private readonly settingsSynchroniser: SettingsSynchroniser;
private readonly keybindingsSynchroniser: KeybindingsSynchroniser;
private readonly extensionsSynchroniser: ExtensionsSynchroniser;
private readonly globalStateSynchroniser: GlobalStateSynchroniser;
constructor(
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IAuthTokenService private readonly authTokenService: IAuthTokenService,
@ISettingsSyncService private readonly settingsSynchroniser: ISettingsSyncService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IUserDataAuthTokenService private readonly userDataAuthTokenService: IUserDataAuthTokenService,
) {
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) {
this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus()));
this._register(this.userDataAuthTokenService.onDidChangeToken(e => this.onDidChangeAuthTokenStatus(e)));
}
this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => s.onDidChangeLocal));
}
async pull(): Promise<void> {
if (!this.userDataSyncStoreService.userDataSyncStore) {
throw new Error('Not enabled');
}
if (!(await this.userDataAuthTokenService.getToken())) {
throw new Error('Not Authenticated. Please sign in to start sync.');
}
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.pull();
} catch (e) {
this.logService.error(`${this.getSyncSource(synchroniser)}: ${toErrorMessage(e)}`);
}
}
}
async push(): Promise<void> {
if (!this.userDataSyncStoreService.userDataSyncStore) {
throw new Error('Not enabled');
}
if (!(await this.userDataAuthTokenService.getToken())) {
throw new Error('Not Authenticated. Please sign in to start sync.');
}
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.push();
} catch (e) {
this.logService.error(`${this.getSyncSource(synchroniser)}: ${toErrorMessage(e)}`);
}
}
}
async sync(_continue?: boolean): Promise<boolean> {
if (!this.userDataSyncStoreService.userDataSyncStore) {
throw new Error('Not enabled');
}
if (this.authTokenService.status === AuthTokenStatus.SignedOut) {
if (!(await this.userDataAuthTokenService.getToken())) {
throw new Error('Not Authenticated. Please sign in to start sync.');
}
for (const synchroniser of this.synchronisers) {
if (!await synchroniser.sync(_continue)) {
return false;
try {
if (!await synchroniser.sync(_continue)) {
return false;
}
} catch (e) {
this.logService.error(`${this.getSyncSource(synchroniser)}: ${toErrorMessage(e)}`);
}
}
return true;
@@ -76,6 +116,101 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
}
async hasPreviouslySynced(): Promise<boolean> {
if (!this.userDataSyncStoreService.userDataSyncStore) {
throw new Error('Not enabled');
}
if (!(await this.userDataAuthTokenService.getToken())) {
throw new Error('Not Authenticated. Please sign in to start sync.');
}
for (const synchroniser of this.synchronisers) {
if (await synchroniser.hasPreviouslySynced()) {
return true;
}
}
return false;
}
async hasRemoteData(): Promise<boolean> {
if (!this.userDataSyncStoreService.userDataSyncStore) {
throw new Error('Not enabled');
}
if (!(await this.userDataAuthTokenService.getToken())) {
throw new Error('Not Authenticated. Please sign in to start sync.');
}
for (const synchroniser of this.synchronisers) {
if (await synchroniser.hasRemoteData()) {
return true;
}
}
return false;
}
async hasLocalData(): Promise<boolean> {
if (!this.userDataSyncStoreService.userDataSyncStore) {
throw new Error('Not enabled');
}
if (!(await this.userDataAuthTokenService.getToken())) {
throw new Error('Not Authenticated. Please sign in to start sync.');
}
for (const synchroniser of this.synchronisers) {
if (await synchroniser.hasLocalData()) {
return true;
}
}
return false;
}
async isFirstTimeSyncAndHasUserData(): Promise<boolean> {
if (!this.userDataSyncStoreService.userDataSyncStore) {
throw new Error('Not enabled');
}
if (!(await this.userDataAuthTokenService.getToken())) {
throw new Error('Not Authenticated. Please sign in to start sync.');
}
if (await this.hasPreviouslySynced()) {
return false;
}
return await this.hasLocalData();
}
async reset(): Promise<void> {
await this.resetRemote();
await this.resetLocal();
}
private async resetRemote(): Promise<void> {
if (!this.userDataSyncStoreService.userDataSyncStore) {
throw new Error('Not enabled');
}
if (!(await this.userDataAuthTokenService.getToken())) {
throw new Error('Not Authenticated. Please sign in to start sync.');
}
try {
await this.userDataSyncStoreService.clear();
this.logService.info('Completed clearing remote data');
} catch (e) {
this.logService.error(e);
}
}
async resetLocal(): Promise<void> {
if (!this.userDataSyncStoreService.userDataSyncStore) {
throw new Error('Not enabled');
}
if (!(await this.userDataAuthTokenService.getToken())) {
throw new Error('Not Authenticated. Please sign in to start sync.');
}
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.resetLocal();
} catch (e) {
this.logService.error(`${this.getSyncSource(synchroniser)}: ${toErrorMessage(e)}`);
}
}
this.logService.info('Completed resetting local cache');
}
removeExtension(identifier: IExtensionIdentifier): Promise<void> {
return this.extensionsSynchroniser.removeExtension(identifier);
}
@@ -106,15 +241,26 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
private computeConflictsSource(): SyncSource | null {
const source = this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)[0];
if (source) {
if (source instanceof SettingsSynchroniser) {
return SyncSource.Settings;
}
if (source instanceof KeybindingsSynchroniser) {
return SyncSource.Keybindings;
}
const synchroniser = this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)[0];
return synchroniser ? this.getSyncSource(synchroniser) : null;
}
private getSyncSource(synchroniser: ISynchroniser): SyncSource {
if (synchroniser instanceof SettingsSynchroniser) {
return SyncSource.Settings;
}
if (synchroniser instanceof KeybindingsSynchroniser) {
return SyncSource.Keybindings;
}
if (synchroniser instanceof ExtensionsSynchroniser) {
return SyncSource.Extensions;
}
return SyncSource.UIState;
}
private onDidChangeAuthTokenStatus(token: string | undefined): void {
if (!token) {
this.stop();
}
return null;
}
}

View File

@@ -4,13 +4,12 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable, } from 'vs/base/common/lifecycle';
import { IUserData, IUserDataSyncStoreService, UserDataSyncStoreErrorCode, UserDataSyncStoreError, IUserDataSyncStore, getUserDataSyncStore } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserData, IUserDataSyncStoreService, UserDataSyncStoreErrorCode, UserDataSyncStoreError, IUserDataSyncStore, getUserDataSyncStore, IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync';
import { IRequestService, asText, isSuccess } from 'vs/platform/request/common/request';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request';
import { IAuthTokenService } from 'vs/platform/auth/common/auth';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
export class UserDataSyncStoreService extends Disposable implements IUserDataSyncStoreService {
@@ -22,7 +21,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
constructor(
@IConfigurationService configurationService: IConfigurationService,
@IRequestService private readonly requestService: IRequestService,
@IAuthTokenService private readonly authTokenService: IAuthTokenService,
@IUserDataAuthTokenService private readonly authTokenService: IUserDataAuthTokenService,
) {
super();
this.userDataSyncStore = getUserDataSyncStore(configurationService);
@@ -35,6 +34,8 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource', key, 'latest').toString();
const headers: IHeaders = {};
// Disable caching as they are cached by synchronisers
headers['Cache-Control'] = 'no-cache';
if (oldValue) {
headers['If-None-Match'] = oldValue.ref;
}
@@ -87,6 +88,21 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
return newRef;
}
async clear(): Promise<void> {
if (!this.userDataSyncStore) {
throw new Error('No settings sync store url configured.');
}
const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource').toString();
const headers: IHeaders = { 'Content-Type': 'text/plain' };
const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new Error('Server returned ' + context.res.statusCode);
}
}
private async request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
const authToken = await this.authTokenService.getToken();
if (!authToken) {
@@ -98,7 +114,6 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
const context = await this.requestService.request(options, token);
if (context.res.statusCode === 401) {
this.authTokenService.refreshToken();
// Throw Unauthorized Error
throw new UserDataSyncStoreError('Unauthorized', UserDataSyncStoreErrorCode.Unauthroized);
}