Merge from vscode 3c6f6af7347d38e87bc6406024e8dcf9e9bce229 (#8962)

* Merge from vscode 3c6f6af7347d38e87bc6406024e8dcf9e9bce229

* skip failing tests

* update mac build image
This commit is contained in:
Anthony Dresser
2020-01-27 15:28:17 -08:00
committed by Karl Burtram
parent 0eaee18dc4
commit fefe1454de
481 changed files with 12764 additions and 7836 deletions

View File

@@ -4,30 +4,87 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService } from 'vs/platform/files/common/files';
import { IFileService, IFileContent } 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 { SyncSource, SyncStatus, IUserData, IUserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSync';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { joinPath } from 'vs/base/common/resources';
import { joinPath, dirname } from 'vs/base/common/resources';
import { toLocalISOString } from 'vs/base/common/date';
import { ThrottledDelayer } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
export abstract class AbstractSynchroniser extends Disposable {
private readonly syncFolder: URI;
protected readonly syncFolder: URI;
private cleanUpDelayer: ThrottledDelayer<void>;
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;
protected readonly _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;
protected readonly lastSyncResource: URI;
constructor(
syncSource: SyncSource,
readonly source: SyncSource,
@IFileService protected readonly fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService
@IEnvironmentService environmentService: IEnvironmentService,
@IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService,
) {
super();
this.syncFolder = joinPath(environmentService.userRoamingDataHome, '.sync', syncSource);
this.syncFolder = joinPath(environmentService.userDataSyncHome, source);
this.lastSyncResource = joinPath(this.syncFolder, `.lastSync${source}.json`);
this.cleanUpDelayer = new ThrottledDelayer(50);
}
protected setStatus(status: SyncStatus): void {
if (this._status !== status) {
this._status = status;
this._onDidChangStatus.fire(status);
}
}
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 resetLocal(): Promise<void> {
try {
await this.fileService.del(this.lastSyncResource);
} catch (e) { /* ignore */ }
}
protected async getLastSyncUserData<T extends IUserData>(): Promise<T | null> {
try {
const content = await this.fileService.readFile(this.lastSyncResource);
return JSON.parse(content.value.toString());
} catch (error) {
return null;
}
}
protected async updateLastSyncUserData<T extends IUserData>(lastSyncUserData: T): Promise<void> {
await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData)));
}
protected getRemoteUserData(lastSyncData?: IUserData | null): Promise<IUserData> {
return this.userDataSyncStoreService.read(this.getRemoteDataResourceKey(), lastSyncData || null, this.source);
}
protected async updateRemoteUserData(content: string, ref: string | null): Promise<string> {
return this.userDataSyncStoreService.write(this.getRemoteDataResourceKey(), content, ref, this.source);
}
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);
@@ -43,4 +100,40 @@ export abstract class AbstractSynchroniser extends Disposable {
}
}
protected abstract getRemoteDataResourceKey(): string;
}
export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
constructor(
protected readonly file: URI,
readonly source: SyncSource,
@IFileService fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
) {
super(source, fileService, environmentService, userDataSyncStoreService);
this._register(this.fileService.watch(dirname(file)));
this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(file))(() => this._onDidChangeLocal.fire()));
}
protected async getLocalFileContent(): Promise<IFileContent | null> {
try {
return await this.fileService.readFile(this.file);
} catch (error) {
return null;
}
}
protected async updateLocalFileContent(newContent: string, oldContent: IFileContent | null): Promise<void> {
if (oldContent) {
// file exists already
await this.backupLocal(oldContent.value);
await this.fileService.writeFile(this.file, VSBuffer.fromString(newContent), oldContent);
} else {
// file does not exist
await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: false });
}
}
}

View File

@@ -100,14 +100,8 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
// Remotely updated extensions
for (const key of values(baseToRemote.updated)) {
// If updated in local
if (baseToLocal.updated.has(key)) {
// Is different from local to remote
if (localToRemote.updated.has(key)) {
// update it in local
updated.push(massageSyncExtension(remoteExtensionsMap.get(key)!, key));
}
}
// Update in local always
updated.push(massageSyncExtension(remoteExtensionsMap.get(key)!, key));
}
// Locally added extensions

View File

@@ -3,29 +3,25 @@
* 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, ISyncExtension, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync';
import { VSBuffer } from 'vs/base/common/buffer';
import { Emitter, Event } from 'vs/base/common/event';
import { IUserData, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
import { 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 { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService } 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';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { AbstractSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
interface ISyncPreviewResult {
readonly added: ISyncExtension[];
readonly removed: IExtensionIdentifier[];
readonly updated: ISyncExtension[];
readonly remote: ISyncExtension[] | null;
readonly remoteUserData: IUserData | null;
readonly remoteUserData: IUserData;
readonly skippedExtensions: ISyncExtension[];
}
@@ -33,33 +29,19 @@ interface ILastSyncUserData extends IUserData {
skippedExtensions: ISyncExtension[] | undefined;
}
export class ExtensionsSynchroniser extends Disposable implements ISynchroniser {
private static EXTERNAL_USER_DATA_EXTENSIONS_KEY: string = 'extensions';
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 lastSyncExtensionsResource: URI;
private readonly replaceQueue: Queue<void>;
export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
constructor(
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService private readonly fileService: IFileService,
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IFileService fileService: IFileService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
super();
this.replaceQueue = this._register(new Queue());
this.lastSyncExtensionsResource = joinPath(environmentService.userRoamingDataHome, '.lastSyncExtensions');
super(SyncSource.Extensions, fileService, environmentService, userDataSyncStoreService);
this._register(
Event.debounce(
Event.any(
@@ -68,12 +50,7 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
() => undefined, 500)(() => this._onDidChangeLocal.fire()));
}
private setStatus(status: SyncStatus): void {
if (this._status !== status) {
this._status = status;
this._onDidChangStatus.fire(status);
}
}
protected getRemoteDataResourceKey(): string { return 'extensions'; }
async pull(): Promise<void> {
if (!this.configurationService.getValue<boolean>('sync.enableExtensions')) {
@@ -121,7 +98,8 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
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: [] });
const remoteUserData = await this.getRemoteUserData();
await this.apply({ added, removed, updated, remote, remoteUserData, skippedExtensions: [] }, true);
this.logService.info('Extensions: Finished pushing extensions.');
} finally {
@@ -130,18 +108,18 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
}
async sync(): Promise<boolean> {
async sync(): Promise<void> {
if (!this.configurationService.getValue<boolean>('sync.enableExtensions')) {
this.logService.trace('Extensions: Skipping synchronizing extensions as it is disabled.');
return false;
return;
}
if (!this.extensionGalleryService.isEnabled()) {
this.logService.trace('Extensions: Skipping synchronizing extensions as gallery is disabled.');
return false;
return;
}
if (this.status !== SyncStatus.Idle) {
this.logService.trace('Extensions: Skipping synchronizing extensions as it is running already.');
return false;
return;
}
this.logService.trace('Extensions: Started synchronizing extensions...');
@@ -152,7 +130,7 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
await this.apply(previewResult);
} catch (e) {
this.setStatus(SyncStatus.Idle);
if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Rejected) {
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.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();
@@ -160,21 +138,18 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
throw e;
}
this.logService.trace('Extensions: Finised synchronizing extensions.');
this.logService.trace('Extensions: Finished synchronizing extensions.');
this.setStatus(SyncStatus.Idle);
return true;
}
stop(): void { }
async stop(): Promise<void> { }
async hasPreviouslySynced(): Promise<boolean> {
const lastSyncData = await this.getLastSyncUserData();
return !!lastSyncData;
async restart(): Promise<void> {
throw new Error('Extensions: Conflicts should not occur');
}
async hasRemoteData(): Promise<boolean> {
const remoteUserData = await this.getRemoteUserData();
return remoteUserData.content !== null;
resolveConflicts(content: string): Promise<void> {
throw new Error('Extensions: Conflicts should not occur');
}
async hasLocalData(): Promise<boolean> {
@@ -189,30 +164,12 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
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);
const remoteExtensions: ISyncExtension[] = remoteData.content ? JSON.parse(remoteData.content) : [];
const ignoredExtensions = this.configurationService.getValue<string[]>('sync.ignoredExtensions') || [];
const removedExtensions = remoteExtensions.filter(e => !ignoredExtensions.some(id => areSameExtensions({ id }, e.identifier)) && areSameExtensions(e.identifier, identifier));
if (removedExtensions.length) {
for (const removedExtension of removedExtensions) {
remoteExtensions.splice(remoteExtensions.indexOf(removedExtension), 1);
}
this.logService.info(`Extensions: Removing extension '${identifier.id}' from remote.`);
await this.writeToRemote(remoteExtensions, remoteData.ref);
}
});
}
async resetLocal(): Promise<void> {
try {
await this.fileService.del(this.lastSyncExtensionsResource);
} catch (e) { /* ignore */ }
async getRemoteContent(): Promise<string | null> {
return null;
}
private async getPreview(): Promise<ISyncPreviewResult> {
const lastSyncData = await this.getLastSyncUserData();
const lastSyncData = await this.getLastSyncUserData<ILastSyncUserData>();
const lastSyncExtensions: ISyncExtension[] | null = lastSyncData ? JSON.parse(lastSyncData.content!) : null;
const skippedExtensions: ISyncExtension[] = lastSyncData ? lastSyncData.skippedExtensions || [] : [];
@@ -236,7 +193,7 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
return this.configurationService.getValue<string[]>('sync.ignoredExtensions') || [];
}
private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions }: ISyncPreviewResult): Promise<void> {
private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions }: ISyncPreviewResult, forcePush?: boolean): Promise<void> {
if (!added.length && !removed.length && !updated.length && !remote) {
this.logService.trace('Extensions: No changes found during synchronizing extensions.');
}
@@ -249,13 +206,15 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
if (remote) {
// update remote
this.logService.info('Extensions: Updating remote extensions...');
remoteUserData = await this.writeToRemote(remote, remoteUserData ? remoteUserData.ref : null);
const content = JSON.stringify(remote);
const ref = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
remoteUserData = { ref, content };
}
if (remoteUserData?.content) {
if (remoteUserData.content) {
// update last sync
this.logService.info('Extensions: Updating last synchronised extensions...');
await this.updateLastSyncValue({ ...remoteUserData, skippedExtensions });
await this.updateLastSyncUserData<ILastSyncUserData>({ ...remoteUserData, skippedExtensions });
}
}
@@ -281,6 +240,11 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
try {
await this.extensionManagementService.installFromGallery(extension);
removeFromSkipped.push(extension.identifier);
if (e.enabled) {
await this.extensionEnablementService.enableExtension(extension.identifier);
} else {
await this.extensionEnablementService.disableExtension(extension.identifier);
}
} catch (error) {
addToSkipped.push(e);
this.logService.error(error);
@@ -308,31 +272,9 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
private async getLocalExtensions(): Promise<ISyncExtension[]> {
const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User);
const disabledExtensions = await this.extensionEnablementService.getDisabledExtensionsAsync();
return installedExtensions
.map(({ identifier }) => ({ identifier, enabled: true }));
}
private async getLastSyncUserData(): Promise<ILastSyncUserData | null> {
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<void> {
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);
return { content, ref };
.map(({ identifier }) => ({ identifier, enabled: !disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier)) }));
}
}

View File

@@ -3,61 +3,42 @@
* 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 { IUserData, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncSource, IUserDataSynchroniser } from 'vs/platform/userDataSync/common/userDataSync';
import { VSBuffer } from 'vs/base/common/buffer';
import { Emitter, Event } from 'vs/base/common/event';
import { 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 { 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';
import { AbstractSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
const argvProperties: string[] = ['locale'];
interface ISyncPreviewResult {
readonly local: IGlobalState | undefined;
readonly remote: IGlobalState | undefined;
readonly remoteUserData: IUserData | null;
readonly remoteUserData: IUserData;
}
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;
export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
constructor(
@IFileService private readonly fileService: IFileService,
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IFileService fileService: IFileService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
super();
this.lastSyncGlobalStateResource = joinPath(environmentService.userRoamingDataHome, '.lastSyncGlobalState');
super(SyncSource.GlobalState, fileService, environmentService, userDataSyncStoreService);
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);
}
}
protected getRemoteDataResourceKey(): string { return 'globalState'; }
async pull(): Promise<void> {
if (!this.configurationService.getValue<boolean>('sync.enableUIState')) {
@@ -102,7 +83,8 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser
this.setStatus(SyncStatus.Syncing);
const remote = await this.getLocalGlobalState();
await this.apply({ local: undefined, remote, remoteUserData: null });
const remoteUserData = await this.getRemoteUserData();
await this.apply({ local: undefined, remote, remoteUserData }, true);
this.logService.info('UI State: Finished pushing UI State.');
} finally {
@@ -111,15 +93,15 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser
}
async sync(): Promise<boolean> {
async sync(): Promise<void> {
if (!this.configurationService.getValue<boolean>('sync.enableUIState')) {
this.logService.trace('UI State: Skipping synchronizing UI state as it is disabled.');
return false;
return;
}
if (this.status !== SyncStatus.Idle) {
this.logService.trace('UI State: Skipping synchronizing ui state as it is running already.');
return false;
return;
}
this.logService.trace('UI State: Started synchronizing ui state...');
@@ -128,11 +110,10 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser
try {
const result = await this.getPreview();
await this.apply(result);
this.logService.trace('UI State: Finised synchronizing ui state.');
return true;
this.logService.trace('UI State: Finished synchronizing ui state.');
} catch (e) {
this.setStatus(SyncStatus.Idle);
if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Rejected) {
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.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();
@@ -143,16 +124,14 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser
}
}
stop(): void { }
async stop(): Promise<void> { }
async hasPreviouslySynced(): Promise<boolean> {
const lastSyncData = await this.getLastSyncUserData();
return !!lastSyncData;
async restart(): Promise<void> {
throw new Error('UI State: Conflicts should not occur');
}
async hasRemoteData(): Promise<boolean> {
const remoteUserData = await this.getRemoteUserData();
return remoteUserData.content !== null;
resolveConflicts(content: string): Promise<void> {
throw new Error('UI State: Conflicts should not occur');
}
async hasLocalData(): Promise<boolean> {
@@ -167,10 +146,8 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser
return false;
}
async resetLocal(): Promise<void> {
try {
await this.fileService.del(this.lastSyncGlobalStateResource);
} catch (e) { /* ignore */ }
async getRemoteContent(): Promise<string | null> {
return null;
}
private async getPreview(): Promise<ISyncPreviewResult> {
@@ -187,7 +164,7 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser
return { local, remote, remoteUserData };
}
private async apply({ local, remote, remoteUserData }: ISyncPreviewResult): Promise<void> {
private async apply({ local, remote, remoteUserData }: ISyncPreviewResult, forcePush?: boolean): Promise<void> {
if (local) {
// update local
this.logService.info('UI State: Updating local ui state...');
@@ -197,13 +174,15 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser
if (remote) {
// update remote
this.logService.info('UI State: Updating remote ui state...');
remoteUserData = await this.writeToRemote(remote, remoteUserData ? remoteUserData.ref : null);
const content = JSON.stringify(remote);
const ref = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
remoteUserData = { ref, content };
}
if (remoteUserData?.content) {
if (remoteUserData.content) {
// update last sync
this.logService.info('UI State: Updating last synchronised ui state...');
await this.updateLastSyncValue(remoteUserData);
await this.updateLastSyncUserData(remoteUserData);
}
}
@@ -233,27 +212,4 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser
}
}
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

@@ -99,16 +99,7 @@ export async function merge(localContent: string, remoteContent: string, baseCon
mergeContent = updateKeybindings(mergeContent, command, keybindings, formattingOptions);
}
const hasConflicts = commandsMergeResult.conflicts.size > 0;
if (hasConflicts) {
mergeContent = `<<<<<<< local${formattingOptions.eol}`
+ mergeContent
+ `${formattingOptions.eol}=======${formattingOptions.eol}`
+ remoteContent
+ `${formattingOptions.eol}>>>>>>> remote`;
}
return { mergeContent, hasChanges: true, hasConflicts };
return { mergeContent, hasChanges: true, hasConflicts: commandsMergeResult.conflicts.size > 0 };
}
function computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompareResult, baseToRemote: ICompareResult): { added: Set<string>, removed: Set<string>, updated: Set<string>, conflicts: Set<string> } {

View File

@@ -4,23 +4,20 @@
*--------------------------------------------------------------------------------------------*/
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 { IUserData, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncSource, IUserDataSynchroniser } 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';
import { localize } from 'vs/nls';
import { Emitter, Event } from 'vs/base/common/event';
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { URI } from 'vs/base/common/uri';
import { joinPath, dirname } from 'vs/base/common/resources';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
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';
import { AbstractFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
interface ISyncContent {
mac?: string;
@@ -31,48 +28,28 @@ interface ISyncContent {
interface ISyncPreviewResult {
readonly fileContent: IFileContent | null;
readonly remoteUserData: IUserData | null;
readonly remoteUserData: IUserData;
readonly hasLocalChanged: boolean;
readonly hasRemoteChanged: boolean;
readonly hasConflicts: boolean;
}
export class KeybindingsSynchroniser extends AbstractSynchroniser implements ISynchroniser {
private static EXTERNAL_USER_DATA_KEYBINDINGS_KEY: string = 'keybindings';
export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements IUserDataSynchroniser {
private syncPreviewResultPromise: CancelablePromise<ISyncPreviewResult> | null = null;
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 lastSyncKeybindingsResource: URI;
constructor(
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IFileService fileService: IFileService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService,
) {
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()));
super(environmentService.keybindingsResource, SyncSource.Keybindings, fileService, environmentService, userDataSyncStoreService);
}
private setStatus(status: SyncStatus): void {
if (this._status !== status) {
this._status = status;
this._onDidChangStatus.fire(status);
}
}
protected getRemoteDataResourceKey(): string { return 'keybindings'; }
async pull(): Promise<void> {
if (!this.configurationService.getValue<boolean>('sync.enableKeybindings')) {
@@ -129,15 +106,16 @@ export class KeybindingsSynchroniser extends AbstractSynchroniser implements ISy
const fileContent = await this.getLocalFileContent();
if (fileContent !== null) {
const remoteUserData = await this.getRemoteUserData();
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, fileContent.value);
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
fileContent,
hasConflicts: false,
hasLocalChanged: false,
hasRemoteChanged: true,
remoteUserData: null
remoteUserData
}));
await this.apply();
await this.apply(undefined, true);
}
// No local exists to push
@@ -152,74 +130,55 @@ export class KeybindingsSynchroniser extends AbstractSynchroniser implements ISy
}
async sync(_continue?: boolean): Promise<boolean> {
async sync(): Promise<void> {
if (!this.configurationService.getValue<boolean>('sync.enableKeybindings')) {
this.logService.trace('Keybindings: Skipping synchronizing keybindings as it is disabled.');
return false;
}
if (_continue) {
this.logService.info('Keybindings: Resumed synchronizing keybindings');
return this.continueSync();
return;
}
if (this.status !== SyncStatus.Idle) {
this.logService.trace('Keybindings: Skipping synchronizing keybindings as it is running already.');
return false;
return;
}
this.logService.trace('Keybindings: Started synchronizing keybindings...');
this.setStatus(SyncStatus.Syncing);
try {
const result = await this.getPreview();
if (result.hasConflicts) {
this.logService.info('Keybindings: Detected conflicts while synchronizing keybindings.');
this.setStatus(SyncStatus.HasConflicts);
return false;
}
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);
if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Rejected) {
// Rejected as there is a new remote version. Syncing again,
this.logService.info('Keybindings: Failed to synchronise keybindings as there is a new remote version available. Synchronizing again...');
return this.sync();
}
if (e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) {
// Rejected as there is a new local version. Syncing again.
this.logService.info('Keybindings: Failed to synchronise keybindings as there is a new local version available. Synchronizing again...');
return this.sync();
}
throw e;
}
return this.doSync();
}
stop(): void {
async stop(): Promise<void> {
if (this.syncPreviewResultPromise) {
this.syncPreviewResultPromise.cancel();
this.syncPreviewResultPromise = null;
this.logService.info('Keybindings: Stopped synchronizing keybindings.');
}
this.fileService.del(this.environmentService.keybindingsSyncPreviewResource);
await this.fileService.del(this.environmentService.keybindingsSyncPreviewResource);
this.setStatus(SyncStatus.Idle);
}
async hasPreviouslySynced(): Promise<boolean> {
const lastSyncData = await this.getLastSyncUserData();
return !!lastSyncData;
async restart(): Promise<void> {
if (this.status === SyncStatus.HasConflicts) {
this.syncPreviewResultPromise!.cancel();
this.syncPreviewResultPromise = null;
await this.doSync();
}
}
async hasRemoteData(): Promise<boolean> {
const remoteUserData = await this.getRemoteUserData();
return remoteUserData.content !== null;
async resolveConflicts(content: string): Promise<void> {
if (this.status === SyncStatus.HasConflicts) {
try {
await this.apply(content, true);
this.setStatus(SyncStatus.Idle);
} catch (e) {
this.logService.error(e);
if ((e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) ||
(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) {
throw new Error('Failed to resolve conflicts as there is a new local version available.');
}
throw e;
}
}
}
async hasLocalData(): Promise<boolean> {
@@ -239,29 +198,63 @@ export class KeybindingsSynchroniser extends AbstractSynchroniser implements ISy
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;
async getRemoteContent(): Promise<string | null> {
let content: string | null | undefined = null;
if (this.syncPreviewResultPromise) {
const preview = await this.syncPreviewResultPromise;
content = preview.remoteUserData?.content;
} else {
const remoteUserData = await this.getRemoteUserData();
content = remoteUserData.content;
}
await this.apply();
this.setStatus(SyncStatus.Idle);
return true;
return content ? this.getKeybindingsContentFromSyncContent(content) : null;
}
private async apply(): Promise<void> {
private async doSync(): Promise<void> {
try {
const result = await this.getPreview();
if (result.hasConflicts) {
this.logService.info('Keybindings: Detected conflicts while synchronizing keybindings.');
this.setStatus(SyncStatus.HasConflicts);
return;
}
try {
await this.apply();
this.logService.trace('Keybindings: Finished synchronizing keybindings...');
} finally {
this.setStatus(SyncStatus.Idle);
}
} catch (e) {
this.syncPreviewResultPromise = null;
this.setStatus(SyncStatus.Idle);
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.Rejected) {
// Rejected as there is a new remote version. Syncing again,
this.logService.info('Keybindings: Failed to synchronise keybindings as there is a new remote version available. Synchronizing again...');
return this.sync();
}
if ((e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) ||
(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) {
// Rejected as there is a new local version. Syncing again.
this.logService.info('Keybindings: Failed to synchronise keybindings as there is a new local version available. Synchronizing again...');
return this.sync();
}
throw e;
}
}
private async apply(content?: string, forcePush?: boolean): Promise<void> {
if (!this.syncPreviewResultPromise) {
return;
}
if (await this.fileService.exists(this.environmentService.keybindingsSyncPreviewResource)) {
const keybindingsPreivew = await this.fileService.readFile(this.environmentService.keybindingsSyncPreviewResource);
const content = keybindingsPreivew.value.toString();
if (content === undefined) {
if (await this.fileService.exists(this.environmentService.keybindingsSyncPreviewResource)) {
const keybindingsPreivew = await this.fileService.readFile(this.environmentService.keybindingsSyncPreviewResource);
content = keybindingsPreivew.value.toString();
}
}
if (content !== undefined) {
if (this.hasErrors(content)) {
const error = new Error(localize('errorInvalidKeybindings', "Unable to sync keybindings. Please resolve conflicts without any errors/warnings and try again."));
this.logService.error(error);
@@ -274,13 +267,12 @@ export class KeybindingsSynchroniser extends AbstractSynchroniser implements ISy
}
if (hasLocalChanged) {
this.logService.info('Keybindings: Updating local keybindings');
await this.updateLocalContent(content, fileContent);
await this.updateLocalFileContent(content, fileContent);
}
if (hasRemoteChanged) {
this.logService.info('Keybindings: Updating remote keybindings');
let remoteContents = remoteUserData ? remoteUserData.content : (await this.getRemoteUserData()).content;
remoteContents = this.updateSyncContent(content, remoteContents);
const ref = await this.updateRemoteUserData(remoteContents, remoteUserData ? remoteUserData.ref : null);
const remoteContents = this.updateSyncContent(content, remoteUserData.content);
const ref = await this.updateRemoteUserData(remoteContents, forcePush ? null : remoteUserData.ref);
remoteUserData = { ref, content: remoteContents };
}
if (remoteUserData?.content) {
@@ -369,46 +361,6 @@ export class KeybindingsSynchroniser extends AbstractSynchroniser implements ISy
return this._formattingOptions;
}
private async getLocalFileContent(): Promise<IFileContent | null> {
try {
return await this.fileService.readFile(this.environmentService.keybindingsResource);
} catch (error) {
return null;
}
}
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
await this.fileService.createFile(this.environmentService.keybindingsResource, VSBuffer.fromString(newContent), { overwrite: false });
}
}
private async getLastSyncUserData(): Promise<IUserData | null> {
try {
const content = await this.fileService.readFile(this.lastSyncKeybindingsResource);
return JSON.parse(content.value.toString());
} catch (error) {
return null;
}
}
private async updateLastSyncUserData(remoteUserData: IUserData): Promise<void> {
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 || null);
}
private async updateRemoteUserData(content: string, ref: string | null): Promise<string> {
return this.userDataSyncStoreService.write(KeybindingsSynchroniser.EXTERNAL_USER_DATA_KEYBINDINGS_KEY, content, ref);
}
private getKeybindingsContentFromSyncContent(syncContent: string): string | null {
try {
const parsed = <ISyncContent>JSON.parse(syncContent);

View File

@@ -4,47 +4,95 @@
*--------------------------------------------------------------------------------------------*/
import * as objects from 'vs/base/common/objects';
import { parse, findNodeAtLocation, parseTree, Node } from 'vs/base/common/json';
import { setProperty } from 'vs/base/common/jsonEdit';
import { parse, JSONVisitor, visit } from 'vs/base/common/json';
import { setProperty, withFormatting, applyEdits } from 'vs/base/common/jsonEdit';
import { values } from 'vs/base/common/map';
import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import { FormattingOptions, Edit, getEOL } from 'vs/base/common/jsonFormatter';
import * as contentUtil from 'vs/platform/userDataSync/common/content';
import { IConflictSetting } from 'vs/platform/userDataSync/common/userDataSync';
import { firstIndex } from 'vs/base/common/arrays';
export interface IMergeResult {
localContent: string | null;
remoteContent: string | null;
hasConflicts: boolean;
conflictsSettings: IConflictSetting[];
}
export function updateIgnoredSettings(targetContent: string, sourceContent: string, ignoredSettings: string[], formattingOptions: FormattingOptions): string {
if (ignoredSettings.length) {
const sourceTree = parseSettings(sourceContent);
const source = parse(sourceContent);
const target = parse(targetContent);
const settingsToAdd: INode[] = [];
for (const key of ignoredSettings) {
targetContent = contentUtil.edit(targetContent, [key], source[key], formattingOptions);
const sourceValue = source[key];
const targetValue = target[key];
// Remove in target
if (sourceValue === undefined) {
targetContent = contentUtil.edit(targetContent, [key], undefined, formattingOptions);
}
// Update in target
else if (targetValue !== undefined) {
targetContent = contentUtil.edit(targetContent, [key], sourceValue, formattingOptions);
}
else {
settingsToAdd.push(findSettingNode(key, sourceTree)!);
}
}
settingsToAdd.sort((a, b) => a.startOffset - b.startOffset);
settingsToAdd.forEach(s => targetContent = addSetting(s.setting!.key, sourceContent, targetContent, formattingOptions));
}
return targetContent;
}
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;
const ignored = ignoredSettings.reduce((set, key) => { set.add(key); return set; }, new Set<string>());
export function merge(originalLocalContent: string, originalRemoteContent: string, baseContent: string | null, ignoredSettings: string[], resolvedConflicts: { key: string, value: any | undefined }[], formattingOptions: FormattingOptions): IMergeResult {
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, conflicts: [] };
const localContentWithoutIgnoredSettings = updateIgnoredSettings(originalLocalContent, originalRemoteContent, ignoredSettings, formattingOptions);
const localForwarded = baseContent !== localContentWithoutIgnoredSettings;
const remoteForwarded = baseContent !== originalRemoteContent;
/* no changes */
if (!localForwarded && !remoteForwarded) {
return { conflictsSettings: [], localContent: null, remoteContent: null, hasConflicts: false };
}
/* local has changed and remote has not */
if (localForwarded && !remoteForwarded) {
return { conflictsSettings: [], localContent: null, remoteContent: localContentWithoutIgnoredSettings, hasConflicts: false };
}
/* remote has changed and local has not */
if (remoteForwarded && !localForwarded) {
return { conflictsSettings: [], localContent: updateIgnoredSettings(originalRemoteContent, originalLocalContent, ignoredSettings, formattingOptions), remoteContent: null, hasConflicts: false };
}
/* remote and local has changed */
let localContent = originalLocalContent;
let remoteContent = originalRemoteContent;
const local = parse(originalLocalContent);
const remote = parse(originalRemoteContent);
const base = baseContent ? parse(baseContent) : null;
const ignored = ignoredSettings.reduce((set, key) => { set.add(key); return set; }, new Set<string>());
const localToRemote = compare(local, remote, ignored);
const baseToLocal = compare(base, local, ignored);
const baseToRemote = compare(base, remote, ignored);
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);
localContent = contentUtil.edit(localContent, [conflictKey], resolvedConflict.value, formattingOptions);
remoteContent = contentUtil.edit(remoteContent, [conflictKey], resolvedConflict.value, formattingOptions);
} else {
conflicts.set(conflictKey, { key: conflictKey, localValue: local[conflictKey], remoteValue: remote[conflictKey] });
}
@@ -52,10 +100,14 @@ export function merge(localContent: string, remoteContent: string, baseContent:
// Removed settings in Local
for (const key of values(baseToLocal.removed)) {
// Got updated in remote
// Conflict - Got updated in remote.
if (baseToRemote.updated.has(key)) {
handleConflict(key);
}
// Also remove in remote
else {
remoteContent = contentUtil.edit(remoteContent, [key], undefined, formattingOptions);
}
}
// Removed settings in Remote
@@ -63,41 +115,13 @@ export function merge(localContent: string, remoteContent: string, baseContent:
if (handledConflicts.has(key)) {
continue;
}
// Got updated in local
// Conflict - Got updated in local
if (baseToLocal.updated.has(key)) {
handleConflict(key);
} else {
mergeContent = contentUtil.edit(mergeContent, [key], undefined, formattingOptions);
}
}
// Added settings in Local
for (const key of values(baseToLocal.added)) {
if (handledConflicts.has(key)) {
continue;
}
// Got added in remote
if (baseToRemote.added.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
handleConflict(key);
}
}
}
// Added settings in remote
for (const key of values(baseToRemote.added)) {
if (handledConflicts.has(key)) {
continue;
}
// Got added in local
if (baseToLocal.added.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
handleConflict(key);
}
} else {
mergeContent = contentUtil.edit(mergeContent, [key], remote[key], formattingOptions);
// Also remove in locals
else {
localContent = contentUtil.edit(localContent, [key], undefined, formattingOptions);
}
}
@@ -112,6 +136,8 @@ export function merge(localContent: string, remoteContent: string, baseContent:
if (localToRemote.updated.has(key)) {
handleConflict(key);
}
} else {
remoteContent = contentUtil.edit(remoteContent, [key], local[key], formattingOptions);
}
}
@@ -127,74 +153,425 @@ export function merge(localContent: string, remoteContent: string, baseContent:
handleConflict(key);
}
} else {
mergeContent = contentUtil.edit(mergeContent, [key], remote[key], formattingOptions);
localContent = contentUtil.edit(localContent, [key], remote[key], formattingOptions);
}
}
if (conflicts.size > 0) {
const conflictNodes: { key: string, node: Node | undefined }[] = [];
const tree = parseTree(mergeContent);
const eol = formattingOptions.eol!;
for (const { key } of values(conflicts)) {
const node = findNodeAtLocation(tree, [key]);
conflictNodes.push({ key, node });
// Added settings in Local
for (const key of values(baseToLocal.added)) {
if (handledConflicts.has(key)) {
continue;
}
conflictNodes.sort((a, b) => {
if (a.node && b.node) {
return b.node.offset - a.node.offset;
}
return a.node ? 1 : -1;
});
const lastNode = tree.children ? tree.children[tree.children.length - 1] : undefined;
for (const { key, node } of conflictNodes) {
const remoteEdit = setProperty(`{${eol}\t${eol}}`, [key], remote[key], { tabSize: 4, insertSpaces: false, eol: eol })[0];
const remoteContent = remoteEdit ? `${remoteEdit.content.substring(remoteEdit.offset + remoteEdit.length + 1)},${eol}` : '';
if (node) {
// Updated in Local and Remote with different value
const localStartOffset = contentUtil.getLineStartOffset(mergeContent, eol, node.parent!.offset);
const localEndOffset = contentUtil.getLineEndOffset(mergeContent, eol, node.offset + node.length);
mergeContent = mergeContent.substring(0, localStartOffset)
+ `<<<<<<< local${eol}`
+ mergeContent.substring(localStartOffset, localEndOffset)
+ `${eol}=======${eol}${remoteContent}>>>>>>> remote`
+ mergeContent.substring(localEndOffset);
} else {
// Removed in Local, but updated in Remote
if (lastNode) {
const localStartOffset = contentUtil.getLineEndOffset(mergeContent, eol, lastNode.offset + lastNode.length);
mergeContent = mergeContent.substring(0, localStartOffset)
+ `${eol}<<<<<<< local${eol}=======${eol}${remoteContent}>>>>>>> remote`
+ mergeContent.substring(localStartOffset);
} else {
const localStartOffset = tree.offset + 1;
mergeContent = mergeContent.substring(0, localStartOffset)
+ `${eol}<<<<<<< local${eol}=======${eol}${remoteContent}>>>>>>> remote${eol}`
+ mergeContent.substring(localStartOffset);
}
// Got added in remote
if (baseToRemote.added.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
handleConflict(key);
}
} else {
remoteContent = addSetting(key, localContent, remoteContent, formattingOptions);
}
}
return { mergeContent, hasChanges: true, conflicts: values(conflicts) };
// Added settings in remote
for (const key of values(baseToRemote.added)) {
if (handledConflicts.has(key)) {
continue;
}
// Got added in local
if (baseToLocal.added.has(key)) {
// Has different value
if (localToRemote.updated.has(key)) {
handleConflict(key);
}
} else {
localContent = addSetting(key, remoteContent, localContent, formattingOptions);
}
}
const hasConflicts = conflicts.size > 0 || !areSame(localContent, remoteContent, ignoredSettings);
const hasLocalChanged = hasConflicts || !areSame(localContent, originalLocalContent, []);
const hasRemoteChanged = hasConflicts || !areSame(remoteContent, originalRemoteContent, []);
return { localContent: hasLocalChanged ? localContent : null, remoteContent: hasRemoteChanged ? remoteContent : null, conflictsSettings: values(conflicts), hasConflicts };
}
function compare(from: IStringDictionary<any>, to: IStringDictionary<any>, ignored: Set<string>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
const fromKeys = Object.keys(from).filter(key => !ignored.has(key));
export function areSame(localContent: string, remoteContent: string, ignoredSettings: string[]): boolean {
if (localContent === remoteContent) {
return true;
}
const local = parse(localContent);
const remote = parse(remoteContent);
const ignored = ignoredSettings.reduce((set, key) => { set.add(key); return set; }, new Set<string>());
const localTree = parseSettings(localContent).filter(node => !(node.setting && ignored.has(node.setting.key)));
const remoteTree = parseSettings(remoteContent).filter(node => !(node.setting && ignored.has(node.setting.key)));
if (localTree.length !== remoteTree.length) {
return false;
}
for (let index = 0; index < localTree.length; index++) {
const localNode = localTree[index];
const remoteNode = remoteTree[index];
if (localNode.setting && remoteNode.setting) {
if (localNode.setting.key !== remoteNode.setting.key) {
return false;
}
if (!objects.equals(local[localNode.setting.key], remote[localNode.setting.key])) {
return false;
}
} else if (!localNode.setting && !remoteNode.setting) {
if (localNode.value !== remoteNode.value) {
return false;
}
} else {
return false;
}
}
return true;
}
function compare(from: IStringDictionary<any> | null, to: IStringDictionary<any>, ignored: Set<string>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
const fromKeys = from ? Object.keys(from).filter(key => !ignored.has(key)) : [];
const toKeys = Object.keys(to).filter(key => !ignored.has(key));
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);
if (from) {
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 };
}
export function addSetting(key: string, sourceContent: string, targetContent: string, formattingOptions: FormattingOptions): string {
const source = parse(sourceContent);
const sourceTree = parseSettings(sourceContent);
const targetTree = parseSettings(targetContent);
const insertLocation = getInsertLocation(key, sourceTree, targetTree);
return insertAtLocation(targetContent, key, source[key], insertLocation, targetTree, formattingOptions);
}
interface InsertLocation {
index: number,
insertAfter: boolean;
}
function getInsertLocation(key: string, sourceTree: INode[], targetTree: INode[]): InsertLocation {
const sourceNodeIndex = firstIndex(sourceTree, (node => node.setting?.key === key));
const sourcePreviousNode: INode = sourceTree[sourceNodeIndex - 1];
if (sourcePreviousNode) {
/*
Previous node in source is a setting.
Find the same setting in the target.
Insert it after that setting
*/
if (sourcePreviousNode.setting) {
const targetPreviousSetting = findSettingNode(sourcePreviousNode.setting.key, targetTree);
if (targetPreviousSetting) {
/* Insert after target's previous setting */
return { index: targetTree.indexOf(targetPreviousSetting), insertAfter: true };
}
}
/* Previous node in source is a comment */
else {
const sourcePreviousSettingNode = findPreviousSettingNode(sourceNodeIndex, sourceTree);
/*
Source has a setting defined before the setting to be added.
Find the same previous setting in the target.
If found, insert before its next setting so that comments are retrieved.
Otherwise, insert at the end.
*/
if (sourcePreviousSettingNode) {
const targetPreviousSetting = findSettingNode(sourcePreviousSettingNode.setting!.key, targetTree);
if (targetPreviousSetting) {
const targetNextSetting = findNextSettingNode(targetTree.indexOf(targetPreviousSetting), targetTree);
const sourceCommentNodes = findNodesBetween(sourceTree, sourcePreviousSettingNode, sourceTree[sourceNodeIndex]);
if (targetNextSetting) {
const targetCommentNodes = findNodesBetween(targetTree, targetPreviousSetting, targetNextSetting);
const targetCommentNode = findLastMatchingTargetCommentNode(sourceCommentNodes, targetCommentNodes);
if (targetCommentNode) {
return { index: targetTree.indexOf(targetCommentNode), insertAfter: true }; /* Insert after comment */
} else {
return { index: targetTree.indexOf(targetNextSetting), insertAfter: false }; /* Insert before target next setting */
}
} else {
const targetCommentNodes = findNodesBetween(targetTree, targetPreviousSetting, targetTree[targetTree.length - 1]);
const targetCommentNode = findLastMatchingTargetCommentNode(sourceCommentNodes, targetCommentNodes);
if (targetCommentNode) {
return { index: targetTree.indexOf(targetCommentNode), insertAfter: true }; /* Insert after comment */
} else {
return { index: targetTree.length - 1, insertAfter: true }; /* Insert at the end */
}
}
}
}
}
const sourceNextNode = sourceTree[sourceNodeIndex + 1];
if (sourceNextNode) {
/*
Next node in source is a setting.
Find the same setting in the target.
Insert it before that setting
*/
if (sourceNextNode.setting) {
const targetNextSetting = findSettingNode(sourceNextNode.setting.key, targetTree);
if (targetNextSetting) {
/* Insert before target's next setting */
return { index: targetTree.indexOf(targetNextSetting), insertAfter: false };
}
}
/* Next node in source is a comment */
else {
const sourceNextSettingNode = findNextSettingNode(sourceNodeIndex, sourceTree);
/*
Source has a setting defined after the setting to be added.
Find the same next setting in the target.
If found, insert after its previous setting so that comments are retrieved.
Otherwise, insert at the beginning.
*/
if (sourceNextSettingNode) {
const targetNextSetting = findSettingNode(sourceNextSettingNode.setting!.key, targetTree);
if (targetNextSetting) {
const targetPreviousSetting = findPreviousSettingNode(targetTree.indexOf(targetNextSetting), targetTree);
const sourceCommentNodes = findNodesBetween(sourceTree, sourceTree[sourceNodeIndex], sourceNextSettingNode);
if (targetPreviousSetting) {
const targetCommentNodes = findNodesBetween(targetTree, targetPreviousSetting, targetNextSetting);
const targetCommentNode = findLastMatchingTargetCommentNode(sourceCommentNodes.reverse(), targetCommentNodes.reverse());
if (targetCommentNode) {
return { index: targetTree.indexOf(targetCommentNode), insertAfter: false }; /* Insert before comment */
} else {
return { index: targetTree.indexOf(targetPreviousSetting), insertAfter: true }; /* Insert after target previous setting */
}
} else {
const targetCommentNodes = findNodesBetween(targetTree, targetTree[0], targetNextSetting);
const targetCommentNode = findLastMatchingTargetCommentNode(sourceCommentNodes.reverse(), targetCommentNodes.reverse());
if (targetCommentNode) {
return { index: targetTree.indexOf(targetCommentNode), insertAfter: false }; /* Insert before comment */
} else {
return { index: 0, insertAfter: false }; /* Insert at the beginning */
}
}
}
}
}
}
}
/* Insert at the end */
return { index: targetTree.length - 1, insertAfter: true };
}
function insertAtLocation(content: string, key: string, value: any, location: InsertLocation, tree: INode[], formattingOptions: FormattingOptions): string {
let edits: Edit[];
/* Insert at the end */
if (location.index === -1) {
edits = setProperty(content, [key], value, formattingOptions);
} else {
edits = getEditToInsertAtLocation(content, key, value, location, tree, formattingOptions).map(edit => withFormatting(content, edit, formattingOptions)[0]);
}
return applyEdits(content, edits);
}
function getEditToInsertAtLocation(content: string, key: string, value: any, location: InsertLocation, tree: INode[], formattingOptions: FormattingOptions): Edit[] {
const newProperty = `${JSON.stringify(key)}: ${JSON.stringify(value)}`;
const eol = getEOL(formattingOptions, content);
const node = tree[location.index];
if (location.insertAfter) {
/* Insert after a setting */
if (node.setting) {
return [{ offset: node.endOffset, length: 0, content: ',' + newProperty }];
}
/*
Insert after a comment and before a setting (or)
Insert between comments and there is a setting after
*/
if (tree[location.index + 1] &&
(tree[location.index + 1].setting || findNextSettingNode(location.index, tree))) {
return [{ offset: node.endOffset, length: 0, content: eol + newProperty + ',' }];
}
/* Insert after the comment at the end */
const edits = [{ offset: node.endOffset, length: 0, content: eol + newProperty }];
const previousSettingNode = findPreviousSettingNode(location.index, tree);
if (previousSettingNode && !previousSettingNode.setting!.hasCommaSeparator) {
edits.splice(0, 0, { offset: previousSettingNode.endOffset, length: 0, content: ',' });
}
return edits;
}
else {
/* Insert before a setting */
if (node.setting) {
return [{ offset: node.startOffset, length: 0, content: newProperty + ',' }];
}
/* Insert before a comment */
const content = (tree[location.index - 1] && !tree[location.index - 1].setting /* previous node is comment */ ? eol : '')
+ newProperty
+ (findNextSettingNode(location.index, tree) ? ',' : '')
+ eol;
return [{ offset: node.startOffset, length: 0, content }];
}
}
function findSettingNode(key: string, tree: INode[]): INode | undefined {
return tree.filter(node => node.setting?.key === key)[0];
}
function findPreviousSettingNode(index: number, tree: INode[]): INode | undefined {
for (let i = index - 1; i >= 0; i--) {
if (tree[i].setting) {
return tree[i];
}
}
return undefined;
}
function findNextSettingNode(index: number, tree: INode[]): INode | undefined {
for (let i = index + 1; i < tree.length; i++) {
if (tree[i].setting) {
return tree[i];
}
}
return undefined;
}
function findNodesBetween(nodes: INode[], from: INode, till: INode): INode[] {
const fromIndex = nodes.indexOf(from);
const tillIndex = nodes.indexOf(till);
return nodes.filter((node, index) => fromIndex < index && index < tillIndex);
}
function findLastMatchingTargetCommentNode(sourceComments: INode[], targetComments: INode[]): INode | undefined {
if (sourceComments.length && targetComments.length) {
let index = 0;
for (; index < targetComments.length && index < sourceComments.length; index++) {
if (sourceComments[index].value !== targetComments[index].value) {
return targetComments[index - 1];
}
}
return targetComments[index - 1];
}
return undefined;
}
interface INode {
readonly startOffset: number;
readonly endOffset: number;
readonly value: string;
readonly setting?: {
readonly key: string;
readonly hasCommaSeparator: boolean;
};
readonly comment?: string;
}
function parseSettings(content: string): INode[] {
const nodes: INode[] = [];
let hierarchyLevel = -1;
let startOffset: number;
let key: string;
const visitor: JSONVisitor = {
onObjectBegin: (offset: number) => {
hierarchyLevel++;
},
onObjectProperty: (name: string, offset: number, length: number) => {
if (hierarchyLevel === 0) {
// this is setting key
startOffset = offset;
key = name;
}
},
onObjectEnd: (offset: number, length: number) => {
hierarchyLevel--;
if (hierarchyLevel === 0) {
nodes.push({
startOffset,
endOffset: offset + length,
value: content.substring(startOffset, offset + length),
setting: {
key,
hasCommaSeparator: false
}
});
}
},
onArrayBegin: (offset: number, length: number) => {
hierarchyLevel++;
},
onArrayEnd: (offset: number, length: number) => {
hierarchyLevel--;
if (hierarchyLevel === 0) {
nodes.push({
startOffset,
endOffset: offset + length,
value: content.substring(startOffset, offset + length),
setting: {
key,
hasCommaSeparator: false
}
});
}
},
onLiteralValue: (value: any, offset: number, length: number) => {
if (hierarchyLevel === 0) {
nodes.push({
startOffset,
endOffset: offset + length,
value: content.substring(startOffset, offset + length),
setting: {
key,
hasCommaSeparator: false
}
});
}
},
onSeparator: (sep: string, offset: number, length: number) => {
if (hierarchyLevel === 0) {
if (sep === ',') {
const node = nodes.pop();
nodes.push({
startOffset: node!.startOffset,
endOffset: node!.endOffset,
value: node!.value,
setting: {
key: node!.setting!.key,
hasCommaSeparator: true
}
});
}
}
},
onComment: (offset: number, length: number) => {
if (hierarchyLevel === 0) {
nodes.push({
startOffset: offset,
endOffset: offset + length,
value: content.substring(offset, offset + length),
});
}
}
};
visit(content, visitor);
return nodes;
}

View File

@@ -4,15 +4,13 @@
*--------------------------------------------------------------------------------------------*/
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 { IUserData, UserDataSyncError, UserDataSyncErrorCode, 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';
import { Emitter, Event } from 'vs/base/common/event';
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { URI } from 'vs/base/common/uri';
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';
@@ -22,59 +20,45 @@ 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';
import { AbstractFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
interface ISyncPreviewResult {
readonly fileContent: IFileContent | null;
readonly remoteUserData: IUserData | null;
readonly remoteUserData: IUserData;
readonly hasLocalChanged: boolean;
readonly hasRemoteChanged: boolean;
readonly conflicts: IConflictSetting[];
readonly remoteContent: string | null;
readonly hasConflicts: boolean;
readonly conflictSettings: IConflictSetting[];
}
export class SettingsSynchroniser extends AbstractSynchroniser implements ISettingsSyncService {
export class SettingsSynchroniser extends AbstractFileSynchroniser implements ISettingsSyncService {
_serviceBrand: any;
private static EXTERNAL_USER_DATA_SETTINGS_KEY: string = 'settings';
private syncPreviewResultPromise: CancelablePromise<ISyncPreviewResult> | null = null;
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 _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 fileService: IFileService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
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()));
super(environmentService.settingsResource, SyncSource.Settings, fileService, environmentService, userDataSyncStoreService);
}
private setStatus(status: SyncStatus): void {
if (this._status !== status) {
this._status = status;
this._onDidChangStatus.fire(status);
}
if (this._status !== SyncStatus.HasConflicts) {
protected getRemoteDataResourceKey(): string { return 'settings'; }
protected setStatus(status: SyncStatus): void {
super.setStatus(status);
if (this.status !== SyncStatus.HasConflicts) {
this.setConflicts([]);
}
}
@@ -110,11 +94,13 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(content));
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
conflicts: [],
hasConflicts: false,
conflictSettings: [],
fileContent,
hasLocalChanged: true,
hasRemoteChanged: false,
remoteUserData
remoteContent: content,
remoteUserData,
}));
await this.apply();
@@ -150,16 +136,19 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
// Remove ignored settings
const content = updateIgnoredSettings(fileContent.value.toString(), '{}', getIgnoredSettings(this.configurationService), formatUtils);
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(content));
const remoteUserData = await this.getRemoteUserData();
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
conflicts: [],
conflictSettings: [],
hasConflicts: false,
fileContent,
hasLocalChanged: false,
hasRemoteChanged: true,
remoteUserData: null
remoteContent: content,
remoteUserData,
}));
await this.apply();
await this.apply(undefined, true);
}
// No local exists to push
@@ -173,20 +162,15 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
}
}
async sync(_continue?: boolean): Promise<boolean> {
async sync(): Promise<void> {
if (!this.configurationService.getValue<boolean>('sync.enableSettings')) {
this.logService.trace('Settings: Skipping synchronizing settings as it is disabled.');
return false;
}
if (_continue) {
this.logService.info('Settings: Resumed synchronizing settings');
return this.continueSync();
return;
}
if (this.status !== SyncStatus.Idle) {
this.logService.trace('Settings: Skipping synchronizing settings as it is running already.');
return false;
return;
}
this.logService.trace('Settings: Started synchronizing settings...');
@@ -194,26 +178,16 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
return this.doSync([]);
}
stop(): void {
async stop(): Promise<void> {
if (this.syncPreviewResultPromise) {
this.syncPreviewResultPromise.cancel();
this.syncPreviewResultPromise = null;
this.logService.info('Settings: Stopped synchronizing settings.');
}
this.fileService.del(this.environmentService.settingsSyncPreviewResource);
await 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 localFileContent = await this.getLocalFileContent();
@@ -233,7 +207,43 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
return false;
}
async resolveConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<void> {
async getRemoteContent(): Promise<string | null> {
let content: string | null | undefined = null;
if (this.syncPreviewResultPromise) {
const preview = await this.syncPreviewResultPromise;
content = preview.remoteUserData?.content;
} else {
const remoteUserData = await this.getRemoteUserData();
content = remoteUserData.content;
}
return content !== undefined ? content : null;
}
async restart(): Promise<void> {
if (this.status === SyncStatus.HasConflicts) {
this.syncPreviewResultPromise!.cancel();
this.syncPreviewResultPromise = null;
await this.doSync([]);
}
}
async resolveConflicts(content: string): Promise<void> {
if (this.status === SyncStatus.HasConflicts) {
try {
await this.apply(content, true);
this.setStatus(SyncStatus.Idle);
} catch (e) {
this.logService.error(e);
if ((e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) ||
(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) {
throw new Error('New local version available.');
}
throw e;
}
}
}
async resolveSettingsConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<void> {
if (this.status === SyncStatus.HasConflicts) {
this.syncPreviewResultPromise!.cancel();
this.syncPreviewResultPromise = null;
@@ -241,36 +251,30 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
}
}
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> {
private async doSync(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<void> {
try {
const result = await this.getPreview(resolvedConflicts);
if (result.conflicts.length) {
if (result.hasConflicts) {
this.logService.info('Settings: Detected conflicts while synchronizing settings.');
this.setStatus(SyncStatus.HasConflicts);
return false;
return;
}
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);
if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Rejected) {
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.Rejected) {
// Rejected as there is a new remote version. Syncing again,
this.logService.info('Settings: Failed to synchronise settings as there is a new remote version available. Synchronizing again...');
return this.sync();
}
if (e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) {
if ((e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) ||
(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) {
// Rejected as there is a new local version. Syncing again.
this.logService.info('Settings: Failed to synchronise settings as there is a new local version available. Synchronizing again...');
return this.sync();
@@ -279,22 +283,20 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
}
}
private async continueSync(): Promise<boolean> {
if (this.status === SyncStatus.HasConflicts) {
await this.apply();
this.setStatus(SyncStatus.Idle);
}
return true;
}
private async apply(): Promise<void> {
private async apply(content?: string, forcePush?: boolean): Promise<void> {
if (!this.syncPreviewResultPromise) {
return;
}
if (await this.fileService.exists(this.environmentService.settingsSyncPreviewResource)) {
const settingsPreivew = await this.fileService.readFile(this.environmentService.settingsSyncPreviewResource);
const content = settingsPreivew.value.toString();
if (content === undefined) {
if (await this.fileService.exists(this.environmentService.settingsSyncPreviewResource)) {
const settingsPreivew = await this.fileService.readFile(this.environmentService.settingsSyncPreviewResource);
content = settingsPreivew.value.toString();
}
}
if (content !== undefined) {
if (this.hasErrors(content)) {
const error = new Error(localize('errorInvalidSettings', "Unable to sync settings. Please resolve conflicts without any errors/warnings and try again."));
this.logService.error(error);
@@ -307,18 +309,18 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
}
if (hasLocalChanged) {
this.logService.info('Settings: Updating local settings');
await this.writeToLocal(content, fileContent);
await this.updateLocalFileContent(content, fileContent);
}
if (hasRemoteChanged) {
const formatUtils = await this.getFormattingOptions();
const remoteContent = remoteUserData?.content ? updateIgnoredSettings(content, remoteUserData.content, getIgnoredSettings(this.configurationService, content), formatUtils) : content;
const remoteContent = updateIgnoredSettings(content, remoteUserData.content || '{}', getIgnoredSettings(this.configurationService, content), formatUtils);
this.logService.info('Settings: Updating remote settings');
const ref = await this.writeToRemote(remoteContent, remoteUserData ? remoteUserData.ref : null);
const ref = await this.updateRemoteUserData(remoteContent, forcePush ? null : remoteUserData.ref);
remoteUserData = { ref, content };
}
if (remoteUserData?.content) {
if (remoteUserData.content) {
this.logService.info('Settings: Updating last synchronised sttings');
await this.updateLastSyncValue(remoteUserData);
await this.updateLastSyncUserData(remoteUserData);
}
// Delete the preview
@@ -346,15 +348,16 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
private async generatePreview(resolvedConflicts: { key: string, value: any }[], token: CancellationToken): Promise<ISyncPreviewResult> {
const lastSyncData = await this.getLastSyncUserData();
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 conflicts: IConflictSetting[] = [];
let previewContent = null;
let hasConflicts: boolean = false;
let conflictSettings: IConflictSetting[] = [];
let previewContent: string | null = null;
let remoteContent: string | null = null;
if (remoteContent) {
if (remoteUserData.content) {
const localContent: string = fileContent ? fileContent.value.toString() : '{}';
// No action when there are errors
@@ -362,20 +365,16 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
this.logService.error('Settings: Unable to sync settings as there are errors/warning in settings file.');
}
else if (!lastSyncData // First time sync
|| lastSyncData.content !== localContent // Local has forwarded
|| lastSyncData.content !== remoteContent // Remote has forwarded
) {
else {
this.logService.trace('Settings: Merging remote settings with local settings...');
const formatUtils = await this.getFormattingOptions();
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;
conflicts = result.conflicts;
previewContent = result.mergeContent;
}
const result = merge(localContent, remoteUserData.content, lastSyncData ? lastSyncData.content : null, getIgnoredSettings(this.configurationService), resolvedConflicts, formatUtils);
hasConflicts = result.hasConflicts;
hasLocalChanged = result.localContent !== null;
hasRemoteChanged = result.remoteContent !== null;
conflictSettings = result.conflictsSettings;
remoteContent = result.remoteContent;
previewContent = result.localContent || result.remoteContent;
}
}
@@ -384,14 +383,15 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
this.logService.info('Settings: Remote settings does not exist. Synchronizing settings for the first time.');
hasRemoteChanged = true;
previewContent = fileContent.value.toString();
remoteContent = fileContent.value.toString();
}
if (previewContent && !token.isCancellationRequested) {
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(previewContent));
}
this.setConflicts(conflicts);
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, conflicts };
this.setConflicts(conflictSettings);
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, remoteContent, conflictSettings, hasConflicts };
}
private _formattingOptions: Promise<FormattingOptions> | undefined = undefined;
@@ -402,46 +402,6 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
return this._formattingOptions;
}
private async getLastSyncUserData(): Promise<IUserData | null> {
try {
const content = await this.fileService.readFile(this.lastSyncSettingsResource);
return JSON.parse(content.value.toString());
} catch (error) {
return null;
}
}
private async getLocalFileContent(): Promise<IFileContent | null> {
try {
return await this.fileService.readFile(this.environmentService.settingsResource);
} catch (error) {
return null;
}
}
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);
}
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
await this.fileService.createFile(this.environmentService.settingsResource, VSBuffer.fromString(newContent), { overwrite: false });
}
}
private async updateLastSyncValue(remoteUserData: IUserData): Promise<void> {
await this.fileService.writeFile(this.lastSyncSettingsResource, VSBuffer.fromString(JSON.stringify(remoteUserData)));
}
}
export function getIgnoredSettings(configurationService: IConfigurationService, settingsContent?: string): string[] {

View File

@@ -4,16 +4,20 @@
*--------------------------------------------------------------------------------------------*/
import { timeout } from 'vs/base/common/async';
import { Event } from 'vs/base/common/event';
import { Event, Emitter } 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';
import { IUserDataSyncLogService, IUserDataSyncService, SyncStatus, IUserDataAuthTokenService, IUserDataAutoSyncService, IUserDataSyncUtilService, UserDataSyncError, UserDataSyncErrorCode, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
export class UserDataAutoSync extends Disposable implements IUserDataAutoSyncService {
_serviceBrand: any;
private enabled: boolean = false;
private successiveFailures: number = 0;
private readonly _onError: Emitter<{ code: UserDataSyncErrorCode, source?: SyncSource }> = this._register(new Emitter<{ code: UserDataSyncErrorCode, source?: SyncSource }>());
readonly onError: Event<{ code: UserDataSyncErrorCode, source?: SyncSource }> = this._onError.event;
constructor(
@IConfigurationService private readonly configurationService: IConfigurationService,
@@ -30,20 +34,21 @@ export class UserDataAutoSync extends Disposable implements IUserDataAutoSyncSer
}
private async updateEnablement(stopIfDisabled: boolean, auto: boolean): Promise<void> {
const enabled = await this.isSyncEnabled();
const enabled = await this.isAutoSyncEnabled();
if (this.enabled === enabled) {
return;
}
this.enabled = enabled;
if (this.enabled) {
this.logService.info('Syncing configuration started');
this.logService.info('Auto sync started');
this.sync(true, auto);
return;
} else {
this.successiveFailures = 0;
if (stopIfDisabled) {
this.userDataSyncService.stop();
this.logService.info('Syncing configuration stopped.');
this.logService.info('Auto sync stopped.');
}
}
@@ -59,13 +64,23 @@ export class UserDataAutoSync extends Disposable implements IUserDataAutoSyncSer
await this.userDataSyncUtilService.updateConfigurationValue('sync.enable', false);
return;
}
if (this.userDataSyncService.status !== SyncStatus.Idle) {
this.logService.info('Skipped auto sync as sync is happening');
return;
}
}
await this.userDataSyncService.sync();
this.successiveFailures = 0;
} catch (e) {
this.successiveFailures++;
this.logService.error(e);
this._onError.fire(e instanceof UserDataSyncError ? { code: e.code, source: e.source } : { code: UserDataSyncErrorCode.Unknown });
}
if (this.successiveFailures > 5) {
this._onError.fire({ code: UserDataSyncErrorCode.TooManyFailures });
}
if (loop) {
await timeout(1000 * 60 * 5); // Loop sync for every 5 min.
await timeout(1000 * 60 * 5 * (this.successiveFailures + 1)); // Loop sync for every (successive failures count + 1) times 5 mins interval.
this.sync(loop, true);
}
}
@@ -77,7 +92,7 @@ export class UserDataAutoSync extends Disposable implements IUserDataAutoSyncSer
return !hasRemote && hasPreviouslySynced;
}
private async isSyncEnabled(): Promise<boolean> {
private async isAutoSyncEnabled(): Promise<boolean> {
return this.configurationService.getValue<boolean>('sync.enable')
&& this.userDataSyncService.status !== SyncStatus.Uninitialized
&& !!(await this.userDataAuthTokenService.getToken());

View File

@@ -18,6 +18,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import { URI } from 'vs/base/common/uri';
import { isEqual } from 'vs/base/common/resources';
export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store';
@@ -122,15 +123,18 @@ export interface IUserData {
content: string | null;
}
export enum UserDataSyncStoreErrorCode {
export enum UserDataSyncErrorCode {
TooLarge = 'TooLarge',
Unauthroized = 'Unauthroized',
Rejected = 'Rejected',
Unknown = 'Unknown'
Unknown = 'Unknown',
TooManyFailures = 'TooManyFailures',
ConnectionRefused = 'ConnectionRefused'
}
export class UserDataSyncStoreError extends Error {
export class UserDataSyncError extends Error {
constructor(message: string, public readonly code: UserDataSyncStoreErrorCode) {
constructor(message: string, public readonly code: UserDataSyncErrorCode, public readonly source?: SyncSource) {
super(message);
}
@@ -138,22 +142,20 @@ export class UserDataSyncStoreError extends Error {
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.authenticationProviderId ? value : undefined;
return value && value.url && 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>;
read(key: string, oldValue: IUserData | null, source?: SyncSource): Promise<IUserData>;
write(key: string, content: string, ref: string | null, source?: SyncSource): Promise<string>;
clear(): Promise<void>;
}
@@ -172,7 +174,7 @@ export const enum SyncSource {
Settings = 'Settings',
Keybindings = 'Keybindings',
Extensions = 'Extensions',
UIState = 'UI State'
GlobalState = 'GlobalState'
}
export const enum SyncStatus {
@@ -188,14 +190,21 @@ export interface ISynchroniser {
readonly onDidChangeLocal: Event<void>;
pull(): Promise<void>;
push(): Promise<void>;
sync(_continue?: boolean): Promise<boolean>;
stop(): void;
sync(): Promise<void>;
stop(): Promise<void>;
restart(): Promise<void>;
hasPreviouslySynced(): Promise<boolean>
hasRemoteData(): Promise<boolean>;
hasLocalData(): Promise<boolean>;
resetLocal(): Promise<void>;
}
export interface IUserDataSynchroniser extends ISynchroniser {
readonly source: SyncSource;
getRemoteContent(): Promise<string | null>;
resolveConflicts(content: string): Promise<void>;
}
export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUserDataSyncService');
export interface IUserDataSyncService extends ISynchroniser {
_serviceBrand: any;
@@ -203,12 +212,14 @@ export interface IUserDataSyncService extends ISynchroniser {
isFirstTimeSyncAndHasUserData(): Promise<boolean>;
reset(): Promise<void>;
resetLocal(): Promise<void>;
removeExtension(identifier: IExtensionIdentifier): Promise<void>;
getRemoteContent(source: SyncSource): Promise<string | null>;
resolveConflictsAndContinueSync(content: string): Promise<void>;
}
export const IUserDataAutoSyncService = createDecorator<IUserDataAutoSyncService>('IUserDataAutoSyncService');
export interface IUserDataAutoSyncService {
_serviceBrand: any;
onError: Event<{ code: UserDataSyncErrorCode, source?: SyncSource }>;
triggerAutoSync(): Promise<void>;
}
@@ -218,7 +229,6 @@ export interface IUserDataSyncUtilService {
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');
@@ -242,11 +252,19 @@ export interface IConflictSetting {
}
export const ISettingsSyncService = createDecorator<ISettingsSyncService>('ISettingsSyncService');
export interface ISettingsSyncService extends ISynchroniser {
export interface ISettingsSyncService extends IUserDataSynchroniser {
_serviceBrand: any;
readonly onDidChangeConflicts: Event<IConflictSetting[]>;
readonly conflicts: IConflictSetting[];
resolveConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<void>;
resolveSettingsConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<void>;
}
export const CONTEXT_SYNC_STATE = new RawContextKey<string>('syncStatus', SyncStatus.Uninitialized);
export const USER_DATA_SYNC_SCHEME = 'vscode-userdata-sync';
export function toRemoteContentResource(source: SyncSource): URI {
return URI.from({ scheme: USER_DATA_SYNC_SCHEME, path: `${source}/remoteContent` });
}
export function getSyncSourceFromRemoteContentResource(uri: URI): SyncSource | undefined {
return [SyncSource.Settings, SyncSource.Keybindings, SyncSource.Extensions, SyncSource.GlobalState].filter(source => isEqual(uri, toRemoteContentResource(source)))[0];
}

View File

@@ -9,7 +9,6 @@ import { IUserDataSyncService, IUserDataSyncUtilService, ISettingsSyncService, I
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,18 +24,20 @@ 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 'sync': return this.service.sync();
case 'resolveConflictsAndContinueSync': return this.service.resolveConflictsAndContinueSync(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 'restart': return this.service.restart().then(() => this.service.status);
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 'getRemoteContent': return this.service.getRemoteContent(args[0]);
case 'isFirstTimeSyncAndHasUserData': return this.service.isFirstTimeSyncAndHasUserData();
}
throw new Error('Invalid call');
@@ -58,9 +59,11 @@ export class SettingsSyncChannel implements IServerChannel {
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'sync': return this.service.sync(args[0]);
case 'sync': return this.service.sync();
case 'resolveConflicts': return this.service.resolveConflicts(args[0]);
case 'pull': return this.service.pull();
case 'push': return this.service.push();
case 'restart': return this.service.restart().then(() => this.service.status);
case '_getInitialStatus': return Promise.resolve(this.service.status);
case '_getInitialConflicts': return Promise.resolve(this.service.conflicts);
case 'stop': this.service.stop(); return Promise.resolve();
@@ -68,7 +71,8 @@ export class SettingsSyncChannel implements IServerChannel {
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]);
case 'resolveSettingsConflicts': return this.service.resolveSettingsConflicts(args[0]);
case 'getRemoteContent': return this.service.getRemoteContent();
}
throw new Error('Invalid call');
}
@@ -79,6 +83,9 @@ export class UserDataAutoSyncChannel implements IServerChannel {
constructor(private readonly service: IUserDataAutoSyncService) { }
listen(_: unknown, event: string): Event<any> {
switch (event) {
case 'onError': return this.service.onError;
}
throw new Error(`Event not found: ${event}`);
}
@@ -122,7 +129,6 @@ export class UserDataSycnUtilServiceChannel implements IServerChannel {
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');
}
@@ -147,9 +153,5 @@ export class UserDataSyncUtilServiceClient implements IUserDataSyncUtilService {
return this.channel.call('updateConfigurationValue', [key, value]);
}
async ignoreExtensionsToSync(extensionIdentifiers: IExtensionIdentifier[]): Promise<void> {
return this.channel.call('ignoreExtensionsToSync', [extensionIdentifiers]);
}
}

View File

@@ -3,22 +3,27 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource, ISettingsSyncService, IUserDataSyncLogService, IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource, ISettingsSyncService, IUserDataSyncLogService, IUserDataAuthTokenService, IUserDataSynchroniser } 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 { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync';
import { GlobalStateSynchroniser } from 'vs/platform/userDataSync/common/globalStateSync';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { localize } from 'vs/nls';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
type SyncConflictsClassification = {
source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
export class UserDataSyncService extends Disposable implements IUserDataSyncService {
_serviceBrand: any;
private readonly synchronisers: ISynchroniser[];
private readonly synchronisers: IUserDataSynchroniser[];
private _status: SyncStatus = SyncStatus.Uninitialized;
get status(): SyncStatus { return this._status; }
@@ -40,6 +45,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
@ISettingsSyncService private readonly settingsSynchroniser: ISettingsSyncService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IUserDataAuthTokenService private readonly userDataAuthTokenService: IUserDataAuthTokenService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
) {
super();
this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser));
@@ -88,31 +94,57 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
}
async sync(_continue?: boolean): Promise<boolean> {
async sync(): 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.');
}
if (this.status === SyncStatus.HasConflicts) {
throw new Error(localize('resolve conflicts', "Please resolve conflicts before resuming sync."));
}
for (const synchroniser of this.synchronisers) {
try {
if (!await synchroniser.sync(_continue)) {
return false;
await synchroniser.sync();
// do not continue if synchroniser has conflicts
if (synchroniser.status === SyncStatus.HasConflicts) {
return;
}
} catch (e) {
this.logService.error(`${this.getSyncSource(synchroniser)}: ${toErrorMessage(e)}`);
}
}
return true;
}
stop(): void {
async resolveConflictsAndContinueSync(content: string): Promise<void> {
const synchroniser = this.getSynchroniserInConflicts();
if (!synchroniser) {
throw new Error(localize('no synchroniser with conflicts', "No conflicts detected."));
}
await synchroniser.resolveConflicts(content);
if (synchroniser.status !== SyncStatus.HasConflicts) {
await this.sync();
}
}
async stop(): Promise<void> {
if (!this.userDataSyncStoreService.userDataSyncStore) {
throw new Error('Not enabled');
}
for (const synchroniser of this.synchronisers) {
synchroniser.stop();
await synchroniser.stop();
}
}
async restart(): Promise<void> {
const synchroniser = this.getSynchroniserInConflicts();
if (!synchroniser) {
throw new Error(localize('no synchroniser with conflicts', "No conflicts detected."));
}
await synchroniser.restart();
if (synchroniser.status !== SyncStatus.HasConflicts) {
await this.sync();
}
}
@@ -161,6 +193,15 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return false;
}
async getRemoteContent(source: SyncSource): Promise<string | null> {
for (const synchroniser of this.synchronisers) {
if (synchroniser.source === source) {
return synchroniser.getRemoteContent();
}
}
return null;
}
async isFirstTimeSyncAndHasUserData(): Promise<boolean> {
if (!this.userDataSyncStoreService.userDataSyncStore) {
throw new Error('Not enabled');
@@ -211,18 +252,21 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.logService.info('Completed resetting local cache');
}
removeExtension(identifier: IExtensionIdentifier): Promise<void> {
return this.extensionsSynchroniser.removeExtension(identifier);
}
private updateStatus(): void {
this._conflictsSource = this.computeConflictsSource();
this.setStatus(this.computeStatus());
}
private setStatus(status: SyncStatus): void {
const status = this.computeStatus();
if (this._status !== status) {
const oldStatus = this._status;
const oldConflictsSource = this._conflictsSource;
this._conflictsSource = this.computeConflictsSource();
this._status = status;
if (status === SyncStatus.HasConflicts) {
// Log to telemetry when there is a sync conflict
this.telemetryService.publicLog2<{ source: string }, SyncConflictsClassification>('sync/conflictsDetected', { source: this._conflictsSource! });
}
if (oldStatus === SyncStatus.HasConflicts && status === SyncStatus.Idle) {
// Log to telemetry when conflicts are resolved
this.telemetryService.publicLog2<{ source: string }, SyncConflictsClassification>('sync/conflictsResolved', { source: oldConflictsSource! });
}
this._onDidChangeStatus.fire(status);
}
}
@@ -245,6 +289,11 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return synchroniser ? this.getSyncSource(synchroniser) : null;
}
private getSynchroniserInConflicts(): IUserDataSynchroniser | null {
const synchroniser = this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)[0];
return synchroniser || null;
}
private getSyncSource(synchroniser: ISynchroniser): SyncSource {
if (synchroniser instanceof SettingsSynchroniser) {
return SyncSource.Settings;
@@ -255,7 +304,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
if (synchroniser instanceof ExtensionsSynchroniser) {
return SyncSource.Extensions;
}
return SyncSource.UIState;
return SyncSource.GlobalState;
}
private onDidChangeAuthTokenStatus(token: string | undefined): void {

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable, } from 'vs/base/common/lifecycle';
import { IUserData, IUserDataSyncStoreService, UserDataSyncStoreErrorCode, UserDataSyncStoreError, IUserDataSyncStore, getUserDataSyncStore, IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncStore, getUserDataSyncStore, IUserDataAuthTokenService, SyncSource } 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';
@@ -27,7 +27,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
this.userDataSyncStore = getUserDataSyncStore(configurationService);
}
async read(key: string, oldValue: IUserData | null): Promise<IUserData> {
async read(key: string, oldValue: IUserData | null, source?: SyncSource): Promise<IUserData> {
if (!this.userDataSyncStore) {
throw new Error('No settings sync store url configured.');
}
@@ -40,7 +40,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
headers['If-None-Match'] = oldValue.ref;
}
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
const context = await this.request({ type: 'GET', url, headers }, source, CancellationToken.None);
if (context.res.statusCode === 304) {
// There is no new value. Hence return the old value.
@@ -59,7 +59,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
return { ref, content };
}
async write(key: string, data: string, ref: string | null): Promise<string> {
async write(key: string, data: string, ref: string | null, source?: SyncSource): Promise<string> {
if (!this.userDataSyncStore) {
throw new Error('No settings sync store url configured.');
}
@@ -70,12 +70,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
headers['If-Match'] = ref;
}
const context = await this.request({ type: 'POST', url, data, headers }, CancellationToken.None);
if (context.res.statusCode === 412) {
// There is a new value. Throw Rejected Error
throw new UserDataSyncStoreError('New data exists', UserDataSyncStoreErrorCode.Rejected);
}
const context = await this.request({ type: 'POST', url, data, headers }, source, CancellationToken.None);
if (!isSuccess(context)) {
throw new Error('Server returned ' + context.res.statusCode);
@@ -96,14 +91,14 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
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);
const context = await this.request({ type: 'DELETE', url, headers }, undefined, CancellationToken.None);
if (!isSuccess(context)) {
throw new Error('Server returned ' + context.res.statusCode);
}
}
private async request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
private async request(options: IRequestOptions, source: SyncSource | undefined, token: CancellationToken): Promise<IRequestContext> {
const authToken = await this.authTokenService.getToken();
if (!authToken) {
throw new Error('No Auth Token Available.');
@@ -111,15 +106,30 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
options.headers = options.headers || {};
options.headers['authorization'] = `Bearer ${authToken}`;
const context = await this.requestService.request(options, token);
let context;
try {
context = await this.requestService.request(options, token);
} catch (e) {
throw new UserDataSyncError(`Connection refused for the request '${options.url?.toString()}'.`, UserDataSyncErrorCode.ConnectionRefused, source);
}
if (context.res.statusCode === 401) {
// Throw Unauthorized Error
throw new UserDataSyncStoreError('Unauthorized', UserDataSyncStoreErrorCode.Unauthroized);
throw new UserDataSyncError(`Request '${options.url?.toString()}' is not authorized.`, UserDataSyncErrorCode.Unauthroized, source);
}
if (context.res.statusCode === 412) {
// There is a new value. Throw Rejected Error
throw new UserDataSyncError(`${options.type} request '${options.url?.toString()}' failed with precondition. There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.Rejected, source);
}
if (context.res.statusCode === 413) {
// Throw Too Large Payload Error
throw new UserDataSyncError(`${options.type} request '${options.url?.toString()}' failed because data is too large.`, UserDataSyncErrorCode.TooLarge, source);
}
return context;
}
}