Merge from vscode 4d91d96e5e121b38d33508cdef17868bab255eae

This commit is contained in:
ADS Merger
2020-06-18 04:32:54 +00:00
committed by AzureDataStudio
parent a971aee5bd
commit 5e7071e466
1002 changed files with 24201 additions and 13193 deletions

View File

@@ -7,10 +7,13 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService, IFileContent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files';
import { VSBuffer } from 'vs/base/common/buffer';
import { URI } from 'vs/base/common/uri';
import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncPreviewResult, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync';
import {
SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService,
IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncPreview, IUserDataManifest, ISyncData, IRemoteUserData
} from 'vs/platform/userDataSync/common/userDataSync';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources';
import { CancelablePromise, RunOnceScheduler } from 'vs/base/common/async';
import { CancelablePromise, RunOnceScheduler, createCancelablePromise } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ParseError, parse } from 'vs/base/common/json';
@@ -23,22 +26,13 @@ import { uppercaseFirstLetter } from 'vs/base/common/strings';
import { equals } from 'vs/base/common/arrays';
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IHeaders } from 'vs/base/parts/request/common/request';
type SyncSourceClassification = {
source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
export interface IRemoteUserData {
ref: string;
syncData: ISyncData | null;
}
export interface ISyncData {
version: number;
machineId?: string;
content: string;
}
function isSyncData(thing: any): thing is ISyncData {
if (thing
&& (thing.version !== undefined && typeof thing.version === 'number')
@@ -58,9 +52,10 @@ function isSyncData(thing: any): thing is ISyncData {
return false;
}
export abstract class AbstractSynchroniser extends Disposable {
private syncPreviewPromise: CancelablePromise<ISyncPreview> | null = null;
protected readonly syncFolder: URI;
private readonly currentMachineIdPromise: Promise<string>;
@@ -81,6 +76,8 @@ export abstract class AbstractSynchroniser extends Disposable {
protected readonly lastSyncResource: URI;
protected readonly syncResourceLogLabel: string;
private syncHeaders: IHeaders = {};
constructor(
readonly resource: SyncResource,
@IFileService protected readonly fileService: IFileService,
@@ -88,7 +85,7 @@ export abstract class AbstractSynchroniser extends Disposable {
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncBackupStoreService protected readonly userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IUserDataSyncEnablementService protected readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
@IUserDataSyncResourceEnablementService protected readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
@ITelemetryService protected readonly telemetryService: ITelemetryService,
@IUserDataSyncLogService protected readonly logService: IUserDataSyncLogService,
@IConfigurationService protected readonly configurationService: IConfigurationService,
@@ -100,6 +97,8 @@ export abstract class AbstractSynchroniser extends Disposable {
this.currentMachineIdPromise = getServiceMachineId(environmentService, fileService, storageService);
}
protected isEnabled(): boolean { return this.userDataSyncResourceEnablementService.isResourceEnabled(this.resource); }
protected async triggerLocalChange(): Promise<void> {
if (this.isEnabled()) {
this.localChangeTriggerScheduler.schedule();
@@ -107,11 +106,24 @@ export abstract class AbstractSynchroniser extends Disposable {
}
protected async doTriggerLocalChange(): Promise<void> {
this.logService.trace(`${this.syncResourceLogLabel}: Checking for local changes...`);
const lastSyncUserData = await this.getLastSyncUserData();
const hasRemoteChanged = lastSyncUserData ? (await this.generatePreview(lastSyncUserData, lastSyncUserData)).hasRemoteChanged : true;
if (hasRemoteChanged) {
this._onDidChangeLocal.fire();
// Sync again if current status is in conflicts
if (this.status === SyncStatus.HasConflicts) {
this.logService.info(`${this.syncResourceLogLabel}: In conflicts state and local change detected. Syncing again...`);
const preview = await this.syncPreviewPromise!;
this.syncPreviewPromise = null;
const status = await this.performSync(preview.remoteUserData, preview.lastSyncUserData);
this.setStatus(status);
}
// Check if local change causes remote change
else {
this.logService.trace(`${this.syncResourceLogLabel}: Checking for local changes...`);
const lastSyncUserData = await this.getLastSyncUserData();
const hasRemoteChanged = lastSyncUserData ? (await this.generatePreview(lastSyncUserData, lastSyncUserData, CancellationToken.None)).hasRemoteChanged : true;
if (hasRemoteChanged) {
this._onDidChangeLocal.fire();
}
}
}
@@ -141,41 +153,91 @@ export abstract class AbstractSynchroniser extends Disposable {
}
}
protected isEnabled(): boolean { return this.userDataSyncEnablementService.isResourceEnabled(this.resource); }
async sync(manifest: IUserDataManifest | null): Promise<void> {
async pull(): Promise<void> {
if (!this.isEnabled()) {
if (this.status !== SyncStatus.Idle) {
await this.stop();
}
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is disabled.`);
return;
}
if (this.status === SyncStatus.HasConflicts) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as there are conflicts.`);
return;
}
if (this.status === SyncStatus.Syncing) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is running already.`);
this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling ${this.syncResourceLogLabel.toLowerCase()} as it is disabled.`);
return;
}
this.logService.trace(`${this.syncResourceLogLabel}: Started synchronizing ${this.resource.toLowerCase()}...`);
this.setStatus(SyncStatus.Syncing);
await this.stop();
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData);
let status: SyncStatus = SyncStatus.Idle;
try {
status = await this.doSync(remoteUserData, lastSyncUserData);
if (status === SyncStatus.HasConflicts) {
this.logService.info(`${this.syncResourceLogLabel}: Detected conflicts while synchronizing ${this.resource.toLowerCase()}.`);
} else if (status === SyncStatus.Idle) {
this.logService.trace(`${this.syncResourceLogLabel}: Finished synchronizing ${this.resource.toLowerCase()}.`);
this.logService.info(`${this.syncResourceLogLabel}: Started pulling ${this.syncResourceLogLabel.toLowerCase()}...`);
this.setStatus(SyncStatus.Syncing);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
const preview = await this.generatePullPreview(remoteUserData, lastSyncUserData, CancellationToken.None);
await this.applyPreview(preview, false);
this.logService.info(`${this.syncResourceLogLabel}: Finished pulling ${this.syncResourceLogLabel.toLowerCase()}.`);
} finally {
this.setStatus(SyncStatus.Idle);
}
}
async push(): Promise<void> {
if (!this.isEnabled()) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped pushing ${this.syncResourceLogLabel.toLowerCase()} as it is disabled.`);
return;
}
this.stop();
try {
this.logService.info(`${this.syncResourceLogLabel}: Started pushing ${this.syncResourceLogLabel.toLowerCase()}...`);
this.setStatus(SyncStatus.Syncing);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
const preview = await this.generatePushPreview(remoteUserData, lastSyncUserData, CancellationToken.None);
await this.applyPreview(preview, true);
this.logService.info(`${this.syncResourceLogLabel}: Finished pushing ${this.syncResourceLogLabel.toLowerCase()}.`);
} finally {
this.setStatus(SyncStatus.Idle);
}
}
async sync(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise<void> {
try {
this.syncHeaders = { ...headers };
if (!this.isEnabled()) {
if (this.status !== SyncStatus.Idle) {
await this.stop();
}
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is disabled.`);
return;
}
if (this.status === SyncStatus.HasConflicts) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as there are conflicts.`);
return;
}
if (this.status === SyncStatus.Syncing) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is running already.`);
return;
}
this.logService.trace(`${this.syncResourceLogLabel}: Started synchronizing ${this.resource.toLowerCase()}...`);
this.setStatus(SyncStatus.Syncing);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData);
let status: SyncStatus = SyncStatus.Idle;
try {
status = await this.performSync(remoteUserData, lastSyncUserData);
if (status === SyncStatus.HasConflicts) {
this.logService.info(`${this.syncResourceLogLabel}: Detected conflicts while synchronizing ${this.resource.toLowerCase()}.`);
} else if (status === SyncStatus.Idle) {
this.logService.trace(`${this.syncResourceLogLabel}: Finished synchronizing ${this.resource.toLowerCase()}.`);
}
} finally {
this.setStatus(status);
}
} finally {
this.setStatus(status);
this.syncHeaders = {};
}
}
@@ -197,7 +259,8 @@ export abstract class AbstractSynchroniser extends Disposable {
this.setStatus(SyncStatus.Syncing);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getLatestRemoteUserData(null, lastSyncUserData);
await this.performReplace(syncData, remoteUserData, lastSyncUserData);
const preview = await this.generateReplacePreview(syncData, remoteUserData, lastSyncUserData);
await this.applyPreview(preview, false);
this.logService.info(`${this.syncResourceLogLabel}: Finished resetting ${this.resource.toLowerCase()}.`);
} finally {
this.setStatus(SyncStatus.Idle);
@@ -224,28 +287,33 @@ export abstract class AbstractSynchroniser extends Disposable {
return this.getRemoteUserData(lastSyncUserData);
}
async getSyncPreview(): Promise<ISyncPreviewResult> {
if (!this.isEnabled()) {
return { hasLocalChanged: false, hasRemoteChanged: false };
async generateSyncPreview(): Promise<ISyncPreview | null> {
if (this.isEnabled()) {
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
return this.generatePreview(remoteUserData, lastSyncUserData, CancellationToken.None);
}
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
return this.generatePreview(remoteUserData, lastSyncUserData);
return null;
}
protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
private async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) {
// current version is not compatible with cloud version
this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/incompatible', { source: this.resource });
throw new UserDataSyncError(localize({ key: 'incompatible', comment: ['This is an error while syncing a resource that its local version is not compatible with its remote version.'] }, "Cannot sync {0} as its local version {1} is not compatible with its remote version {2}", this.resource, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.Incompatible, this.resource);
}
try {
const status = await this.performSync(remoteUserData, lastSyncUserData);
return status;
return await this.doSync(remoteUserData, lastSyncUserData);
} catch (e) {
if (e instanceof UserDataSyncError) {
switch (e.code) {
case UserDataSyncErrorCode.LocalPreconditionFailed:
// Rejected as there is a new local version. Syncing again...
this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize ${this.syncResourceLogLabel} as there is a new local version available. Synchronizing again...`);
return this.performSync(remoteUserData, lastSyncUserData);
case UserDataSyncErrorCode.PreconditionFailed:
// Rejected as there is a new remote version. Syncing again...
this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize as there is a new remote version available. Synchronizing again...`);
@@ -257,18 +325,79 @@ export abstract class AbstractSynchroniser extends Disposable {
// and one of them successfully updated remote and last sync state.
lastSyncUserData = await this.getLastSyncUserData();
return this.doSync(remoteUserData, lastSyncUserData);
return this.performSync(remoteUserData, lastSyncUserData);
}
}
throw e;
}
}
protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
try {
// generate or use existing preview
if (!this.syncPreviewPromise) {
this.syncPreviewPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token));
}
const preview = await this.syncPreviewPromise;
if (preview.hasConflicts) {
return SyncStatus.HasConflicts;
}
// apply preview
await this.applyPreview(preview, false);
// reset preview
this.syncPreviewPromise = null;
return SyncStatus.Idle;
} catch (error) {
// reset preview on error
this.syncPreviewPromise = null;
throw error;
}
}
protected async getSyncPreviewInProgress(): Promise<ISyncPreview | null> {
return this.syncPreviewPromise ? this.syncPreviewPromise : null;
}
async acceptConflict(conflictUri: URI, conflictContent: string): Promise<void> {
let preview = await this.getSyncPreviewInProgress();
if (!preview || !preview.hasConflicts) {
return;
}
this.syncPreviewPromise = createCancelablePromise(token => this.updatePreviewWithConflict(preview!, conflictUri, conflictContent, token));
preview = await this.syncPreviewPromise;
if (!preview.hasConflicts) {
// apply preview
await this.applyPreview(preview, false);
// reset preview
this.syncPreviewPromise = null;
this.setStatus(SyncStatus.Idle);
}
}
async hasPreviouslySynced(): Promise<boolean> {
const lastSyncData = await this.getLastSyncUserData();
return !!lastSyncData;
}
protected async isLastSyncFromCurrentMachine(remoteUserData: IRemoteUserData): Promise<boolean> {
const machineId = await this.currentMachineIdPromise;
return !!remoteUserData.syncData?.machineId && remoteUserData.syncData.machineId === machineId;
}
async getRemoteSyncResourceHandles(): Promise<ISyncResourceHandle[]> {
const handles = await this.userDataSyncStoreService.getAllRefs(this.resource);
return handles.map(({ created, ref }) => ({ created, uri: this.toRemoteBackupResource(ref) }));
@@ -373,14 +502,14 @@ export abstract class AbstractSynchroniser extends Disposable {
return { ref: refOrLastSyncData, content };
} else {
const lastSyncUserData: IUserData | null = refOrLastSyncData ? { ref: refOrLastSyncData.ref, content: refOrLastSyncData.syncData ? JSON.stringify(refOrLastSyncData.syncData) : null } : null;
return this.userDataSyncStoreService.read(this.resource, lastSyncUserData);
return this.userDataSyncStoreService.read(this.resource, lastSyncUserData, this.syncHeaders);
}
}
protected async updateRemoteUserData(content: string, ref: string | null): Promise<IRemoteUserData> {
const machineId = await this.currentMachineIdPromise;
const syncData: ISyncData = { version: this.version, machineId, content };
ref = await this.userDataSyncStoreService.write(this.resource, JSON.stringify(syncData), ref);
ref = await this.userDataSyncStoreService.write(this.resource, JSON.stringify(syncData), ref, this.syncHeaders);
return { ref, syncData };
}
@@ -389,26 +518,31 @@ export abstract class AbstractSynchroniser extends Disposable {
return this.userDataSyncBackupStoreService.backup(this.resource, JSON.stringify(syncData));
}
abstract stop(): Promise<void>;
async stop(): Promise<void> {
this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`);
if (this.syncPreviewPromise) {
this.syncPreviewPromise.cancel();
this.syncPreviewPromise = null;
}
this.setStatus(SyncStatus.Idle);
}
protected abstract readonly version: number;
protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus>;
protected abstract performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void>;
protected abstract generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncPreviewResult>;
protected abstract generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncPreview>;
protected abstract generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncPreview>;
protected abstract generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncPreview>;
protected abstract generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncPreview>;
protected abstract updatePreviewWithConflict(preview: ISyncPreview, conflictResource: URI, content: string, token: CancellationToken): Promise<ISyncPreview>;
protected abstract applyPreview(preview: ISyncPreview, forcePush: boolean): Promise<void>;
}
export interface IFileSyncPreviewResult extends ISyncPreviewResult {
export interface IFileSyncPreview extends ISyncPreview {
readonly fileContent: IFileContent | null;
readonly remoteUserData: IRemoteUserData;
readonly lastSyncUserData: IRemoteUserData | null;
readonly content: string | null;
readonly hasConflicts: boolean;
}
export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
protected syncPreviewResultPromise: CancelablePromise<IFileSyncPreviewResult> | null = null;
constructor(
protected readonly file: URI,
resource: SyncResource,
@@ -417,34 +551,32 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
@ITelemetryService telemetryService: ITelemetryService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@IConfigurationService configurationService: IConfigurationService,
) {
super(resource, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
super(resource, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, configurationService);
this._register(this.fileService.watch(dirname(file)));
this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e)));
}
async stop(): Promise<void> {
this.cancel();
this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`);
await super.stop();
try {
await this.fileService.del(this.localPreviewResource);
} catch (e) { /* ignore */ }
this.setStatus(SyncStatus.Idle);
}
protected async getConflictContent(conflictResource: URI): Promise<string | null> {
if (isEqual(this.remotePreviewResource, conflictResource) || isEqual(this.localPreviewResource, conflictResource)) {
if (this.syncPreviewResultPromise) {
const result = await this.syncPreviewResultPromise;
const syncPreview = await this.getSyncPreviewInProgress();
if (syncPreview) {
if (isEqual(this.remotePreviewResource, conflictResource)) {
return result.remoteUserData && result.remoteUserData.syncData ? result.remoteUserData.syncData.content : null;
return syncPreview.remoteUserData && syncPreview.remoteUserData.syncData ? syncPreview.remoteUserData.syncData.content : null;
}
if (isEqual(this.localPreviewResource, conflictResource)) {
return result.fileContent ? result.fileContent.value.toString() : null;
return (syncPreview as IFileSyncPreview).fileContent ? (syncPreview as IFileSyncPreview).fileContent!.value.toString() : null;
}
}
}
@@ -482,31 +614,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
if (!e.contains(this.file)) {
return;
}
if (!this.isEnabled()) {
return;
}
// Sync again if local file has changed and current status is in conflicts
if (this.status === SyncStatus.HasConflicts) {
this.syncPreviewResultPromise?.then(result => {
this.cancel();
this.doSync(result.remoteUserData, result.lastSyncUserData).then(status => this.setStatus(status));
});
}
// Otherwise fire change event
else {
this.triggerLocalChange();
}
}
protected cancel(): void {
if (this.syncPreviewResultPromise) {
this.syncPreviewResultPromise.cancel();
this.syncPreviewResultPromise = null;
}
this.triggerLocalChange();
}
protected abstract readonly localPreviewResource: URI;
@@ -523,13 +631,13 @@ export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroni
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
@ITelemetryService telemetryService: ITelemetryService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@IUserDataSyncUtilService protected readonly userDataSyncUtilService: IUserDataSyncUtilService,
@IConfigurationService configurationService: IConfigurationService,
) {
super(file, resource, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
super(file, resource, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, configurationService);
}
protected hasErrors(content: string): boolean {

View File

@@ -3,7 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, ISyncPreviewResult, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync';
import {
IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncResourceEnablementService,
IUserDataSyncBackupStoreService, ISyncResourceHandle, ISyncPreview, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData
} from 'vs/platform/userDataSync/common/userDataSync';
import { Event } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
@@ -12,8 +15,7 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens
import { IFileService } from 'vs/platform/files/common/files';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { merge, getIgnoredExtensions } from 'vs/platform/userDataSync/common/extensionsMerge';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { AbstractSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { URI } from 'vs/base/common/uri';
import { joinPath, dirname, basename, isEqual } from 'vs/base/common/resources';
@@ -21,8 +23,9 @@ import { format } from 'vs/base/common/jsonFormatter';
import { applyEdits } from 'vs/base/common/jsonEdit';
import { compare } from 'vs/base/common/strings';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { CancellationToken } from 'vs/base/common/cancellation';
interface IExtensionsSyncPreviewResult extends ISyncPreviewResult {
interface IExtensionsSyncPreview extends ISyncPreview {
readonly localExtensions: ISyncExtension[];
readonly remoteUserData: IRemoteUserData;
readonly lastSyncUserData: ILastSyncUserData | null;
@@ -37,7 +40,6 @@ interface ILastSyncUserData extends IRemoteUserData {
skippedExtensions: ISyncExtension[] | undefined;
}
export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/current.json` });
@@ -58,10 +60,10 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IConfigurationService configurationService: IConfigurationService,
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
@ITelemetryService telemetryService: ITelemetryService,
) {
super(SyncResource.Extensions, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
super(SyncResource.Extensions, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, configurationService);
this._register(
Event.debounce(
Event.any<any>(
@@ -71,77 +73,134 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
() => undefined, 500)(() => this.triggerLocalChange()));
}
async pull(): Promise<void> {
if (!this.isEnabled()) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling extensions as it is disabled.`);
return;
}
this.stop();
try {
this.logService.info(`${this.syncResourceLogLabel}: Started pulling extensions...`);
this.setStatus(SyncStatus.Syncing);
const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
if (remoteUserData.syncData !== null) {
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const remoteExtensions = await this.parseAndMigrateExtensions(remoteUserData.syncData);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const { added, updated, remote, removed } = merge(localExtensions, remoteExtensions, localExtensions, [], ignoredExtensions);
await this.apply({
added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
hasRemoteChanged: remote !== null
});
}
// No remote exists to pull
else {
this.logService.info(`${this.syncResourceLogLabel}: Remote extensions does not exist.`);
}
this.logService.info(`${this.syncResourceLogLabel}: Finished pulling extensions.`);
} finally {
this.setStatus(SyncStatus.Idle);
}
}
async push(): Promise<void> {
if (!this.isEnabled()) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped pushing extensions as it is disabled.`);
return;
}
this.stop();
try {
this.logService.info(`${this.syncResourceLogLabel}: Started pushing extensions...`);
this.setStatus(SyncStatus.Syncing);
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const { added, removed, updated, remote } = merge(localExtensions, null, null, [], ignoredExtensions);
const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
await this.apply({
added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IExtensionsSyncPreview> {
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
if (remoteUserData.syncData !== null) {
const remoteExtensions = await this.parseAndMigrateExtensions(remoteUserData.syncData);
const { added, updated, remote, removed } = merge(localExtensions, remoteExtensions, localExtensions, [], ignoredExtensions);
return {
remoteUserData, lastSyncUserData,
added, removed, updated, remote, localExtensions, skippedExtensions: [],
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
hasRemoteChanged: remote !== null
}, true);
this.logService.info(`${this.syncResourceLogLabel}: Finished pushing extensions.`);
} finally {
this.setStatus(SyncStatus.Idle);
hasRemoteChanged: remote !== null,
hasConflicts: false,
isLastSyncFromCurrentMachine: false,
};
} else {
return {
remoteUserData, lastSyncUserData,
added: [], removed: [], updated: [], remote: null, localExtensions, skippedExtensions: [],
hasLocalChanged: false,
hasRemoteChanged: false,
hasConflicts: false,
isLastSyncFromCurrentMachine: false,
};
}
}
async stop(): Promise<void> { }
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IExtensionsSyncPreview> {
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const { added, removed, updated, remote } = merge(localExtensions, null, null, [], ignoredExtensions);
return {
added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
hasRemoteChanged: remote !== null,
isLastSyncFromCurrentMachine: false,
hasConflicts: false,
};
}
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionsSyncPreview> {
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const syncExtensions = await this.parseAndMigrateExtensions(syncData);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const { added, updated, removed } = merge(localExtensions, syncExtensions, localExtensions, [], ignoredExtensions);
return {
added, removed, updated, remote: syncExtensions, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
hasRemoteChanged: true,
isLastSyncFromCurrentMachine: false,
hasConflicts: false,
};
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionsSyncPreview> {
const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null;
const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : [];
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
let lastSyncExtensions: ISyncExtension[] | null = null;
if (lastSyncUserData === null) {
if (isLastSyncFromCurrentMachine) {
lastSyncExtensions = await this.parseAndMigrateExtensions(remoteUserData.syncData!);
}
} else {
lastSyncExtensions = await this.parseAndMigrateExtensions(lastSyncUserData.syncData!);
}
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
if (remoteExtensions) {
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote extensions with local extensions...`);
} else {
this.logService.trace(`${this.syncResourceLogLabel}: Remote extensions does not exist. Synchronizing extensions for the first time.`);
}
const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions);
return {
added,
removed,
updated,
remote,
skippedExtensions,
remoteUserData,
localExtensions,
lastSyncUserData,
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
hasRemoteChanged: remote !== null,
isLastSyncFromCurrentMachine,
hasConflicts: false
};
}
protected async updatePreviewWithConflict(preview: IExtensionsSyncPreview, conflictResource: URI, content: string, token: CancellationToken): Promise<IExtensionsSyncPreview> {
throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`);
}
protected async applyPreview({ added, removed, updated, remote, remoteUserData, skippedExtensions, lastSyncUserData, localExtensions, hasLocalChanged, hasRemoteChanged }: IExtensionsSyncPreview, forcePush: boolean): Promise<void> {
if (!hasLocalChanged && !hasRemoteChanged) {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing extensions.`);
}
if (hasLocalChanged) {
await this.backupLocal(JSON.stringify(localExtensions));
skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions);
}
if (remote) {
// update remote
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote extensions...`);
const content = JSON.stringify(remote);
remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote extensions`);
}
if (lastSyncUserData?.ref !== remoteUserData.ref) {
// update last sync
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized extensions...`);
await this.updateLastSyncUserData(remoteUserData, { skippedExtensions });
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized extensions`);
}
}
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
return [{ resource: joinPath(uri, 'extensions.json'), comparableResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI }];
@@ -188,15 +247,11 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
return applyEdits(content, edits);
}
async acceptConflict(conflict: URI, content: string): Promise<void> {
throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`);
}
async hasLocalData(): Promise<boolean> {
try {
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
if (isNonEmptyArray(localExtensions)) {
if (localExtensions.some(e => e.installed || e.disabled)) {
return true;
}
} catch (error) {
@@ -205,84 +260,6 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
return false;
}
protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<SyncStatus> {
const previewResult = await this.generatePreview(remoteUserData, lastSyncUserData);
await this.apply(previewResult);
return SyncStatus.Idle;
}
protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<void> {
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const syncExtensions = await this.parseAndMigrateExtensions(syncData);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const { added, updated, removed } = merge(localExtensions, syncExtensions, localExtensions, [], ignoredExtensions);
await this.apply({
added, removed, updated, remote: syncExtensions, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
hasRemoteChanged: true
});
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionsSyncPreviewResult> {
const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null;
const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? await this.parseAndMigrateExtensions(lastSyncUserData.syncData!) : null;
const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : [];
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
if (remoteExtensions) {
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote extensions with local extensions...`);
} else {
this.logService.trace(`${this.syncResourceLogLabel}: Remote extensions does not exist. Synchronizing extensions for the first time.`);
}
const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions);
return {
added,
removed,
updated,
remote,
skippedExtensions,
remoteUserData,
localExtensions,
lastSyncUserData,
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
hasRemoteChanged: remote !== null
};
}
private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions, lastSyncUserData, localExtensions, hasLocalChanged, hasRemoteChanged }: IExtensionsSyncPreviewResult, forcePush?: boolean): Promise<void> {
if (!hasLocalChanged && !hasRemoteChanged) {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing extensions.`);
}
if (hasLocalChanged) {
await this.backupLocal(JSON.stringify(localExtensions));
skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions);
}
if (remote) {
// update remote
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote extensions...`);
const content = JSON.stringify(remote);
remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote extensions`);
}
if (lastSyncUserData?.ref !== remoteUserData.ref) {
// update last sync
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized extensions...`);
await this.updateLastSyncUserData(remoteUserData, { skippedExtensions });
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized extensions`);
}
}
private async updateLocalExtensions(added: ISyncExtension[], removed: IExtensionIdentifier[], updated: ISyncExtension[], skippedExtensions: ISyncExtension[]): Promise<ISyncExtension[]> {
const removeFromSkipped: IExtensionIdentifier[] = [];
const addToSkipped: ISyncExtension[] = [];

View File

@@ -3,7 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, ISyncPreviewResult, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync';
import {
IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncResourceEnablementService,
IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, ISyncPreview, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData
} from 'vs/platform/userDataSync/common/userDataSync';
import { VSBuffer } from 'vs/base/common/buffer';
import { Event } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
@@ -13,7 +16,7 @@ import { IStringDictionary } from 'vs/base/common/collections';
import { edit } from 'vs/platform/userDataSync/common/content';
import { merge } from 'vs/platform/userDataSync/common/globalStateMerge';
import { parse } from 'vs/base/common/json';
import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { AbstractSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { URI } from 'vs/base/common/uri';
@@ -22,11 +25,12 @@ import { applyEdits } from 'vs/base/common/jsonEdit';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageKeysSyncRegistryService, IStorageKey } from 'vs/platform/userDataSync/common/storageKeys';
import { equals } from 'vs/base/common/arrays';
import { CancellationToken } from 'vs/base/common/cancellation';
const argvStoragePrefx = 'globalState.argv.';
const argvProperties: string[] = ['locale'];
interface IGlobalSyncPreviewResult extends ISyncPreviewResult {
interface IGlobalStateSyncPreview extends ISyncPreview {
readonly local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> };
readonly remote: IStringDictionary<IStorageValue> | null;
readonly skippedStorageKeys: string[];
@@ -50,13 +54,13 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
@ITelemetryService telemetryService: ITelemetryService,
@IConfigurationService configurationService: IConfigurationService,
@IStorageService private readonly storageService: IStorageService,
@IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
) {
super(SyncResource.GlobalState, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
super(SyncResource.GlobalState, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, configurationService);
this._register(this.fileService.watch(dirname(this.environmentService.argvResource)));
this._register(
Event.any(
@@ -70,74 +74,122 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
);
}
async pull(): Promise<void> {
if (!this.isEnabled()) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling ui state as it is disabled.`);
return;
}
this.stop();
try {
this.logService.info(`${this.syncResourceLogLabel}: Started pulling ui state...`);
this.setStatus(SyncStatus.Syncing);
const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
if (remoteUserData.syncData !== null) {
const localGlobalState = await this.getLocalGlobalState();
const remoteGlobalState: IGlobalState = JSON.parse(remoteUserData.syncData.content);
const { local, remote, skipped } = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
await this.apply({
local, remote, remoteUserData, localUserData: localGlobalState, lastSyncUserData,
skippedStorageKeys: skipped,
hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0,
hasRemoteChanged: remote !== null
});
}
// No remote exists to pull
else {
this.logService.info(`${this.syncResourceLogLabel}: Remote UI state does not exist.`);
}
this.logService.info(`${this.syncResourceLogLabel}: Finished pulling UI state.`);
} finally {
this.setStatus(SyncStatus.Idle);
}
}
async push(): Promise<void> {
if (!this.isEnabled()) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped pushing UI State as it is disabled.`);
return;
}
this.stop();
try {
this.logService.info(`${this.syncResourceLogLabel}: Started pushing UI State...`);
this.setStatus(SyncStatus.Syncing);
const localUserData = await this.getLocalGlobalState();
const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
await this.apply({
local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, remoteUserData, localUserData, lastSyncUserData,
skippedStorageKeys: [],
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateSyncPreview> {
const localGlobalState = await this.getLocalGlobalState();
if (remoteUserData.syncData !== null) {
const remoteGlobalState: IGlobalState = JSON.parse(remoteUserData.syncData.content);
const { local, remote, skipped } = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
return {
remoteUserData, lastSyncUserData,
local, remote, localUserData: localGlobalState, skippedStorageKeys: skipped,
hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0,
hasRemoteChanged: remote !== null,
hasConflicts: false,
isLastSyncFromCurrentMachine: false,
};
} else {
return {
remoteUserData, lastSyncUserData,
local: { added: {}, removed: [], updated: {} }, remote: null, localUserData: localGlobalState, skippedStorageKeys: [],
hasLocalChanged: false,
hasRemoteChanged: true
}, true);
this.logService.info(`${this.syncResourceLogLabel}: Finished pushing UI State.`);
} finally {
this.setStatus(SyncStatus.Idle);
hasRemoteChanged: false,
hasConflicts: false,
isLastSyncFromCurrentMachine: false,
};
}
}
async stop(): Promise<void> { }
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateSyncPreview> {
const localUserData = await this.getLocalGlobalState();
return {
local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, remoteUserData, localUserData, lastSyncUserData,
skippedStorageKeys: [],
hasLocalChanged: false,
hasRemoteChanged: true,
isLastSyncFromCurrentMachine: false,
hasConflicts: false
};
}
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IGlobalStateSyncPreview> {
const localUserData = await this.getLocalGlobalState();
const syncGlobalState: IGlobalState = JSON.parse(syncData.content);
const { local, skipped } = merge(localUserData.storage, syncGlobalState.storage, localUserData.storage, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
return {
local, remote: syncGlobalState.storage, remoteUserData, localUserData, lastSyncUserData,
skippedStorageKeys: skipped,
hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0,
hasRemoteChanged: true,
isLastSyncFromCurrentMachine: false,
hasConflicts: false
};
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateSyncPreview> {
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
let lastSyncGlobalState: IGlobalState | null = null;
if (lastSyncUserData === null) {
if (isLastSyncFromCurrentMachine) {
lastSyncGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
}
} else {
lastSyncGlobalState = lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;
}
const localGloablState = await this.getLocalGlobalState();
if (remoteGlobalState) {
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote ui state with local ui state...`);
} else {
this.logService.trace(`${this.syncResourceLogLabel}: Remote ui state does not exist. Synchronizing ui state for the first time.`);
}
const { local, remote, skipped } = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
return {
local, remote, remoteUserData, localUserData: localGloablState, lastSyncUserData,
skippedStorageKeys: skipped,
hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0,
hasRemoteChanged: remote !== null,
isLastSyncFromCurrentMachine,
hasConflicts: false
};
}
protected async updatePreviewWithConflict(preview: IGlobalStateSyncPreview, conflictResource: URI, content: string, token: CancellationToken): Promise<IGlobalStateSyncPreview> {
throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`);
}
protected async applyPreview({ local, remote, remoteUserData, lastSyncUserData, localUserData, hasLocalChanged, hasRemoteChanged, skippedStorageKeys }: IGlobalStateSyncPreview, forcePush: boolean): Promise<void> {
if (!hasLocalChanged && !hasRemoteChanged) {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ui state.`);
}
if (hasLocalChanged) {
// update local
this.logService.trace(`${this.syncResourceLogLabel}: Updating local ui state...`);
await this.backupLocal(JSON.stringify(localUserData));
await this.writeLocalGlobalState(local);
this.logService.info(`${this.syncResourceLogLabel}: Updated local ui state`);
}
if (hasRemoteChanged) {
// update remote
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote ui state...`);
const content = JSON.stringify(<IGlobalState>{ storage: remote });
remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote ui state`);
}
if (lastSyncUserData?.ref !== remoteUserData.ref || !equals(lastSyncUserData.skippedStorageKeys, skippedStorageKeys)) {
// update last sync
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized ui state...`);
await this.updateLastSyncUserData(remoteUserData, { skippedStorageKeys });
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized ui state`);
}
}
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
return [{ resource: joinPath(uri, 'globalState.json'), comparableResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI }];
@@ -178,10 +230,6 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
return applyEdits(content, edits);
}
async acceptConflict(conflict: URI, content: string): Promise<void> {
throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`);
}
async hasLocalData(): Promise<boolean> {
try {
const { storage } = await this.getLocalGlobalState();
@@ -194,76 +242,6 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
return false;
}
protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<SyncStatus> {
const result = await this.generatePreview(remoteUserData, lastSyncUserData);
await this.apply(result);
return SyncStatus.Idle;
}
protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<void> {
const localUserData = await this.getLocalGlobalState();
const syncGlobalState: IGlobalState = JSON.parse(syncData.content);
const { local, skipped } = merge(localUserData.storage, syncGlobalState.storage, localUserData.storage, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
await this.apply({
local, remote: syncGlobalState.storage, remoteUserData, localUserData, lastSyncUserData,
skippedStorageKeys: skipped,
hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0,
hasRemoteChanged: true
});
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IGlobalSyncPreviewResult> {
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
const lastSyncGlobalState: IGlobalState = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;
const localGloablState = await this.getLocalGlobalState();
if (remoteGlobalState) {
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote ui state with local ui state...`);
} else {
this.logService.trace(`${this.syncResourceLogLabel}: Remote ui state does not exist. Synchronizing ui state for the first time.`);
}
const { local, remote, skipped } = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
return {
local, remote, remoteUserData, localUserData: localGloablState, lastSyncUserData,
skippedStorageKeys: skipped,
hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0,
hasRemoteChanged: remote !== null
};
}
private async apply({ local, remote, remoteUserData, lastSyncUserData, localUserData, hasLocalChanged, hasRemoteChanged, skippedStorageKeys }: IGlobalSyncPreviewResult, forcePush?: boolean): Promise<void> {
if (!hasLocalChanged && !hasRemoteChanged) {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ui state.`);
}
if (hasLocalChanged) {
// update local
this.logService.trace(`${this.syncResourceLogLabel}: Updating local ui state...`);
await this.backupLocal(JSON.stringify(localUserData));
await this.writeLocalGlobalState(local);
this.logService.info(`${this.syncResourceLogLabel}: Updated local ui state`);
}
if (hasRemoteChanged) {
// update remote
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote ui state...`);
const content = JSON.stringify(<IGlobalState>{ storage: remote });
remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote ui state`);
}
if (lastSyncUserData?.ref !== remoteUserData.ref || !equals(lastSyncUserData.skippedStorageKeys, skippedStorageKeys)) {
// update last sync
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized ui state...`);
await this.updateLastSyncUserData(remoteUserData, { skippedStorageKeys });
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized ui state`);
}
}
private async getLocalGlobalState(): Promise<IGlobalState> {
const storage: IStringDictionary<IStorageValue> = {};
const argvContent: string = await this.getLocalArgvContent();

View File

@@ -4,19 +4,22 @@
*--------------------------------------------------------------------------------------------*/
import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
import {
UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncResource,
IUserDataSynchroniser, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, ISyncResourceHandle,
IRemoteUserData, ISyncData
} from 'vs/platform/userDataSync/common/userDataSync';
import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge';
import { VSBuffer } from 'vs/base/common/buffer';
import { parse } from 'vs/base/common/json';
import { localize } from 'vs/nls';
import { createCancelablePromise } from 'vs/base/common/async';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
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 { isNonEmptyArray } from 'vs/base/common/arrays';
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { IFileSyncPreview, AbstractJsonFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { URI } from 'vs/base/common/uri';
import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources';
@@ -40,109 +43,167 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@IConfigurationService configurationService: IConfigurationService,
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
@IFileService fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IStorageService storageService: IStorageService,
@IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService,
@ITelemetryService telemetryService: ITelemetryService,
) {
super(environmentService.keybindingsResource, SyncResource.Keybindings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
super(environmentService.keybindingsResource, SyncResource.Keybindings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
}
async pull(): Promise<void> {
if (!this.isEnabled()) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling keybindings as it is disabled.`);
return;
}
this.stop();
try {
this.logService.info(`${this.syncResourceLogLabel}: Started pulling keybindings...`);
this.setStatus(SyncStatus.Syncing);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
const content = remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
if (content !== null) {
const fileContent = await this.getLocalFileContent();
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<IFileSyncPreviewResult>({
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasConflicts: false,
hasLocalChanged: true,
hasRemoteChanged: false,
}));
await this.apply();
}
// No remote exists to pull
else {
this.logService.info(`${this.syncResourceLogLabel}: Remote keybindings does not exist.`);
}
this.logService.info(`${this.syncResourceLogLabel}: Finished pulling keybindings.`);
} finally {
this.setStatus(SyncStatus.Idle);
}
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileSyncPreview> {
const fileContent = await this.getLocalFileContent();
const content = remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
const hasLocalChanged = content !== null;
return {
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasConflicts: false,
hasLocalChanged,
hasRemoteChanged: false,
isLastSyncFromCurrentMachine: false
};
}
async push(): Promise<void> {
if (!this.isEnabled()) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped pushing keybindings as it is disabled.`);
return;
}
this.stop();
try {
this.logService.info(`${this.syncResourceLogLabel}: Started pushing keybindings...`);
this.setStatus(SyncStatus.Syncing);
const fileContent = await this.getLocalFileContent();
if (fileContent !== null) {
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<IFileSyncPreviewResult>({
fileContent,
remoteUserData,
lastSyncUserData,
content: fileContent.value.toString(),
hasLocalChanged: false,
hasRemoteChanged: true,
hasConflicts: false,
}));
await this.apply(true);
}
// No local exists to push
else {
this.logService.info(`${this.syncResourceLogLabel}: Local keybindings does not exist.`);
}
this.logService.info(`${this.syncResourceLogLabel}: Finished pushing keybindings.`);
} finally {
this.setStatus(SyncStatus.Idle);
}
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileSyncPreview> {
const fileContent = await this.getLocalFileContent();
const content: string | null = fileContent ? fileContent.value.toString() : null;
return {
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasLocalChanged: false,
hasRemoteChanged: content !== null,
hasConflicts: false,
isLastSyncFromCurrentMachine: false
};
}
async acceptConflict(conflict: URI, content: string): Promise<void> {
if (this.status === SyncStatus.HasConflicts
&& (isEqual(this.localPreviewResource, conflict) || isEqual(this.remotePreviewResource, conflict))
) {
const preview = await this.syncPreviewResultPromise!;
this.cancel();
this.syncPreviewResultPromise = createCancelablePromise(async () => ({ ...preview, content }));
await this.apply(true);
this.setStatus(SyncStatus.Idle);
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileSyncPreview> {
const fileContent = await this.getLocalFileContent();
const content = this.getKeybindingsContentFromSyncContent(syncData.content);
return {
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasConflicts: false,
hasLocalChanged: content !== null,
hasRemoteChanged: content !== null,
isLastSyncFromCurrentMachine: false
};
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise<IFileSyncPreview> {
const remoteContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
let lastSyncContent: string | null = null;
if (lastSyncUserData === null) {
if (isLastSyncFromCurrentMachine) {
lastSyncContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
}
} else {
lastSyncContent = lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null;
}
// Get file content last to get the latest
const fileContent = await this.getLocalFileContent();
const formattingOptions = await this.getFormattingOptions();
let content: string | null = null;
let hasLocalChanged: boolean = false;
let hasRemoteChanged: boolean = false;
let hasConflicts: boolean = false;
if (remoteContent) {
const localContent: string = fileContent ? fileContent.value.toString() : '[]';
if (!localContent.trim() || this.hasErrors(localContent)) {
throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
}
if (!lastSyncContent // First time sync
|| lastSyncContent !== localContent // Local has forwarded
|| lastSyncContent !== remoteContent // Remote has forwarded
) {
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote keybindings with local keybindings...`);
const result = await merge(localContent, remoteContent, lastSyncContent, formattingOptions, this.userDataSyncUtilService);
// Sync only if there are changes
if (result.hasChanges) {
content = result.mergeContent;
hasConflicts = result.hasConflicts;
hasLocalChanged = hasConflicts || result.mergeContent !== localContent;
hasRemoteChanged = hasConflicts || result.mergeContent !== remoteContent;
}
}
}
// First time syncing to remote
else if (fileContent) {
this.logService.trace(`${this.syncResourceLogLabel}: Remote keybindings does not exist. Synchronizing keybindings for the first time.`);
content = fileContent.value.toString();
hasRemoteChanged = true;
}
if (content && !token.isCancellationRequested) {
await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(content));
}
this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []);
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts, isLastSyncFromCurrentMachine };
}
protected async updatePreviewWithConflict(preview: IFileSyncPreview, conflictResource: URI, conflictContent: string, token: CancellationToken): Promise<IFileSyncPreview> {
if (isEqual(this.localPreviewResource, conflictResource) || isEqual(this.remotePreviewResource, conflictResource)) {
preview = { ...preview, content: conflictContent, hasConflicts: false };
}
return preview;
}
protected async applyPreview(preview: IFileSyncPreview, forcePush: boolean): Promise<void> {
let { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged } = preview;
if (content !== null) {
if (this.hasErrors(content)) {
throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
}
if (hasLocalChanged) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating local keybindings...`);
if (fileContent) {
await this.backupLocal(this.toSyncContent(fileContent.value.toString(), null));
}
await this.updateLocalFileContent(content, fileContent);
this.logService.info(`${this.syncResourceLogLabel}: Updated local keybindings`);
}
if (hasRemoteChanged) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote keybindings...`);
const remoteContents = this.toSyncContent(content, remoteUserData.syncData ? remoteUserData.syncData.content : null);
remoteUserData = await this.updateRemoteUserData(remoteContents, forcePush ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote keybindings`);
}
// Delete the preview
try {
await this.fileService.del(this.localPreviewResource);
} catch (e) { /* ignore */ }
} else {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing keybindings.`);
}
if (lastSyncUserData?.ref !== remoteUserData.ref) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized keybindings...`);
const lastSyncContent = content !== null || fileContent !== null ? this.toSyncContent(content !== null ? content : fileContent!.value.toString(), null) : null;
await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: lastSyncContent ? { version: remoteUserData.syncData!.version, machineId: remoteUserData.syncData!.machineId, content: lastSyncContent } : null });
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized keybindings`);
}
}
async hasLocalData(): Promise<boolean> {
@@ -192,149 +253,6 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
return content !== null ? this.getKeybindingsContentFromSyncContent(content) : null;
}
protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
try {
const result = await this.getPreview(remoteUserData, lastSyncUserData);
if (result.hasConflicts) {
return SyncStatus.HasConflicts;
}
await this.apply();
return SyncStatus.Idle;
} catch (e) {
this.syncPreviewResultPromise = null;
if (e instanceof UserDataSyncError) {
switch (e.code) {
case UserDataSyncErrorCode.LocalPreconditionFailed:
// Rejected as there is a new local version. Syncing again.
this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize keybindings as there is a new local version available. Synchronizing again...`);
return this.performSync(remoteUserData, lastSyncUserData);
}
}
throw e;
}
}
protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void> {
const content = this.getKeybindingsContentFromSyncContent(syncData.content);
if (content !== null) {
const fileContent = await this.getLocalFileContent();
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<IFileSyncPreviewResult>({
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasConflicts: false,
hasLocalChanged: true,
hasRemoteChanged: true,
}));
await this.apply();
}
}
private async apply(forcePush?: boolean): Promise<void> {
if (!this.syncPreviewResultPromise) {
return;
}
let { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise;
if (content !== null) {
if (this.hasErrors(content)) {
throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings as there are errors/warning in keybindings file."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
}
if (hasLocalChanged) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating local keybindings...`);
if (fileContent) {
await this.backupLocal(this.toSyncContent(fileContent.value.toString(), null));
}
await this.updateLocalFileContent(content, fileContent);
this.logService.info(`${this.syncResourceLogLabel}: Updated local keybindings`);
}
if (hasRemoteChanged) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote keybindings...`);
const remoteContents = this.toSyncContent(content, remoteUserData.syncData ? remoteUserData.syncData.content : null);
remoteUserData = await this.updateRemoteUserData(remoteContents, forcePush ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote keybindings`);
}
// Delete the preview
try {
await this.fileService.del(this.localPreviewResource);
} catch (e) { /* ignore */ }
} else {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing keybindings.`);
}
if (lastSyncUserData?.ref !== remoteUserData.ref) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized keybindings...`);
const lastSyncContent = content !== null || fileContent !== null ? this.toSyncContent(content !== null ? content : fileContent!.value.toString(), null) : null;
await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: lastSyncContent ? { version: remoteUserData.syncData!.version, machineId: remoteUserData.syncData!.machineId, content: lastSyncContent } : null });
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized keybindings`);
}
this.syncPreviewResultPromise = null;
}
private getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileSyncPreviewResult> {
if (!this.syncPreviewResultPromise) {
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token));
}
return this.syncPreviewResultPromise;
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise<IFileSyncPreviewResult> {
const remoteContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
const lastSyncContent = lastSyncUserData && lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null;
// Get file content last to get the latest
const fileContent = await this.getLocalFileContent();
const formattingOptions = await this.getFormattingOptions();
let content: string | null = null;
let hasLocalChanged: boolean = false;
let hasRemoteChanged: boolean = false;
let hasConflicts: boolean = false;
if (remoteContent) {
const localContent: string = fileContent ? fileContent.value.toString() : '[]';
if (this.hasErrors(localContent)) {
throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings as there are errors/warning in keybindings file."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
}
if (!lastSyncContent // First time sync
|| lastSyncContent !== localContent // Local has forwarded
|| lastSyncContent !== remoteContent // Remote has forwarded
) {
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote keybindings with local keybindings...`);
const result = await merge(localContent, remoteContent, lastSyncContent, formattingOptions, this.userDataSyncUtilService);
// Sync only if there are changes
if (result.hasChanges) {
content = result.mergeContent;
hasConflicts = result.hasConflicts;
hasLocalChanged = hasConflicts || result.mergeContent !== localContent;
hasRemoteChanged = hasConflicts || result.mergeContent !== remoteContent;
}
}
}
// First time syncing to remote
else if (fileContent) {
this.logService.trace(`${this.syncResourceLogLabel}: Remote keybindings does not exist. Synchronizing keybindings for the first time.`);
content = fileContent.value.toString();
hasRemoteChanged = true;
}
if (content && !token.isCancellationRequested) {
await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(content));
}
this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []);
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts };
}
getKeybindingsContentFromSyncContent(syncContent: string): string | null {
try {
const parsed = <ISyncContent>JSON.parse(syncContent);

View File

@@ -4,17 +4,20 @@
*--------------------------------------------------------------------------------------------*/
import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, CONFIGURATION_SYNC_STORE_KEY, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
import {
UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, CONFIGURATION_SYNC_STORE_KEY,
SyncResource, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, ISyncResourceHandle, IUserDataSynchroniser,
IRemoteUserData, ISyncData
} from 'vs/platform/userDataSync/common/userDataSync';
import { VSBuffer } from 'vs/base/common/buffer';
import { localize } from 'vs/nls';
import { Event } from 'vs/base/common/event';
import { createCancelablePromise } from 'vs/base/common/async';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { CancellationToken } from 'vs/base/common/cancellation';
import { updateIgnoredSettings, merge, getIgnoredSettings, isEmpty } from 'vs/platform/userDataSync/common/settingsMerge';
import { edit } from 'vs/platform/userDataSync/common/content';
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { IFileSyncPreview, AbstractJsonFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { URI } from 'vs/base/common/uri';
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
@@ -33,9 +36,7 @@ function isSettingsSyncContent(thing: any): thing is ISettingsSyncContent {
&& Object.keys(thing).length === 1;
}
export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
_serviceBrand: any;
export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser {
protected readonly version: number = 1;
protected readonly localPreviewResource: URI = joinPath(this.syncFolder, PREVIEW_DIR_NAME, 'settings.json');
@@ -50,110 +51,187 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService,
@IConfigurationService configurationService: IConfigurationService,
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
@ITelemetryService telemetryService: ITelemetryService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
) {
super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
}
protected setStatus(status: SyncStatus): void {
super.setStatus(status);
if (this.status !== SyncStatus.HasConflicts) {
this.setConflicts([]);
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileSyncPreview> {
const fileContent = await this.getLocalFileContent();
const formatUtils = await this.getFormattingOptions();
const ignoredSettings = await this.getIgnoredSettings();
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
let content: string | null = null;
if (remoteSettingsSyncContent !== null) {
// Update ignored settings from local file content
content = updateIgnoredSettings(remoteSettingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
}
return {
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasLocalChanged: content !== null,
hasRemoteChanged: false,
hasConflicts: false,
isLastSyncFromCurrentMachine: false
};
}
async pull(): Promise<void> {
if (!this.isEnabled()) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling settings as it is disabled.`);
return;
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileSyncPreview> {
const fileContent = await this.getLocalFileContent();
const formatUtils = await this.getFormattingOptions();
const ignoredSettings = await this.getIgnoredSettings();
let content: string | null = null;
if (fileContent !== null) {
// Remove ignored settings
content = updateIgnoredSettings(fileContent.value.toString(), '{}', ignoredSettings, formatUtils);
}
this.stop();
return {
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasLocalChanged: false,
hasRemoteChanged: content !== null,
hasConflicts: false,
isLastSyncFromCurrentMachine: false
};
}
try {
this.logService.info(`${this.syncResourceLogLabel}: Started pulling settings...`);
this.setStatus(SyncStatus.Syncing);
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileSyncPreview> {
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
const fileContent = await this.getLocalFileContent();
const formatUtils = await this.getFormattingOptions();
const ignoredSettings = await this.getIgnoredSettings();
if (remoteSettingsSyncContent !== null) {
const fileContent = await this.getLocalFileContent();
let content: string | null = null;
const settingsSyncContent = this.parseSettingsSyncContent(syncData.content);
if (settingsSyncContent) {
content = updateIgnoredSettings(settingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
}
return {
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasLocalChanged: content !== null,
hasRemoteChanged: content !== null,
hasConflicts: false,
isLastSyncFromCurrentMachine: false
};
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise<IFileSyncPreview> {
const fileContent = await this.getLocalFileContent();
const formattingOptions = await this.getFormattingOptions();
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
let lastSettingsSyncContent: ISettingsSyncContent | null = null;
if (lastSyncUserData === null) {
if (isLastSyncFromCurrentMachine) {
lastSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
}
} else {
lastSettingsSyncContent = this.getSettingsSyncContent(lastSyncUserData);
}
let content: string | null = null;
let hasLocalChanged: boolean = false;
let hasRemoteChanged: boolean = false;
let hasConflicts: boolean = false;
if (remoteSettingsSyncContent) {
const localContent: string = fileContent ? fileContent.value.toString() : '{}';
this.validateContent(localContent);
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote settings with local settings...`);
const ignoredSettings = await this.getIgnoredSettings();
const result = merge(localContent, remoteSettingsSyncContent.settings, lastSettingsSyncContent ? lastSettingsSyncContent.settings : null, ignoredSettings, [], formattingOptions);
content = result.localContent || result.remoteContent;
hasLocalChanged = result.localContent !== null;
hasRemoteChanged = result.remoteContent !== null;
hasConflicts = result.hasConflicts;
}
// First time syncing to remote
else if (fileContent) {
this.logService.trace(`${this.syncResourceLogLabel}: Remote settings does not exist. Synchronizing settings for the first time.`);
content = fileContent.value.toString();
hasRemoteChanged = true;
}
if (content && !token.isCancellationRequested) {
// Remove the ignored settings from the preview.
const ignoredSettings = await this.getIgnoredSettings();
const previewContent = updateIgnoredSettings(content, '{}', ignoredSettings, formattingOptions);
await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(previewContent));
}
this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []);
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts, isLastSyncFromCurrentMachine };
}
protected async updatePreviewWithConflict(preview: IFileSyncPreview, conflictResource: URI, conflictContent: string, token: CancellationToken): Promise<IFileSyncPreview> {
if (isEqual(this.localPreviewResource, conflictResource) || isEqual(this.remotePreviewResource, conflictResource)) {
const formatUtils = await this.getFormattingOptions();
// Add ignored settings from local file content
const ignoredSettings = await this.getIgnoredSettings();
const content = updateIgnoredSettings(conflictContent, preview.fileContent ? preview.fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
preview = { ...preview, content, hasConflicts: false };
}
return preview;
}
protected async applyPreview(preview: IFileSyncPreview, forcePush: boolean): Promise<void> {
let { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged } = preview;
if (content !== null) {
this.validateContent(content);
if (hasLocalChanged) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating local settings...`);
if (fileContent) {
await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(fileContent.value.toString())));
}
await this.updateLocalFileContent(content, fileContent);
this.logService.info(`${this.syncResourceLogLabel}: Updated local settings`);
}
if (hasRemoteChanged) {
const formatUtils = await this.getFormattingOptions();
// Update ignored settings from local file content
const ignoredSettings = await this.getIgnoredSettings();
const content = updateIgnoredSettings(remoteSettingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<IFileSyncPreviewResult>({
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasLocalChanged: true,
hasRemoteChanged: false,
hasConflicts: false,
}));
await this.apply();
// Update ignored settings from remote
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
const ignoredSettings = await this.getIgnoredSettings(content);
content = updateIgnoredSettings(content, remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : '{}', ignoredSettings, formatUtils);
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote settings...`);
remoteUserData = await this.updateRemoteUserData(JSON.stringify(this.toSettingsSyncContent(content)), forcePush ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote settings`);
}
// No remote exists to pull
else {
this.logService.info(`${this.syncResourceLogLabel}: Remote settings does not exist.`);
}
this.logService.info(`${this.syncResourceLogLabel}: Finished pulling settings.`);
} finally {
this.setStatus(SyncStatus.Idle);
}
}
async push(): Promise<void> {
if (!this.isEnabled()) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped pushing settings as it is disabled.`);
return;
// Delete the preview
try {
await this.fileService.del(this.localPreviewResource);
} catch (e) { /* ignore */ }
} else {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing settings.`);
}
this.stop();
try {
this.logService.info(`${this.syncResourceLogLabel}: Started pushing settings...`);
this.setStatus(SyncStatus.Syncing);
const fileContent = await this.getLocalFileContent();
if (fileContent !== null) {
const formatUtils = await this.getFormattingOptions();
// Remove ignored settings
const ignoredSettings = await this.getIgnoredSettings();
const content = updateIgnoredSettings(fileContent.value.toString(), '{}', ignoredSettings, formatUtils);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<IFileSyncPreviewResult>({
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasRemoteChanged: true,
hasLocalChanged: false,
hasConflicts: false,
}));
await this.apply(true);
}
// No local exists to push
else {
this.logService.info(`${this.syncResourceLogLabel}: Local settings does not exist.`);
}
this.logService.info(`${this.syncResourceLogLabel}: Finished pushing settings.`);
} finally {
this.setStatus(SyncStatus.Idle);
if (lastSyncUserData?.ref !== remoteUserData.ref) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized settings...`);
await this.updateLastSyncUserData(remoteUserData);
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized settings`);
}
}
async hasLocalData(): Promise<boolean> {
@@ -215,169 +293,6 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
return content;
}
async acceptConflict(conflict: URI, content: string): Promise<void> {
if (this.status === SyncStatus.HasConflicts
&& (isEqual(this.localPreviewResource, conflict) || isEqual(this.remotePreviewResource, conflict))
) {
const preview = await this.syncPreviewResultPromise!;
this.cancel();
const formatUtils = await this.getFormattingOptions();
// Add ignored settings from local file content
const ignoredSettings = await this.getIgnoredSettings();
content = updateIgnoredSettings(content, preview.fileContent ? preview.fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
this.syncPreviewResultPromise = createCancelablePromise(async () => ({ ...preview, content }));
await this.apply(true);
this.setStatus(SyncStatus.Idle);
}
}
async resolveSettingsConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<void> {
if (this.status === SyncStatus.HasConflicts) {
const preview = await this.syncPreviewResultPromise!;
this.cancel();
await this.performSync(preview.remoteUserData, preview.lastSyncUserData, resolvedConflicts);
}
}
protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any | undefined }[] = []): Promise<SyncStatus> {
try {
const result = await this.getPreview(remoteUserData, lastSyncUserData, resolvedConflicts);
if (result.hasConflicts) {
return SyncStatus.HasConflicts;
}
await this.apply();
return SyncStatus.Idle;
} catch (e) {
this.syncPreviewResultPromise = null;
if (e instanceof UserDataSyncError) {
switch (e.code) {
case UserDataSyncErrorCode.LocalPreconditionFailed:
// Rejected as there is a new local version. Syncing again.
this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize settings as there is a new local version available. Synchronizing again...`);
return this.performSync(remoteUserData, lastSyncUserData, resolvedConflicts);
}
}
throw e;
}
}
protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void> {
const settingsSyncContent = this.parseSettingsSyncContent(syncData.content);
if (settingsSyncContent) {
const fileContent = await this.getLocalFileContent();
const formatUtils = await this.getFormattingOptions();
const ignoredSettings = await this.getIgnoredSettings();
const content = updateIgnoredSettings(settingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<IFileSyncPreviewResult>({
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasLocalChanged: true,
hasRemoteChanged: true,
hasConflicts: false,
}));
await this.apply();
}
}
private async apply(forcePush?: boolean): Promise<void> {
if (!this.syncPreviewResultPromise) {
return;
}
let { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise;
if (content !== null) {
this.validateContent(content);
if (hasLocalChanged) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating local settings...`);
if (fileContent) {
await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(fileContent.value.toString())));
}
await this.updateLocalFileContent(content, fileContent);
this.logService.info(`${this.syncResourceLogLabel}: Updated local settings`);
}
if (hasRemoteChanged) {
const formatUtils = await this.getFormattingOptions();
// Update ignored settings from remote
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
const ignoredSettings = await this.getIgnoredSettings(content);
content = updateIgnoredSettings(content, remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : '{}', ignoredSettings, formatUtils);
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote settings...`);
remoteUserData = await this.updateRemoteUserData(JSON.stringify(this.toSettingsSyncContent(content)), forcePush ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote settings`);
}
// Delete the preview
try {
await this.fileService.del(this.localPreviewResource);
} catch (e) { /* ignore */ }
} else {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing settings.`);
}
if (lastSyncUserData?.ref !== remoteUserData.ref) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized settings...`);
await this.updateLastSyncUserData(remoteUserData);
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized settings`);
}
this.syncPreviewResultPromise = null;
}
private getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any }[] = []): Promise<IFileSyncPreviewResult> {
if (!this.syncPreviewResultPromise) {
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, resolvedConflicts, token));
}
return this.syncPreviewResultPromise;
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any }[] = [], token: CancellationToken = CancellationToken.None): Promise<IFileSyncPreviewResult> {
const fileContent = await this.getLocalFileContent();
const formattingOptions = await this.getFormattingOptions();
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
const lastSettingsSyncContent = lastSyncUserData ? this.getSettingsSyncContent(lastSyncUserData) : null;
let content: string | null = null;
let hasLocalChanged: boolean = false;
let hasRemoteChanged: boolean = false;
let hasConflicts: boolean = false;
if (remoteSettingsSyncContent) {
const localContent: string = fileContent ? fileContent.value.toString() : '{}';
this.validateContent(localContent);
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote settings with local settings...`);
const ignoredSettings = await this.getIgnoredSettings();
const result = merge(localContent, remoteSettingsSyncContent.settings, lastSettingsSyncContent ? lastSettingsSyncContent.settings : null, ignoredSettings, resolvedConflicts, formattingOptions);
content = result.localContent || result.remoteContent;
hasLocalChanged = result.localContent !== null;
hasRemoteChanged = result.remoteContent !== null;
hasConflicts = result.hasConflicts;
}
// First time syncing to remote
else if (fileContent) {
this.logService.trace(`${this.syncResourceLogLabel}: Remote settings does not exist. Synchronizing settings for the first time.`);
content = fileContent.value.toString();
hasRemoteChanged = true;
}
if (content && !token.isCancellationRequested) {
// Remove the ignored settings from the preview.
const ignoredSettings = await this.getIgnoredSettings();
const previewContent = updateIgnoredSettings(content, '{}', ignoredSettings, formattingOptions);
await this.fileService.writeFile(this.localPreviewResource, VSBuffer.fromString(previewContent));
}
this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []);
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts };
}
private getSettingsSyncContent(remoteUserData: IRemoteUserData): ISettingsSyncContent | null {
return remoteUserData.syncData ? this.parseSettingsSyncContent(remoteUserData.syncData.content) : null;
}
@@ -397,7 +312,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
}
private _defaultIgnoredSettings: Promise<string[]> | undefined = undefined;
protected async getIgnoredSettings(content?: string): Promise<string[]> {
private async getIgnoredSettings(content?: string): Promise<string[]> {
if (!this._defaultIgnoredSettings) {
this._defaultIgnoredSettings = this.userDataSyncUtilService.resolveDefaultIgnoredSettings();
const disposable = Event.any<any>(

View File

@@ -3,25 +3,25 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, UserDataSyncError, UserDataSyncErrorCode, ISyncResourceHandle, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync';
import {
IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService,
Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, ISyncResourceHandle, IRemoteUserData, ISyncData, ISyncPreview
} from 'vs/platform/userDataSync/common/userDataSync';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService, FileChangesEvent, IFileStat, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { AbstractSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IStringDictionary } from 'vs/base/common/collections';
import { URI } from 'vs/base/common/uri';
import { joinPath, extname, relativePath, isEqualOrParent, isEqual, basename, dirname } from 'vs/base/common/resources';
import { VSBuffer } from 'vs/base/common/buffer';
import { merge } from 'vs/platform/userDataSync/common/snippetsMerge';
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStorageService } from 'vs/platform/storage/common/storage';
interface ISinppetsSyncPreviewResult extends ISyncPreviewResult {
interface ISinppetsSyncPreview extends ISyncPreview {
readonly local: IStringDictionary<IFileContent>;
readonly remoteUserData: IRemoteUserData;
readonly lastSyncUserData: IRemoteUserData | null;
readonly added: IStringDictionary<string>;
readonly updated: IStringDictionary<string>;
readonly removed: string[];
@@ -35,7 +35,6 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
protected readonly version: number = 1;
private readonly snippetsFolder: URI;
private readonly snippetsPreviewFolder: URI;
private syncPreviewResultPromise: CancelablePromise<ISinppetsSyncPreviewResult> | null = null;
constructor(
@IEnvironmentService environmentService: IEnvironmentService,
@@ -45,10 +44,10 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@IConfigurationService configurationService: IConfigurationService,
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
@ITelemetryService telemetryService: ITelemetryService,
) {
super(SyncResource.Snippets, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
super(SyncResource.Snippets, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, configurationService);
this.snippetsFolder = environmentService.snippetsHome;
this.snippetsPreviewFolder = joinPath(this.syncFolder, PREVIEW_DIR_NAME);
this._register(this.fileService.watch(environmentService.userRoamingDataHome));
@@ -60,98 +59,171 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
if (!e.changes.some(change => isEqualOrParent(change.resource, this.snippetsFolder))) {
return;
}
if (!this.isEnabled()) {
return;
}
// Sync again if local file has changed and current status is in conflicts
if (this.status === SyncStatus.HasConflicts) {
this.syncPreviewResultPromise!.then(result => {
this.cancel();
this.doSync(result.remoteUserData, result.lastSyncUserData).then(status => this.setStatus(status));
});
}
// Otherwise fire change event
else {
this.triggerLocalChange();
}
this.triggerLocalChange();
}
async pull(): Promise<void> {
if (!this.isEnabled()) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling snippets as it is disabled.`);
return;
}
this.stop();
try {
this.logService.info(`${this.syncResourceLogLabel}: Started pulling snippets...`);
this.setStatus(SyncStatus.Syncing);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
if (remoteUserData.syncData !== null) {
const local = await this.getSnippetsFileContents();
const localSnippets = this.toSnippetsContents(local);
const remoteSnippets = this.parseSnippets(remoteUserData.syncData);
const { added, updated, remote, removed } = merge(localSnippets, remoteSnippets, localSnippets);
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISinppetsSyncPreviewResult>({
added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {},
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
hasRemoteChanged: remote !== null
}));
await this.apply();
}
// No remote exists to pull
else {
this.logService.info(`${this.syncResourceLogLabel}: Remote snippets does not exist.`);
}
this.logService.info(`${this.syncResourceLogLabel}: Finished pulling snippets.`);
} finally {
this.setStatus(SyncStatus.Idle);
}
}
async push(): Promise<void> {
if (!this.isEnabled()) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped pushing snippets as it is disabled.`);
return;
}
this.stop();
try {
this.logService.info(`${this.syncResourceLogLabel}: Started pushing snippets...`);
this.setStatus(SyncStatus.Syncing);
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISinppetsSyncPreview> {
if (remoteUserData.syncData !== null) {
const local = await this.getSnippetsFileContents();
const localSnippets = this.toSnippetsContents(local);
const { added, removed, updated, remote } = merge(localSnippets, null, null);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISinppetsSyncPreviewResult>({
added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {},
const remoteSnippets = this.parseSnippets(remoteUserData.syncData);
const { added, updated, remote, removed } = merge(localSnippets, remoteSnippets, localSnippets);
return {
remoteUserData, lastSyncUserData,
added, removed, updated, remote, local,
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
hasRemoteChanged: remote !== null
}));
hasRemoteChanged: remote !== null,
conflicts: [], resolvedConflicts: {}, hasConflicts: false,
isLastSyncFromCurrentMachine: false,
};
} else {
return {
remoteUserData, lastSyncUserData,
added: {}, removed: [], updated: {}, remote: null, local: {},
hasLocalChanged: false,
hasRemoteChanged: false,
conflicts: [], resolvedConflicts: {}, hasConflicts: false,
isLastSyncFromCurrentMachine: false,
};
}
}
await this.apply(true);
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISinppetsSyncPreview> {
const local = await this.getSnippetsFileContents();
const localSnippets = this.toSnippetsContents(local);
const { added, removed, updated, remote } = merge(localSnippets, null, null);
return {
added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {},
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
hasRemoteChanged: remote !== null,
isLastSyncFromCurrentMachine: false,
hasConflicts: false,
};
}
this.logService.info(`${this.syncResourceLogLabel}: Finished pushing snippets.`);
} finally {
this.setStatus(SyncStatus.Idle);
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISinppetsSyncPreview> {
const local = await this.getSnippetsFileContents();
const localSnippets = this.toSnippetsContents(local);
const snippets = this.parseSnippets(syncData);
const { added, updated, removed } = merge(localSnippets, snippets, localSnippets);
return {
added, removed, updated, remote: snippets, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {}, hasConflicts: false,
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
hasRemoteChanged: true,
isLastSyncFromCurrentMachine: false,
};
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise<ISinppetsSyncPreview> {
const local = await this.getSnippetsFileContents();
return this.doGeneratePreview(local, remoteUserData, lastSyncUserData, {}, token);
}
private async doGeneratePreview(local: IStringDictionary<IFileContent>, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary<string | null> = {}, token: CancellationToken = CancellationToken.None): Promise<ISinppetsSyncPreview> {
const localSnippets = this.toSnippetsContents(local);
const remoteSnippets: IStringDictionary<string> | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null;
const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData);
let lastSyncSnippets: IStringDictionary<string> | null = null;
if (lastSyncUserData === null) {
if (isLastSyncFromCurrentMachine) {
lastSyncSnippets = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null;
}
} else {
lastSyncSnippets = lastSyncUserData.syncData ? this.parseSnippets(lastSyncUserData.syncData) : null;
}
if (remoteSnippets) {
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote snippets with local snippets...`);
} else {
this.logService.trace(`${this.syncResourceLogLabel}: Remote snippets does not exist. Synchronizing snippets for the first time.`);
}
const mergeResult = merge(localSnippets, remoteSnippets, lastSyncSnippets, resolvedConflicts);
const conflicts: Conflict[] = [];
for (const key of mergeResult.conflicts) {
const localPreview = joinPath(this.snippetsPreviewFolder, key);
conflicts.push({ local: localPreview, remote: localPreview.with({ scheme: USER_DATA_SYNC_SCHEME }) });
const content = local[key];
if (!token.isCancellationRequested) {
await this.fileService.writeFile(localPreview, content ? content.value : VSBuffer.fromString(''));
}
}
for (const conflict of this.conflicts) {
// clear obsolete conflicts
if (!conflicts.some(({ local }) => isEqual(local, conflict.local))) {
try {
await this.fileService.del(conflict.local);
} catch (error) {
// Ignore & log
this.logService.error(error);
}
}
}
this.setConflicts(conflicts);
return {
remoteUserData, local,
lastSyncUserData,
added: mergeResult.added,
removed: mergeResult.removed,
updated: mergeResult.updated,
conflicts,
hasConflicts: conflicts.length > 0,
remote: mergeResult.remote,
resolvedConflicts,
hasLocalChanged: Object.keys(mergeResult.added).length > 0 || mergeResult.removed.length > 0 || Object.keys(mergeResult.updated).length > 0,
hasRemoteChanged: mergeResult.remote !== null,
isLastSyncFromCurrentMachine
};
}
protected async updatePreviewWithConflict(preview: ISinppetsSyncPreview, conflictResource: URI, content: string, token: CancellationToken): Promise<ISinppetsSyncPreview> {
const conflict = this.conflicts.filter(({ local, remote }) => isEqual(local, conflictResource) || isEqual(remote, conflictResource))[0];
if (conflict) {
const key = relativePath(this.snippetsPreviewFolder, conflict.local)!;
preview.resolvedConflicts[key] = content || null;
preview = await this.doGeneratePreview(preview.local, preview.remoteUserData, preview.lastSyncUserData, preview.resolvedConflicts, token);
}
return preview;
}
protected async applyPreview(preview: ISinppetsSyncPreview, forcePush: boolean): Promise<void> {
let { added, removed, updated, local, remote, remoteUserData, lastSyncUserData, hasLocalChanged, hasRemoteChanged } = preview;
if (!hasLocalChanged && !hasRemoteChanged) {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing snippets.`);
}
if (hasLocalChanged) {
// back up all snippets
await this.backupLocal(JSON.stringify(this.toSnippetsContents(local)));
await this.updateLocalSnippets(added, removed, updated, local);
}
if (remote) {
// update remote
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote snippets...`);
const content = JSON.stringify(remote);
remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote snippets`);
}
if (lastSyncUserData?.ref !== remoteUserData.ref) {
// update last sync
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized snippets...`);
await this.updateLastSyncUserData(remoteUserData);
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized snippets`);
}
}
async stop(): Promise<void> {
await this.clearConflicts();
this.cancel();
this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.syncResourceLogLabel}.`);
this.setStatus(SyncStatus.Idle);
return super.stop();
}
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
@@ -192,37 +264,20 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
return null;
}
protected async getConflictContent(conflictResource: URI): Promise<string | null> {
if (this.syncPreviewResultPromise) {
const result = await this.syncPreviewResultPromise;
private async getConflictContent(conflictResource: URI): Promise<string | null> {
const syncPreview = await this.getSyncPreviewInProgress();
if (syncPreview) {
const key = relativePath(this.snippetsPreviewFolder, conflictResource.with({ scheme: this.snippetsPreviewFolder.scheme }))!;
if (conflictResource.scheme === this.snippetsPreviewFolder.scheme) {
return result.local[key] ? result.local[key].value.toString() : null;
} else if (result.remoteUserData && result.remoteUserData.syncData) {
const snippets = this.parseSnippets(result.remoteUserData.syncData);
return (syncPreview as ISinppetsSyncPreview).local[key] ? (syncPreview as ISinppetsSyncPreview).local[key].value.toString() : null;
} else if (syncPreview.remoteUserData && syncPreview.remoteUserData.syncData) {
const snippets = this.parseSnippets(syncPreview.remoteUserData.syncData);
return snippets[key] || null;
}
}
return null;
}
async acceptConflict(conflictResource: URI, content: string): Promise<void> {
const conflict = this.conflicts.filter(({ local, remote }) => isEqual(local, conflictResource) || isEqual(remote, conflictResource))[0];
if (this.status === SyncStatus.HasConflicts && conflict) {
const key = relativePath(this.snippetsPreviewFolder, conflict.local)!;
let previewResult = await this.syncPreviewResultPromise!;
this.cancel();
previewResult.resolvedConflicts[key] = content || null;
this.syncPreviewResultPromise = createCancelablePromise(token => this.doGeneratePreview(previewResult.local, previewResult.remoteUserData, previewResult.lastSyncUserData, previewResult.resolvedConflicts, token));
previewResult = await this.syncPreviewResultPromise;
this.setConflicts(previewResult.conflicts);
if (!this.conflicts.length) {
await this.apply();
this.setStatus(SyncStatus.Idle);
}
}
}
async hasLocalData(): Promise<boolean> {
try {
const localSnippets = await this.getSnippetsFileContents();
@@ -235,56 +290,6 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
return false;
}
protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
try {
const previewResult = await this.getPreview(remoteUserData, lastSyncUserData);
this.setConflicts(previewResult.conflicts);
if (this.conflicts.length) {
return SyncStatus.HasConflicts;
}
await this.apply();
return SyncStatus.Idle;
} catch (e) {
this.syncPreviewResultPromise = null;
if (e instanceof UserDataSyncError) {
switch (e.code) {
case UserDataSyncErrorCode.LocalPreconditionFailed:
// Rejected as there is a new local version. Syncing again.
this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize snippets as there is a new local version available. Synchronizing again...`);
return this.performSync(remoteUserData, lastSyncUserData);
}
}
throw e;
}
}
protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void> {
const local = await this.getSnippetsFileContents();
const localSnippets = this.toSnippetsContents(local);
const snippets = this.parseSnippets(syncData);
const { added, updated, removed } = merge(localSnippets, snippets, localSnippets);
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISinppetsSyncPreviewResult>({
added, removed, updated, remote: snippets, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {},
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
hasRemoteChanged: true
}));
await this.apply();
}
protected getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISinppetsSyncPreviewResult> {
if (!this.syncPreviewResultPromise) {
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token));
}
return this.syncPreviewResultPromise;
}
protected cancel(): void {
if (this.syncPreviewResultPromise) {
this.syncPreviewResultPromise.cancel();
this.syncPreviewResultPromise = null;
}
}
private async clearConflicts(): Promise<void> {
if (this.conflicts.length) {
await Promise.all(this.conflicts.map(({ local }) => this.fileService.del(local)));
@@ -292,95 +297,6 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
}
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise<ISinppetsSyncPreviewResult> {
return this.getSnippetsFileContents()
.then(local => this.doGeneratePreview(local, remoteUserData, lastSyncUserData, {}, token));
}
private async doGeneratePreview(local: IStringDictionary<IFileContent>, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary<string | null> = {}, token: CancellationToken = CancellationToken.None): Promise<ISinppetsSyncPreviewResult> {
const localSnippets = this.toSnippetsContents(local);
const remoteSnippets: IStringDictionary<string> | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null;
const lastSyncSnippets: IStringDictionary<string> | null = lastSyncUserData && lastSyncUserData.syncData ? this.parseSnippets(lastSyncUserData.syncData) : null;
if (remoteSnippets) {
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote snippets with local snippets...`);
} else {
this.logService.trace(`${this.syncResourceLogLabel}: Remote snippets does not exist. Synchronizing snippets for the first time.`);
}
const mergeResult = merge(localSnippets, remoteSnippets, lastSyncSnippets, resolvedConflicts);
const conflicts: Conflict[] = [];
for (const key of mergeResult.conflicts) {
const localPreview = joinPath(this.snippetsPreviewFolder, key);
conflicts.push({ local: localPreview, remote: localPreview.with({ scheme: USER_DATA_SYNC_SCHEME }) });
const content = local[key];
if (!token.isCancellationRequested) {
await this.fileService.writeFile(localPreview, content ? content.value : VSBuffer.fromString(''));
}
}
for (const conflict of this.conflicts) {
// clear obsolete conflicts
if (!conflicts.some(({ local }) => isEqual(local, conflict.local))) {
try {
await this.fileService.del(conflict.local);
} catch (error) {
// Ignore & log
this.logService.error(error);
}
}
}
return {
remoteUserData, local,
lastSyncUserData,
added: mergeResult.added,
removed: mergeResult.removed,
updated: mergeResult.updated,
conflicts,
remote: mergeResult.remote,
resolvedConflicts,
hasLocalChanged: Object.keys(mergeResult.added).length > 0 || mergeResult.removed.length > 0 || Object.keys(mergeResult.updated).length > 0,
hasRemoteChanged: mergeResult.remote !== null
};
}
private async apply(forcePush?: boolean): Promise<void> {
if (!this.syncPreviewResultPromise) {
return;
}
let { added, removed, updated, local, remote, remoteUserData, lastSyncUserData, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise;
if (!hasLocalChanged && !hasRemoteChanged) {
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing snippets.`);
}
if (hasLocalChanged) {
// back up all snippets
await this.backupLocal(JSON.stringify(this.toSnippetsContents(local)));
await this.updateLocalSnippets(added, removed, updated, local);
}
if (remote) {
// update remote
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote snippets...`);
const content = JSON.stringify(remote);
remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote snippets`);
}
if (lastSyncUserData?.ref !== remoteUserData.ref) {
// update last sync
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized snippets...`);
await this.updateLastSyncUserData(remoteUserData);
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized snippets`);
}
this.syncPreviewResultPromise = null;
}
private async updateLocalSnippets(added: IStringDictionary<string>, removed: string[], updated: IStringDictionary<string>, local: IStringDictionary<IFileContent>): Promise<void> {
for (const key of removed) {
const resource = joinPath(this.snippetsFolder, key);

View File

@@ -3,22 +3,73 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Delayer, disposableTimeout } from 'vs/base/common/async';
import { Delayer, disposableTimeout, CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable, toDisposable, MutableDisposable, IDisposable } from 'vs/base/common/lifecycle';
import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncEnablementService, ALL_SYNC_RESOURCES, getUserDataSyncStore } from 'vs/platform/userDataSync/common/userDataSync';
import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication';
import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, UserDataAutoSyncError } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IProductService } from 'vs/platform/product/common/productService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';
import { localize } from 'vs/nls';
type AutoSyncClassification = {
sources: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
export const RESOURCE_ENABLEMENT_SOURCE = 'resourceEnablement';
type AutoSyncEnablementClassification = {
enabled?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
export class UserDataAutoSyncService extends Disposable implements IUserDataAutoSyncService {
type AutoSyncErrorClassification = {
code: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
const enablementKey = 'sync.enable';
const disableMachineEventuallyKey = 'sync.disableMachineEventually';
const sessionIdKey = 'sync.sessionId';
export class UserDataAutoSyncEnablementService extends Disposable {
private _onDidChangeEnablement = new Emitter<boolean>();
readonly onDidChangeEnablement: Event<boolean> = this._onDidChangeEnablement.event;
constructor(
@IStorageService protected readonly storageService: IStorageService,
@IEnvironmentService private readonly environmentService: IEnvironmentService
) {
super();
this._register(storageService.onDidChangeStorage(e => this.onDidStorageChange(e)));
}
isEnabled(): boolean {
switch (this.environmentService.sync) {
case 'on':
return true;
case 'off':
return false;
}
return this.storageService.getBoolean(enablementKey, StorageScope.GLOBAL, this.environmentService.enableSyncByDefault);
}
canToggleEnablement(): boolean {
return this.environmentService.sync === undefined;
}
private onDidStorageChange(workspaceStorageChangeEvent: IWorkspaceStorageChangeEvent): void {
if (workspaceStorageChangeEvent.scope === StorageScope.GLOBAL) {
if (enablementKey === workspaceStorageChangeEvent.key) {
this._onDidChangeEnablement.fire(this.isEnabled());
}
}
}
}
export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService implements IUserDataAutoSyncService {
_serviceBrand: any;
@@ -31,21 +82,27 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
readonly onError: Event<UserDataSyncError> = this._onError.event;
constructor(
@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IAuthenticationTokenService private readonly authTokenService: IAuthenticationTokenService,
@IUserDataSyncAccountService private readonly userDataSyncAccountService: IUserDataSyncAccountService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IProductService private readonly productService: IProductService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IUserDataSyncMachinesService private readonly userDataSyncMachinesService: IUserDataSyncMachinesService,
@IStorageService storageService: IStorageService,
@IEnvironmentService environmentService: IEnvironmentService
) {
super();
super(storageService, environmentService);
this.syncTriggerDelayer = this._register(new Delayer<void>(0));
if (getUserDataSyncStore(this.productService, this.configurationService)) {
if (userDataSyncStoreService.userDataSyncStore) {
this.updateAutoSync();
this._register(Event.any(authTokenService.onDidChangeToken, this.userDataSyncEnablementService.onDidChangeEnablement)(() => this.updateAutoSync()));
this._register(Event.filter(this.userDataSyncEnablementService.onDidChangeResourceEnablement, ([, enabled]) => enabled)(() => this.triggerAutoSync([RESOURCE_ENABLEMENT_SOURCE])));
if (this.hasToDisableMachineEventually()) {
this.disableMachineEventually();
}
this._register(userDataSyncAccountService.onDidChangeAccount(() => this.updateAutoSync()));
this._register(Event.debounce<string, string[]>(userDataSyncService.onDidChangeLocal, (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, false)));
this._register(Event.filter(this.userDataSyncResourceEnablementService.onDidChangeResourceEnablement, ([, enabled]) => enabled)(() => this.triggerSync(['resourceEnablement'], false)));
}
}
@@ -53,7 +110,7 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
const { enabled, reason } = this.isAutoSyncEnabled();
if (enabled) {
if (this.autoSync.value === undefined) {
this.autoSync.value = new AutoSync(1000 * 60 * 5 /* 5 miutes */, this.userDataSyncService, this.logService);
this.autoSync.value = new AutoSync(1000 * 60 * 5 /* 5 miutes */, this.userDataSyncStoreService, this.userDataSyncService, this.userDataSyncMachinesService, this.logService, this.storageService);
this.autoSync.value.register(this.autoSync.value.onDidStartSync(() => this.lastSyncTriggerTime = new Date().getTime()));
this.autoSync.value.register(this.autoSync.value.onDidFinishSync(e => this.onDidFinishSync(e)));
if (this.startAutoSync()) {
@@ -61,6 +118,7 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
}
}
} else {
this.syncTriggerDelayer.cancel();
if (this.autoSync.value !== undefined) {
this.logService.info('Auto Sync: Disabled because', reason);
this.autoSync.clear();
@@ -72,15 +130,66 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
protected startAutoSync(): boolean { return true; }
private isAutoSyncEnabled(): { enabled: boolean, reason?: string } {
if (!this.userDataSyncEnablementService.isEnabled()) {
if (!this.isEnabled()) {
return { enabled: false, reason: 'sync is disabled' };
}
if (!this.authTokenService.token) {
if (!this.userDataSyncAccountService.account) {
return { enabled: false, reason: 'token is not avaialable' };
}
return { enabled: true };
}
async turnOn(pullFirst: boolean): Promise<void> {
this.stopDisableMachineEventually();
if (pullFirst) {
await this.userDataSyncService.pull();
} else {
await this.userDataSyncService.sync();
}
this.setEnablement(true);
}
async turnOff(everywhere: boolean, softTurnOffOnError?: boolean, donotRemoveMachine?: boolean): Promise<void> {
try {
// Remove machine
if (!donotRemoveMachine) {
await this.userDataSyncMachinesService.removeCurrentMachine();
}
// Disable Auto Sync
this.setEnablement(false);
// Reset Session
this.storageService.remove(sessionIdKey, StorageScope.GLOBAL);
// Reset
if (everywhere) {
this.telemetryService.publicLog2('sync/turnOffEveryWhere');
await this.userDataSyncService.reset();
} else {
await this.userDataSyncService.resetLocal();
}
} catch (error) {
if (softTurnOffOnError) {
this.logService.error(error);
this.setEnablement(false);
} else {
throw error;
}
}
}
private setEnablement(enabled: boolean): void {
if (this.isEnabled() !== enabled) {
this.telemetryService.publicLog2<{ enabled: boolean }, AutoSyncEnablementClassification>(enablementKey, { enabled });
this.storageService.store(enablementKey, enabled, StorageScope.GLOBAL);
this.updateAutoSync();
}
}
private async onDidFinishSync(error: Error | undefined): Promise<void> {
if (!error) {
// Sync finished without errors
@@ -90,52 +199,89 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
// Error while syncing
const userDataSyncError = UserDataSyncError.toUserDataSyncError(error);
// Log to telemetry
if (userDataSyncError instanceof UserDataAutoSyncError) {
this.telemetryService.publicLog2<{ code: string }, AutoSyncErrorClassification>(`autosync/error`, { code: userDataSyncError.code });
}
// Turned off from another device or session got expired
if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff || userDataSyncError.code === UserDataSyncErrorCode.SessionExpired) {
this.logService.info('Auto Sync: Sync is turned off in the cloud.');
await this.userDataSyncService.resetLocal();
this.logService.info('Auto Sync: Did reset the local sync state.');
this.userDataSyncEnablementService.setEnablement(false);
await this.turnOff(false, true /* force soft turnoff on error */);
this.logService.info('Auto Sync: Turned off sync because sync is turned off in the cloud');
} else if (userDataSyncError.code === UserDataSyncErrorCode.LocalTooManyRequests) {
this.userDataSyncEnablementService.setEnablement(false);
}
// Exceeded Rate Limit
else if (userDataSyncError.code === UserDataSyncErrorCode.LocalTooManyRequests || userDataSyncError.code === UserDataSyncErrorCode.TooManyRequests) {
await this.turnOff(false, true /* force soft turnoff on error */,
true /* do not disable machine because disabling a machine makes request to server and can fail with TooManyRequests */);
this.disableMachineEventually();
this.logService.info('Auto Sync: Turned off sync because of making too many requests to server');
} else {
}
else {
this.logService.error(userDataSyncError);
this.successiveFailures++;
}
this._onError.fire(userDataSyncError);
}
private async disableMachineEventually(): Promise<void> {
this.storageService.store(disableMachineEventuallyKey, true, StorageScope.GLOBAL);
await timeout(1000 * 60 * 10);
// Return if got stopped meanwhile.
if (!this.hasToDisableMachineEventually()) {
return;
}
this.stopDisableMachineEventually();
// disable only if sync is disabled
if (!this.isEnabled() && this.userDataSyncAccountService.account) {
await this.userDataSyncMachinesService.removeCurrentMachine();
}
}
private hasToDisableMachineEventually(): boolean {
return this.storageService.getBoolean(disableMachineEventuallyKey, StorageScope.GLOBAL, false);
}
private stopDisableMachineEventually(): void {
this.storageService.remove(disableMachineEventuallyKey, StorageScope.GLOBAL);
}
private sources: string[] = [];
async triggerAutoSync(sources: string[]): Promise<void> {
async triggerSync(sources: string[], skipIfSyncedRecently: boolean): Promise<void> {
if (this.autoSync.value === undefined) {
return this.syncTriggerDelayer.cancel();
}
/*
If sync is not triggered by sync resource (triggered by other sources like window focus etc.,) or by resource enablement
then limit sync to once per 10s
*/
const hasToLimitSync = sources.indexOf(RESOURCE_ENABLEMENT_SOURCE) === -1 && ALL_SYNC_RESOURCES.every(syncResource => sources.indexOf(syncResource) === -1);
if (hasToLimitSync && this.lastSyncTriggerTime
if (skipIfSyncedRecently && this.lastSyncTriggerTime
&& Math.round((new Date().getTime() - this.lastSyncTriggerTime) / 1000) < 10) {
this.logService.debug('Auto Sync Skipped: Limited to once per 10 seconds.');
this.logService.debug('Auto Sync: Skipped. Limited to once per 10 seconds.');
return;
}
this.sources.push(...sources);
return this.syncTriggerDelayer.trigger(async () => {
this.logService.trace('activity sources', ...this.sources);
this.telemetryService.publicLog2<{ sources: string[] }, AutoSyncClassification>('sync/triggered', { sources: this.sources });
this.sources = [];
if (this.autoSync.value) {
await this.autoSync.value.sync('Activity');
}
}, this.successiveFailures
? 1000 * 1 * Math.min(Math.pow(2, this.successiveFailures), 60) /* Delay exponentially until max 1 minute */
: 1000); /* Debounce for a second if there are no failures */
? this.getSyncTriggerDelayTime() * 1 * Math.min(Math.pow(2, this.successiveFailures), 60) /* Delay exponentially until max 1 minute */
: this.getSyncTriggerDelayTime());
}
protected getSyncTriggerDelayTime(): number {
return 1000; /* Debounce for a second if there are no failures */
}
}
class AutoSync extends Disposable {
@@ -150,10 +296,15 @@ class AutoSync extends Disposable {
private readonly _onDidFinishSync = this._register(new Emitter<Error | undefined>());
readonly onDidFinishSync = this._onDidFinishSync.event;
private syncPromise: CancelablePromise<void> | undefined;
constructor(
private readonly interval: number /* in milliseconds */,
private readonly userDataSyncStoreService: IUserDataSyncStoreService,
private readonly userDataSyncService: IUserDataSyncService,
private readonly userDataSyncMachinesService: IUserDataSyncMachinesService,
private readonly logService: IUserDataSyncLogService,
private readonly storageService: IStorageService,
) {
super();
}
@@ -161,6 +312,11 @@ class AutoSync extends Disposable {
start(): void {
this._register(this.onDidFinishSync(() => this.waitUntilNextIntervalAndSync()));
this._register(toDisposable(() => {
if (this.syncPromise) {
this.syncPromise.cancel();
this.logService.info('Auto sync: Canelled sync that is in progress');
this.syncPromise = undefined;
}
this.userDataSyncService.stop();
this.logService.info('Auto Sync: Stopped');
}));
@@ -172,16 +328,87 @@ class AutoSync extends Disposable {
this.intervalHandler.value = disposableTimeout(() => this.sync(AutoSync.INTERVAL_SYNCING), this.interval);
}
async sync(reason: string): Promise<void> {
sync(reason: string): Promise<void> {
const syncPromise = createCancelablePromise(async token => {
if (this.syncPromise) {
try {
// Wait until existing sync is finished
this.logService.debug('Auto Sync: Waiting until sync is finished.');
await this.syncPromise;
} catch (error) {
if (isPromiseCanceledError(error)) {
// Cancelled => Disposed. Donot continue sync.
return;
}
}
}
return this.doSync(reason, token);
});
this.syncPromise = syncPromise;
this.syncPromise.finally(() => this.syncPromise = undefined);
return this.syncPromise;
}
private async doSync(reason: string, token: CancellationToken): Promise<void> {
this.logService.info(`Auto Sync: Triggered by ${reason}`);
this._onDidStartSync.fire();
let error: Error | undefined;
try {
await this.userDataSyncService.sync();
const syncTask = await this.userDataSyncService.createSyncTask();
let manifest = syncTask.manifest;
// Server has no data but this machine was synced before
if (manifest === null && await this.userDataSyncService.hasPreviouslySynced()) {
// Sync was turned off in the cloud
throw new UserDataAutoSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff);
}
const sessionId = this.storageService.get(sessionIdKey, StorageScope.GLOBAL);
// Server session is different from client session
if (sessionId && manifest && sessionId !== manifest.session) {
throw new UserDataAutoSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired);
}
const machines = await this.userDataSyncMachinesService.getMachines(manifest || undefined);
// Return if cancellation is requested
if (token.isCancellationRequested) {
return;
}
const currentMachine = machines.find(machine => machine.isCurrent);
// Check if sync was turned off from other machine
if (currentMachine?.disabled) {
// Throw TurnedOff error
throw new UserDataAutoSyncError(localize('turned off machine', "Cannot sync because syncing is turned off on this machine from another machine."), UserDataSyncErrorCode.TurnedOff);
}
await syncTask.run(token);
// After syncing, get the manifest if it was not available before
if (manifest === null) {
manifest = await this.userDataSyncStoreService.manifest();
}
// Update local session id
if (manifest && manifest.session !== sessionId) {
this.storageService.store(sessionIdKey, manifest.session, StorageScope.GLOBAL);
}
// Return if cancellation is requested
if (token.isCancellationRequested) {
return;
}
// Add current machine
if (!currentMachine) {
await this.userDataSyncMachinesService.addCurrentMachine(manifest || undefined);
}
} catch (e) {
this.logService.error(e);
error = e;
}
this._onDidFinishSync.fire(error);
}

View File

@@ -22,6 +22,8 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
import { IProductService, ConfigurationSyncStore } from 'vs/platform/product/common/productService';
import { distinct } from 'vs/base/common/arrays';
import { isArray, isString, isObject } from 'vs/base/common/types';
import { IHeaders } from 'vs/base/parts/request/common/request';
import { CancellationToken } from 'vs/base/common/cancellation';
export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store';
@@ -160,20 +162,27 @@ export interface IResourceRefHandle {
export const IUserDataSyncStoreService = createDecorator<IUserDataSyncStoreService>('IUserDataSyncStoreService');
export type ServerResource = SyncResource | 'machines';
export interface IUserDataSyncStoreService {
_serviceBrand: undefined;
readonly _serviceBrand: undefined;
readonly userDataSyncStore: IUserDataSyncStore | undefined;
read(resource: ServerResource, oldValue: IUserData | null): Promise<IUserData>;
write(resource: ServerResource, content: string, ref: string | null): Promise<string>;
manifest(): Promise<IUserDataManifest | null>;
readonly onTokenFailed: Event<void>;
readonly onTokenSucceed: Event<void>;
setAuthToken(token: string, type: string): void;
// Sync requests
manifest(headers?: IHeaders): Promise<IUserDataManifest | null>;
read(resource: ServerResource, oldValue: IUserData | null, headers?: IHeaders): Promise<IUserData>;
write(resource: ServerResource, content: string, ref: string | null, headers?: IHeaders): Promise<string>;
clear(): Promise<void>;
delete(resource: ServerResource): Promise<void>;
getAllRefs(resource: ServerResource): Promise<IResourceRefHandle[]>;
resolveContent(resource: ServerResource, ref: string): Promise<string | null>;
delete(resource: ServerResource): Promise<void>;
}
export const IUserDataSyncBackupStoreService = createDecorator<IUserDataSyncBackupStoreService>('IUserDataSyncBackupStoreService');
export interface IUserDataSyncBackupStoreService {
_serviceBrand: undefined;
readonly _serviceBrand: undefined;
backup(resource: SyncResource, content: string): Promise<void>;
getAllRefs(resource: SyncResource): Promise<IResourceRefHandle[]>;
resolveContent(resource: SyncResource, ref?: string): Promise<string | null>;
@@ -186,6 +195,7 @@ export interface IUserDataSyncBackupStoreService {
export enum UserDataSyncErrorCode {
// Client Errors (>= 400 )
Unauthorized = 'Unauthorized', /* 401 */
Gone = 'Gone', /* 410 */
PreconditionFailed = 'PreconditionFailed', /* 412 */
TooLarge = 'TooLarge', /* 413 */
UpgradeRequired = 'UpgradeRequired', /* 426 */
@@ -210,11 +220,11 @@ export class UserDataSyncError extends Error {
constructor(message: string, public readonly code: UserDataSyncErrorCode, public readonly resource?: SyncResource) {
super(message);
this.name = `${this.code} (UserDataSyncError) ${this.resource}`;
this.name = `${this.code} (UserDataSyncError) ${this.resource || ''}`;
}
static toUserDataSyncError(error: Error): UserDataSyncError {
if (error instanceof UserDataSyncStoreError) {
if (error instanceof UserDataSyncError) {
return error;
}
const match = /^(.+) \(UserDataSyncError\) (.+)?$/.exec(error.name);
@@ -232,6 +242,12 @@ export class UserDataSyncStoreError extends UserDataSyncError {
}
}
export class UserDataAutoSyncError extends UserDataSyncError {
constructor(message: string, code: UserDataSyncErrorCode) {
super(message, code);
}
}
//#endregion
// #region User Data Synchroniser
@@ -266,9 +282,24 @@ export interface ISyncResourceHandle {
export type Conflict = { remote: URI, local: URI };
export interface ISyncPreviewResult {
export interface IRemoteUserData {
ref: string;
syncData: ISyncData | null;
}
export interface ISyncData {
version: number;
machineId?: string;
content: string;
}
export interface ISyncPreview {
readonly remoteUserData: IRemoteUserData;
readonly lastSyncUserData: IRemoteUserData | null;
readonly isLastSyncFromCurrentMachine: boolean;
readonly hasLocalChanged: boolean;
readonly hasRemoteChanged: boolean;
readonly hasConflicts: boolean;
}
export interface IUserDataSynchroniser {
@@ -282,11 +313,11 @@ export interface IUserDataSynchroniser {
pull(): Promise<void>;
push(): Promise<void>;
sync(manifest: IUserDataManifest | null): Promise<void>;
sync(manifest: IUserDataManifest | null, headers?: IHeaders): Promise<void>;
replace(uri: URI): Promise<boolean>;
stop(): Promise<void>;
getSyncPreview(): Promise<ISyncPreviewResult>
generateSyncPreview(): Promise<ISyncPreview | null>
hasPreviouslySynced(): Promise<boolean>
hasLocalData(): Promise<boolean>;
resetLocal(): Promise<void>;
@@ -304,23 +335,22 @@ export interface IUserDataSynchroniser {
// #region User Data Sync Services
export const IUserDataSyncEnablementService = createDecorator<IUserDataSyncEnablementService>('IUserDataSyncEnablementService');
export interface IUserDataSyncEnablementService {
export const IUserDataSyncResourceEnablementService = createDecorator<IUserDataSyncResourceEnablementService>('IUserDataSyncResourceEnablementService');
export interface IUserDataSyncResourceEnablementService {
_serviceBrand: any;
readonly onDidChangeEnablement: Event<boolean>;
readonly onDidChangeResourceEnablement: Event<[SyncResource, boolean]>;
isEnabled(): boolean;
setEnablement(enabled: boolean): void;
canToggleEnablement(): boolean;
isResourceEnabled(resource: SyncResource): boolean;
setResourceEnablement(resource: SyncResource, enabled: boolean): void;
}
export type SyncResourceConflicts = { syncResource: SyncResource, conflicts: Conflict[] };
export interface ISyncTask {
manifest: IUserDataManifest | null;
run(token: CancellationToken): Promise<void>;
}
export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUserDataSyncService');
export interface IUserDataSyncService {
_serviceBrand: any;
@@ -344,7 +374,10 @@ export interface IUserDataSyncService {
reset(): Promise<void>;
resetLocal(): Promise<void>;
isFirstTimeSyncWithMerge(): Promise<boolean>;
createSyncTask(): Promise<ISyncTask>
isFirstTimeSyncingWithAnotherMachine(): Promise<boolean>;
hasPreviouslySynced(): Promise<boolean>;
resolveContent(resource: URI): Promise<string | null>;
acceptConflict(conflictResource: URI, content: string): Promise<void>;
@@ -358,12 +391,17 @@ export const IUserDataAutoSyncService = createDecorator<IUserDataAutoSyncService
export interface IUserDataAutoSyncService {
_serviceBrand: any;
readonly onError: Event<UserDataSyncError>;
triggerAutoSync(sources: string[]): Promise<void>;
readonly onDidChangeEnablement: Event<boolean>;
isEnabled(): boolean;
canToggleEnablement(): boolean;
turnOn(pullFirst: boolean): Promise<void>;
turnOff(everywhere: boolean): Promise<void>;
triggerSync(sources: string[], hasToLimitSync: boolean): Promise<void>;
}
export const IUserDataSyncUtilService = createDecorator<IUserDataSyncUtilService>('IUserDataSyncUtilService');
export interface IUserDataSyncUtilService {
_serviceBrand: undefined;
readonly _serviceBrand: undefined;
resolveUserBindings(userbindings: string[]): Promise<IStringDictionary<string>>;
resolveFormattingOptions(resource: URI): Promise<FormattingOptions>;
resolveDefaultIgnoredSettings(): Promise<string[]>;

View File

@@ -0,0 +1,64 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IUserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSync';
export interface IUserDataSyncAccount {
readonly authenticationProviderId: string;
readonly token: string;
}
export const IUserDataSyncAccountService = createDecorator<IUserDataSyncAccountService>('IUserDataSyncAccountService');
export interface IUserDataSyncAccountService {
readonly _serviceBrand: undefined;
readonly onTokenFailed: Event<boolean>;
readonly account: IUserDataSyncAccount | undefined;
readonly onDidChangeAccount: Event<IUserDataSyncAccount | undefined>;
updateAccount(account: IUserDataSyncAccount | undefined): Promise<void>;
}
export class UserDataSyncAccountService extends Disposable implements IUserDataSyncAccountService {
_serviceBrand: any;
private _account: IUserDataSyncAccount | undefined;
get account(): IUserDataSyncAccount | undefined { return this._account; }
private _onDidChangeAccount = this._register(new Emitter<IUserDataSyncAccount | undefined>());
readonly onDidChangeAccount = this._onDidChangeAccount.event;
private _onTokenFailed: Emitter<boolean> = this._register(new Emitter<boolean>());
readonly onTokenFailed: Event<boolean> = this._onTokenFailed.event;
private wasTokenFailed: boolean = false;
constructor(
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService
) {
super();
this._register(userDataSyncStoreService.onTokenFailed(() => {
this.updateAccount(undefined);
this._onTokenFailed.fire(this.wasTokenFailed);
this.wasTokenFailed = true;
}));
this._register(userDataSyncStoreService.onTokenSucceed(() => this.wasTokenFailed = false));
}
async updateAccount(account: IUserDataSyncAccount | undefined): Promise<void> {
if (account && this._account ? account.token !== this._account.token || account.authenticationProviderId !== this._account.authenticationProviderId : account !== this._account) {
this._account = account;
if (this._account) {
this.userDataSyncStoreService.setAuthToken(this._account.token, this._account.authenticationProviderId);
}
this._onDidChangeAccount.fire(account);
}
}
}

View File

@@ -13,6 +13,7 @@ import { IStorageKeysSyncRegistryService, IStorageKey } from 'vs/platform/userDa
import { Disposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';
import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount';
export class UserDataSyncChannel implements IServerChannel {
@@ -48,7 +49,8 @@ export class UserDataSyncChannel implements IServerChannel {
case 'replace': return this.service.replace(URI.revive(args[0]));
case 'reset': return this.service.reset();
case 'resetLocal': return this.service.resetLocal();
case 'isFirstTimeSyncWithMerge': return this.service.isFirstTimeSyncWithMerge();
case 'hasPreviouslySynced': return this.service.hasPreviouslySynced();
case 'isFirstTimeSyncingWithAnotherMachine': return this.service.isFirstTimeSyncingWithAnotherMachine();
case 'acceptConflict': return this.service.acceptConflict(URI.revive(args[0]), args[1]);
case 'resolveContent': return this.service.resolveContent(URI.revive(args[0]));
case 'getLocalSyncResourceHandles': return this.service.getLocalSyncResourceHandles(args[0]);
@@ -73,7 +75,9 @@ export class UserDataAutoSyncChannel implements IServerChannel {
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'triggerAutoSync': return this.service.triggerAutoSync(args[0]);
case 'triggerSync': return this.service.triggerSync(args[0], args[1]);
case 'turnOn': return this.service.turnOn(args[0]);
case 'turnOff': return this.service.turnOff(args[0]);
}
throw new Error('Invalid call');
}
@@ -99,7 +103,7 @@ export class UserDataSycnUtilServiceChannel implements IServerChannel {
export class UserDataSyncUtilServiceClient implements IUserDataSyncUtilService {
_serviceBrand: undefined;
declare readonly _serviceBrand: undefined;
constructor(private readonly channel: IChannel) {
}
@@ -140,7 +144,7 @@ export class StorageKeysSyncRegistryChannel implements IServerChannel {
export class StorageKeysSyncRegistryChannelClient extends Disposable implements IStorageKeysSyncRegistryService {
_serviceBrand: undefined;
declare readonly _serviceBrand: undefined;
private _storageKeys: ReadonlyArray<IStorageKey> = [];
get storageKeys(): ReadonlyArray<IStorageKey> { return this._storageKeys; }
@@ -177,12 +181,33 @@ export class UserDataSyncMachinesServiceChannel implements IServerChannel {
async call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'getMachines': return this.service.getMachines();
case 'addCurrentMachine': return this.service.addCurrentMachine(args[0]);
case 'addCurrentMachine': return this.service.addCurrentMachine();
case 'removeCurrentMachine': return this.service.removeCurrentMachine();
case 'renameMachine': return this.service.renameMachine(args[0], args[1]);
case 'disableMachine': return this.service.disableMachine(args[0]);
case 'setEnablement': return this.service.setEnablement(args[0], args[1]);
}
throw new Error('Invalid call');
}
}
export class UserDataSyncAccountServiceChannel implements IServerChannel {
constructor(private readonly service: IUserDataSyncAccountService) { }
listen(_: unknown, event: string): Event<any> {
switch (event) {
case 'onDidChangeAccount': return this.service.onDidChangeAccount;
case 'onTokenFailed': return this.service.onTokenFailed;
}
throw new Error(`Event not found: ${event}`);
}
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case '_getInitialData': return Promise.resolve(this.service.account);
case 'updateAccount': return this.service.updateAccount(args);
}
throw new Error('Invalid call');
}
}

View File

@@ -9,7 +9,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
export class UserDataSyncLogService extends AbstractLogService implements IUserDataSyncLogService {
_serviceBrand: undefined;
declare readonly _serviceBrand: undefined;
private readonly logger: ILogger;
constructor(

View File

@@ -8,10 +8,12 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IUserDataSyncStoreService, IUserData, IUserDataSyncLogService, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync';
import { localize } from 'vs/nls';
import { IProductService } from 'vs/platform/product/common/productService';
import { PlatformToString, isWeb, Platform, platform } from 'vs/base/common/platform';
import { escapeRegExpCharacters } from 'vs/base/common/strings';
interface IMachineData {
id: string;
@@ -26,20 +28,20 @@ interface IMachinesData {
export type IUserDataSyncMachine = Readonly<IMachineData> & { readonly isCurrent: boolean };
export const IUserDataSyncMachinesService = createDecorator<IUserDataSyncMachinesService>('IUserDataSyncMachinesService');
export interface IUserDataSyncMachinesService {
_serviceBrand: any;
getMachines(manifest?: IUserDataManifest): Promise<IUserDataSyncMachine[]>;
addCurrentMachine(name: string, manifest?: IUserDataManifest): Promise<void>;
addCurrentMachine(manifest?: IUserDataManifest): Promise<void>;
removeCurrentMachine(manifest?: IUserDataManifest): Promise<void>;
renameMachine(machineId: string, name: string): Promise<void>;
disableMachine(machineId: string): Promise<void>
setEnablement(machineId: string, enabled: boolean): Promise<void>;
}
const currentMachineNameKey = 'sync.currentMachineName';
export class UserDataSyncMachinesService extends Disposable implements IUserDataSyncMachinesService {
private static readonly VERSION = 1;
@@ -53,7 +55,7 @@ export class UserDataSyncMachinesService extends Disposable implements IUserData
constructor(
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService fileService: IFileService,
@IStorageService storageService: IStorageService,
@IStorageService private readonly storageService: IStorageService,
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IProductService private readonly productService: IProductService,
@@ -68,16 +70,13 @@ export class UserDataSyncMachinesService extends Disposable implements IUserData
return machineData.machines.map<IUserDataSyncMachine>(machine => ({ ...machine, ...{ isCurrent: machine.id === currentMachineId } }));
}
async addCurrentMachine(name: string, manifest?: IUserDataManifest): Promise<void> {
async addCurrentMachine(manifest?: IUserDataManifest): Promise<void> {
const currentMachineId = await this.currentMachineIdPromise;
const machineData = await this.readMachinesData(manifest);
let currentMachine = machineData.machines.find(({ id }) => id === currentMachineId);
if (currentMachine) {
currentMachine.name = name;
} else {
machineData.machines.push({ id: currentMachineId, name });
if (!machineData.machines.some(({ id }) => id === currentMachineId)) {
machineData.machines.push({ id: currentMachineId, name: this.computeCurrentMachineName(machineData.machines) });
await this.writeMachinesData(machineData);
}
await this.writeMachinesData(machineData);
}
async removeCurrentMachine(manifest?: IUserDataManifest): Promise<void> {
@@ -91,21 +90,42 @@ export class UserDataSyncMachinesService extends Disposable implements IUserData
}
async renameMachine(machineId: string, name: string, manifest?: IUserDataManifest): Promise<void> {
const currentMachineId = await this.currentMachineIdPromise;
const machineData = await this.readMachinesData(manifest);
const currentMachine = machineData.machines.find(({ id }) => id === machineId);
if (currentMachine) {
currentMachine.name = name;
const machine = machineData.machines.find(({ id }) => id === machineId);
if (machine) {
machine.name = name;
await this.writeMachinesData(machineData);
if (machineData.machines.some(({ id }) => id === currentMachineId)) {
this.storageService.store(currentMachineNameKey, name, StorageScope.GLOBAL);
}
}
}
async setEnablement(machineId: string, enabled: boolean): Promise<void> {
const machineData = await this.readMachinesData();
const machine = machineData.machines.find(({ id }) => id === machineId);
if (machine) {
machine.disabled = enabled ? undefined : true;
await this.writeMachinesData(machineData);
}
}
async disableMachine(machineId: string): Promise<void> {
const machineData = await this.readMachinesData();
const machine = machineData.machines.find(({ id }) => id === machineId);
if (machine) {
machine.disabled = true;
await this.writeMachinesData(machineData);
private computeCurrentMachineName(machines: IMachineData[]): string {
const previousName = this.storageService.get(currentMachineNameKey, StorageScope.GLOBAL);
if (previousName) {
return previousName;
}
const namePrefix = `${this.productService.nameLong} (${PlatformToString(isWeb ? Platform.Web : platform)})`;
const nameRegEx = new RegExp(`${escapeRegExpCharacters(namePrefix)}\\s#(\\d)`);
let nameIndex = 0;
for (const machine of machines) {
const matches = nameRegEx.exec(machine.name);
const index = matches ? parseInt(matches[1]) : 0;
nameIndex = index > nameIndex ? index : nameIndex;
}
return `${namePrefix} #${nameIndex + 1}`;
}
private async readMachinesData(manifest?: IUserDataManifest): Promise<IMachinesData> {

View File

@@ -3,12 +3,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUserDataSyncEnablementService, ALL_SYNC_RESOURCES, SyncResource } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncResourceEnablementService, ALL_SYNC_RESOURCES, SyncResource } from 'vs/platform/userDataSync/common/userDataSync';
import { Disposable } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
import { IStorageService, IWorkspaceStorageChangeEvent, StorageScope } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
type SyncEnablementClassification = {
enabled?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
@@ -17,46 +16,21 @@ type SyncEnablementClassification = {
const enablementKey = 'sync.enable';
function getEnablementKey(resource: SyncResource) { return `${enablementKey}.${resource}`; }
export class UserDataSyncEnablementService extends Disposable implements IUserDataSyncEnablementService {
export class UserDataSyncResourceEnablementService extends Disposable implements IUserDataSyncResourceEnablementService {
_serviceBrand: any;
private _onDidChangeEnablement = new Emitter<boolean>();
readonly onDidChangeEnablement: Event<boolean> = this._onDidChangeEnablement.event;
private _onDidChangeResourceEnablement = new Emitter<[SyncResource, boolean]>();
readonly onDidChangeResourceEnablement: Event<[SyncResource, boolean]> = this._onDidChangeResourceEnablement.event;
constructor(
@IStorageService private readonly storageService: IStorageService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
) {
super();
this._register(storageService.onDidChangeStorage(e => this.onDidStorageChange(e)));
}
canToggleEnablement(): boolean {
return this.environmentService.sync === undefined;
}
isEnabled(): boolean {
switch (this.environmentService.sync) {
case 'on':
return true;
case 'off':
return false;
}
return this.storageService.getBoolean(enablementKey, StorageScope.GLOBAL, this.environmentService.enableSyncByDefault);
}
setEnablement(enabled: boolean): void {
if (this.isEnabled() !== enabled) {
this.telemetryService.publicLog2<{ enabled: boolean }, SyncEnablementClassification>(enablementKey, { enabled });
this.storageService.store(enablementKey, enabled, StorageScope.GLOBAL);
}
}
isResourceEnabled(resource: SyncResource): boolean {
return this.storageService.getBoolean(getEnablementKey(resource), StorageScope.GLOBAL, true);
}
@@ -71,13 +45,9 @@ export class UserDataSyncEnablementService extends Disposable implements IUserDa
private onDidStorageChange(workspaceStorageChangeEvent: IWorkspaceStorageChangeEvent): void {
if (workspaceStorageChangeEvent.scope === StorageScope.GLOBAL) {
if (enablementKey === workspaceStorageChangeEvent.key) {
this._onDidChangeEnablement.fire(this.isEnabled());
return;
}
const resourceKey = ALL_SYNC_RESOURCES.filter(resourceKey => getEnablementKey(resourceKey) === workspaceStorageChangeEvent.key)[0];
if (resourceKey) {
this._onDidChangeResourceEnablement.fire([resourceKey, this.isEnabled()]);
this._onDidChangeResourceEnablement.fire([resourceKey, this.isResourceEnabled(resourceKey)]);
return;
}
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle, IUserDataManifest, ISyncTask } from 'vs/platform/userDataSync/common/userDataSync';
import { Disposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Emitter, Event } from 'vs/base/common/event';
@@ -13,30 +13,25 @@ import { GlobalStateSynchroniser } from 'vs/platform/userDataSync/common/globalS
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { equals } from 'vs/base/common/arrays';
import { localize } from 'vs/nls';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { URI } from 'vs/base/common/uri';
import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync';
import { isEqual } from 'vs/base/common/resources';
import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync';
import { Throttler } from 'vs/base/common/async';
import { IUserDataSyncMachinesService, IUserDataSyncMachine } from 'vs/platform/userDataSync/common/userDataSyncMachines';
import { IProductService } from 'vs/platform/product/common/productService';
import { platform, PlatformToString } from 'vs/base/common/platform';
import { escapeRegExpCharacters } from 'vs/base/common/strings';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IHeaders } from 'vs/base/parts/request/common/request';
import { generateUuid } from 'vs/base/common/uuid';
type SyncClassification = {
resource?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
const SESSION_ID_KEY = 'sync.sessionId';
const LAST_SYNC_TIME_KEY = 'sync.lastSyncTime';
export class UserDataSyncService extends Disposable implements IUserDataSyncService {
_serviceBrand: any;
private readonly syncThrottler: Throttler;
private readonly synchronisers: IUserDataSynchroniser[];
private _status: SyncStatus = SyncStatus.Uninitialized;
@@ -72,11 +67,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IStorageService private readonly storageService: IStorageService,
@IUserDataSyncMachinesService private readonly userDataSyncMachinesService: IUserDataSyncMachinesService,
@IProductService private readonly productService: IProductService
) {
super();
this.syncThrottler = new Throttler();
this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser));
this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser));
this.snippetsSynchroniser = this._register(this.instantiationService.createInstance(SnippetsSynchroniser));
@@ -107,7 +99,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.updateLastSyncTime();
} catch (error) {
if (error instanceof UserDataSyncError) {
this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${UserDataSyncErrorCode.TooLarge}`, { resource: error.resource });
this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${error.code}`, { resource: error.resource });
}
throw error;
}
@@ -126,7 +118,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.updateLastSyncTime();
} catch (error) {
if (error instanceof UserDataSyncError) {
this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${UserDataSyncErrorCode.TooLarge}`, { resource: error.resource });
this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${error.code}`, { resource: error.resource });
}
throw error;
}
@@ -134,6 +126,29 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
private recoveredSettings: boolean = false;
async sync(): Promise<void> {
const syncTask = await this.createSyncTask();
return syncTask.run(CancellationToken.None);
}
async createSyncTask(): Promise<ISyncTask> {
this.telemetryService.publicLog2('sync/getmanifest');
const syncHeaders: IHeaders = { 'X-Execution-Id': generateUuid() };
const manifest = await this.userDataSyncStoreService.manifest(syncHeaders);
let executed = false;
const that = this;
return {
manifest,
run(token: CancellationToken): Promise<void> {
if (executed) {
throw new Error('Can run a task only once');
}
return that.doSync(manifest, syncHeaders, token);
}
};
}
private async doSync(manifest: IUserDataManifest | null, syncHeaders: IHeaders, token: CancellationToken): Promise<void> {
await this.checkEnablement();
if (!this.recoveredSettings) {
@@ -141,10 +156,11 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.recoveredSettings = true;
}
await this.syncThrottler.queue(() => this.doSync());
}
// Return if cancellation is requested
if (token.isCancellationRequested) {
return;
}
private async doSync(): Promise<void> {
const startTime = new Date().getTime();
this._syncErrors = [];
try {
@@ -153,62 +169,24 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.setStatus(SyncStatus.Syncing);
}
this.telemetryService.publicLog2('sync/getmanifest');
let manifest = await this.userDataSyncStoreService.manifest();
// Server has no data but this machine was synced before
if (manifest === null && await this.hasPreviouslySynced()) {
// Sync was turned off in the cloud
throw new UserDataSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff);
}
const sessionId = this.storageService.get(SESSION_ID_KEY, StorageScope.GLOBAL);
// Server session is different from client session
if (sessionId && manifest && sessionId !== manifest.session) {
throw new UserDataSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired);
}
const machines = await this.userDataSyncMachinesService.getMachines(manifest || undefined);
const currentMachine = machines.find(machine => machine.isCurrent);
// Check if sync was turned off from other machine
if (currentMachine?.disabled) {
// Unset the current machine
await this.userDataSyncMachinesService.removeCurrentMachine(manifest || undefined);
// Throw TurnedOff error
throw new UserDataSyncError(localize('turned off machine', "Cannot sync because syncing is turned off on this machine from another machine."), UserDataSyncErrorCode.TurnedOff);
}
for (const synchroniser of this.synchronisers) {
// Return if cancellation is requested
if (token.isCancellationRequested) {
return;
}
try {
await synchroniser.sync(manifest);
await synchroniser.sync(manifest, syncHeaders);
} catch (e) {
this.handleSynchronizerError(e, synchroniser.resource);
this._syncErrors.push([synchroniser.resource, UserDataSyncError.toUserDataSyncError(e)]);
}
}
// After syncing, get the manifest if it was not available before
if (manifest === null) {
manifest = await this.userDataSyncStoreService.manifest();
}
// Update local session id
if (manifest && manifest.session !== sessionId) {
this.storageService.store(SESSION_ID_KEY, manifest.session, StorageScope.GLOBAL);
}
if (!currentMachine) {
const name = this.computeDefaultMachineName(machines);
await this.userDataSyncMachinesService.addCurrentMachine(name, manifest || undefined);
}
this.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`);
this.updateLastSyncTime();
} catch (error) {
if (error instanceof UserDataSyncError) {
this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${UserDataSyncErrorCode.TooLarge}`, { resource: error.resource });
this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${error.code}`, { resource: error.resource });
}
throw error;
} finally {
@@ -228,9 +206,11 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
async stop(): Promise<void> {
await this.checkEnablement();
if (this.status === SyncStatus.Idle) {
return;
}
for (const synchroniser of this.synchronisers) {
try {
if (synchroniser.status !== SyncStatus.Idle) {
@@ -240,6 +220,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.logService.error(e);
}
}
}
async acceptConflict(conflict: URI, content: string): Promise<void> {
@@ -277,39 +258,47 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return this.getSynchroniser(resource).getMachineId(syncResourceHandle);
}
async isFirstTimeSyncWithMerge(): Promise<boolean> {
async isFirstTimeSyncingWithAnotherMachine(): Promise<boolean> {
await this.checkEnablement();
if (!await this.userDataSyncStoreService.manifest()) {
return false;
}
if (await this.hasPreviouslySynced()) {
// skip global state synchronizer
const synchronizers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.extensionsSynchroniser];
let hasLocalData: boolean = false;
for (const synchroniser of synchronizers) {
if (await synchroniser.hasLocalData()) {
hasLocalData = true;
break;
}
}
if (!hasLocalData) {
return false;
}
if (!(await this.hasLocalData())) {
return false;
}
for (const synchroniser of [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.extensionsSynchroniser]) {
const preview = await synchroniser.getSyncPreview();
if (preview.hasLocalChanged || preview.hasRemoteChanged) {
for (const synchroniser of synchronizers) {
const preview = await synchroniser.generateSyncPreview();
if (preview && !preview.isLastSyncFromCurrentMachine && (preview.hasLocalChanged || preview.hasRemoteChanged)) {
return true;
}
}
return false;
}
async reset(): Promise<void> {
await this.checkEnablement();
await this.resetRemote();
await this.resetLocal(true);
await this.resetLocal();
}
async resetLocal(donotUnsetMachine?: boolean): Promise<void> {
async resetLocal(): Promise<void> {
await this.checkEnablement();
this.storageService.remove(SESSION_ID_KEY, StorageScope.GLOBAL);
this.storageService.remove(LAST_SYNC_TIME_KEY, StorageScope.GLOBAL);
if (!donotUnsetMachine) {
await this.userDataSyncMachinesService.removeCurrentMachine();
}
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.resetLocal();
@@ -318,9 +307,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.logService.error(e);
}
}
this.logService.info('Did reset the local sync state.');
}
private async hasPreviouslySynced(): Promise<boolean> {
async hasPreviouslySynced(): Promise<boolean> {
for (const synchroniser of this.synchronisers) {
if (await synchroniser.hasPreviouslySynced()) {
return true;
@@ -329,19 +319,11 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return false;
}
private async hasLocalData(): Promise<boolean> {
for (const synchroniser of this.synchronisers) {
if (await synchroniser.hasLocalData()) {
return true;
}
}
return false;
}
private async resetRemote(): Promise<void> {
await this.checkEnablement();
try {
await this.userDataSyncStoreService.clear();
this.logService.info('Cleared data on server');
} catch (e) {
this.logService.error(e);
}
@@ -397,8 +379,11 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
if (e instanceof UserDataSyncError) {
switch (e.code) {
case UserDataSyncErrorCode.TooLarge:
throw new UserDataSyncError(e.message, e.code, source);
case UserDataSyncErrorCode.TooManyRequests:
case UserDataSyncErrorCode.LocalTooManyRequests:
case UserDataSyncErrorCode.Gone:
case UserDataSyncErrorCode.UpgradeRequired:
case UserDataSyncErrorCode.Incompatible:
throw e;
@@ -413,20 +398,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
.map(s => ({ syncResource: s.resource, conflicts: s.conflicts }));
}
private computeDefaultMachineName(machines: IUserDataSyncMachine[]): string {
const namePrefix = `${this.productService.nameLong} (${PlatformToString(platform)})`;
const nameRegEx = new RegExp(`${escapeRegExpCharacters(namePrefix)}\\s#(\\d)`);
let nameIndex = 0;
for (const machine of machines) {
const matches = nameRegEx.exec(machine.name);
const index = matches ? parseInt(matches[1]) : 0;
nameIndex = index > nameIndex ? index : nameIndex;
}
return `${namePrefix} #${nameIndex + 1}`;
}
getSynchroniser(source: SyncResource): IUserDataSynchroniser {
return this.synchronisers.filter(s => s.resource === source)[0];
}

View File

@@ -10,7 +10,6 @@ import { joinPath, relativePath } from 'vs/base/common/resources';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication';
import { IProductService } from 'vs/platform/product/common/productService';
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
@@ -19,6 +18,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag
import { assign } from 'vs/base/common/objects';
import { generateUuid } from 'vs/base/common/uuid';
import { isWeb } from 'vs/base/common/platform';
import { Emitter, Event } from 'vs/base/common/event';
const USER_SESSION_ID_KEY = 'sync.user-session-id';
const MACHINE_SESSION_ID_KEY = 'sync.machine-session-id';
@@ -30,14 +30,20 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
_serviceBrand: any;
readonly userDataSyncStore: IUserDataSyncStore | undefined;
private authToken: { token: string, type: string } | undefined;
private readonly commonHeadersPromise: Promise<{ [key: string]: string; }>;
private readonly session: RequestsSession;
private _onTokenFailed: Emitter<void> = this._register(new Emitter<void>());
readonly onTokenFailed: Event<void> = this._onTokenFailed.event;
private _onTokenSucceed: Emitter<void> = this._register(new Emitter<void>());
readonly onTokenSucceed: Event<void> = this._onTokenSucceed.event;
constructor(
@IProductService productService: IProductService,
@IConfigurationService configurationService: IConfigurationService,
@IRequestService private readonly requestService: IRequestService,
@IAuthenticationTokenService private readonly authTokenService: IAuthenticationTokenService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService fileService: IFileService,
@@ -62,6 +68,10 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
this.session = new RequestsSession(REQUEST_SESSION_LIMIT, REQUEST_SESSION_INTERVAL, this.requestService);
}
setAuthToken(token: string, type: string): void {
this.authToken = { token, type };
}
async getAllRefs(resource: ServerResource): Promise<IResourceRefHandle[]> {
if (!this.userDataSyncStore) {
throw new Error('No settings sync store url configured.');
@@ -114,13 +124,13 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
}
}
async read(resource: ServerResource, oldValue: IUserData | null): Promise<IUserData> {
async read(resource: ServerResource, oldValue: IUserData | null, headers: IHeaders = {}): Promise<IUserData> {
if (!this.userDataSyncStore) {
throw new Error('No settings sync store url configured.');
}
const url = joinPath(this.userDataSyncStore.url, 'resource', resource, 'latest').toString();
const headers: IHeaders = {};
headers = { ...headers };
// Disable caching as they are cached by synchronisers
headers['Cache-Control'] = 'no-cache';
if (oldValue) {
@@ -146,13 +156,14 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
return { ref, content };
}
async write(resource: ServerResource, data: string, ref: string | null): Promise<string> {
async write(resource: ServerResource, data: string, ref: string | null, headers: IHeaders = {}): Promise<string> {
if (!this.userDataSyncStore) {
throw new Error('No settings sync store url configured.');
}
const url = joinPath(this.userDataSyncStore.url, 'resource', resource).toString();
const headers: IHeaders = { 'Content-Type': 'text/plain' };
headers = { ...headers };
headers['Content-Type'] = 'text/plain';
if (ref) {
headers['If-Match'] = ref;
}
@@ -170,13 +181,14 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
return newRef;
}
async manifest(): Promise<IUserDataManifest | null> {
async manifest(headers: IHeaders = {}): Promise<IUserDataManifest | null> {
if (!this.userDataSyncStore) {
throw new Error('No settings sync store url configured.');
}
const url = joinPath(this.userDataSyncStore.url, 'manifest').toString();
const headers: IHeaders = { 'Content-Type': 'application/json' };
headers = { ...headers };
headers['Content-Type'] = 'application/json';
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
if (!isSuccess(context)) {
@@ -228,15 +240,14 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
}
private async request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
const authToken = this.authTokenService.token;
if (!authToken) {
if (!this.authToken) {
throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized);
}
const commonHeaders = await this.commonHeadersPromise;
options.headers = assign(options.headers || {}, commonHeaders, {
'X-Account-Type': authToken.authenticationProviderId,
'authorization': `Bearer ${authToken.token}`,
'X-Account-Type': this.authToken.type,
'authorization': `Bearer ${this.authToken.token}`,
});
// Add session headers
@@ -256,10 +267,17 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
}
if (context.res.statusCode === 401) {
this.authTokenService.sendTokenFailed();
this.authToken = undefined;
this._onTokenFailed.fire();
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized);
}
this._onTokenSucceed.fire();
if (context.res.statusCode === 410) {
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because the requested resource is not longer available (410).`, UserDataSyncErrorCode.Gone);
}
if (context.res.statusCode === 412) {
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.PreconditionFailed);
}