mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-09 09:42:34 -05:00
Merge from vscode 4d91d96e5e121b38d33508cdef17868bab255eae
This commit is contained in:
committed by
AzureDataStudio
parent
a971aee5bd
commit
5e7071e466
@@ -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 {
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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[]>;
|
||||
|
||||
64
src/vs/platform/userDataSync/common/userDataSyncAccount.ts
Normal file
64
src/vs/platform/userDataSync/common/userDataSyncAccount.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user