mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-04 17:23:45 -05:00
Merge from vscode 8a997f7321ae6612fc0e6eb3eac4f358a6233bfb
This commit is contained in:
@@ -4,15 +4,22 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IFileService, IFileContent } from 'vs/platform/files/common/files';
|
||||
import { IFileService, IFileContent, FileChangesEvent, FileSystemProviderError, FileSystemProviderErrorCode, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { SyncSource, SyncStatus, IUserData, IUserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { SyncSource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { joinPath, dirname } from 'vs/base/common/resources';
|
||||
import { toLocalISOString } from 'vs/base/common/date';
|
||||
import { ThrottledDelayer } from 'vs/base/common/async';
|
||||
import { ThrottledDelayer, CancelablePromise } 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';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
|
||||
type SyncConflictsClassification = {
|
||||
source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
};
|
||||
|
||||
export abstract class AbstractSynchroniser extends Disposable {
|
||||
|
||||
@@ -34,6 +41,9 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
@IFileService protected readonly fileService: IFileService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncEnablementService protected readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IUserDataSyncLogService protected readonly logService: IUserDataSyncLogService,
|
||||
) {
|
||||
super();
|
||||
this.syncFolder = joinPath(environmentService.userDataSyncHome, source);
|
||||
@@ -43,20 +53,53 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
|
||||
protected setStatus(status: SyncStatus): void {
|
||||
if (this._status !== status) {
|
||||
const oldStatus = this._status;
|
||||
this._status = status;
|
||||
this._onDidChangStatus.fire(status);
|
||||
if (status === SyncStatus.HasConflicts) {
|
||||
// Log to telemetry when there is a sync conflict
|
||||
this.telemetryService.publicLog2<{ source: string }, SyncConflictsClassification>('sync/conflictsDetected', { source: this.source });
|
||||
}
|
||||
if (oldStatus === SyncStatus.HasConflicts && status === SyncStatus.Idle) {
|
||||
// Log to telemetry when conflicts are resolved
|
||||
this.telemetryService.publicLog2<{ source: string }, SyncConflictsClassification>('sync/conflictsResolved', { source: this.source });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected get enabled(): boolean { return this.userDataSyncEnablementService.isResourceEnabled(this.resourceKey); }
|
||||
|
||||
async sync(ref?: string): Promise<void> {
|
||||
if (!this.enabled) {
|
||||
this.logService.info(`${this.source}: Skipped synchronizing ${this.source.toLowerCase()} as it is disabled.`);
|
||||
return;
|
||||
}
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
this.logService.info(`${this.source}: Skipped synchronizing ${this.source.toLowerCase()} as there are conflicts.`);
|
||||
return;
|
||||
}
|
||||
if (this.status === SyncStatus.Syncing) {
|
||||
this.logService.info(`${this.source}: Skipped synchronizing ${this.source.toLowerCase()} as it is running already.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.trace(`${this.source}: Started synchronizing ${this.source.toLowerCase()}...`);
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = ref && lastSyncUserData && lastSyncUserData.ref === ref ? lastSyncUserData : await this.getRemoteUserData(lastSyncUserData);
|
||||
return this.doSync(remoteUserData, lastSyncUserData);
|
||||
}
|
||||
|
||||
async hasPreviouslySynced(): Promise<boolean> {
|
||||
const lastSyncData = await this.getLastSyncUserData();
|
||||
return !!lastSyncData;
|
||||
}
|
||||
|
||||
async hasRemoteData(): Promise<boolean> {
|
||||
async getRemoteContent(): Promise<string | null> {
|
||||
const lastSyncData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncData);
|
||||
return remoteUserData.content !== null;
|
||||
return remoteUserData.content;
|
||||
}
|
||||
|
||||
async resetLocal(): Promise<void> {
|
||||
@@ -79,11 +122,11 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
|
||||
protected async getRemoteUserData(lastSyncData: IUserData | null): Promise<IUserData> {
|
||||
return this.userDataSyncStoreService.read(this.getRemoteDataResourceKey(), lastSyncData, this.source);
|
||||
return this.userDataSyncStoreService.read(this.resourceKey, lastSyncData, this.source);
|
||||
}
|
||||
|
||||
protected async updateRemoteUserData(content: string, ref: string | null): Promise<string> {
|
||||
return this.userDataSyncStoreService.write(this.getRemoteDataResourceKey(), content, ref, this.source);
|
||||
return this.userDataSyncStoreService.write(this.resourceKey, content, ref, this.source);
|
||||
}
|
||||
|
||||
protected async backupLocal(content: VSBuffer): Promise<void> {
|
||||
@@ -101,21 +144,56 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract getRemoteDataResourceKey(): string;
|
||||
abstract readonly resourceKey: ResourceKey;
|
||||
protected abstract doSync(remoteUserData: IUserData, lastSyncUserData: IUserData | null): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IFileSyncPreviewResult {
|
||||
readonly fileContent: IFileContent | null;
|
||||
readonly remoteUserData: IUserData;
|
||||
readonly lastSyncUserData: IUserData | null;
|
||||
readonly content: string | null;
|
||||
readonly hasLocalChanged: boolean;
|
||||
readonly hasRemoteChanged: boolean;
|
||||
readonly hasConflicts: boolean;
|
||||
}
|
||||
|
||||
export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
||||
|
||||
protected syncPreviewResultPromise: CancelablePromise<IFileSyncPreviewResult> | null = null;
|
||||
|
||||
constructor(
|
||||
protected readonly file: URI,
|
||||
readonly source: SyncSource,
|
||||
source: SyncSource,
|
||||
@IFileService fileService: IFileService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
) {
|
||||
super(source, fileService, environmentService, userDataSyncStoreService);
|
||||
super(source, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService);
|
||||
this._register(this.fileService.watch(dirname(file)));
|
||||
this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(file))(() => this._onDidChangeLocal.fire()));
|
||||
this._register(this.fileService.onFileChanges(e => this.onFileChanges(e)));
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
this.cancel();
|
||||
this.logService.trace(`${this.source}: Stopped synchronizing ${this.source.toLowerCase()}.`);
|
||||
try {
|
||||
await this.fileService.del(this.conflictsPreviewResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
|
||||
async getRemoteContent(preview?: boolean): Promise<string | null> {
|
||||
if (preview) {
|
||||
if (this.syncPreviewResultPromise) {
|
||||
const result = await this.syncPreviewResultPromise;
|
||||
return result.remoteUserData ? result.remoteUserData.content : null;
|
||||
}
|
||||
}
|
||||
return super.getRemoteContent();
|
||||
}
|
||||
|
||||
protected async getLocalFileContent(): Promise<IFileContent | null> {
|
||||
@@ -127,14 +205,87 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
||||
}
|
||||
|
||||
protected async updateLocalFileContent(newContent: string, oldContent: IFileContent | null): Promise<void> {
|
||||
if (oldContent) {
|
||||
// file exists already
|
||||
await this.backupLocal(oldContent.value);
|
||||
await this.fileService.writeFile(this.file, VSBuffer.fromString(newContent), oldContent);
|
||||
} else {
|
||||
// file does not exist
|
||||
await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: false });
|
||||
try {
|
||||
if (oldContent) {
|
||||
// file exists already
|
||||
await this.backupLocal(oldContent.value);
|
||||
await this.fileService.writeFile(this.file, VSBuffer.fromString(newContent), oldContent);
|
||||
} else {
|
||||
// file does not exist
|
||||
await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: false });
|
||||
}
|
||||
} catch (e) {
|
||||
if ((e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) ||
|
||||
(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) {
|
||||
throw new UserDataSyncError(e.message, UserDataSyncErrorCode.LocalPreconditionFailed);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onFileChanges(e: FileChangesEvent): void {
|
||||
if (!e.contains(this.file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.enabled) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise fire change event
|
||||
else {
|
||||
this._onDidChangeLocal.fire();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected cancel(): void {
|
||||
if (this.syncPreviewResultPromise) {
|
||||
this.syncPreviewResultPromise.cancel();
|
||||
this.syncPreviewResultPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract readonly conflictsPreviewResource: URI;
|
||||
}
|
||||
|
||||
export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroniser {
|
||||
|
||||
constructor(
|
||||
file: URI,
|
||||
source: SyncSource,
|
||||
@IFileService fileService: IFileService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
@IUserDataSyncUtilService protected readonly userDataSyncUtilService: IUserDataSyncUtilService,
|
||||
) {
|
||||
super(file, source, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService);
|
||||
}
|
||||
|
||||
protected hasErrors(content: string): boolean {
|
||||
const parseErrors: ParseError[] = [];
|
||||
parse(content, parseErrors, { allowEmptyContent: true, allowTrailingComma: true });
|
||||
return parseErrors.length > 0;
|
||||
}
|
||||
|
||||
private _formattingOptions: Promise<FormattingOptions> | undefined = undefined;
|
||||
protected getFormattingOptions(): Promise<FormattingOptions> {
|
||||
if (!this._formattingOptions) {
|
||||
this._formattingOptions = this.userDataSyncUtilService.resolveFormattingOptions(this.file);
|
||||
}
|
||||
return this._formattingOptions;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IUserData, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserData, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncSource, ResourceKey, IUserDataSyncEnablementService } 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 } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
@@ -15,6 +15,7 @@ import { localize } from 'vs/nls';
|
||||
import { merge } from 'vs/platform/userDataSync/common/extensionsMerge';
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { AbstractSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
interface ISyncPreviewResult {
|
||||
readonly added: ISyncExtension[];
|
||||
@@ -32,17 +33,21 @@ interface ILastSyncUserData extends IUserData {
|
||||
|
||||
export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
readonly resourceKey: ResourceKey = 'extensions';
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
|
||||
@IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService,
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
) {
|
||||
super(SyncSource.Extensions, fileService, environmentService, userDataSyncStoreService);
|
||||
super(SyncSource.Extensions, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService);
|
||||
this._register(
|
||||
Event.debounce(
|
||||
Event.any<any>(
|
||||
@@ -52,10 +57,8 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
() => undefined, 500)(() => this._onDidChangeLocal.fire()));
|
||||
}
|
||||
|
||||
protected getRemoteDataResourceKey(): string { return 'extensions'; }
|
||||
|
||||
async pull(): Promise<void> {
|
||||
if (!this.configurationService.getValue<boolean>('sync.enableExtensions')) {
|
||||
if (!this.enabled) {
|
||||
this.logService.info('Extensions: Skipped pulling extensions as it is disabled.');
|
||||
return;
|
||||
}
|
||||
@@ -88,7 +91,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
}
|
||||
|
||||
async push(): Promise<void> {
|
||||
if (!this.configurationService.getValue<boolean>('sync.enableExtensions')) {
|
||||
if (!this.enabled) {
|
||||
this.logService.info('Extensions: Skipped pushing extensions as it is disabled.');
|
||||
return;
|
||||
}
|
||||
@@ -112,46 +115,16 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
|
||||
}
|
||||
|
||||
async sync(): Promise<void> {
|
||||
if (!this.configurationService.getValue<boolean>('sync.enableExtensions')) {
|
||||
this.logService.trace('Extensions: Skipping synchronizing extensions as it is disabled.');
|
||||
return;
|
||||
}
|
||||
async sync(ref?: string): Promise<void> {
|
||||
if (!this.extensionGalleryService.isEnabled()) {
|
||||
this.logService.trace('Extensions: Skipping synchronizing extensions as gallery is disabled.');
|
||||
this.logService.info('Extensions: Skipping synchronizing extensions as gallery is disabled.');
|
||||
return;
|
||||
}
|
||||
if (this.status !== SyncStatus.Idle) {
|
||||
this.logService.trace('Extensions: Skipping synchronizing extensions as it is running already.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.trace('Extensions: Started synchronizing extensions...');
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
try {
|
||||
const previewResult = await this.getPreview();
|
||||
await this.apply(previewResult);
|
||||
} catch (e) {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.Rejected) {
|
||||
// Rejected as there is a new remote version. Syncing again,
|
||||
this.logService.info('Extensions: Failed to synchronize extensions as there is a new remote version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
this.logService.trace('Extensions: Finished synchronizing extensions.');
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
return super.sync(ref);
|
||||
}
|
||||
|
||||
async stop(): Promise<void> { }
|
||||
|
||||
async restart(): Promise<void> {
|
||||
throw new Error('Extensions: Conflicts should not occur');
|
||||
}
|
||||
|
||||
accept(content: string): Promise<void> {
|
||||
throw new Error('Extensions: Conflicts should not occur');
|
||||
}
|
||||
@@ -172,14 +145,29 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getPreview(): Promise<ISyncPreviewResult> {
|
||||
const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
|
||||
protected async doSync(remoteUserData: IUserData, lastSyncUserData: ILastSyncUserData | null): Promise<void> {
|
||||
try {
|
||||
const previewResult = await this.getPreview(remoteUserData, lastSyncUserData);
|
||||
await this.apply(previewResult);
|
||||
} catch (e) {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.RemotePreconditionFailed) {
|
||||
// Rejected as there is a new remote version. Syncing again,
|
||||
this.logService.info('Extensions: Failed to synchronize extensions as there is a new remote version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
this.logService.trace('Extensions: Finished synchronizing extensions.');
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
|
||||
private async getPreview(remoteUserData: IUserData, lastSyncUserData: ILastSyncUserData | null): Promise<ISyncPreviewResult> {
|
||||
const remoteExtensions: ISyncExtension[] = remoteUserData.content ? JSON.parse(remoteUserData.content) : null;
|
||||
const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? JSON.parse(lastSyncUserData.content!) : null;
|
||||
const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : [];
|
||||
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
const remoteExtensions: ISyncExtension[] = remoteUserData.content ? JSON.parse(remoteUserData.content) : null;
|
||||
|
||||
const localExtensions = await this.getLocalExtensions();
|
||||
|
||||
if (remoteExtensions) {
|
||||
@@ -202,7 +190,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
const hasChanges = added.length || removed.length || updated.length || remote;
|
||||
|
||||
if (!hasChanges) {
|
||||
this.logService.trace('Extensions: No changes found during synchronizing extensions.');
|
||||
this.logService.info('Extensions: No changes found during synchronizing extensions.');
|
||||
}
|
||||
|
||||
if (added.length || removed.length || updated.length) {
|
||||
@@ -255,7 +243,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
} else {
|
||||
this.logService.trace('Extensions: Disabling extension...', e.identifier.id);
|
||||
await this.extensionEnablementService.disableExtension(e.identifier);
|
||||
this.logService.info('Extensions: Disabled extension.', e.identifier.id);
|
||||
this.logService.info('Extensions: Disabled extension', e.identifier.id);
|
||||
}
|
||||
removeFromSkipped.push(e.identifier);
|
||||
return;
|
||||
@@ -307,7 +295,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
|
||||
private async getLocalExtensions(): Promise<ISyncExtension[]> {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const disabledExtensions = await this.extensionEnablementService.getDisabledExtensionsAsync();
|
||||
const disabledExtensions = await this.extensionEnablementService.getDisabledExtensions();
|
||||
return installedExtensions
|
||||
.map(({ identifier }) => ({ identifier, enabled: !disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier)) }));
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IUserData, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncSource, IUserDataSynchroniser } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserData, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncSource, IUserDataSynchroniser, ResourceKey, IUserDataSyncEnablementService } 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';
|
||||
@@ -12,9 +12,9 @@ import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { edit } from 'vs/platform/userDataSync/common/content';
|
||||
import { merge } from 'vs/platform/userDataSync/common/globalStateMerge';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { parse } from 'vs/base/common/json';
|
||||
import { AbstractSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
const argvProperties: string[] = ['locale'];
|
||||
|
||||
@@ -27,22 +27,23 @@ interface ISyncPreviewResult {
|
||||
|
||||
export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
readonly resourceKey: ResourceKey = 'globalState';
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
) {
|
||||
super(SyncSource.GlobalState, fileService, environmentService, userDataSyncStoreService);
|
||||
super(SyncSource.GlobalState, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService);
|
||||
this._register(this.fileService.watch(dirname(this.environmentService.argvResource)));
|
||||
this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.argvResource))(() => this._onDidChangeLocal.fire()));
|
||||
}
|
||||
|
||||
protected getRemoteDataResourceKey(): string { return 'globalState'; }
|
||||
|
||||
async pull(): Promise<void> {
|
||||
if (!this.configurationService.getValue<boolean>('sync.enableUIState')) {
|
||||
if (!this.enabled) {
|
||||
this.logService.info('UI State: Skipped pulling ui state as it is disabled.');
|
||||
return;
|
||||
}
|
||||
@@ -73,7 +74,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
}
|
||||
|
||||
async push(): Promise<void> {
|
||||
if (!this.configurationService.getValue<boolean>('sync.enableUIState')) {
|
||||
if (!this.enabled) {
|
||||
this.logService.info('UI State: Skipped pushing UI State as it is disabled.');
|
||||
return;
|
||||
}
|
||||
@@ -96,43 +97,8 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
|
||||
}
|
||||
|
||||
async sync(): Promise<void> {
|
||||
if (!this.configurationService.getValue<boolean>('sync.enableUIState')) {
|
||||
this.logService.trace('UI State: Skipping synchronizing UI state as it is disabled.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.status !== SyncStatus.Idle) {
|
||||
this.logService.trace('UI State: Skipping synchronizing ui state as it is running already.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.trace('UI State: Started synchronizing ui state...');
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
try {
|
||||
const result = await this.getPreview();
|
||||
await this.apply(result);
|
||||
this.logService.trace('UI State: Finished synchronizing ui state.');
|
||||
} catch (e) {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.Rejected) {
|
||||
// Rejected as there is a new remote version. Syncing again,
|
||||
this.logService.info('UI State: Failed to synchronize ui state as there is a new remote version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<void> { }
|
||||
|
||||
async restart(): Promise<void> {
|
||||
throw new Error('UI State: Conflicts should not occur');
|
||||
}
|
||||
|
||||
accept(content: string): Promise<void> {
|
||||
throw new Error('UI State: Conflicts should not occur');
|
||||
}
|
||||
@@ -153,12 +119,27 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getPreview(): Promise<ISyncPreviewResult> {
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const lastSyncGlobalState = lastSyncUserData && lastSyncUserData.content ? JSON.parse(lastSyncUserData.content) : null;
|
||||
protected async doSync(remoteUserData: IUserData, lastSyncUserData: IUserData | null): Promise<void> {
|
||||
try {
|
||||
const result = await this.getPreview(remoteUserData, lastSyncUserData);
|
||||
await this.apply(result);
|
||||
this.logService.trace('UI State: Finished synchronizing ui state.');
|
||||
} catch (e) {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.RemotePreconditionFailed) {
|
||||
// Rejected as there is a new remote version. Syncing again,
|
||||
this.logService.info('UI State: Failed to synchronize ui state as there is a new remote version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
}
|
||||
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
private async getPreview(remoteUserData: IUserData, lastSyncUserData: IUserData | null, ): Promise<ISyncPreviewResult> {
|
||||
const remoteGlobalState: IGlobalState = remoteUserData.content ? JSON.parse(remoteUserData.content) : null;
|
||||
const lastSyncGlobalState = lastSyncUserData && lastSyncUserData.content ? JSON.parse(lastSyncUserData.content) : null;
|
||||
|
||||
const localGloablState = await this.getLocalGlobalState();
|
||||
|
||||
@@ -178,7 +159,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
const hasChanges = local || remote;
|
||||
|
||||
if (!hasChanges) {
|
||||
this.logService.trace('UI State: No changes found during synchronizing ui state.');
|
||||
this.logService.info('UI State: No changes found during synchronizing ui state.');
|
||||
}
|
||||
|
||||
if (local) {
|
||||
@@ -208,26 +189,32 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
private async getLocalGlobalState(): Promise<IGlobalState> {
|
||||
const argv: IStringDictionary<any> = {};
|
||||
const storage: IStringDictionary<any> = {};
|
||||
try {
|
||||
const content = await this.fileService.readFile(this.environmentService.argvResource);
|
||||
const argvValue: IStringDictionary<any> = parse(content.value.toString());
|
||||
for (const argvProperty of argvProperties) {
|
||||
if (argvValue[argvProperty] !== undefined) {
|
||||
argv[argvProperty] = argvValue[argvProperty];
|
||||
}
|
||||
const argvContent: string = await this.getLocalArgvContent();
|
||||
const argvValue: IStringDictionary<any> = parse(argvContent);
|
||||
for (const argvProperty of argvProperties) {
|
||||
if (argvValue[argvProperty] !== undefined) {
|
||||
argv[argvProperty] = argvValue[argvProperty];
|
||||
}
|
||||
} catch (error) { }
|
||||
}
|
||||
return { argv, storage };
|
||||
}
|
||||
|
||||
private async getLocalArgvContent(): Promise<string> {
|
||||
try {
|
||||
const content = await this.fileService.readFile(this.environmentService.argvResource);
|
||||
return content.value.toString();
|
||||
} catch (error) { }
|
||||
return '{}';
|
||||
}
|
||||
|
||||
private async writeLocalGlobalState(globalState: IGlobalState): Promise<void> {
|
||||
const content = await this.fileService.readFile(this.environmentService.argvResource);
|
||||
let argvContent = content.value.toString();
|
||||
const argvContent = await this.getLocalArgvContent();
|
||||
let content = argvContent;
|
||||
for (const argvProperty of Object.keys(globalState.argv)) {
|
||||
argvContent = edit(argvContent, [argvProperty], globalState.argv[argvProperty], {});
|
||||
content = edit(content, [argvProperty], globalState.argv[argvProperty], {});
|
||||
}
|
||||
if (argvContent !== content.value.toString()) {
|
||||
await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(argvContent));
|
||||
if (argvContent !== content) {
|
||||
await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(content));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,21 +3,22 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { IUserData, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncSource, IUserDataSynchroniser } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncSource, IUserDataSynchroniser, IUserData, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { parse, ParseError } from 'vs/base/common/json';
|
||||
import { parse } from 'vs/base/common/json';
|
||||
import { localize } from 'vs/nls';
|
||||
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
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 { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { AbstractFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
interface ISyncContent {
|
||||
mac?: string;
|
||||
@@ -26,34 +27,26 @@ interface ISyncContent {
|
||||
all?: string;
|
||||
}
|
||||
|
||||
interface ISyncPreviewResult {
|
||||
readonly fileContent: IFileContent | null;
|
||||
readonly remoteUserData: IUserData;
|
||||
readonly lastSyncUserData: IUserData | null;
|
||||
readonly hasLocalChanged: boolean;
|
||||
readonly hasRemoteChanged: boolean;
|
||||
readonly hasConflicts: boolean;
|
||||
}
|
||||
export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
private syncPreviewResultPromise: CancelablePromise<ISyncPreviewResult> | null = null;
|
||||
readonly resourceKey: ResourceKey = 'keybindings';
|
||||
protected get conflictsPreviewResource(): URI { return this.environmentService.keybindingsSyncPreviewResource; }
|
||||
|
||||
constructor(
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService,
|
||||
@IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
) {
|
||||
super(environmentService.keybindingsResource, SyncSource.Keybindings, fileService, environmentService, userDataSyncStoreService);
|
||||
super(environmentService.keybindingsResource, SyncSource.Keybindings, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService);
|
||||
}
|
||||
|
||||
protected getRemoteDataResourceKey(): string { return 'keybindings'; }
|
||||
|
||||
async pull(): Promise<void> {
|
||||
if (!this.configurationService.getValue<boolean>('sync.enableKeybindings')) {
|
||||
if (!this.enabled) {
|
||||
this.logService.info('Keybindings: Skipped pulling keybindings as it is disabled.');
|
||||
return;
|
||||
}
|
||||
@@ -66,18 +59,18 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
const remoteContent = remoteUserData.content !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.content) : null;
|
||||
const content = remoteUserData.content !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.content) : null;
|
||||
|
||||
if (remoteContent !== null) {
|
||||
await this.fileService.writeFile(this.environmentService.keybindingsSyncPreviewResource, VSBuffer.fromString(remoteContent));
|
||||
if (content !== null) {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<IFileSyncPreviewResult>({
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasConflicts: false,
|
||||
hasLocalChanged: true,
|
||||
hasRemoteChanged: false,
|
||||
remoteUserData,
|
||||
lastSyncUserData
|
||||
}));
|
||||
await this.apply();
|
||||
}
|
||||
@@ -95,7 +88,7 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
}
|
||||
|
||||
async push(): Promise<void> {
|
||||
if (!this.configurationService.getValue<boolean>('sync.enableKeybindings')) {
|
||||
if (!this.enabled) {
|
||||
this.logService.info('Keybindings: Skipped pushing keybindings as it is disabled.');
|
||||
return;
|
||||
}
|
||||
@@ -111,16 +104,16 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
if (fileContent !== null) {
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, fileContent.value);
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<IFileSyncPreviewResult>({
|
||||
fileContent,
|
||||
hasConflicts: false,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content: fileContent.value.toString(),
|
||||
hasLocalChanged: false,
|
||||
hasRemoteChanged: true,
|
||||
remoteUserData,
|
||||
lastSyncUserData
|
||||
hasConflicts: false,
|
||||
}));
|
||||
await this.apply(undefined, true);
|
||||
await this.apply(true);
|
||||
}
|
||||
|
||||
// No local exists to push
|
||||
@@ -135,53 +128,13 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
|
||||
}
|
||||
|
||||
async sync(): Promise<void> {
|
||||
if (!this.configurationService.getValue<boolean>('sync.enableKeybindings')) {
|
||||
this.logService.trace('Keybindings: Skipping synchronizing keybindings as it is disabled.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.status !== SyncStatus.Idle) {
|
||||
this.logService.trace('Keybindings: Skipping synchronizing keybindings as it is running already.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.trace('Keybindings: Started synchronizing keybindings...');
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
return this.doSync();
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.syncPreviewResultPromise) {
|
||||
this.syncPreviewResultPromise.cancel();
|
||||
this.syncPreviewResultPromise = null;
|
||||
this.logService.trace('Keybindings: Stopped synchronizing keybindings.');
|
||||
}
|
||||
await this.fileService.del(this.environmentService.keybindingsSyncPreviewResource);
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
this.syncPreviewResultPromise!.cancel();
|
||||
this.syncPreviewResultPromise = null;
|
||||
await this.doSync();
|
||||
}
|
||||
}
|
||||
|
||||
async accept(content: string): Promise<void> {
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
try {
|
||||
await this.apply(content, true);
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
} catch (e) {
|
||||
if ((e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) ||
|
||||
(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) {
|
||||
throw new UserDataSyncError('Failed to resolve conflicts as there is a new local version available.', UserDataSyncErrorCode.NewLocal);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
const preview = await this.syncPreviewResultPromise!;
|
||||
this.cancel();
|
||||
this.syncPreviewResultPromise = createCancelablePromise(async () => ({ ...preview, content }));
|
||||
await this.apply(true);
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,22 +155,14 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
return false;
|
||||
}
|
||||
|
||||
async getRemoteContent(): Promise<string | null> {
|
||||
let content: string | null | undefined = null;
|
||||
if (this.syncPreviewResultPromise) {
|
||||
const preview = await this.syncPreviewResultPromise;
|
||||
content = preview.remoteUserData?.content;
|
||||
} else {
|
||||
const lastSyncData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncData);
|
||||
content = remoteUserData.content;
|
||||
}
|
||||
return content ? this.getKeybindingsContentFromSyncContent(content) : null;
|
||||
async getRemoteContent(preview?: boolean): Promise<string | null> {
|
||||
const content = await super.getRemoteContent(preview);
|
||||
return content !== null ? this.getKeybindingsContentFromSyncContent(content) : null;
|
||||
}
|
||||
|
||||
private async doSync(): Promise<void> {
|
||||
protected async doSync(remoteUserData: IUserData, lastSyncUserData: IUserData | null): Promise<void> {
|
||||
try {
|
||||
const result = await this.getPreview();
|
||||
const result = await this.getPreview(remoteUserData, lastSyncUserData);
|
||||
if (result.hasConflicts) {
|
||||
this.logService.info('Keybindings: Detected conflicts while synchronizing keybindings.');
|
||||
this.setStatus(SyncStatus.HasConflicts);
|
||||
@@ -232,44 +177,32 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
} catch (e) {
|
||||
this.syncPreviewResultPromise = null;
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.Rejected) {
|
||||
// Rejected as there is a new remote version. Syncing again,
|
||||
this.logService.info('Keybindings: Failed to synchronize keybindings as there is a new remote version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
}
|
||||
if ((e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) ||
|
||||
(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) {
|
||||
// Rejected as there is a new local version. Syncing again.
|
||||
this.logService.info('Keybindings: Failed to synchronize keybindings as there is a new local version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
if (e instanceof UserDataSyncError) {
|
||||
switch (e.code) {
|
||||
case UserDataSyncErrorCode.RemotePreconditionFailed:
|
||||
// Rejected as there is a new remote version. Syncing again,
|
||||
this.logService.info('Keybindings: Failed to synchronize keybindings as there is a new remote version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
case UserDataSyncErrorCode.LocalPreconditionFailed:
|
||||
// Rejected as there is a new local version. Syncing again.
|
||||
this.logService.info('Keybindings: Failed to synchronize keybindings as there is a new local version available. Synchronizing again...');
|
||||
return this.sync(remoteUserData.ref);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async apply(content?: string, forcePush?: boolean): Promise<void> {
|
||||
private async apply(forcePush?: boolean): Promise<void> {
|
||||
if (!this.syncPreviewResultPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { fileContent, remoteUserData, lastSyncUserData, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise;
|
||||
let { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise;
|
||||
|
||||
if (content === undefined) {
|
||||
if (await this.fileService.exists(this.environmentService.keybindingsSyncPreviewResource)) {
|
||||
const keybindingsPreivew = await this.fileService.readFile(this.environmentService.keybindingsSyncPreviewResource);
|
||||
content = keybindingsPreivew.value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (content !== undefined) {
|
||||
if (content !== null) {
|
||||
if (this.hasErrors(content)) {
|
||||
const error = new Error(localize('errorInvalidKeybindings', "Unable to sync keybindings. Please resolve conflicts without any errors/warnings and try again."));
|
||||
this.logService.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!hasLocalChanged && !hasRemoteChanged) {
|
||||
this.logService.trace('Keybindings: No changes found during synchronizing keybindings.');
|
||||
throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings as there are errors/warning in keybindings file."), UserDataSyncErrorCode.LocalInvalidContent, this.source);
|
||||
}
|
||||
|
||||
if (hasLocalChanged) {
|
||||
@@ -287,14 +220,16 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
}
|
||||
|
||||
// Delete the preview
|
||||
await this.fileService.del(this.environmentService.keybindingsSyncPreviewResource);
|
||||
try {
|
||||
await this.fileService.del(this.conflictsPreviewResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
} else {
|
||||
this.logService.trace('Keybindings: No changes found during synchronizing keybindings.');
|
||||
this.logService.info('Keybindings: No changes found during synchronizing keybindings.');
|
||||
}
|
||||
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref && (content !== undefined || fileContent !== null)) {
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref && (content !== null || fileContent !== null)) {
|
||||
this.logService.trace('Keybindings: Updating last synchronized keybindings...');
|
||||
const lastSyncContent = this.updateSyncContent(content !== undefined ? content : fileContent!.value.toString(), null);
|
||||
const lastSyncContent = this.updateSyncContent(content !== null ? content : fileContent!.value.toString(), null);
|
||||
await this.updateLastSyncUserData({ ref: remoteUserData.ref, content: lastSyncContent });
|
||||
this.logService.info('Keybindings: Updated last synchronized keybindings');
|
||||
}
|
||||
@@ -302,36 +237,29 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
this.syncPreviewResultPromise = null;
|
||||
}
|
||||
|
||||
private hasErrors(content: string): boolean {
|
||||
const parseErrors: ParseError[] = [];
|
||||
parse(content, parseErrors, { allowEmptyContent: true, allowTrailingComma: true });
|
||||
return parseErrors.length > 0;
|
||||
}
|
||||
|
||||
private getPreview(): Promise<ISyncPreviewResult> {
|
||||
private getPreview(remoteUserData: IUserData, lastSyncUserData: IUserData | null): Promise<IFileSyncPreviewResult> {
|
||||
if (!this.syncPreviewResultPromise) {
|
||||
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(token));
|
||||
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token));
|
||||
}
|
||||
return this.syncPreviewResultPromise;
|
||||
}
|
||||
|
||||
private async generatePreview(token: CancellationToken): Promise<ISyncPreviewResult> {
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const lastSyncContent = lastSyncUserData && lastSyncUserData.content ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.content) : null;
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
private async generatePreview(remoteUserData: IUserData, lastSyncUserData: IUserData | null, token: CancellationToken): Promise<IFileSyncPreviewResult> {
|
||||
const remoteContent = remoteUserData.content ? this.getKeybindingsContentFromSyncContent(remoteUserData.content) : null;
|
||||
const lastSyncContent = lastSyncUserData && lastSyncUserData.content ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.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;
|
||||
let previewContent = null;
|
||||
|
||||
if (remoteContent) {
|
||||
const localContent: string = fileContent ? fileContent.value.toString() : '[]';
|
||||
if (this.hasErrors(localContent)) {
|
||||
this.logService.error('Keybindings: Unable to sync keybindings as there are errors/warning in keybindings file.');
|
||||
return { fileContent, remoteUserData, lastSyncUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
|
||||
throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings as there are errors/warning in keybindings file."), UserDataSyncErrorCode.LocalInvalidContent, this.source);
|
||||
}
|
||||
|
||||
if (!lastSyncContent // First time sync
|
||||
@@ -339,14 +267,13 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
|| lastSyncContent !== remoteContent // Remote has forwarded
|
||||
) {
|
||||
this.logService.trace('Keybindings: Merging remote keybindings with local keybindings...');
|
||||
const formattingOptions = await this.getFormattingOptions();
|
||||
const result = await merge(localContent, remoteContent, lastSyncContent, formattingOptions, this.userDataSyncUtilService);
|
||||
// Sync only if there are changes
|
||||
if (result.hasChanges) {
|
||||
hasLocalChanged = result.mergeContent !== localContent;
|
||||
hasRemoteChanged = result.mergeContent !== remoteContent;
|
||||
content = result.mergeContent;
|
||||
hasConflicts = result.hasConflicts;
|
||||
previewContent = result.mergeContent;
|
||||
hasLocalChanged = hasConflicts || result.mergeContent !== localContent;
|
||||
hasRemoteChanged = hasConflicts || result.mergeContent !== remoteContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -354,23 +281,15 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
// First time syncing to remote
|
||||
else if (fileContent) {
|
||||
this.logService.trace('Keybindings: Remote keybindings does not exist. Synchronizing keybindings for the first time.');
|
||||
content = fileContent.value.toString();
|
||||
hasRemoteChanged = true;
|
||||
previewContent = fileContent.value.toString();
|
||||
}
|
||||
|
||||
if (previewContent && !token.isCancellationRequested) {
|
||||
await this.fileService.writeFile(this.environmentService.keybindingsSyncPreviewResource, VSBuffer.fromString(previewContent));
|
||||
if (content && !token.isCancellationRequested) {
|
||||
await this.fileService.writeFile(this.environmentService.keybindingsSyncPreviewResource, VSBuffer.fromString(content));
|
||||
}
|
||||
|
||||
return { fileContent, remoteUserData, lastSyncUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
|
||||
}
|
||||
|
||||
private _formattingOptions: Promise<FormattingOptions> | undefined = undefined;
|
||||
private getFormattingOptions(): Promise<FormattingOptions> {
|
||||
if (!this._formattingOptions) {
|
||||
this._formattingOptions = this.userDataSyncUtilService.resolveFormattingOptions(this.environmentService.keybindingsResource);
|
||||
}
|
||||
return this._formattingOptions;
|
||||
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts };
|
||||
}
|
||||
|
||||
private getKeybindingsContentFromSyncContent(syncContent: string): string | null {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { values } from 'vs/base/common/map';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { FormattingOptions, Edit, getEOL } from 'vs/base/common/jsonFormatter';
|
||||
import * as contentUtil from 'vs/platform/userDataSync/common/content';
|
||||
import { IConflictSetting, DEFAULT_IGNORED_SETTINGS } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IConflictSetting, CONFIGURATION_SYNC_STORE_KEY } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { firstIndex } from 'vs/base/common/arrays';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { startsWith } from 'vs/base/common/strings';
|
||||
@@ -42,7 +42,7 @@ export function getIgnoredSettings(configurationService: IConfigurationService,
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...DEFAULT_IGNORED_SETTINGS, ...added].filter(setting => removed.indexOf(setting) === -1);
|
||||
return [CONFIGURATION_SYNC_STORE_KEY, ...added].filter(setting => removed.indexOf(setting) === -1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,40 +3,31 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { IUserData, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, IConflictSetting, ISettingsSyncService, CONFIGURATION_SYNC_STORE_KEY, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, IConflictSetting, ISettingsSyncService, CONFIGURATION_SYNC_STORE_KEY, SyncSource, IUserData, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { parse, ParseError } from 'vs/base/common/json';
|
||||
import { parse } from 'vs/base/common/json';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { 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 } from 'vs/platform/userDataSync/common/settingsMerge';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import * as arrays from 'vs/base/common/arrays';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { isEmptyObject, isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { isEmptyObject } from 'vs/base/common/types';
|
||||
import { edit } from 'vs/platform/userDataSync/common/content';
|
||||
import { AbstractFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
interface ISyncPreviewResult {
|
||||
readonly fileContent: IFileContent | null;
|
||||
readonly remoteUserData: IUserData;
|
||||
readonly lastSyncUserData: IUserData | null;
|
||||
readonly content: string | null;
|
||||
readonly hasLocalChanged: boolean;
|
||||
readonly hasRemoteChanged: boolean;
|
||||
readonly hasConflicts: boolean;
|
||||
readonly conflictSettings: IConflictSetting[];
|
||||
}
|
||||
|
||||
export class SettingsSynchroniser extends AbstractFileSynchroniser implements ISettingsSyncService {
|
||||
export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implements ISettingsSyncService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private syncPreviewResultPromise: CancelablePromise<ISyncPreviewResult> | null = null;
|
||||
readonly resourceKey: ResourceKey = 'settings';
|
||||
protected get conflictsPreviewResource(): URI { return this.environmentService.settingsSyncPreviewResource; }
|
||||
|
||||
private _conflicts: IConflictSetting[] = [];
|
||||
get conflicts(): IConflictSetting[] { return this._conflicts; }
|
||||
@@ -47,15 +38,15 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
@IFileService fileService: IFileService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
@IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
@IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
) {
|
||||
super(environmentService.settingsResource, SyncSource.Settings, fileService, environmentService, userDataSyncStoreService);
|
||||
super(environmentService.settingsResource, SyncSource.Settings, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService);
|
||||
}
|
||||
|
||||
protected getRemoteDataResourceKey(): string { return 'settings'; }
|
||||
|
||||
protected setStatus(status: SyncStatus): void {
|
||||
super.setStatus(status);
|
||||
if (this.status !== SyncStatus.HasConflicts) {
|
||||
@@ -73,7 +64,7 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
}
|
||||
|
||||
async pull(): Promise<void> {
|
||||
if (!this.configurationService.getValue<boolean>('sync.enableSettings')) {
|
||||
if (!this.enabled) {
|
||||
this.logService.info('Settings: Skipped pulling settings as it is disabled.');
|
||||
return;
|
||||
}
|
||||
@@ -92,7 +83,7 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// Update ignored settings from local file content
|
||||
const content = updateIgnoredSettings(remoteUserData.content, fileContent ? fileContent.value.toString() : '{}', getIgnoredSettings(this.configurationService), formatUtils);
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<IFileSyncPreviewResult>({
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
@@ -100,7 +91,6 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
hasLocalChanged: true,
|
||||
hasRemoteChanged: false,
|
||||
hasConflicts: false,
|
||||
conflictSettings: [],
|
||||
}));
|
||||
|
||||
await this.apply();
|
||||
@@ -118,7 +108,7 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
}
|
||||
|
||||
async push(): Promise<void> {
|
||||
if (!this.configurationService.getValue<boolean>('sync.enableSettings')) {
|
||||
if (!this.enabled) {
|
||||
this.logService.info('Settings: Skipped pushing settings as it is disabled.');
|
||||
return;
|
||||
}
|
||||
@@ -138,7 +128,7 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<IFileSyncPreviewResult>({
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
@@ -146,7 +136,6 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
hasRemoteChanged: true,
|
||||
hasLocalChanged: false,
|
||||
hasConflicts: false,
|
||||
conflictSettings: [],
|
||||
}));
|
||||
|
||||
await this.apply(true);
|
||||
@@ -163,32 +152,6 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
}
|
||||
}
|
||||
|
||||
async sync(): Promise<void> {
|
||||
if (!this.configurationService.getValue<boolean>('sync.enableSettings')) {
|
||||
this.logService.trace('Settings: Skipping synchronizing settings as it is disabled.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.status !== SyncStatus.Idle) {
|
||||
this.logService.trace('Settings: Skipping synchronizing settings as it is running already.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.trace('Settings: Started synchronizing settings...');
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
return this.doSync([]);
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.syncPreviewResultPromise) {
|
||||
this.syncPreviewResultPromise.cancel();
|
||||
this.syncPreviewResultPromise = null;
|
||||
this.logService.trace('Settings: Stopped synchronizing settings.');
|
||||
}
|
||||
await this.fileService.del(this.environmentService.settingsSyncPreviewResource);
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
|
||||
async hasLocalData(): Promise<boolean> {
|
||||
try {
|
||||
const localFileContent = await this.getLocalFileContent();
|
||||
@@ -209,62 +172,39 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
}
|
||||
|
||||
async getRemoteContent(preview?: boolean): Promise<string | null> {
|
||||
let content: string | null | undefined = null;
|
||||
if (this.syncPreviewResultPromise) {
|
||||
const preview = await this.syncPreviewResultPromise;
|
||||
content = preview.remoteUserData?.content;
|
||||
} else {
|
||||
const lastSyncData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncData);
|
||||
content = remoteUserData.content;
|
||||
}
|
||||
if (preview && !isUndefinedOrNull(content)) {
|
||||
let content = await super.getRemoteContent(preview);
|
||||
if (preview && content !== null) {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// remove ignored settings from the remote content for preview
|
||||
content = updateIgnoredSettings(content, '{}', getIgnoredSettings(this.configurationService), formatUtils);
|
||||
}
|
||||
return content !== undefined ? content : null;
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
this.syncPreviewResultPromise!.cancel();
|
||||
this.syncPreviewResultPromise = null;
|
||||
await this.doSync([]);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
async accept(content: string): Promise<void> {
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
try {
|
||||
const preview = await this.syncPreviewResultPromise!;
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// Add ignored settings from local file content
|
||||
content = updateIgnoredSettings(content, preview.fileContent ? preview.fileContent.value.toString() : '{}', getIgnoredSettings(this.configurationService), formatUtils);
|
||||
this.syncPreviewResultPromise = createCancelablePromise(async () => ({ ...preview, content }));
|
||||
await this.apply(true);
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
} catch (e) {
|
||||
if ((e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) ||
|
||||
(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) {
|
||||
throw new UserDataSyncError('Failed to resolve conflicts as there is a new local version available.', UserDataSyncErrorCode.NewLocal);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
const preview = await this.syncPreviewResultPromise!;
|
||||
this.cancel();
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// Add ignored settings from local file content
|
||||
content = updateIgnoredSettings(content, preview.fileContent ? preview.fileContent.value.toString() : '{}', getIgnoredSettings(this.configurationService), 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) {
|
||||
this.syncPreviewResultPromise!.cancel();
|
||||
this.syncPreviewResultPromise = null;
|
||||
await this.doSync(resolvedConflicts);
|
||||
const preview = await this.syncPreviewResultPromise!;
|
||||
this.cancel();
|
||||
await this.doSync(preview.remoteUserData, preview.lastSyncUserData, resolvedConflicts);
|
||||
}
|
||||
}
|
||||
|
||||
private async doSync(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<void> {
|
||||
protected async doSync(remoteUserData: IUserData, lastSyncUserData: IUserData | null, resolvedConflicts: { key: string, value: any | undefined }[] = []): Promise<void> {
|
||||
try {
|
||||
const result = await this.getPreview(resolvedConflicts);
|
||||
const result = await this.getPreview(remoteUserData, lastSyncUserData, resolvedConflicts);
|
||||
if (result.hasConflicts) {
|
||||
this.logService.info('Settings: Detected conflicts while synchronizing settings.');
|
||||
this.setStatus(SyncStatus.HasConflicts);
|
||||
@@ -279,16 +219,17 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
} catch (e) {
|
||||
this.syncPreviewResultPromise = null;
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.Rejected) {
|
||||
// Rejected as there is a new remote version. Syncing again,
|
||||
this.logService.info('Settings: Failed to synchronize settings as there is a new remote version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
}
|
||||
if ((e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) ||
|
||||
(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) {
|
||||
// Rejected as there is a new local version. Syncing again.
|
||||
this.logService.info('Settings: Failed to synchronize settings as there is a new local version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
if (e instanceof UserDataSyncError) {
|
||||
switch (e.code) {
|
||||
case UserDataSyncErrorCode.RemotePreconditionFailed:
|
||||
// Rejected as there is a new remote version. Syncing again,
|
||||
this.logService.info('Settings: Failed to synchronize settings as there is a new remote version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
case UserDataSyncErrorCode.LocalPreconditionFailed:
|
||||
// Rejected as there is a new local version. Syncing again.
|
||||
this.logService.info('Settings: Failed to synchronize settings as there is a new local version available. Synchronizing again...');
|
||||
return this.sync(remoteUserData.ref);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
@@ -304,9 +245,7 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
if (content !== null) {
|
||||
|
||||
if (this.hasErrors(content)) {
|
||||
const error = new Error(localize('errorInvalidSettings', "Unable to sync settings. Please resolve conflicts without any errors/warnings and try again."));
|
||||
this.logService.error(error);
|
||||
throw error;
|
||||
throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync settings as there are errors/warning in settings file."), UserDataSyncErrorCode.LocalInvalidContent, this.source);
|
||||
}
|
||||
|
||||
if (hasLocalChanged) {
|
||||
@@ -325,9 +264,11 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
}
|
||||
|
||||
// Delete the preview
|
||||
await this.fileService.del(this.environmentService.settingsSyncPreviewResource);
|
||||
try {
|
||||
await this.fileService.del(this.conflictsPreviewResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
} else {
|
||||
this.logService.trace('Settings: No changes found during synchronizing settings.');
|
||||
this.logService.info('Settings: No changes found during synchronizing settings.');
|
||||
}
|
||||
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref) {
|
||||
@@ -339,25 +280,16 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
this.syncPreviewResultPromise = null;
|
||||
}
|
||||
|
||||
private hasErrors(content: string): boolean {
|
||||
const parseErrors: ParseError[] = [];
|
||||
parse(content, parseErrors, { allowEmptyContent: true, allowTrailingComma: true });
|
||||
return parseErrors.length > 0;
|
||||
}
|
||||
|
||||
private getPreview(resolvedConflicts: { key: string, value: any }[]): Promise<ISyncPreviewResult> {
|
||||
private getPreview(remoteUserData: IUserData, lastSyncUserData: IUserData | null, resolvedConflicts: { key: string, value: any }[]): Promise<IFileSyncPreviewResult> {
|
||||
if (!this.syncPreviewResultPromise) {
|
||||
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(resolvedConflicts, token));
|
||||
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, resolvedConflicts, token));
|
||||
}
|
||||
return this.syncPreviewResultPromise;
|
||||
}
|
||||
|
||||
private async generatePreview(resolvedConflicts: { key: string, value: any }[], token: CancellationToken): Promise<ISyncPreviewResult> {
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
// Get file content last to get the latest
|
||||
protected async generatePreview(remoteUserData: IUserData, lastSyncUserData: IUserData | null, resolvedConflicts: { key: string, value: any }[], token: CancellationToken): Promise<IFileSyncPreviewResult> {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
const formattingOptions = await this.getFormattingOptions();
|
||||
|
||||
let content: string | null = null;
|
||||
let hasLocalChanged: boolean = false;
|
||||
@@ -370,12 +302,12 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
|
||||
// No action when there are errors
|
||||
if (this.hasErrors(localContent)) {
|
||||
this.logService.error('Settings: Unable to sync settings as there are errors/warning in settings file.');
|
||||
throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync settings as there are errors/warning in settings file."), UserDataSyncErrorCode.LocalInvalidContent, this.source);
|
||||
}
|
||||
|
||||
else {
|
||||
this.logService.trace('Settings: Merging remote settings with local settings...');
|
||||
const result = merge(localContent, remoteUserData.content, lastSyncUserData ? lastSyncUserData.content : null, getIgnoredSettings(this.configurationService), resolvedConflicts, formatUtils);
|
||||
const result = merge(localContent, remoteUserData.content, lastSyncUserData ? lastSyncUserData.content : null, getIgnoredSettings(this.configurationService), resolvedConflicts, formattingOptions);
|
||||
content = result.localContent || result.remoteContent;
|
||||
hasLocalChanged = result.localContent !== null;
|
||||
hasRemoteChanged = result.remoteContent !== null;
|
||||
@@ -393,20 +325,12 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
|
||||
if (content && !token.isCancellationRequested) {
|
||||
// Remove the ignored settings from the preview.
|
||||
const previewContent = updateIgnoredSettings(content, '{}', getIgnoredSettings(this.configurationService), formatUtils);
|
||||
const previewContent = updateIgnoredSettings(content, '{}', getIgnoredSettings(this.configurationService), formattingOptions);
|
||||
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(previewContent));
|
||||
}
|
||||
|
||||
this.setConflicts(conflictSettings);
|
||||
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, conflictSettings, hasConflicts };
|
||||
}
|
||||
|
||||
private _formattingOptions: Promise<FormattingOptions> | undefined = undefined;
|
||||
private getFormattingOptions(): Promise<FormattingOptions> {
|
||||
if (!this._formattingOptions) {
|
||||
this._formattingOptions = this.userDataSyncUtilService.resolveFormattingOptions(this.environmentService.settingsResource);
|
||||
}
|
||||
return this._formattingOptions;
|
||||
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
import { timeout, Delayer } from 'vs/base/common/async';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IUserDataSyncLogService, IUserDataSyncService, SyncStatus, IUserDataAuthTokenService, IUserDataAutoSyncService, IUserDataSyncUtilService, UserDataSyncError, UserDataSyncErrorCode, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncLogService, IUserDataSyncService, SyncStatus, IUserDataAuthTokenService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, SyncSource, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
|
||||
export class UserDataAutoSyncService extends Disposable implements IUserDataAutoSyncService {
|
||||
|
||||
@@ -21,18 +20,18 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
|
||||
readonly onError: Event<{ code: UserDataSyncErrorCode, source?: SyncSource }> = this._onError.event;
|
||||
|
||||
constructor(
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
|
||||
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
@IUserDataAuthTokenService private readonly userDataAuthTokenService: IUserDataAuthTokenService,
|
||||
@IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService,
|
||||
) {
|
||||
super();
|
||||
this.updateEnablement(false, true);
|
||||
this.syncDelayer = this._register(new Delayer<void>(0));
|
||||
this._register(Event.any<any>(userDataAuthTokenService.onDidChangeToken)(() => this.updateEnablement(true, true)));
|
||||
this._register(Event.any<any>(userDataSyncService.onDidChangeStatus)(() => this.updateEnablement(true, true)));
|
||||
this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('sync.enable'))(() => this.updateEnablement(true, false)));
|
||||
this._register(this.userDataSyncEnablementService.onDidChangeEnablement(() => this.updateEnablement(true, false)));
|
||||
this._register(this.userDataSyncEnablementService.onDidChangeResourceEnablement(() => this.triggerAutoSync()));
|
||||
}
|
||||
|
||||
private async updateEnablement(stopIfDisabled: boolean, auto: boolean): Promise<void> {
|
||||
@@ -59,24 +58,22 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
|
||||
private async sync(loop: boolean, auto: boolean): Promise<void> {
|
||||
if (this.enabled) {
|
||||
try {
|
||||
if (auto) {
|
||||
if (await this.isTurnedOffEverywhere()) {
|
||||
// Turned off everywhere. Reset & Stop Sync.
|
||||
this.logService.info('Auto Sync: Turning off sync as it is turned off everywhere.');
|
||||
await this.userDataSyncService.resetLocal();
|
||||
await this.userDataSyncUtilService.updateConfigurationValue('sync.enable', false);
|
||||
return;
|
||||
}
|
||||
if (this.userDataSyncService.status !== SyncStatus.Idle) {
|
||||
this.logService.trace('Auto Sync: Skipped once as it is syncing already');
|
||||
return;
|
||||
}
|
||||
}
|
||||
await this.userDataSyncService.sync();
|
||||
this.resetFailures();
|
||||
} catch (e) {
|
||||
this.successiveFailures++;
|
||||
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.TurnedOff) {
|
||||
this.logService.info('Auto Sync: Sync is turned off in the cloud.');
|
||||
this.logService.info('Auto Sync: Resetting the local sync state.');
|
||||
await this.userDataSyncService.resetLocal();
|
||||
this.logService.info('Auto Sync: Completed resetting the local sync state.');
|
||||
if (auto) {
|
||||
return this.userDataSyncEnablementService.setEnablement(false);
|
||||
} else {
|
||||
return this.sync(loop, auto);
|
||||
}
|
||||
}
|
||||
this.logService.error(e);
|
||||
this.successiveFailures++;
|
||||
this._onError.fire(e instanceof UserDataSyncError ? { code: e.code, source: e.source } : { code: UserDataSyncErrorCode.Unknown });
|
||||
}
|
||||
if (loop) {
|
||||
@@ -88,14 +85,8 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
|
||||
}
|
||||
}
|
||||
|
||||
private async isTurnedOffEverywhere(): Promise<boolean> {
|
||||
const hasRemote = await this.userDataSyncService.hasRemoteData();
|
||||
const hasPreviouslySynced = await this.userDataSyncService.hasPreviouslySynced();
|
||||
return !hasRemote && hasPreviouslySynced;
|
||||
}
|
||||
|
||||
private async isAutoSyncEnabled(): Promise<boolean> {
|
||||
return this.configurationService.getValue<boolean>('sync.enable')
|
||||
return this.userDataSyncEnablementService.isEnabled()
|
||||
&& this.userDataSyncService.status !== SyncStatus.Uninitialized
|
||||
&& !!(await this.userDataAuthTokenService.getToken());
|
||||
}
|
||||
|
||||
@@ -19,16 +19,10 @@ import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
|
||||
export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store';
|
||||
|
||||
export const DEFAULT_IGNORED_SETTINGS = [
|
||||
CONFIGURATION_SYNC_STORE_KEY,
|
||||
'sync.enable',
|
||||
'sync.enableSettings',
|
||||
'sync.enableExtensions',
|
||||
];
|
||||
|
||||
export interface ISyncConfiguration {
|
||||
sync: {
|
||||
enable: boolean,
|
||||
@@ -53,33 +47,33 @@ export function registerConfiguration(): IDisposable {
|
||||
properties: {
|
||||
'sync.enable': {
|
||||
type: 'boolean',
|
||||
description: localize('sync.enable', "Enable synchronization."),
|
||||
default: false,
|
||||
scope: ConfigurationScope.APPLICATION
|
||||
scope: ConfigurationScope.APPLICATION,
|
||||
deprecationMessage: 'deprecated'
|
||||
},
|
||||
'sync.enableSettings': {
|
||||
type: 'boolean',
|
||||
description: localize('sync.enableSettings', "Enable synchronizing settings."),
|
||||
default: true,
|
||||
scope: ConfigurationScope.APPLICATION,
|
||||
deprecationMessage: 'deprecated'
|
||||
},
|
||||
'sync.enableKeybindings': {
|
||||
type: 'boolean',
|
||||
description: localize('sync.enableKeybindings', "Enable synchronizing keybindings."),
|
||||
default: true,
|
||||
scope: ConfigurationScope.APPLICATION,
|
||||
deprecationMessage: 'Deprecated'
|
||||
},
|
||||
'sync.enableUIState': {
|
||||
type: 'boolean',
|
||||
description: localize('sync.enableUIState', "Enable synchronizing UI state (Only Display Language)."),
|
||||
default: true,
|
||||
scope: ConfigurationScope.APPLICATION,
|
||||
deprecationMessage: 'deprecated'
|
||||
},
|
||||
'sync.enableExtensions': {
|
||||
type: 'boolean',
|
||||
description: localize('sync.enableExtensions', "Enable synchronizing extensions."),
|
||||
default: true,
|
||||
scope: ConfigurationScope.APPLICATION,
|
||||
deprecationMessage: 'deprecated'
|
||||
},
|
||||
'sync.keybindingsPerPlatform': {
|
||||
type: 'boolean',
|
||||
@@ -96,7 +90,7 @@ export function registerConfiguration(): IDisposable {
|
||||
},
|
||||
'sync.ignoredSettings': {
|
||||
'type': 'array',
|
||||
description: localize('sync.ignoredSettings', "Configure settings to be ignored while synchronizing. \nDefault Ignored Settings:\n\n{0}", DEFAULT_IGNORED_SETTINGS.sort().map(setting => `- ${setting}`).join('\n')),
|
||||
description: localize('sync.ignoredSettings', "Configure settings to be ignored while synchronizing."),
|
||||
'default': [],
|
||||
'scope': ConfigurationScope.APPLICATION,
|
||||
$ref: ignoredSettingsSchemaId,
|
||||
@@ -110,7 +104,7 @@ export function registerConfiguration(): IDisposable {
|
||||
const ignoredSettingsSchema: IJSONSchema = {
|
||||
items: {
|
||||
type: 'string',
|
||||
enum: [...Object.keys(allSettings.properties).filter(setting => DEFAULT_IGNORED_SETTINGS.indexOf(setting) === -1), ...DEFAULT_IGNORED_SETTINGS.map(setting => `-${setting}`)]
|
||||
enum: Object.keys(allSettings.properties)
|
||||
}
|
||||
};
|
||||
jsonRegistry.registerSchema(ignoredSettingsSchemaId, ignoredSettingsSchema);
|
||||
@@ -118,19 +112,61 @@ export function registerConfiguration(): IDisposable {
|
||||
return configurationRegistry.onDidUpdateConfiguration(() => registerIgnoredSettingsSchema());
|
||||
}
|
||||
|
||||
// #region User Data Sync Store
|
||||
|
||||
export interface IUserData {
|
||||
ref: string;
|
||||
content: string | null;
|
||||
}
|
||||
|
||||
export interface IUserDataSyncStore {
|
||||
url: string;
|
||||
authenticationProviderId: string;
|
||||
}
|
||||
|
||||
export function getUserDataSyncStore(configurationService: IConfigurationService): IUserDataSyncStore | undefined {
|
||||
const value = configurationService.getValue<IUserDataSyncStore>(CONFIGURATION_SYNC_STORE_KEY);
|
||||
return value && value.url && value.authenticationProviderId ? value : undefined;
|
||||
}
|
||||
|
||||
export const ALL_RESOURCE_KEYS: ResourceKey[] = ['settings', 'keybindings', 'extensions', 'globalState'];
|
||||
export type ResourceKey = 'settings' | 'keybindings' | 'extensions' | 'globalState';
|
||||
|
||||
export interface IUserDataManifest {
|
||||
settings: string;
|
||||
keybindings: string;
|
||||
extensions: string;
|
||||
globalState: string;
|
||||
}
|
||||
|
||||
export const IUserDataSyncStoreService = createDecorator<IUserDataSyncStoreService>('IUserDataSyncStoreService');
|
||||
export interface IUserDataSyncStoreService {
|
||||
_serviceBrand: undefined;
|
||||
readonly userDataSyncStore: IUserDataSyncStore | undefined;
|
||||
read(key: ResourceKey, oldValue: IUserData | null, source?: SyncSource): Promise<IUserData>;
|
||||
write(key: ResourceKey, content: string, ref: string | null, source?: SyncSource): Promise<string>;
|
||||
manifest(): Promise<IUserDataManifest | null>;
|
||||
clear(): Promise<void>;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
// #region User Data Sync Error
|
||||
|
||||
export enum UserDataSyncErrorCode {
|
||||
// Server Errors
|
||||
Unauthorized = 'Unauthorized',
|
||||
Forbidden = 'Forbidden',
|
||||
ConnectionRefused = 'ConnectionRefused',
|
||||
Rejected = 'Rejected',
|
||||
RemotePreconditionFailed = 'RemotePreconditionFailed',
|
||||
TooLarge = 'TooLarge',
|
||||
NoRef = 'NoRef',
|
||||
NewLocal = 'NewLocal',
|
||||
TurnedOff = 'TurnedOff',
|
||||
|
||||
// Local Errors
|
||||
LocalPreconditionFailed = 'LocalPreconditionFailed',
|
||||
LocalInvalidContent = 'LocalInvalidContent',
|
||||
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
@@ -156,24 +192,9 @@ export class UserDataSyncError extends Error {
|
||||
|
||||
export class UserDataSyncStoreError extends UserDataSyncError { }
|
||||
|
||||
export interface IUserDataSyncStore {
|
||||
url: string;
|
||||
authenticationProviderId: string;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
export function getUserDataSyncStore(configurationService: IConfigurationService): IUserDataSyncStore | undefined {
|
||||
const value = configurationService.getValue<IUserDataSyncStore>(CONFIGURATION_SYNC_STORE_KEY);
|
||||
return value && value.url && value.authenticationProviderId ? value : undefined;
|
||||
}
|
||||
|
||||
export const IUserDataSyncStoreService = createDecorator<IUserDataSyncStoreService>('IUserDataSyncStoreService');
|
||||
export interface IUserDataSyncStoreService {
|
||||
_serviceBrand: undefined;
|
||||
readonly userDataSyncStore: IUserDataSyncStore | undefined;
|
||||
read(key: string, oldValue: IUserData | null, source?: SyncSource): Promise<IUserData>;
|
||||
write(key: string, content: string, ref: string | null, source?: SyncSource): Promise<string>;
|
||||
clear(): Promise<void>;
|
||||
}
|
||||
// #region User Data Synchroniser
|
||||
|
||||
export interface ISyncExtension {
|
||||
identifier: IExtensionIdentifier;
|
||||
@@ -200,34 +221,64 @@ export const enum SyncStatus {
|
||||
HasConflicts = 'hasConflicts',
|
||||
}
|
||||
|
||||
export interface ISynchroniser {
|
||||
export interface IUserDataSynchroniser {
|
||||
|
||||
readonly resourceKey: ResourceKey;
|
||||
readonly source: SyncSource;
|
||||
readonly status: SyncStatus;
|
||||
readonly onDidChangeStatus: Event<SyncStatus>;
|
||||
readonly onDidChangeLocal: Event<void>;
|
||||
|
||||
pull(): Promise<void>;
|
||||
push(): Promise<void>;
|
||||
sync(): Promise<void>;
|
||||
sync(ref?: string): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
restart(): Promise<void>;
|
||||
|
||||
hasPreviouslySynced(): Promise<boolean>
|
||||
hasRemoteData(): Promise<boolean>;
|
||||
hasLocalData(): Promise<boolean>;
|
||||
resetLocal(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IUserDataSynchroniser extends ISynchroniser {
|
||||
readonly source: SyncSource;
|
||||
getRemoteContent(preivew?: boolean): Promise<string | null>;
|
||||
accept(content: string): Promise<void>;
|
||||
}
|
||||
|
||||
export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUserDataSyncService');
|
||||
export interface IUserDataSyncService extends ISynchroniser {
|
||||
//#endregion
|
||||
|
||||
// #region User Data Sync Services
|
||||
|
||||
export const IUserDataSyncEnablementService = createDecorator<IUserDataSyncEnablementService>('IUserDataSyncEnablementService');
|
||||
export interface IUserDataSyncEnablementService {
|
||||
_serviceBrand: any;
|
||||
readonly conflictsSource: SyncSource | null;
|
||||
isFirstTimeSyncAndHasUserData(): Promise<boolean>;
|
||||
|
||||
readonly onDidChangeEnablement: Event<boolean>;
|
||||
readonly onDidChangeResourceEnablement: Event<[ResourceKey, boolean]>;
|
||||
|
||||
isEnabled(): boolean;
|
||||
setEnablement(enabled: boolean): void;
|
||||
|
||||
isResourceEnabled(key: ResourceKey): boolean;
|
||||
setResourceEnablement(key: ResourceKey, enabled: boolean): void;
|
||||
}
|
||||
|
||||
export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUserDataSyncService');
|
||||
export interface IUserDataSyncService {
|
||||
_serviceBrand: any;
|
||||
|
||||
readonly status: SyncStatus;
|
||||
readonly onDidChangeStatus: Event<SyncStatus>;
|
||||
|
||||
readonly conflictsSources: SyncSource[];
|
||||
readonly onDidChangeConflicts: Event<SyncSource[]>;
|
||||
|
||||
readonly onDidChangeLocal: Event<void>;
|
||||
|
||||
pull(): Promise<void>;
|
||||
sync(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
reset(): Promise<void>;
|
||||
resetLocal(): Promise<void>;
|
||||
|
||||
isFirstTimeSyncWithMerge(): Promise<boolean>;
|
||||
getRemoteContent(source: SyncSource, preview: boolean): Promise<string | null>;
|
||||
accept(source: SyncSource, content: string): Promise<void>;
|
||||
}
|
||||
@@ -242,7 +293,6 @@ export interface IUserDataAutoSyncService {
|
||||
export const IUserDataSyncUtilService = createDecorator<IUserDataSyncUtilService>('IUserDataSyncUtilService');
|
||||
export interface IUserDataSyncUtilService {
|
||||
_serviceBrand: undefined;
|
||||
updateConfigurationValue(key: string, value: any): Promise<void>;
|
||||
resolveUserBindings(userbindings: string[]): Promise<IStringDictionary<string>>;
|
||||
resolveFormattingOptions(resource: URI): Promise<FormattingOptions>;
|
||||
}
|
||||
@@ -275,6 +325,8 @@ export interface ISettingsSyncService extends IUserDataSynchroniser {
|
||||
resolveSettingsConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<void>;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
export const CONTEXT_SYNC_STATE = new RawContextKey<string>('syncStatus', SyncStatus.Uninitialized);
|
||||
|
||||
export const USER_DATA_SYNC_SCHEME = 'vscode-userdata-sync';
|
||||
@@ -284,3 +336,12 @@ export function toRemoteContentResource(source: SyncSource): URI {
|
||||
export function getSyncSourceFromRemoteContentResource(uri: URI): SyncSource | undefined {
|
||||
return [SyncSource.Settings, SyncSource.Keybindings, SyncSource.Extensions, SyncSource.GlobalState].filter(source => isEqual(uri, toRemoteContentResource(source)))[0];
|
||||
}
|
||||
export function getSyncSourceFromPreviewResource(uri: URI, environmentService: IEnvironmentService): SyncSource | undefined {
|
||||
if (isEqual(uri, environmentService.settingsSyncPreviewResource)) {
|
||||
return SyncSource.Settings;
|
||||
}
|
||||
if (isEqual(uri, environmentService.keybindingsSyncPreviewResource)) {
|
||||
return SyncSource.Keybindings;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IUserDataSyncEnablementService, ResourceKey, ALL_RESOURCE_KEYS } 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';
|
||||
|
||||
const enablementKey = 'sync.enable';
|
||||
function getEnablementKey(resourceKey: ResourceKey) { return `${enablementKey}.${resourceKey}`; }
|
||||
|
||||
export class UserDataSyncEnablementService extends Disposable implements IUserDataSyncEnablementService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private _onDidChangeEnablement = new Emitter<boolean>();
|
||||
readonly onDidChangeEnablement: Event<boolean> = this._onDidChangeEnablement.event;
|
||||
|
||||
private _onDidChangeResourceEnablement = new Emitter<[ResourceKey, boolean]>();
|
||||
readonly onDidChangeResourceEnablement: Event<[ResourceKey, boolean]> = this._onDidChangeResourceEnablement.event;
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly storageService: IStorageService
|
||||
) {
|
||||
super();
|
||||
this._register(storageService.onDidChangeStorage(e => this.onDidStorageChange(e)));
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this.storageService.getBoolean(enablementKey, StorageScope.GLOBAL, false);
|
||||
}
|
||||
|
||||
setEnablement(enabled: boolean): void {
|
||||
if (this.isEnabled() !== enabled) {
|
||||
this.storageService.store(enablementKey, enabled, StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
|
||||
isResourceEnabled(resourceKey: ResourceKey): boolean {
|
||||
return this.storageService.getBoolean(getEnablementKey(resourceKey), StorageScope.GLOBAL, true);
|
||||
}
|
||||
|
||||
setResourceEnablement(resourceKey: ResourceKey, enabled: boolean): void {
|
||||
if (this.isResourceEnabled(resourceKey) !== enabled) {
|
||||
this.storageService.store(getEnablementKey(resourceKey), enabled, StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
|
||||
private onDidStorageChange(workspaceStorageChangeEvent: IWorkspaceStorageChangeEvent): void {
|
||||
if (workspaceStorageChangeEvent.scope === StorageScope.GLOBAL) {
|
||||
if (enablementKey === workspaceStorageChangeEvent.key) {
|
||||
this._onDidChangeEnablement.fire(this.isEnabled());
|
||||
return;
|
||||
}
|
||||
const resourceKey = ALL_RESOURCE_KEYS.filter(resourceKey => getEnablementKey(resourceKey) === workspaceStorageChangeEvent.key)[0];
|
||||
if (resourceKey) {
|
||||
this._onDidChangeResourceEnablement.fire([resourceKey, this.isEnabled()]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export class UserDataSyncChannel implements IServerChannel {
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
switch (event) {
|
||||
case 'onDidChangeStatus': return this.service.onDidChangeStatus;
|
||||
case 'onDidChangeConflicts': return this.service.onDidChangeConflicts;
|
||||
case 'onDidChangeLocal': return this.service.onDidChangeLocal;
|
||||
}
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
@@ -24,21 +25,15 @@ export class UserDataSyncChannel implements IServerChannel {
|
||||
|
||||
call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case '_getInitialData': return Promise.resolve([this.service.status, this.service.conflictsSources]);
|
||||
case 'sync': return this.service.sync();
|
||||
case 'accept': return this.service.accept(args[0], args[1]);
|
||||
case 'pull': return this.service.pull();
|
||||
case 'push': return this.service.push();
|
||||
case '_getInitialStatus': return Promise.resolve(this.service.status);
|
||||
case 'getConflictsSource': return Promise.resolve(this.service.conflictsSource);
|
||||
case 'stop': this.service.stop(); return Promise.resolve();
|
||||
case 'restart': return this.service.restart().then(() => this.service.status);
|
||||
case 'reset': return this.service.reset();
|
||||
case 'resetLocal': return this.service.resetLocal();
|
||||
case 'hasPreviouslySynced': return this.service.hasPreviouslySynced();
|
||||
case 'hasRemoteData': return this.service.hasRemoteData();
|
||||
case 'hasLocalData': return this.service.hasLocalData();
|
||||
case 'getRemoteContent': return this.service.getRemoteContent(args[0], args[1]);
|
||||
case 'isFirstTimeSyncAndHasUserData': return this.service.isFirstTimeSyncAndHasUserData();
|
||||
case 'isFirstTimeSyncWithMerge': return this.service.isFirstTimeSyncWithMerge();
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
@@ -63,13 +58,11 @@ export class SettingsSyncChannel implements IServerChannel {
|
||||
case 'accept': return this.service.accept(args[0]);
|
||||
case 'pull': return this.service.pull();
|
||||
case 'push': return this.service.push();
|
||||
case 'restart': return this.service.restart().then(() => this.service.status);
|
||||
case '_getInitialStatus': return Promise.resolve(this.service.status);
|
||||
case '_getInitialConflicts': return Promise.resolve(this.service.conflicts);
|
||||
case 'stop': this.service.stop(); return Promise.resolve();
|
||||
case 'resetLocal': return this.service.resetLocal();
|
||||
case 'hasPreviouslySynced': return this.service.hasPreviouslySynced();
|
||||
case 'hasRemoteData': return this.service.hasRemoteData();
|
||||
case 'hasLocalData': return this.service.hasLocalData();
|
||||
case 'resolveSettingsConflicts': return this.service.resolveSettingsConflicts(args[0]);
|
||||
case 'getRemoteContent': return this.service.getRemoteContent(args[0]);
|
||||
@@ -128,7 +121,6 @@ export class UserDataSycnUtilServiceChannel implements IServerChannel {
|
||||
switch (command) {
|
||||
case 'resolveUserKeybindings': return this.service.resolveUserBindings(args[0]);
|
||||
case 'resolveFormattingOptions': return this.service.resolveFormattingOptions(URI.revive(args[0]));
|
||||
case 'updateConfigurationValue': return this.service.updateConfigurationValue(args[0], args[1]);
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
@@ -149,9 +141,5 @@ export class UserDataSyncUtilServiceClient implements IUserDataSyncUtilService {
|
||||
return this.channel.call('resolveFormattingOptions', [file]);
|
||||
}
|
||||
|
||||
async updateConfigurationValue(key: string, value: any): Promise<void> {
|
||||
return this.channel.call('updateConfigurationValue', [key, value]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncSource, ISettingsSyncService, IUserDataSyncLogService, IUserDataAuthTokenService, IUserDataSynchroniser, UserDataSyncStoreError, UserDataSyncErrorCode } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncSource, ISettingsSyncService, IUserDataSyncLogService, IUserDataAuthTokenService, IUserDataSynchroniser, UserDataSyncStoreError, UserDataSyncErrorCode, UserDataSyncError } 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';
|
||||
@@ -11,12 +11,9 @@ import { ExtensionsSynchroniser } from 'vs/platform/userDataSync/common/extensio
|
||||
import { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync';
|
||||
import { GlobalStateSynchroniser } from 'vs/platform/userDataSync/common/globalStateSync';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { localize } from 'vs/nls';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
type SyncConflictsClassification = {
|
||||
source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
};
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
type SyncErrorClassification = {
|
||||
source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
@@ -35,8 +32,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
|
||||
readonly onDidChangeLocal: Event<void>;
|
||||
|
||||
private _conflictsSource: SyncSource | null = null;
|
||||
get conflictsSource(): SyncSource | null { return this._conflictsSource; }
|
||||
private _conflictsSources: SyncSource[] = [];
|
||||
get conflictsSources(): SyncSource[] { return this._conflictsSources; }
|
||||
private _onDidChangeConflicts: Emitter<SyncSource[]> = this._register(new Emitter<SyncSource[]>());
|
||||
readonly onDidChangeConflicts: Event<SyncSource[]> = this._onDidChangeConflicts.event;
|
||||
|
||||
private readonly keybindingsSynchroniser: KeybindingsSynchroniser;
|
||||
private readonly extensionsSynchroniser: ExtensionsSynchroniser;
|
||||
@@ -66,12 +65,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
}
|
||||
|
||||
async pull(): Promise<void> {
|
||||
if (!this.userDataSyncStoreService.userDataSyncStore) {
|
||||
throw new Error('Not enabled');
|
||||
}
|
||||
if (!(await this.userDataAuthTokenService.getToken())) {
|
||||
throw new Error('Not Authenticated. Please sign in to start sync.');
|
||||
}
|
||||
await this.checkEnablement();
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
try {
|
||||
await synchroniser.pull();
|
||||
@@ -82,12 +76,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
}
|
||||
|
||||
async push(): Promise<void> {
|
||||
if (!this.userDataSyncStoreService.userDataSyncStore) {
|
||||
throw new Error('Not enabled');
|
||||
}
|
||||
if (!(await this.userDataAuthTokenService.getToken())) {
|
||||
throw new Error('Not Authenticated. Please sign in to start sync.');
|
||||
}
|
||||
await this.checkEnablement();
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
try {
|
||||
await synchroniser.push();
|
||||
@@ -98,105 +87,79 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
}
|
||||
|
||||
async sync(): Promise<void> {
|
||||
if (!this.userDataSyncStoreService.userDataSyncStore) {
|
||||
throw new Error('Not enabled');
|
||||
}
|
||||
if (!(await this.userDataAuthTokenService.getToken())) {
|
||||
throw new Error('Not Authenticated. Please sign in to start sync.');
|
||||
}
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
throw new Error(localize('resolve conflicts', "Please resolve conflicts before resuming sync."));
|
||||
}
|
||||
const startTime = new Date().getTime();
|
||||
this.logService.trace('Started Syncing...');
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
try {
|
||||
await synchroniser.sync();
|
||||
// do not continue if synchroniser has conflicts
|
||||
if (synchroniser.status === SyncStatus.HasConflicts) {
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
this.handleSyncError(e, synchroniser.source);
|
||||
}
|
||||
}
|
||||
this.logService.trace(`Finished Syncing. Took ${new Date().getTime() - startTime}ms`);
|
||||
}
|
||||
await this.checkEnablement();
|
||||
|
||||
async accept(source: SyncSource, content: string): Promise<void> {
|
||||
const synchroniser = this.getSynchroniser(source);
|
||||
await synchroniser.accept(content);
|
||||
if (synchroniser.status !== SyncStatus.HasConflicts) {
|
||||
await this.sync();
|
||||
const startTime = new Date().getTime();
|
||||
try {
|
||||
this.logService.trace('Sync started.');
|
||||
if (this.status !== SyncStatus.HasConflicts) {
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
}
|
||||
|
||||
const 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 from other machine
|
||||
throw new UserDataSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff);
|
||||
}
|
||||
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
try {
|
||||
await synchroniser.sync(manifest ? manifest[synchroniser.resourceKey] : undefined);
|
||||
} catch (e) {
|
||||
this.handleSyncError(e, synchroniser.source);
|
||||
}
|
||||
}
|
||||
|
||||
this.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`);
|
||||
|
||||
} finally {
|
||||
this.updateStatus();
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (!this.userDataSyncStoreService.userDataSyncStore) {
|
||||
throw new Error('Not enabled');
|
||||
if (this.status === SyncStatus.Idle) {
|
||||
return;
|
||||
}
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
await synchroniser.stop();
|
||||
try {
|
||||
if (synchroniser.status !== SyncStatus.Idle) {
|
||||
await synchroniser.stop();
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
const synchroniser = this.getSynchroniserInConflicts();
|
||||
if (!synchroniser) {
|
||||
throw new Error(localize('no synchroniser with conflicts', "No conflicts detected."));
|
||||
}
|
||||
await synchroniser.restart();
|
||||
if (synchroniser.status !== SyncStatus.HasConflicts) {
|
||||
await this.sync();
|
||||
}
|
||||
async accept(source: SyncSource, content: string): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
const synchroniser = this.getSynchroniser(source);
|
||||
return synchroniser.accept(content);
|
||||
}
|
||||
|
||||
async hasPreviouslySynced(): Promise<boolean> {
|
||||
if (!this.userDataSyncStoreService.userDataSyncStore) {
|
||||
throw new Error('Not enabled');
|
||||
}
|
||||
if (!(await this.userDataAuthTokenService.getToken())) {
|
||||
throw new Error('Not Authenticated. Please sign in to start sync.');
|
||||
}
|
||||
await this.checkEnablement();
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
if (await synchroniser.hasPreviouslySynced()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async hasRemoteData(): Promise<boolean> {
|
||||
if (!this.userDataSyncStoreService.userDataSyncStore) {
|
||||
throw new Error('Not enabled');
|
||||
}
|
||||
if (!(await this.userDataAuthTokenService.getToken())) {
|
||||
throw new Error('Not Authenticated. Please sign in to start sync.');
|
||||
}
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
if (await synchroniser.hasRemoteData()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async hasLocalData(): Promise<boolean> {
|
||||
if (!this.userDataSyncStoreService.userDataSyncStore) {
|
||||
throw new Error('Not enabled');
|
||||
}
|
||||
if (!(await this.userDataAuthTokenService.getToken())) {
|
||||
throw new Error('Not Authenticated. Please sign in to start sync.');
|
||||
}
|
||||
private async hasLocalData(): Promise<boolean> {
|
||||
await this.checkEnablement();
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
if (await synchroniser.hasLocalData()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async getRemoteContent(source: SyncSource, preview: boolean): Promise<string | null> {
|
||||
await this.checkEnablement();
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
if (synchroniser.source === source) {
|
||||
return synchroniser.getRemoteContent(preview);
|
||||
@@ -205,12 +168,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
return null;
|
||||
}
|
||||
|
||||
async isFirstTimeSyncAndHasUserData(): Promise<boolean> {
|
||||
if (!this.userDataSyncStoreService.userDataSyncStore) {
|
||||
throw new Error('Not enabled');
|
||||
}
|
||||
if (!(await this.userDataAuthTokenService.getToken())) {
|
||||
throw new Error('Not Authenticated. Please sign in to start sync.');
|
||||
async isFirstTimeSyncWithMerge(): Promise<boolean> {
|
||||
await this.checkEnablement();
|
||||
if (!await this.userDataSyncStoreService.manifest()) {
|
||||
return false;
|
||||
}
|
||||
if (await this.hasPreviouslySynced()) {
|
||||
return false;
|
||||
@@ -219,60 +180,46 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
}
|
||||
|
||||
async reset(): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
await this.resetRemote();
|
||||
await this.resetLocal();
|
||||
}
|
||||
|
||||
private async resetRemote(): Promise<void> {
|
||||
if (!this.userDataSyncStoreService.userDataSyncStore) {
|
||||
throw new Error('Not enabled');
|
||||
}
|
||||
if (!(await this.userDataAuthTokenService.getToken())) {
|
||||
throw new Error('Not Authenticated. Please sign in to start sync.');
|
||||
}
|
||||
try {
|
||||
await this.userDataSyncStoreService.clear();
|
||||
this.logService.info('Completed clearing remote data');
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async resetLocal(): Promise<void> {
|
||||
if (!this.userDataSyncStoreService.userDataSyncStore) {
|
||||
throw new Error('Not enabled');
|
||||
}
|
||||
if (!(await this.userDataAuthTokenService.getToken())) {
|
||||
throw new Error('Not Authenticated. Please sign in to start sync.');
|
||||
}
|
||||
await this.checkEnablement();
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
try {
|
||||
await synchroniser.resetLocal();
|
||||
} catch (e) {
|
||||
this.logService.error(`${synchroniser.source}: ${toErrorMessage(e)}`);
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
this.logService.info('Completed resetting local cache');
|
||||
}
|
||||
|
||||
private async resetRemote(): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
try {
|
||||
await this.userDataSyncStoreService.clear();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private setStatus(status: SyncStatus): void {
|
||||
if (this._status !== status) {
|
||||
this._status = status;
|
||||
this._onDidChangeStatus.fire(status);
|
||||
}
|
||||
}
|
||||
|
||||
private updateStatus(): void {
|
||||
const status = this.computeStatus();
|
||||
if (this._status !== status) {
|
||||
const oldStatus = this._status;
|
||||
const oldConflictsSource = this._conflictsSource;
|
||||
this._conflictsSource = this.computeConflictsSource();
|
||||
this._status = status;
|
||||
if (status === SyncStatus.HasConflicts) {
|
||||
// Log to telemetry when there is a sync conflict
|
||||
this.telemetryService.publicLog2<{ source: string }, SyncConflictsClassification>('sync/conflictsDetected', { source: this._conflictsSource! });
|
||||
}
|
||||
if (oldStatus === SyncStatus.HasConflicts && status === SyncStatus.Idle) {
|
||||
// Log to telemetry when conflicts are resolved
|
||||
this.telemetryService.publicLog2<{ source: string }, SyncConflictsClassification>('sync/conflictsResolved', { source: oldConflictsSource! });
|
||||
}
|
||||
this._onDidChangeStatus.fire(status);
|
||||
const conflictsSources = this.computeConflictsSources();
|
||||
if (!equals(this._conflictsSources, conflictsSources)) {
|
||||
this._conflictsSources = this.computeConflictsSources();
|
||||
this._onDidChangeConflicts.fire(conflictsSources);
|
||||
}
|
||||
const status = this.computeStatus();
|
||||
this.setStatus(status);
|
||||
}
|
||||
|
||||
private computeStatus(): SyncStatus {
|
||||
@@ -300,14 +247,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
this.logService.error(`${source}: ${toErrorMessage(e)}`);
|
||||
}
|
||||
|
||||
private computeConflictsSource(): SyncSource | null {
|
||||
const synchroniser = this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)[0];
|
||||
return synchroniser ? synchroniser.source : null;
|
||||
}
|
||||
|
||||
private getSynchroniserInConflicts(): IUserDataSynchroniser | null {
|
||||
const synchroniser = this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)[0];
|
||||
return synchroniser || null;
|
||||
private computeConflictsSources(): SyncSource[] {
|
||||
return this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts).map(s => s.source);
|
||||
}
|
||||
|
||||
private getSynchroniser(source: SyncSource): IUserDataSynchroniser {
|
||||
@@ -319,6 +260,15 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
}
|
||||
}
|
||||
|
||||
private async checkEnablement(): Promise<void> {
|
||||
if (!this.userDataSyncStoreService.userDataSyncStore) {
|
||||
throw new Error('Not enabled');
|
||||
}
|
||||
if (!(await this.userDataAuthTokenService.getToken())) {
|
||||
throw new UserDataSyncError('Not Authenticated. Please sign in to start sync.', UserDataSyncErrorCode.Unauthorized);
|
||||
}
|
||||
}
|
||||
|
||||
private onDidChangeAuthTokenStatus(token: string | undefined): void {
|
||||
if (!token) {
|
||||
this.stop();
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, } from 'vs/base/common/lifecycle';
|
||||
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, IUserDataAuthTokenService, SyncSource, UserDataSyncStoreError, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IRequestService, asText, isSuccess } from 'vs/platform/request/common/request';
|
||||
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, IUserDataAuthTokenService, SyncSource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IRequestService, asText, isSuccess, asJson } from 'vs/platform/request/common/request';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
@@ -84,6 +84,22 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
return newRef;
|
||||
}
|
||||
|
||||
async manifest(): Promise<IUserDataManifest | null> {
|
||||
if (!this.userDataSyncStore) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource', 'latest').toString();
|
||||
const headers: IHeaders = { 'Content-Type': 'application/json' };
|
||||
|
||||
const context = await this.request({ type: 'GET', url, headers }, undefined, CancellationToken.None);
|
||||
if (!isSuccess(context)) {
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
|
||||
}
|
||||
|
||||
return asJson(context);
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
if (!this.userDataSyncStore) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
@@ -107,7 +123,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
options.headers = options.headers || {};
|
||||
options.headers['authorization'] = `Bearer ${authToken}`;
|
||||
|
||||
this.logService.trace('Sending request to server', { url: options.url, headers: { ...options.headers, ...{ authorization: undefined } } });
|
||||
this.logService.trace('Sending request to server', { url: options.url, type: options.type, headers: { ...options.headers, ...{ authorization: undefined } } });
|
||||
|
||||
let context;
|
||||
try {
|
||||
@@ -126,7 +142,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
}
|
||||
|
||||
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.Rejected, source);
|
||||
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.RemotePreconditionFailed, source);
|
||||
}
|
||||
|
||||
if (context.res.statusCode === 413) {
|
||||
|
||||
@@ -3,23 +3,21 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IUserDataSyncService, IUserDataSyncLogService, IUserDataAuthTokenService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncService, IUserDataSyncLogService, IUserDataAuthTokenService, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IElectronService } from 'vs/platform/electron/node/electron';
|
||||
import { UserDataAutoSyncService as BaseUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
export class UserDataAutoSyncService extends BaseUserDataAutoSyncService {
|
||||
|
||||
constructor(
|
||||
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
|
||||
@IUserDataSyncService userDataSyncService: IUserDataSyncService,
|
||||
@IElectronService electronService: IElectronService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
@IUserDataAuthTokenService authTokenService: IUserDataAuthTokenService,
|
||||
@IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService,
|
||||
) {
|
||||
super(configurationService, userDataSyncService, logService, authTokenService, userDataSyncUtilService);
|
||||
super(userDataSyncEnablementService, userDataSyncService, logService, authTokenService);
|
||||
|
||||
// Sync immediately if there is a local change.
|
||||
this._register(Event.debounce(Event.any<any>(
|
||||
|
||||
@@ -638,7 +638,5 @@ class MockUserDataSyncUtilService implements IUserDataSyncUtilService {
|
||||
return { eol: '\n', insertSpaces: false, tabSize: 4 };
|
||||
}
|
||||
|
||||
async updateConfigurationValue(key: string, value: any): Promise<void> { }
|
||||
|
||||
async ignoreExtensionsToSync(extensions: IExtensionIdentifier[]): Promise<void> { }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user