mirror of
https://github.com/coder/code-server.git
synced 2026-05-28 07:59:34 +00:00
Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@@ -0,0 +1,866 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IFileService, IFileContent, FileChangesEvent, FileOperationResult, FileOperationError, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import {
|
||||
SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService,
|
||||
IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncResourcePreview as IBaseSyncResourcePreview,
|
||||
IUserDataManifest, ISyncData, IRemoteUserData, PREVIEW_DIR_NAME, IResourcePreview as IBaseResourcePreview, Change, MergeState, IUserDataInitializer
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IExtUri, extUri, extUriIgnorePathCase } from 'vs/base/common/resources';
|
||||
import { CancelablePromise, RunOnceScheduler, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { ParseError, parse } from 'vs/base/common/json';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { isString } from 'vs/base/common/types';
|
||||
import { uppercaseFirstLetter } from 'vs/base/common/strings';
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IHeaders } from 'vs/base/parts/request/common/request';
|
||||
|
||||
type SyncSourceClassification = {
|
||||
source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
};
|
||||
|
||||
function isSyncData(thing: any): thing is ISyncData {
|
||||
if (thing
|
||||
&& (thing.version !== undefined && typeof thing.version === 'number')
|
||||
&& (thing.content !== undefined && typeof thing.content === 'string')) {
|
||||
|
||||
// backward compatibility
|
||||
if (Object.keys(thing).length === 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Object.keys(thing).length === 3
|
||||
&& (thing.machineId !== undefined && typeof thing.machineId === 'string')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getLastSyncResourceUri(syncResource: SyncResource, environmentService: IEnvironmentService, extUri: IExtUri): URI {
|
||||
return extUri.joinPath(environmentService.userDataSyncHome, syncResource, `lastSync${syncResource}.json`);
|
||||
}
|
||||
|
||||
export interface IResourcePreview {
|
||||
|
||||
readonly remoteResource: URI;
|
||||
readonly remoteContent: string | null;
|
||||
readonly remoteChange: Change;
|
||||
|
||||
readonly localResource: URI;
|
||||
readonly localContent: string | null;
|
||||
readonly localChange: Change;
|
||||
|
||||
readonly previewResource: URI;
|
||||
readonly acceptedResource: URI;
|
||||
}
|
||||
|
||||
export interface IAcceptResult {
|
||||
readonly content: string | null;
|
||||
readonly localChange: Change;
|
||||
readonly remoteChange: Change;
|
||||
}
|
||||
|
||||
export interface IMergeResult extends IAcceptResult {
|
||||
readonly hasConflicts: boolean;
|
||||
}
|
||||
|
||||
interface IEditableResourcePreview extends IBaseResourcePreview, IResourcePreview {
|
||||
localChange: Change;
|
||||
remoteChange: Change;
|
||||
mergeState: MergeState;
|
||||
acceptResult?: IAcceptResult;
|
||||
}
|
||||
|
||||
interface ISyncResourcePreview extends IBaseSyncResourcePreview {
|
||||
readonly remoteUserData: IRemoteUserData;
|
||||
readonly lastSyncUserData: IRemoteUserData | null;
|
||||
readonly resourcePreviews: IEditableResourcePreview[];
|
||||
}
|
||||
|
||||
export abstract class AbstractSynchroniser extends Disposable {
|
||||
|
||||
private syncPreviewPromise: CancelablePromise<ISyncResourcePreview> | null = null;
|
||||
|
||||
protected readonly syncFolder: URI;
|
||||
protected readonly syncPreviewFolder: URI;
|
||||
protected readonly extUri: IExtUri;
|
||||
private readonly currentMachineIdPromise: Promise<string>;
|
||||
|
||||
private _status: SyncStatus = SyncStatus.Idle;
|
||||
get status(): SyncStatus { return this._status; }
|
||||
private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
|
||||
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;
|
||||
|
||||
private _conflicts: IBaseResourcePreview[] = [];
|
||||
get conflicts(): IBaseResourcePreview[] { return this._conflicts; }
|
||||
private _onDidChangeConflicts: Emitter<IBaseResourcePreview[]> = this._register(new Emitter<IBaseResourcePreview[]>());
|
||||
readonly onDidChangeConflicts: Event<IBaseResourcePreview[]> = this._onDidChangeConflicts.event;
|
||||
|
||||
private readonly localChangeTriggerScheduler = new RunOnceScheduler(() => this.doTriggerLocalChange(), 50);
|
||||
private readonly _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;
|
||||
|
||||
protected readonly lastSyncResource: URI;
|
||||
protected readonly syncResourceLogLabel: string;
|
||||
|
||||
private syncHeaders: IHeaders = {};
|
||||
|
||||
constructor(
|
||||
readonly resource: SyncResource,
|
||||
@IFileService protected readonly fileService: IFileService,
|
||||
@IEnvironmentService protected readonly environmentService: IEnvironmentService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncBackupStoreService protected readonly userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
|
||||
@IUserDataSyncResourceEnablementService protected readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
|
||||
@ITelemetryService protected readonly telemetryService: ITelemetryService,
|
||||
@IUserDataSyncLogService protected readonly logService: IUserDataSyncLogService,
|
||||
@IConfigurationService protected readonly configurationService: IConfigurationService,
|
||||
) {
|
||||
super();
|
||||
this.syncResourceLogLabel = uppercaseFirstLetter(this.resource);
|
||||
this.extUri = this.fileService.hasCapability(environmentService.userDataSyncHome, FileSystemProviderCapabilities.PathCaseSensitive) ? extUri : extUriIgnorePathCase;
|
||||
this.syncFolder = this.extUri.joinPath(environmentService.userDataSyncHome, resource);
|
||||
this.syncPreviewFolder = this.extUri.joinPath(this.syncFolder, PREVIEW_DIR_NAME);
|
||||
this.lastSyncResource = getLastSyncResourceUri(resource, environmentService, this.extUri);
|
||||
this.currentMachineIdPromise = getServiceMachineId(environmentService, fileService, storageService);
|
||||
}
|
||||
|
||||
protected isEnabled(): boolean { return this.userDataSyncResourceEnablementService.isResourceEnabled(this.resource); }
|
||||
|
||||
protected async triggerLocalChange(): Promise<void> {
|
||||
if (this.isEnabled()) {
|
||||
this.localChangeTriggerScheduler.schedule();
|
||||
}
|
||||
}
|
||||
|
||||
protected async doTriggerLocalChange(): Promise<void> {
|
||||
|
||||
// Sync again if current status is in conflicts
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: In conflicts state and local change detected. Syncing again...`);
|
||||
const preview = await this.syncPreviewPromise!;
|
||||
this.syncPreviewPromise = null;
|
||||
const status = await this.performSync(preview.remoteUserData, preview.lastSyncUserData, true);
|
||||
this.setStatus(status);
|
||||
}
|
||||
|
||||
// Check if local change causes remote change
|
||||
else {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Checking for local changes...`);
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const hasRemoteChanged = lastSyncUserData ? (await this.doGenerateSyncResourcePreview(lastSyncUserData, lastSyncUserData, true, CancellationToken.None)).resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None) : true;
|
||||
if (hasRemoteChanged) {
|
||||
this._onDidChangeLocal.fire();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected setStatus(status: SyncStatus): void {
|
||||
if (this._status !== status) {
|
||||
const oldStatus = this._status;
|
||||
if (status === SyncStatus.HasConflicts) {
|
||||
// Log to telemetry when there is a sync conflict
|
||||
this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/conflictsDetected', { source: this.resource });
|
||||
}
|
||||
if (oldStatus === SyncStatus.HasConflicts && status === SyncStatus.Idle) {
|
||||
// Log to telemetry when conflicts are resolved
|
||||
this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/conflictsResolved', { source: this.resource });
|
||||
}
|
||||
this._status = status;
|
||||
this._onDidChangStatus.fire(status);
|
||||
}
|
||||
}
|
||||
|
||||
async sync(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise<void> {
|
||||
await this._sync(manifest, true, headers);
|
||||
}
|
||||
|
||||
async preview(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise<ISyncResourcePreview | null> {
|
||||
return this._sync(manifest, false, headers);
|
||||
}
|
||||
|
||||
async apply(force: boolean, headers: IHeaders = {}): Promise<ISyncResourcePreview | null> {
|
||||
try {
|
||||
this.syncHeaders = { ...headers };
|
||||
|
||||
const status = await this.doApply(force);
|
||||
this.setStatus(status);
|
||||
|
||||
return this.syncPreviewPromise;
|
||||
} finally {
|
||||
this.syncHeaders = {};
|
||||
}
|
||||
}
|
||||
|
||||
private async _sync(manifest: IUserDataManifest | null, apply: boolean, headers: IHeaders): Promise<ISyncResourcePreview | null> {
|
||||
try {
|
||||
this.syncHeaders = { ...headers };
|
||||
|
||||
if (!this.isEnabled()) {
|
||||
if (this.status !== SyncStatus.Idle) {
|
||||
await this.stop();
|
||||
}
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is disabled.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as there are conflicts.`);
|
||||
return this.syncPreviewPromise;
|
||||
}
|
||||
|
||||
if (this.status === SyncStatus.Syncing) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is running already.`);
|
||||
return this.syncPreviewPromise;
|
||||
}
|
||||
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Started synchronizing ${this.resource.toLowerCase()}...`);
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
let status: SyncStatus = SyncStatus.Idle;
|
||||
try {
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData);
|
||||
status = await this.performSync(remoteUserData, lastSyncUserData, apply);
|
||||
if (status === SyncStatus.HasConflicts) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Detected conflicts while synchronizing ${this.resource.toLowerCase()}.`);
|
||||
} else if (status === SyncStatus.Idle) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Finished synchronizing ${this.resource.toLowerCase()}.`);
|
||||
}
|
||||
return this.syncPreviewPromise || null;
|
||||
} finally {
|
||||
this.setStatus(status);
|
||||
}
|
||||
} finally {
|
||||
this.syncHeaders = {};
|
||||
}
|
||||
}
|
||||
|
||||
async replace(uri: URI): Promise<boolean> {
|
||||
const content = await this.resolveContent(uri);
|
||||
if (!content) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const syncData = this.parseSyncData(content);
|
||||
if (!syncData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.stop();
|
||||
|
||||
try {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Started resetting ${this.resource.toLowerCase()}...`);
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getLatestRemoteUserData(null, lastSyncUserData);
|
||||
|
||||
/* use replace sync data */
|
||||
const resourcePreviewResults = await this.generateSyncPreview({ ref: remoteUserData.ref, syncData }, lastSyncUserData, CancellationToken.None);
|
||||
|
||||
const resourcePreviews: [IResourcePreview, IAcceptResult][] = [];
|
||||
for (const resourcePreviewResult of resourcePreviewResults) {
|
||||
/* Accept remote resource */
|
||||
const acceptResult: IAcceptResult = await this.getAcceptResult(resourcePreviewResult, resourcePreviewResult.remoteResource, undefined, CancellationToken.None);
|
||||
/* compute remote change */
|
||||
const { remoteChange } = await this.getAcceptResult(resourcePreviewResult, resourcePreviewResult.previewResource, resourcePreviewResult.remoteContent, CancellationToken.None);
|
||||
resourcePreviews.push([resourcePreviewResult, { ...acceptResult, remoteChange: remoteChange !== Change.None ? remoteChange : Change.Modified }]);
|
||||
}
|
||||
|
||||
await this.applyResult(remoteUserData, lastSyncUserData, resourcePreviews, false);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished resetting ${this.resource.toLowerCase()}.`);
|
||||
} finally {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise<IRemoteUserData> {
|
||||
if (lastSyncUserData) {
|
||||
|
||||
const latestRef = manifest && manifest.latest ? manifest.latest[this.resource] : undefined;
|
||||
|
||||
// Last time synced resource and latest resource on server are same
|
||||
if (lastSyncUserData.ref === latestRef) {
|
||||
return lastSyncUserData;
|
||||
}
|
||||
|
||||
// There is no resource on server and last time it was synced with no resource
|
||||
if (latestRef === undefined && lastSyncUserData.syncData === null) {
|
||||
return lastSyncUserData;
|
||||
}
|
||||
}
|
||||
return this.getRemoteUserData(lastSyncUserData);
|
||||
}
|
||||
|
||||
private async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean): Promise<SyncStatus> {
|
||||
if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) {
|
||||
// current version is not compatible with cloud version
|
||||
this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/incompatible', { source: this.resource });
|
||||
throw new UserDataSyncError(localize({ key: 'incompatible', comment: ['This is an error while syncing a resource that its local version is not compatible with its remote version.'] }, "Cannot sync {0} as its local version {1} is not compatible with its remote version {2}", this.resource, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.IncompatibleLocalContent, this.resource);
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.doSync(remoteUserData, lastSyncUserData, apply);
|
||||
} catch (e) {
|
||||
if (e instanceof UserDataSyncError) {
|
||||
switch (e.code) {
|
||||
|
||||
case UserDataSyncErrorCode.LocalPreconditionFailed:
|
||||
// Rejected as there is a new local version. Syncing again...
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize ${this.syncResourceLogLabel} as there is a new local version available. Synchronizing again...`);
|
||||
return this.performSync(remoteUserData, lastSyncUserData, apply);
|
||||
|
||||
case UserDataSyncErrorCode.Conflict:
|
||||
case UserDataSyncErrorCode.PreconditionFailed:
|
||||
// Rejected as there is a new remote version. Syncing again...
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize as there is a new remote version available. Synchronizing again...`);
|
||||
|
||||
// Avoid cache and get latest remote user data - https://github.com/microsoft/vscode/issues/90624
|
||||
remoteUserData = await this.getRemoteUserData(null);
|
||||
|
||||
// Get the latest last sync user data. Because multiples parallel syncs (in Web) could share same last sync data
|
||||
// and one of them successfully updated remote and last sync state.
|
||||
lastSyncUserData = await this.getLastSyncUserData();
|
||||
|
||||
return this.performSync(remoteUserData, lastSyncUserData, apply);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean): Promise<SyncStatus> {
|
||||
try {
|
||||
// generate or use existing preview
|
||||
if (!this.syncPreviewPromise) {
|
||||
this.syncPreviewPromise = createCancelablePromise(token => this.doGenerateSyncResourcePreview(remoteUserData, lastSyncUserData, apply, token));
|
||||
}
|
||||
|
||||
const preview = await this.syncPreviewPromise;
|
||||
this.updateConflicts(preview.resourcePreviews);
|
||||
if (preview.resourcePreviews.some(({ mergeState }) => mergeState === MergeState.Conflict)) {
|
||||
return SyncStatus.HasConflicts;
|
||||
}
|
||||
|
||||
if (apply) {
|
||||
return await this.doApply(false);
|
||||
}
|
||||
|
||||
return SyncStatus.Syncing;
|
||||
|
||||
} catch (error) {
|
||||
|
||||
// reset preview on error
|
||||
this.syncPreviewPromise = null;
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async merge(resource: URI): Promise<ISyncResourcePreview | null> {
|
||||
await this.updateSyncResourcePreview(resource, async (resourcePreview) => {
|
||||
const mergeResult = await this.getMergeResult(resourcePreview, CancellationToken.None);
|
||||
await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(mergeResult?.content || ''));
|
||||
const acceptResult: IAcceptResult | undefined = mergeResult && !mergeResult.hasConflicts
|
||||
? await this.getAcceptResult(resourcePreview, resourcePreview.previewResource, undefined, CancellationToken.None)
|
||||
: undefined;
|
||||
resourcePreview.acceptResult = acceptResult;
|
||||
resourcePreview.mergeState = mergeResult.hasConflicts ? MergeState.Conflict : acceptResult ? MergeState.Accepted : MergeState.Preview;
|
||||
resourcePreview.localChange = acceptResult ? acceptResult.localChange : mergeResult.localChange;
|
||||
resourcePreview.remoteChange = acceptResult ? acceptResult.remoteChange : mergeResult.remoteChange;
|
||||
return resourcePreview;
|
||||
});
|
||||
return this.syncPreviewPromise;
|
||||
}
|
||||
|
||||
async accept(resource: URI, content?: string | null): Promise<ISyncResourcePreview | null> {
|
||||
await this.updateSyncResourcePreview(resource, async (resourcePreview) => {
|
||||
const acceptResult = await this.getAcceptResult(resourcePreview, resource, content, CancellationToken.None);
|
||||
resourcePreview.acceptResult = acceptResult;
|
||||
resourcePreview.mergeState = MergeState.Accepted;
|
||||
resourcePreview.localChange = acceptResult.localChange;
|
||||
resourcePreview.remoteChange = acceptResult.remoteChange;
|
||||
return resourcePreview;
|
||||
});
|
||||
return this.syncPreviewPromise;
|
||||
}
|
||||
|
||||
async discard(resource: URI): Promise<ISyncResourcePreview | null> {
|
||||
await this.updateSyncResourcePreview(resource, async (resourcePreview) => {
|
||||
const mergeResult = await this.getMergeResult(resourcePreview, CancellationToken.None);
|
||||
await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(mergeResult.content || ''));
|
||||
resourcePreview.acceptResult = undefined;
|
||||
resourcePreview.mergeState = MergeState.Preview;
|
||||
resourcePreview.localChange = mergeResult.localChange;
|
||||
resourcePreview.remoteChange = mergeResult.remoteChange;
|
||||
return resourcePreview;
|
||||
});
|
||||
return this.syncPreviewPromise;
|
||||
}
|
||||
|
||||
private async updateSyncResourcePreview(resource: URI, updateResourcePreview: (resourcePreview: IEditableResourcePreview) => Promise<IEditableResourcePreview>): Promise<void> {
|
||||
if (!this.syncPreviewPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
let preview = await this.syncPreviewPromise;
|
||||
const index = preview.resourcePreviews.findIndex(({ localResource, remoteResource, previewResource }) =>
|
||||
this.extUri.isEqual(localResource, resource) || this.extUri.isEqual(remoteResource, resource) || this.extUri.isEqual(previewResource, resource));
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.syncPreviewPromise = createCancelablePromise(async token => {
|
||||
const resourcePreviews = [...preview.resourcePreviews];
|
||||
resourcePreviews[index] = await updateResourcePreview(resourcePreviews[index]);
|
||||
return {
|
||||
...preview,
|
||||
resourcePreviews
|
||||
};
|
||||
});
|
||||
|
||||
preview = await this.syncPreviewPromise;
|
||||
this.updateConflicts(preview.resourcePreviews);
|
||||
if (preview.resourcePreviews.some(({ mergeState }) => mergeState === MergeState.Conflict)) {
|
||||
this.setStatus(SyncStatus.HasConflicts);
|
||||
} else {
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
}
|
||||
}
|
||||
|
||||
private async doApply(force: boolean): Promise<SyncStatus> {
|
||||
if (!this.syncPreviewPromise) {
|
||||
return SyncStatus.Idle;
|
||||
}
|
||||
|
||||
const preview = await this.syncPreviewPromise;
|
||||
|
||||
// check for conflicts
|
||||
if (preview.resourcePreviews.some(({ mergeState }) => mergeState === MergeState.Conflict)) {
|
||||
return SyncStatus.HasConflicts;
|
||||
}
|
||||
|
||||
// check if all are accepted
|
||||
if (preview.resourcePreviews.some(({ mergeState }) => mergeState !== MergeState.Accepted)) {
|
||||
return SyncStatus.Syncing;
|
||||
}
|
||||
|
||||
// apply preview
|
||||
await this.applyResult(preview.remoteUserData, preview.lastSyncUserData, preview.resourcePreviews.map(resourcePreview => ([resourcePreview, resourcePreview.acceptResult!])), force);
|
||||
|
||||
// reset preview
|
||||
this.syncPreviewPromise = null;
|
||||
|
||||
// reset preview folder
|
||||
await this.clearPreviewFolder();
|
||||
|
||||
return SyncStatus.Idle;
|
||||
}
|
||||
|
||||
private async clearPreviewFolder(): Promise<void> {
|
||||
try {
|
||||
await this.fileService.del(this.syncPreviewFolder, { recursive: true });
|
||||
} catch (error) { /* Ignore */ }
|
||||
}
|
||||
|
||||
private updateConflicts(resourcePreviews: IEditableResourcePreview[]): void {
|
||||
const conflicts = resourcePreviews.filter(({ mergeState }) => mergeState === MergeState.Conflict);
|
||||
if (!equals(this._conflicts, conflicts, (a, b) => this.extUri.isEqual(a.previewResource, b.previewResource))) {
|
||||
this._conflicts = conflicts;
|
||||
this._onDidChangeConflicts.fire(conflicts);
|
||||
}
|
||||
}
|
||||
|
||||
async hasPreviouslySynced(): Promise<boolean> {
|
||||
const lastSyncData = await this.getLastSyncUserData();
|
||||
return !!lastSyncData;
|
||||
}
|
||||
|
||||
async getRemoteSyncResourceHandles(): Promise<ISyncResourceHandle[]> {
|
||||
const handles = await this.userDataSyncStoreService.getAllRefs(this.resource);
|
||||
return handles.map(({ created, ref }) => ({ created, uri: this.toRemoteBackupResource(ref) }));
|
||||
}
|
||||
|
||||
async getLocalSyncResourceHandles(): Promise<ISyncResourceHandle[]> {
|
||||
const handles = await this.userDataSyncBackupStoreService.getAllRefs(this.resource);
|
||||
return handles.map(({ created, ref }) => ({ created, uri: this.toLocalBackupResource(ref) }));
|
||||
}
|
||||
|
||||
private toRemoteBackupResource(ref: string): URI {
|
||||
return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote-backup', path: `/${this.resource}/${ref}` });
|
||||
}
|
||||
|
||||
private toLocalBackupResource(ref: string): URI {
|
||||
return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local-backup', path: `/${this.resource}/${ref}` });
|
||||
}
|
||||
|
||||
async getMachineId({ uri }: ISyncResourceHandle): Promise<string | undefined> {
|
||||
const ref = this.extUri.basename(uri);
|
||||
if (this.extUri.isEqual(uri, this.toRemoteBackupResource(ref))) {
|
||||
const { content } = await this.getUserData(ref);
|
||||
if (content) {
|
||||
const syncData = this.parseSyncData(content);
|
||||
return syncData?.machineId;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async resolveContent(uri: URI): Promise<string | null> {
|
||||
const ref = this.extUri.basename(uri);
|
||||
if (this.extUri.isEqual(uri, this.toRemoteBackupResource(ref))) {
|
||||
const { content } = await this.getUserData(ref);
|
||||
return content;
|
||||
}
|
||||
if (this.extUri.isEqual(uri, this.toLocalBackupResource(ref))) {
|
||||
return this.userDataSyncBackupStoreService.resolveContent(this.resource, ref);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async resolvePreviewContent(uri: URI): Promise<string | null> {
|
||||
const syncPreview = this.syncPreviewPromise ? await this.syncPreviewPromise : null;
|
||||
if (syncPreview) {
|
||||
for (const resourcePreview of syncPreview.resourcePreviews) {
|
||||
if (this.extUri.isEqual(resourcePreview.acceptedResource, uri)) {
|
||||
return resourcePreview.acceptResult ? resourcePreview.acceptResult.content : null;
|
||||
}
|
||||
if (this.extUri.isEqual(resourcePreview.remoteResource, uri)) {
|
||||
return resourcePreview.remoteContent;
|
||||
}
|
||||
if (this.extUri.isEqual(resourcePreview.localResource, uri)) {
|
||||
return resourcePreview.localContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async resetLocal(): Promise<void> {
|
||||
try {
|
||||
await this.fileService.del(this.lastSyncResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
private async doGenerateSyncResourcePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean, token: CancellationToken): Promise<ISyncResourcePreview> {
|
||||
const machineId = await this.currentMachineIdPromise;
|
||||
const isLastSyncFromCurrentMachine = !!remoteUserData.syncData?.machineId && remoteUserData.syncData.machineId === machineId;
|
||||
|
||||
// For preview, use remoteUserData if lastSyncUserData does not exists and last sync is from current machine
|
||||
const lastSyncUserDataForPreview = lastSyncUserData === null && isLastSyncFromCurrentMachine ? remoteUserData : lastSyncUserData;
|
||||
const resourcePreviewResults = await this.generateSyncPreview(remoteUserData, lastSyncUserDataForPreview, token);
|
||||
|
||||
const resourcePreviews: IEditableResourcePreview[] = [];
|
||||
for (const resourcePreviewResult of resourcePreviewResults) {
|
||||
const acceptedResource = resourcePreviewResult.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' });
|
||||
|
||||
/* No change -> Accept */
|
||||
if (resourcePreviewResult.localChange === Change.None && resourcePreviewResult.remoteChange === Change.None) {
|
||||
resourcePreviews.push({
|
||||
...resourcePreviewResult,
|
||||
acceptedResource,
|
||||
acceptResult: { content: null, localChange: Change.None, remoteChange: Change.None },
|
||||
mergeState: MergeState.Accepted
|
||||
});
|
||||
}
|
||||
|
||||
/* Changed -> Apply ? (Merge ? Conflict | Accept) : Preview */
|
||||
else {
|
||||
/* Merge */
|
||||
const mergeResult = apply ? await this.getMergeResult(resourcePreviewResult, token) : undefined;
|
||||
if (token.isCancellationRequested) {
|
||||
break;
|
||||
}
|
||||
await this.fileService.writeFile(resourcePreviewResult.previewResource, VSBuffer.fromString(mergeResult?.content || ''));
|
||||
|
||||
/* Conflict | Accept */
|
||||
const acceptResult = mergeResult && !mergeResult.hasConflicts
|
||||
/* Accept if merged and there are no conflicts */
|
||||
? await this.getAcceptResult(resourcePreviewResult, resourcePreviewResult.previewResource, undefined, token)
|
||||
: undefined;
|
||||
|
||||
resourcePreviews.push({
|
||||
...resourcePreviewResult,
|
||||
acceptResult,
|
||||
mergeState: mergeResult?.hasConflicts ? MergeState.Conflict : acceptResult ? MergeState.Accepted : MergeState.Preview,
|
||||
localChange: acceptResult ? acceptResult.localChange : mergeResult ? mergeResult.localChange : resourcePreviewResult.localChange,
|
||||
remoteChange: acceptResult ? acceptResult.remoteChange : mergeResult ? mergeResult.remoteChange : resourcePreviewResult.remoteChange
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { remoteUserData, lastSyncUserData, resourcePreviews, isLastSyncFromCurrentMachine };
|
||||
}
|
||||
|
||||
async getLastSyncUserData<T extends IRemoteUserData>(): Promise<T | null> {
|
||||
try {
|
||||
const content = await this.fileService.readFile(this.lastSyncResource);
|
||||
const parsed = JSON.parse(content.value.toString());
|
||||
const userData: IUserData = parsed as IUserData;
|
||||
if (userData.content === null) {
|
||||
return { ref: parsed.ref, syncData: null } as T;
|
||||
}
|
||||
const syncData: ISyncData = JSON.parse(userData.content);
|
||||
|
||||
/* Check if syncData is of expected type. Return only if matches */
|
||||
if (isSyncData(syncData)) {
|
||||
return { ...parsed, ...{ syncData, content: undefined } };
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (!(error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) {
|
||||
// log error always except when file does not exist
|
||||
this.logService.error(error);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async updateLastSyncUserData(lastSyncRemoteUserData: IRemoteUserData, additionalProps: IStringDictionary<any> = {}): Promise<void> {
|
||||
const lastSyncUserData: IUserData = { ref: lastSyncRemoteUserData.ref, content: lastSyncRemoteUserData.syncData ? JSON.stringify(lastSyncRemoteUserData.syncData) : null, ...additionalProps };
|
||||
await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData)));
|
||||
}
|
||||
|
||||
async getRemoteUserData(lastSyncData: IRemoteUserData | null): Promise<IRemoteUserData> {
|
||||
const { ref, content } = await this.getUserData(lastSyncData);
|
||||
let syncData: ISyncData | null = null;
|
||||
if (content !== null) {
|
||||
syncData = this.parseSyncData(content);
|
||||
}
|
||||
return { ref, syncData };
|
||||
}
|
||||
|
||||
protected parseSyncData(content: string): ISyncData {
|
||||
try {
|
||||
const syncData: ISyncData = JSON.parse(content);
|
||||
if (isSyncData(syncData)) {
|
||||
return syncData;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
}
|
||||
throw new UserDataSyncError(localize('incompatible sync data', "Cannot parse sync data as it is not compatible with the current version."), UserDataSyncErrorCode.IncompatibleRemoteContent, this.resource);
|
||||
}
|
||||
|
||||
private async getUserData(refOrLastSyncData: string | IRemoteUserData | null): Promise<IUserData> {
|
||||
if (isString(refOrLastSyncData)) {
|
||||
const content = await this.userDataSyncStoreService.resolveContent(this.resource, refOrLastSyncData);
|
||||
return { ref: refOrLastSyncData, content };
|
||||
} else {
|
||||
const lastSyncUserData: IUserData | null = refOrLastSyncData ? { ref: refOrLastSyncData.ref, content: refOrLastSyncData.syncData ? JSON.stringify(refOrLastSyncData.syncData) : null } : null;
|
||||
return this.userDataSyncStoreService.read(this.resource, lastSyncUserData, this.syncHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
protected async updateRemoteUserData(content: string, ref: string | null): Promise<IRemoteUserData> {
|
||||
const machineId = await this.currentMachineIdPromise;
|
||||
const syncData: ISyncData = { version: this.version, machineId, content };
|
||||
ref = await this.userDataSyncStoreService.write(this.resource, JSON.stringify(syncData), ref, this.syncHeaders);
|
||||
return { ref, syncData };
|
||||
}
|
||||
|
||||
protected async backupLocal(content: string): Promise<void> {
|
||||
const syncData: ISyncData = { version: this.version, content };
|
||||
return this.userDataSyncBackupStoreService.backup(this.resource, JSON.stringify(syncData));
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.status === SyncStatus.Idle) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Stopping synchronizing ${this.resource.toLowerCase()}.`);
|
||||
if (this.syncPreviewPromise) {
|
||||
this.syncPreviewPromise.cancel();
|
||||
this.syncPreviewPromise = null;
|
||||
}
|
||||
|
||||
this.updateConflicts([]);
|
||||
await this.clearPreviewFolder();
|
||||
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`);
|
||||
}
|
||||
|
||||
protected abstract readonly version: number;
|
||||
protected abstract generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]>;
|
||||
protected abstract getMergeResult(resourcePreview: IResourcePreview, token: CancellationToken): Promise<IMergeResult>;
|
||||
protected abstract getAcceptResult(resourcePreview: IResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult>;
|
||||
protected abstract applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, result: [IResourcePreview, IAcceptResult][], force: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IFileResourcePreview extends IResourcePreview {
|
||||
readonly fileContent: IFileContent | null;
|
||||
}
|
||||
|
||||
export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
||||
|
||||
constructor(
|
||||
protected readonly file: URI,
|
||||
resource: SyncResource,
|
||||
@IFileService fileService: IFileService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
|
||||
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
) {
|
||||
super(resource, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, configurationService);
|
||||
this._register(this.fileService.watch(this.extUri.dirname(file)));
|
||||
this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e)));
|
||||
}
|
||||
|
||||
protected async getLocalFileContent(): Promise<IFileContent | null> {
|
||||
try {
|
||||
return await this.fileService.readFile(this.file);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected async updateLocalFileContent(newContent: string, oldContent: IFileContent | null, force: boolean): Promise<void> {
|
||||
try {
|
||||
if (oldContent) {
|
||||
// file exists already
|
||||
await this.fileService.writeFile(this.file, VSBuffer.fromString(newContent), force ? undefined : oldContent);
|
||||
} else {
|
||||
// file does not exist
|
||||
await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: force });
|
||||
}
|
||||
} catch (e) {
|
||||
if ((e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) ||
|
||||
(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;
|
||||
}
|
||||
this.triggerLocalChange();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroniser {
|
||||
|
||||
constructor(
|
||||
file: URI,
|
||||
resource: SyncResource,
|
||||
@IFileService fileService: IFileService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
|
||||
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
@IUserDataSyncUtilService protected readonly userDataSyncUtilService: IUserDataSyncUtilService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
) {
|
||||
super(file, resource, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, configurationService);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export abstract class AbstractInitializer implements IUserDataInitializer {
|
||||
|
||||
protected readonly extUri: IExtUri;
|
||||
private readonly lastSyncResource: URI;
|
||||
|
||||
constructor(
|
||||
readonly resource: SyncResource,
|
||||
@IEnvironmentService protected readonly environmentService: IEnvironmentService,
|
||||
@IUserDataSyncLogService protected readonly logService: IUserDataSyncLogService,
|
||||
@IFileService protected readonly fileService: IFileService,
|
||||
) {
|
||||
this.extUri = this.fileService.hasCapability(environmentService.userDataSyncHome, FileSystemProviderCapabilities.PathCaseSensitive) ? extUri : extUriIgnorePathCase;
|
||||
this.lastSyncResource = getLastSyncResourceUri(this.resource, environmentService, extUri);
|
||||
}
|
||||
|
||||
async initialize({ ref, content }: IUserData): Promise<void> {
|
||||
if (!content) {
|
||||
this.logService.info('Remote content does not exist.', this.resource);
|
||||
return;
|
||||
}
|
||||
|
||||
const syncData = this.parseSyncData(content);
|
||||
if (!syncData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isPreviouslySynced = await this.fileService.exists(this.lastSyncResource);
|
||||
if (isPreviouslySynced) {
|
||||
this.logService.info('Remote content does not exist.', this.resource);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.doInitialize({ ref, syncData });
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
private parseSyncData(content: string): ISyncData | undefined {
|
||||
try {
|
||||
const syncData: ISyncData = JSON.parse(content);
|
||||
if (isSyncData(syncData)) {
|
||||
return syncData;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
}
|
||||
this.logService.info('Cannot parse sync data as it is not compatible with the current version.', this.resource);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected async updateLastSyncUserData(lastSyncRemoteUserData: IRemoteUserData, additionalProps: IStringDictionary<any> = {}): Promise<void> {
|
||||
const lastSyncUserData: IUserData = { ref: lastSyncRemoteUserData.ref, content: lastSyncRemoteUserData.syncData ? JSON.stringify(lastSyncRemoteUserData.syncData) : null, ...additionalProps };
|
||||
await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData)));
|
||||
}
|
||||
|
||||
protected abstract doInitialize(remoteUserData: IRemoteUserData): Promise<void>;
|
||||
|
||||
}
|
||||
53
lib/vscode/src/vs/platform/userDataSync/common/content.ts
Normal file
53
lib/vscode/src/vs/platform/userDataSync/common/content.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { JSONPath } from 'vs/base/common/json';
|
||||
import { setProperty } from 'vs/base/common/jsonEdit';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
|
||||
|
||||
export function edit(content: string, originalPath: JSONPath, value: any, formattingOptions: FormattingOptions): string {
|
||||
const edit = setProperty(content, originalPath, value, formattingOptions)[0];
|
||||
if (edit) {
|
||||
content = content.substring(0, edit.offset) + edit.content + content.substring(edit.offset + edit.length);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
export function getLineStartOffset(content: string, eol: string, atOffset: number): number {
|
||||
let lineStartingOffset = atOffset;
|
||||
while (lineStartingOffset >= 0) {
|
||||
if (content.charAt(lineStartingOffset) === eol.charAt(eol.length - 1)) {
|
||||
if (eol.length === 1) {
|
||||
return lineStartingOffset + 1;
|
||||
}
|
||||
}
|
||||
lineStartingOffset--;
|
||||
if (eol.length === 2) {
|
||||
if (lineStartingOffset >= 0 && content.charAt(lineStartingOffset) === eol.charAt(0)) {
|
||||
return lineStartingOffset + 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function getLineEndOffset(content: string, eol: string, atOffset: number): number {
|
||||
let lineEndOffset = atOffset;
|
||||
while (lineEndOffset >= 0) {
|
||||
if (content.charAt(lineEndOffset) === eol.charAt(eol.length - 1)) {
|
||||
if (eol.length === 1) {
|
||||
return lineEndOffset;
|
||||
}
|
||||
}
|
||||
lineEndOffset++;
|
||||
if (eol.length === 2) {
|
||||
if (lineEndOffset >= 0 && content.charAt(lineEndOffset) === eol.charAt(1)) {
|
||||
return lineEndOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
return content.length - 1;
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ISyncExtension, ISyncExtensionWithVersion } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { deepClone, equals } from 'vs/base/common/objects';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import * as semver from 'vs/base/common/semver/semver';
|
||||
|
||||
export interface IMergeResult {
|
||||
added: ISyncExtension[];
|
||||
removed: IExtensionIdentifier[];
|
||||
updated: ISyncExtensionWithVersion[];
|
||||
remote: ISyncExtension[] | null;
|
||||
}
|
||||
|
||||
export function merge(localExtensions: ISyncExtensionWithVersion[], remoteExtensions: ISyncExtension[] | null, lastSyncExtensions: ISyncExtension[] | null, skippedExtensions: ISyncExtension[], ignoredExtensions: string[]): IMergeResult {
|
||||
const added: ISyncExtension[] = [];
|
||||
const removed: IExtensionIdentifier[] = [];
|
||||
const updated: ISyncExtensionWithVersion[] = [];
|
||||
|
||||
if (!remoteExtensions) {
|
||||
const remote = localExtensions.filter(({ identifier }) => ignoredExtensions.every(id => id.toLowerCase() !== identifier.id.toLowerCase()));
|
||||
return {
|
||||
added,
|
||||
removed,
|
||||
updated,
|
||||
remote: remote.length > 0 ? remote : null
|
||||
};
|
||||
}
|
||||
|
||||
localExtensions = localExtensions.map(massageIncomingExtension);
|
||||
remoteExtensions = remoteExtensions.map(massageIncomingExtension);
|
||||
lastSyncExtensions = lastSyncExtensions ? lastSyncExtensions.map(massageIncomingExtension) : null;
|
||||
|
||||
const uuids: Map<string, string> = new Map<string, string>();
|
||||
const addUUID = (identifier: IExtensionIdentifier) => { if (identifier.uuid) { uuids.set(identifier.id.toLowerCase(), identifier.uuid); } };
|
||||
localExtensions.forEach(({ identifier }) => addUUID(identifier));
|
||||
remoteExtensions.forEach(({ identifier }) => addUUID(identifier));
|
||||
if (lastSyncExtensions) {
|
||||
lastSyncExtensions.forEach(({ identifier }) => addUUID(identifier));
|
||||
}
|
||||
|
||||
const getKey = (extension: ISyncExtension): string => {
|
||||
const uuid = extension.identifier.uuid || uuids.get(extension.identifier.id.toLowerCase());
|
||||
return uuid ? `uuid:${uuid}` : `id:${extension.identifier.id.toLowerCase()}`;
|
||||
};
|
||||
const addExtensionToMap = <T extends ISyncExtension>(map: Map<string, T>, extension: T) => {
|
||||
map.set(getKey(extension), extension);
|
||||
return map;
|
||||
};
|
||||
const localExtensionsMap: Map<string, ISyncExtensionWithVersion> = localExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtensionWithVersion>());
|
||||
const remoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
|
||||
const newRemoteExtensionsMap = remoteExtensions.reduce((map: Map<string, ISyncExtension>, extension: ISyncExtension) => {
|
||||
const key = getKey(extension);
|
||||
extension = deepClone(extension);
|
||||
const localExtension = localExtensionsMap.get(key);
|
||||
if (localExtension) {
|
||||
if (localExtension.installed) {
|
||||
extension.installed = true;
|
||||
}
|
||||
if (!extension.version) {
|
||||
extension.version = localExtension.version;
|
||||
}
|
||||
}
|
||||
return addExtensionToMap(map, extension);
|
||||
}, new Map<string, ISyncExtension>());
|
||||
const lastSyncExtensionsMap = lastSyncExtensions ? lastSyncExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>()) : null;
|
||||
const skippedExtensionsMap = skippedExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
|
||||
const ignoredExtensionsSet = ignoredExtensions.reduce((set, id) => {
|
||||
const uuid = uuids.get(id.toLowerCase());
|
||||
return set.add(uuid ? `uuid:${uuid}` : `id:${id.toLowerCase()}`);
|
||||
}, new Set<string>());
|
||||
|
||||
const localToRemote = compare(localExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet);
|
||||
if (localToRemote.added.size > 0 || localToRemote.removed.size > 0 || localToRemote.updated.size > 0) {
|
||||
|
||||
const baseToLocal = compare(lastSyncExtensionsMap, localExtensionsMap, ignoredExtensionsSet);
|
||||
const baseToRemote = compare(lastSyncExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet);
|
||||
|
||||
const merge = (key: string, updatedInRemote: boolean): ISyncExtensionWithVersion | undefined => {
|
||||
const localExtension = localExtensionsMap.get(key);
|
||||
if (localExtension) {
|
||||
const remoteExtension = remoteExtensionsMap.get(key)!;
|
||||
return {
|
||||
...(updatedInRemote ? remoteExtension : localExtension),
|
||||
version: remoteExtension.version && semver.gt(remoteExtension.version, localExtension.version) ? localExtension.version : localExtension.version,
|
||||
state: mergeExtensionState(localExtension, remoteExtension, lastSyncExtensionsMap?.get(key))
|
||||
};
|
||||
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Remotely removed extension.
|
||||
for (const key of baseToRemote.removed.values()) {
|
||||
const e = localExtensionsMap.get(key);
|
||||
if (e) {
|
||||
removed.push(e.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
// Remotely added extension
|
||||
for (const key of baseToRemote.added.values()) {
|
||||
// Got added in local
|
||||
if (baseToLocal.added.has(key)) {
|
||||
// Is different from local to remote
|
||||
if (localToRemote.updated.has(key)) {
|
||||
const mergedExtension = merge(key, true);
|
||||
if (mergedExtension) {
|
||||
updated.push(massageOutgoingExtension(mergedExtension, key));
|
||||
newRemoteExtensionsMap.set(key, mergedExtension);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Add only installed extension to local
|
||||
const remoteExtension = remoteExtensionsMap.get(key)!;
|
||||
if (remoteExtension.installed) {
|
||||
added.push(massageOutgoingExtension(remoteExtension, key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remotely updated extensions
|
||||
for (const key of baseToRemote.updated.values()) {
|
||||
// Update in local always
|
||||
const mergedExtension = merge(key, true);
|
||||
if (mergedExtension) {
|
||||
updated.push(massageOutgoingExtension(mergedExtension, key));
|
||||
newRemoteExtensionsMap.set(key, mergedExtension);
|
||||
}
|
||||
}
|
||||
|
||||
// Locally added extensions
|
||||
for (const key of baseToLocal.added.values()) {
|
||||
// Not there in remote
|
||||
if (!baseToRemote.added.has(key)) {
|
||||
newRemoteExtensionsMap.set(key, localExtensionsMap.get(key)!);
|
||||
}
|
||||
}
|
||||
|
||||
// Locally updated extensions
|
||||
for (const key of baseToLocal.updated.values()) {
|
||||
// If removed in remote
|
||||
if (baseToRemote.removed.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If not updated in remote
|
||||
if (!baseToRemote.updated.has(key)) {
|
||||
const mergedExtension = merge(key, false);
|
||||
if (mergedExtension) {
|
||||
// Retain installed property
|
||||
if (newRemoteExtensionsMap.get(key)?.installed) {
|
||||
mergedExtension.installed = true;
|
||||
}
|
||||
newRemoteExtensionsMap.set(key, mergedExtension);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Locally removed extensions
|
||||
for (const key of baseToLocal.removed.values()) {
|
||||
// If not skipped and not updated in remote
|
||||
if (!skippedExtensionsMap.has(key) && !baseToRemote.updated.has(key)) {
|
||||
// Remove only if it is an installed extension
|
||||
if (lastSyncExtensionsMap?.get(key)?.installed) {
|
||||
newRemoteExtensionsMap.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const remote: ISyncExtension[] = [];
|
||||
const remoteChanges = compare(remoteExtensionsMap, newRemoteExtensionsMap, new Set<string>(), { checkInstalledProperty: true, checkVersionProperty: true });
|
||||
if (remoteChanges.added.size > 0 || remoteChanges.updated.size > 0 || remoteChanges.removed.size > 0) {
|
||||
newRemoteExtensionsMap.forEach((value, key) => remote.push(massageOutgoingExtension(value, key)));
|
||||
}
|
||||
|
||||
return { added, removed, updated, remote: remote.length ? remote : null };
|
||||
}
|
||||
|
||||
function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISyncExtension>, ignoredExtensions: Set<string>, { checkInstalledProperty, checkVersionProperty }: { checkInstalledProperty: boolean, checkVersionProperty: boolean } = { checkInstalledProperty: false, checkVersionProperty: false }): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
|
||||
const fromKeys = from ? [...from.keys()].filter(key => !ignoredExtensions.has(key)) : [];
|
||||
const toKeys = [...to.keys()].filter(key => !ignoredExtensions.has(key));
|
||||
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const updated: Set<string> = new Set<string>();
|
||||
|
||||
for (const key of fromKeys) {
|
||||
if (removed.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const fromExtension = from!.get(key)!;
|
||||
const toExtension = to.get(key);
|
||||
if (!toExtension
|
||||
|| fromExtension.disabled !== toExtension.disabled
|
||||
|| !isSameExtensionState(fromExtension.state, toExtension.state)
|
||||
|| (checkVersionProperty && fromExtension.version !== toExtension.version)
|
||||
|| (checkInstalledProperty && fromExtension.installed !== toExtension.installed)
|
||||
) {
|
||||
updated.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return { added, removed, updated };
|
||||
}
|
||||
|
||||
function mergeExtensionState(localExtension: ISyncExtensionWithVersion, remoteExtension: ISyncExtension, lastSyncExtension: ISyncExtension | undefined): IStringDictionary<any> | undefined {
|
||||
const localState = localExtension.state;
|
||||
const remoteState = remoteExtension.state;
|
||||
const baseState = lastSyncExtension?.state;
|
||||
|
||||
// If remote extension has no version, use local state
|
||||
if (!remoteExtension.version) {
|
||||
return localState;
|
||||
}
|
||||
|
||||
// If local state exists and local extension is latest then use local state
|
||||
if (localState && semver.gt(localExtension.version, remoteExtension.version)) {
|
||||
return localState;
|
||||
}
|
||||
// If remote state exists and remote extension is latest, use remote state
|
||||
if (remoteState && semver.gt(remoteExtension.version, localExtension.version)) {
|
||||
return remoteState;
|
||||
}
|
||||
|
||||
|
||||
/* Remote and local are on same version */
|
||||
|
||||
// If local state is not yet set, use remote state
|
||||
if (!localState) {
|
||||
return remoteState;
|
||||
}
|
||||
// If remote state is not yet set, use local state
|
||||
if (!remoteState) {
|
||||
return localState;
|
||||
}
|
||||
|
||||
const mergedState: IStringDictionary<any> = deepClone(localState);
|
||||
const baseToRemote = baseState ? compareExtensionState(baseState, remoteState) : { added: Object.keys(remoteState).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
const baseToLocal = baseState ? compareExtensionState(baseState, localState) : { added: Object.keys(localState).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
// Added/Updated in remote
|
||||
for (const key of [...baseToRemote.added.values(), ...baseToRemote.updated.values()]) {
|
||||
mergedState[key] = remoteState[key];
|
||||
}
|
||||
// Removed in remote
|
||||
for (const key of baseToRemote.removed.values()) {
|
||||
// Not updated in local
|
||||
if (!baseToLocal.updated.has(key)) {
|
||||
delete mergedState[key];
|
||||
}
|
||||
}
|
||||
return mergedState;
|
||||
}
|
||||
|
||||
function compareExtensionState(from: IStringDictionary<any>, to: IStringDictionary<any>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
|
||||
const fromKeys = Object.keys(from);
|
||||
const toKeys = Object.keys(to);
|
||||
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const updated: Set<string> = new Set<string>();
|
||||
|
||||
for (const key of fromKeys) {
|
||||
if (removed.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const value1 = from[key];
|
||||
const value2 = to[key];
|
||||
if (!equals(value1, value2)) {
|
||||
updated.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return { added, removed, updated };
|
||||
}
|
||||
|
||||
function isSameExtensionState(a: IStringDictionary<any> = {}, b: IStringDictionary<any> = {}): boolean {
|
||||
const { added, removed, updated } = compareExtensionState(a, b);
|
||||
return added.size === 0 && removed.size === 0 && updated.size === 0;
|
||||
}
|
||||
|
||||
// massage incoming extension - add optional properties
|
||||
function massageIncomingExtension<T extends ISyncExtension>(extension: T): T {
|
||||
return { ...extension, ...{ disabled: !!extension.disabled, installed: !!extension.installed } };
|
||||
}
|
||||
|
||||
// massage outgoing extension - remove optional properties
|
||||
function massageOutgoingExtension<T extends ISyncExtension>(extension: T, key: string): T {
|
||||
const massagedExtension: ISyncExtension = {
|
||||
identifier: {
|
||||
id: extension.identifier.id,
|
||||
uuid: key.startsWith('uuid:') ? key.substring('uuid:'.length) : undefined
|
||||
},
|
||||
};
|
||||
if (extension.version) {
|
||||
massagedExtension.version = extension.version;
|
||||
}
|
||||
if (extension.disabled) {
|
||||
massagedExtension.disabled = true;
|
||||
}
|
||||
if (extension.installed) {
|
||||
massagedExtension.installed = true;
|
||||
}
|
||||
if (extension.state) {
|
||||
massagedExtension.state = extension.state;
|
||||
}
|
||||
return massagedExtension as T;
|
||||
}
|
||||
561
lib/vscode/src/vs/platform/userDataSync/common/extensionsSync.ts
Normal file
561
lib/vscode/src/vs/platform/userDataSync/common/extensionsSync.ts
Normal file
@@ -0,0 +1,561 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import {
|
||||
IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncResourceEnablementService,
|
||||
IUserDataSyncBackupStoreService, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, Change, ISyncExtensionWithVersion
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { merge } from 'vs/platform/userDataSync/common/extensionsMerge';
|
||||
import { AbstractInitializer, AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { format } from 'vs/base/common/jsonFormatter';
|
||||
import { applyEdits } from 'vs/base/common/jsonEdit';
|
||||
import { compare } from 'vs/base/common/strings';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions';
|
||||
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import { forEach, IStringDictionary } from 'vs/base/common/collections';
|
||||
|
||||
interface IExtensionResourceMergeResult extends IAcceptResult {
|
||||
readonly added: ISyncExtension[];
|
||||
readonly removed: IExtensionIdentifier[];
|
||||
readonly updated: ISyncExtension[];
|
||||
readonly remote: ISyncExtension[] | null;
|
||||
}
|
||||
|
||||
interface IExtensionResourcePreview extends IResourcePreview {
|
||||
readonly localExtensions: ISyncExtensionWithVersion[];
|
||||
readonly skippedExtensions: ISyncExtension[];
|
||||
readonly previewResult: IExtensionResourceMergeResult;
|
||||
}
|
||||
|
||||
interface ILastSyncUserData extends IRemoteUserData {
|
||||
skippedExtensions: ISyncExtension[] | undefined;
|
||||
}
|
||||
|
||||
async function parseAndMigrateExtensions(syncData: ISyncData, extensionManagementService: IExtensionManagementService): Promise<ISyncExtension[]> {
|
||||
const extensions = JSON.parse(syncData.content);
|
||||
if (syncData.version === 1
|
||||
|| syncData.version === 2
|
||||
) {
|
||||
const builtinExtensions = (await extensionManagementService.getInstalled(ExtensionType.System)).filter(e => e.isBuiltin);
|
||||
for (const extension of extensions) {
|
||||
// #region Migration from v1 (enabled -> disabled)
|
||||
if (syncData.version === 1) {
|
||||
if ((<any>extension).enabled === false) {
|
||||
extension.disabled = true;
|
||||
}
|
||||
delete (<any>extension).enabled;
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Migration from v2 (set installed property on extension)
|
||||
if (syncData.version === 2) {
|
||||
if (builtinExtensions.every(installed => !areSameExtensions(installed.identifier, extension.identifier))) {
|
||||
extension.installed = true;
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
}
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/extensions.json` });
|
||||
|
||||
/*
|
||||
Version 3 - Introduce installed property to skip installing built in extensions
|
||||
protected readonly version: number = 3;
|
||||
*/
|
||||
/* Version 4: Change settings from `sync.${setting}` to `settingsSync.{setting}` */
|
||||
/* Version 5: Introduce extension state */
|
||||
protected readonly version: number = 5;
|
||||
|
||||
protected isEnabled(): boolean { return super.isEnabled() && this.extensionGalleryService.isEnabled(); }
|
||||
private readonly previewResource: URI = this.extUri.joinPath(this.syncPreviewFolder, 'extensions.json');
|
||||
private readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' });
|
||||
private readonly remoteResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' });
|
||||
private readonly acceptedResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' });
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
|
||||
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
|
||||
@IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService,
|
||||
@IIgnoredExtensionsManagementService private readonly extensionSyncManagementService: IIgnoredExtensionsManagementService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
|
||||
) {
|
||||
super(SyncResource.Extensions, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, configurationService);
|
||||
this._register(
|
||||
Event.debounce(
|
||||
Event.any<any>(
|
||||
Event.filter(this.extensionManagementService.onDidInstallExtension, (e => !!e.gallery)),
|
||||
Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error)),
|
||||
this.extensionEnablementService.onDidChangeEnablement,
|
||||
this.storageKeysSyncRegistryService.onDidChangeExtensionStorageKeys,
|
||||
Event.filter(this.storageService.onDidChangeStorage, e => e.scope === StorageScope.GLOBAL
|
||||
&& this.storageKeysSyncRegistryService.extensionsStorageKeys.some(([extensionIdentifier]) => areSameExtensions(extensionIdentifier, { id: e.key })))),
|
||||
() => undefined, 500)(() => this.triggerLocalChange()));
|
||||
}
|
||||
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionResourcePreview[]> {
|
||||
const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await parseAndMigrateExtensions(remoteUserData.syncData, this.extensionManagementService) : null;
|
||||
const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : [];
|
||||
const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? await parseAndMigrateExtensions(lastSyncUserData.syncData!, this.extensionManagementService) : null;
|
||||
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const localExtensions = this.getLocalExtensions(installedExtensions);
|
||||
const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions);
|
||||
|
||||
if (remoteExtensions) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote extensions with local extensions...`);
|
||||
} else {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Remote extensions does not exist. Synchronizing extensions for the first time.`);
|
||||
}
|
||||
|
||||
const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions);
|
||||
const previewResult: IExtensionResourceMergeResult = {
|
||||
added,
|
||||
removed,
|
||||
updated,
|
||||
remote,
|
||||
content: this.getPreviewContent(localExtensions, added, updated, removed),
|
||||
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
};
|
||||
|
||||
return [{
|
||||
skippedExtensions,
|
||||
localResource: this.localResource,
|
||||
localContent: this.format(localExtensions),
|
||||
localExtensions,
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent: remoteExtensions ? this.format(remoteExtensions) : null,
|
||||
previewResource: this.previewResource,
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: this.acceptedResource,
|
||||
}];
|
||||
}
|
||||
|
||||
private getPreviewContent(localExtensions: ISyncExtension[], added: ISyncExtension[], updated: ISyncExtension[], removed: IExtensionIdentifier[]): string {
|
||||
const preview: ISyncExtension[] = [...added, ...updated];
|
||||
|
||||
const idsOrUUIDs: Set<string> = new Set<string>();
|
||||
const addIdentifier = (identifier: IExtensionIdentifier) => {
|
||||
idsOrUUIDs.add(identifier.id.toLowerCase());
|
||||
if (identifier.uuid) {
|
||||
idsOrUUIDs.add(identifier.uuid);
|
||||
}
|
||||
};
|
||||
preview.forEach(({ identifier }) => addIdentifier(identifier));
|
||||
removed.forEach(addIdentifier);
|
||||
|
||||
for (const localExtension of localExtensions) {
|
||||
if (idsOrUUIDs.has(localExtension.identifier.id.toLowerCase()) || (localExtension.identifier.uuid && idsOrUUIDs.has(localExtension.identifier.uuid))) {
|
||||
// skip
|
||||
continue;
|
||||
}
|
||||
preview.push(localExtension);
|
||||
}
|
||||
|
||||
return this.format(preview);
|
||||
}
|
||||
|
||||
protected async getMergeResult(resourcePreview: IExtensionResourcePreview, token: CancellationToken): Promise<IMergeResult> {
|
||||
return { ...resourcePreview.previewResult, hasConflicts: false };
|
||||
}
|
||||
|
||||
protected async getAcceptResult(resourcePreview: IExtensionResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IExtensionResourceMergeResult> {
|
||||
|
||||
/* Accept local resource */
|
||||
if (this.extUri.isEqual(resource, this.localResource)) {
|
||||
return this.acceptLocal(resourcePreview);
|
||||
}
|
||||
|
||||
/* Accept remote resource */
|
||||
if (this.extUri.isEqual(resource, this.remoteResource)) {
|
||||
return this.acceptRemote(resourcePreview);
|
||||
}
|
||||
|
||||
/* Accept preview resource */
|
||||
if (this.extUri.isEqual(resource, this.previewResource)) {
|
||||
return resourcePreview.previewResult;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid Resource: ${resource.toString()}`);
|
||||
}
|
||||
|
||||
private async acceptLocal(resourcePreview: IExtensionResourcePreview): Promise<IExtensionResourceMergeResult> {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions);
|
||||
const mergeResult = merge(resourcePreview.localExtensions, null, null, resourcePreview.skippedExtensions, ignoredExtensions);
|
||||
const { added, removed, updated, remote } = mergeResult;
|
||||
return {
|
||||
content: resourcePreview.localContent,
|
||||
added,
|
||||
removed,
|
||||
updated,
|
||||
remote,
|
||||
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
};
|
||||
}
|
||||
|
||||
private async acceptRemote(resourcePreview: IExtensionResourcePreview): Promise<IExtensionResourceMergeResult> {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions);
|
||||
const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null;
|
||||
if (remoteExtensions !== null) {
|
||||
const mergeResult = merge(resourcePreview.localExtensions, remoteExtensions, resourcePreview.localExtensions, [], ignoredExtensions);
|
||||
const { added, removed, updated, remote } = mergeResult;
|
||||
return {
|
||||
content: resourcePreview.remoteContent,
|
||||
added,
|
||||
removed,
|
||||
updated,
|
||||
remote,
|
||||
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: resourcePreview.remoteContent,
|
||||
added: [], removed: [], updated: [], remote: null,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [IExtensionResourcePreview, IExtensionResourceMergeResult][], force: boolean): Promise<void> {
|
||||
let { skippedExtensions, localExtensions } = resourcePreviews[0][0];
|
||||
let { added, removed, updated, remote, localChange, remoteChange } = resourcePreviews[0][1];
|
||||
|
||||
if (localChange === Change.None && remoteChange === Change.None) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing extensions.`);
|
||||
}
|
||||
|
||||
if (localChange !== Change.None) {
|
||||
await this.backupLocal(JSON.stringify(localExtensions));
|
||||
skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions);
|
||||
}
|
||||
|
||||
if (remote) {
|
||||
// update remote
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote extensions...`);
|
||||
const content = JSON.stringify(remote);
|
||||
remoteUserData = await this.updateRemoteUserData(content, force ? null : remoteUserData.ref);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote extensions`);
|
||||
}
|
||||
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref) {
|
||||
// update last sync
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized extensions...`);
|
||||
await this.updateLastSyncUserData(remoteUserData, { skippedExtensions });
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized extensions`);
|
||||
}
|
||||
}
|
||||
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
|
||||
return [{ resource: this.extUri.joinPath(uri, 'extensions.json'), comparableResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI }];
|
||||
}
|
||||
|
||||
async resolveContent(uri: URI): Promise<string | null> {
|
||||
if (this.extUri.isEqual(uri, ExtensionsSynchroniser.EXTENSIONS_DATA_URI)) {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions);
|
||||
const localExtensions = this.getLocalExtensions(installedExtensions).filter(e => !ignoredExtensions.some(id => areSameExtensions({ id }, e.identifier)));
|
||||
return this.format(localExtensions);
|
||||
}
|
||||
|
||||
if (this.extUri.isEqual(this.remoteResource, uri) || this.extUri.isEqual(this.localResource, uri) || this.extUri.isEqual(this.acceptedResource, uri)) {
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
|
||||
let content = await super.resolveContent(uri);
|
||||
if (content) {
|
||||
return content;
|
||||
}
|
||||
|
||||
content = await super.resolveContent(this.extUri.dirname(uri));
|
||||
if (content) {
|
||||
const syncData = this.parseSyncData(content);
|
||||
if (syncData) {
|
||||
switch (this.extUri.basename(uri)) {
|
||||
case 'extensions.json':
|
||||
return this.format(this.parseExtensions(syncData));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private format(extensions: ISyncExtension[]): string {
|
||||
extensions.sort((e1, e2) => {
|
||||
if (!e1.identifier.uuid && e2.identifier.uuid) {
|
||||
return -1;
|
||||
}
|
||||
if (e1.identifier.uuid && !e2.identifier.uuid) {
|
||||
return 1;
|
||||
}
|
||||
return compare(e1.identifier.id, e2.identifier.id);
|
||||
});
|
||||
const content = JSON.stringify(extensions);
|
||||
const edits = format(content, undefined, {});
|
||||
return applyEdits(content, edits);
|
||||
}
|
||||
|
||||
async hasLocalData(): Promise<boolean> {
|
||||
try {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const localExtensions = this.getLocalExtensions(installedExtensions);
|
||||
if (localExtensions.some(e => e.installed || e.disabled)) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
/* ignore error */
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async updateLocalExtensions(added: ISyncExtension[], removed: IExtensionIdentifier[], updated: ISyncExtension[], skippedExtensions: ISyncExtension[]): Promise<ISyncExtension[]> {
|
||||
const removeFromSkipped: IExtensionIdentifier[] = [];
|
||||
const addToSkipped: ISyncExtension[] = [];
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
|
||||
if (removed.length) {
|
||||
const extensionsToRemove = installedExtensions.filter(({ identifier, isBuiltin }) => !isBuiltin && removed.some(r => areSameExtensions(identifier, r)));
|
||||
await Promise.all(extensionsToRemove.map(async extensionToRemove => {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Uninstalling local extension...`, extensionToRemove.identifier.id);
|
||||
await this.extensionManagementService.uninstall(extensionToRemove);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Uninstalled local extension.`, extensionToRemove.identifier.id);
|
||||
removeFromSkipped.push(extensionToRemove.identifier);
|
||||
}));
|
||||
}
|
||||
|
||||
if (added.length || updated.length) {
|
||||
await Promise.all([...added, ...updated].map(async e => {
|
||||
const installedExtension = installedExtensions.find(installed => areSameExtensions(installed.identifier, e.identifier));
|
||||
|
||||
// Builtin Extension Sync: Enablement & State
|
||||
if (installedExtension && installedExtension.isBuiltin) {
|
||||
if (e.state && installedExtension.manifest.version === e.version) {
|
||||
const extensionState = JSON.parse(this.storageService.get(e.identifier.id, StorageScope.GLOBAL) || '{}');
|
||||
forEach(e.state, ({ key, value }) => extensionState[key] = value);
|
||||
this.storageService.store(e.identifier.id, JSON.stringify(extensionState), StorageScope.GLOBAL);
|
||||
}
|
||||
if (e.disabled) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...`, e.identifier.id);
|
||||
await this.extensionEnablementService.disableExtension(e.identifier);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Disabled extension`, e.identifier.id);
|
||||
} else {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Enabling extension...`, e.identifier.id);
|
||||
await this.extensionEnablementService.enableExtension(e.identifier);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Enabled extension`, e.identifier.id);
|
||||
}
|
||||
removeFromSkipped.push(e.identifier);
|
||||
return;
|
||||
}
|
||||
|
||||
// User Extension Sync: Install/Update, Enablement & State
|
||||
const extension = await this.extensionGalleryService.getCompatibleExtension(e.identifier);
|
||||
|
||||
/* Update extension state only if
|
||||
* extension is installed and version is same as synced version or
|
||||
* extension is not installed and installable
|
||||
*/
|
||||
if (e.state &&
|
||||
(installedExtension ? installedExtension.manifest.version === e.version /* Installed and has same version */
|
||||
: !!extension /* Installable */)
|
||||
) {
|
||||
const extensionState = JSON.parse(this.storageService.get(e.identifier.id, StorageScope.GLOBAL) || '{}');
|
||||
forEach(e.state, ({ key, value }) => extensionState[key] = value);
|
||||
this.storageService.store(e.identifier.id, JSON.stringify(extensionState), StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
if (extension) {
|
||||
try {
|
||||
if (e.disabled) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...`, e.identifier.id, extension.version);
|
||||
await this.extensionEnablementService.disableExtension(extension.identifier);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Disabled extension`, e.identifier.id, extension.version);
|
||||
} else {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Enabling extension...`, e.identifier.id, extension.version);
|
||||
await this.extensionEnablementService.enableExtension(extension.identifier);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Enabled extension`, e.identifier.id, extension.version);
|
||||
}
|
||||
|
||||
// Install only if the extension does not exist
|
||||
if (!installedExtension) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Installing extension...`, e.identifier.id, extension.version);
|
||||
await this.extensionManagementService.installFromGallery(extension, { isMachineScoped: false } /* pass options to prevent install and sync dialog in web */);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Installed extension.`, e.identifier.id, extension.version);
|
||||
removeFromSkipped.push(extension.identifier);
|
||||
}
|
||||
} catch (error) {
|
||||
addToSkipped.push(e);
|
||||
this.logService.error(error);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing extension`, extension.displayName || extension.identifier.id);
|
||||
}
|
||||
} else {
|
||||
addToSkipped.push(e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const newSkippedExtensions: ISyncExtension[] = [];
|
||||
for (const skippedExtension of skippedExtensions) {
|
||||
if (!removeFromSkipped.some(e => areSameExtensions(e, skippedExtension.identifier))) {
|
||||
newSkippedExtensions.push(skippedExtension);
|
||||
}
|
||||
}
|
||||
for (const skippedExtension of addToSkipped) {
|
||||
if (!newSkippedExtensions.some(e => areSameExtensions(e.identifier, skippedExtension.identifier))) {
|
||||
newSkippedExtensions.push(skippedExtension);
|
||||
}
|
||||
}
|
||||
return newSkippedExtensions;
|
||||
}
|
||||
|
||||
private parseExtensions(syncData: ISyncData): ISyncExtension[] {
|
||||
return JSON.parse(syncData.content);
|
||||
}
|
||||
|
||||
private getLocalExtensions(installedExtensions: ILocalExtension[]): ISyncExtensionWithVersion[] {
|
||||
const disabledExtensions = this.extensionEnablementService.getDisabledExtensions();
|
||||
return installedExtensions
|
||||
.map(({ identifier, isBuiltin, manifest }) => {
|
||||
const syncExntesion: ISyncExtensionWithVersion = { identifier, version: manifest.version };
|
||||
if (disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier))) {
|
||||
syncExntesion.disabled = true;
|
||||
}
|
||||
if (!isBuiltin) {
|
||||
syncExntesion.installed = true;
|
||||
}
|
||||
const keys = this.storageKeysSyncRegistryService.getExtensioStorageKeys({ id: identifier.id, version: manifest.version });
|
||||
if (keys) {
|
||||
const extensionStorageValue = this.storageService.get(identifier.id, StorageScope.GLOBAL) || '{}';
|
||||
try {
|
||||
const extensionStorageState = JSON.parse(extensionStorageValue);
|
||||
syncExntesion.state = Object.keys(extensionStorageState).reduce((state: IStringDictionary<any>, key) => {
|
||||
if (keys.includes(key)) {
|
||||
state[key] = extensionStorageState[key];
|
||||
}
|
||||
return state;
|
||||
}, {});
|
||||
} catch (error) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Error while parsing extension state`, getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
return syncExntesion;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class ExtensionsInitializer extends AbstractInitializer {
|
||||
|
||||
constructor(
|
||||
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
|
||||
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
|
||||
@IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
) {
|
||||
super(SyncResource.Extensions, environmentService, logService, fileService);
|
||||
}
|
||||
|
||||
async doInitialize(remoteUserData: IRemoteUserData): Promise<void> {
|
||||
const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await parseAndMigrateExtensions(remoteUserData.syncData, this.extensionManagementService) : null;
|
||||
if (!remoteExtensions) {
|
||||
this.logService.info('Skipping initializing extensions because remote extensions does not exist.');
|
||||
return;
|
||||
}
|
||||
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const newExtensionsToSync = new Map<string, ISyncExtension>();
|
||||
const installedExtensionsToSync: ISyncExtension[] = [];
|
||||
const toInstall: { names: string[], uuids: string[] } = { names: [], uuids: [] };
|
||||
const toDisable: IExtensionIdentifier[] = [];
|
||||
for (const extension of remoteExtensions) {
|
||||
if (installedExtensions.some(i => areSameExtensions(i.identifier, extension.identifier))) {
|
||||
installedExtensionsToSync.push(extension);
|
||||
if (extension.disabled) {
|
||||
toDisable.push(extension.identifier);
|
||||
}
|
||||
} else {
|
||||
if (extension.installed) {
|
||||
newExtensionsToSync.set(extension.identifier.id.toLowerCase(), extension);
|
||||
if (extension.identifier.uuid) {
|
||||
toInstall.uuids.push(extension.identifier.uuid);
|
||||
} else {
|
||||
toInstall.names.push(extension.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toInstall.names.length || toInstall.uuids.length) {
|
||||
const galleryExtensions = (await this.galleryService.query({ ids: toInstall.uuids, names: toInstall.names, pageSize: toInstall.uuids.length + toInstall.names.length }, CancellationToken.None)).firstPage;
|
||||
for (const galleryExtension of galleryExtensions) {
|
||||
try {
|
||||
const extensionToSync = newExtensionsToSync.get(galleryExtension.identifier.id.toLowerCase())!;
|
||||
if (extensionToSync.state) {
|
||||
this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionToSync.state), StorageScope.GLOBAL);
|
||||
}
|
||||
this.logService.trace(`Installing extension...`, galleryExtension.identifier.id);
|
||||
await this.extensionManagementService.installFromGallery(galleryExtension, { isMachineScoped: false } /* pass options to prevent install and sync dialog in web */);
|
||||
this.logService.info(`Installed extension.`, galleryExtension.identifier.id);
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toDisable.length) {
|
||||
for (const identifier of toDisable) {
|
||||
this.logService.trace(`Enabling extension...`, identifier.id);
|
||||
await this.extensionEnablementService.disableExtension(identifier);
|
||||
this.logService.info(`Enabled extension`, identifier.id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const extensionToSync of installedExtensionsToSync) {
|
||||
if (extensionToSync.state) {
|
||||
const extensionState = JSON.parse(this.storageService.get(extensionToSync.identifier.id, StorageScope.GLOBAL) || '{}');
|
||||
forEach(extensionToSync.state, ({ key, value }) => extensionState[key] = value);
|
||||
this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionState), StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { IStorageValue } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { IStorageKey } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
export interface IMergeResult {
|
||||
local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> };
|
||||
remote: IStringDictionary<IStorageValue> | null;
|
||||
skipped: string[];
|
||||
}
|
||||
|
||||
export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStorage: IStringDictionary<IStorageValue> | null, baseStorage: IStringDictionary<IStorageValue> | null, storageKeys: ReadonlyArray<IStorageKey>, previouslySkipped: string[], logService: ILogService): IMergeResult {
|
||||
if (!remoteStorage) {
|
||||
return { remote: Object.keys(localStorage).length > 0 ? localStorage : null, local: { added: {}, removed: [], updated: {} }, skipped: [] };
|
||||
}
|
||||
|
||||
const localToRemote = compare(localStorage, remoteStorage);
|
||||
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
|
||||
// No changes found between local and remote.
|
||||
return { remote: null, local: { added: {}, removed: [], updated: {} }, skipped: [] };
|
||||
}
|
||||
|
||||
const baseToRemote = baseStorage ? compare(baseStorage, remoteStorage) : { added: Object.keys(remoteStorage).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
const baseToLocal = baseStorage ? compare(baseStorage, localStorage) : { added: Object.keys(localStorage).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
|
||||
const local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> } = { added: {}, removed: [], updated: {} };
|
||||
const remote: IStringDictionary<IStorageValue> = objects.deepClone(remoteStorage);
|
||||
const skipped: string[] = [];
|
||||
|
||||
// Added in remote
|
||||
for (const key of baseToRemote.added.values()) {
|
||||
const remoteValue = remoteStorage[key];
|
||||
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
|
||||
if (!storageKey) {
|
||||
skipped.push(key);
|
||||
logService.trace(`GlobalState: Skipped adding ${key} in local storage as it is not registered.`);
|
||||
continue;
|
||||
}
|
||||
if (storageKey.version !== remoteValue.version) {
|
||||
logService.info(`GlobalState: Skipped adding ${key} in local storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`);
|
||||
continue;
|
||||
}
|
||||
const localValue = localStorage[key];
|
||||
if (localValue && localValue.value === remoteValue.value) {
|
||||
continue;
|
||||
}
|
||||
if (baseToLocal.added.has(key)) {
|
||||
local.updated[key] = remoteValue;
|
||||
} else {
|
||||
local.added[key] = remoteValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Updated in Remote
|
||||
for (const key of baseToRemote.updated.values()) {
|
||||
const remoteValue = remoteStorage[key];
|
||||
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
|
||||
if (!storageKey) {
|
||||
skipped.push(key);
|
||||
logService.trace(`GlobalState: Skipped updating ${key} in local storage as is not registered.`);
|
||||
continue;
|
||||
}
|
||||
if (storageKey.version !== remoteValue.version) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in local storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`);
|
||||
continue;
|
||||
}
|
||||
const localValue = localStorage[key];
|
||||
if (localValue && localValue.value === remoteValue.value) {
|
||||
continue;
|
||||
}
|
||||
local.updated[key] = remoteValue;
|
||||
}
|
||||
|
||||
// Removed in remote
|
||||
for (const key of baseToRemote.removed.values()) {
|
||||
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
|
||||
if (!storageKey) {
|
||||
logService.trace(`GlobalState: Skipped removing ${key} in local storage. It is not registered to sync.`);
|
||||
continue;
|
||||
}
|
||||
local.removed.push(key);
|
||||
}
|
||||
|
||||
// Added in local
|
||||
for (const key of baseToLocal.added.values()) {
|
||||
if (!baseToRemote.added.has(key)) {
|
||||
remote[key] = localStorage[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Updated in local
|
||||
for (const key of baseToLocal.updated.values()) {
|
||||
if (baseToRemote.updated.has(key) || baseToRemote.removed.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const remoteValue = remote[key];
|
||||
const localValue = localStorage[key];
|
||||
if (localValue.version < remoteValue.version) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in remote storage. Local version '${localValue.version}' and remote version '${remoteValue.version} are not same.`);
|
||||
continue;
|
||||
}
|
||||
remote[key] = localValue;
|
||||
}
|
||||
|
||||
// Removed in local
|
||||
for (const key of baseToLocal.removed.values()) {
|
||||
// do not remove from remote if it is updated in remote
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
|
||||
// do not remove from remote if storage key is not found
|
||||
if (!storageKey) {
|
||||
skipped.push(key);
|
||||
logService.trace(`GlobalState: Skipped removing ${key} in remote storage. It is not registered to sync.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const remoteValue = remote[key];
|
||||
// do not remove from remote if local data version is old
|
||||
if (storageKey.version < remoteValue.version) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in remote storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// add to local if it was skipped before
|
||||
if (previouslySkipped.indexOf(key) !== -1) {
|
||||
local.added[key] = remote[key];
|
||||
continue;
|
||||
}
|
||||
|
||||
delete remote[key];
|
||||
}
|
||||
|
||||
return { local, remote: areSame(remote, remoteStorage) ? null : remote, skipped };
|
||||
}
|
||||
|
||||
function compare(from: IStringDictionary<any>, to: IStringDictionary<any>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
|
||||
const fromKeys = Object.keys(from);
|
||||
const toKeys = Object.keys(to);
|
||||
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const updated: Set<string> = new Set<string>();
|
||||
|
||||
for (const key of fromKeys) {
|
||||
if (removed.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const value1 = from[key];
|
||||
const value2 = to[key];
|
||||
if (!objects.equals(value1, value2)) {
|
||||
updated.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return { added, removed, updated };
|
||||
}
|
||||
|
||||
function areSame(a: IStringDictionary<IStorageValue>, b: IStringDictionary<IStorageValue>): boolean {
|
||||
const { added, removed, updated } = compare(a, b);
|
||||
return added.size === 0 && removed.size === 0 && updated.size === 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import {
|
||||
IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncResourceEnablementService,
|
||||
IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, USER_DATA_SYNC_SCHEME, IRemoteUserData, Change
|
||||
} 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';
|
||||
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 { parse } from 'vs/base/common/json';
|
||||
import { AbstractInitializer, AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { format } from 'vs/base/common/jsonFormatter';
|
||||
import { applyEdits } from 'vs/base/common/jsonEdit';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { IStorageKeysSyncRegistryService, IStorageKey } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
const argvStoragePrefx = 'globalState.argv.';
|
||||
const argvProperties: string[] = ['locale'];
|
||||
|
||||
interface IGlobalStateResourceMergeResult extends IAcceptResult {
|
||||
readonly local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> };
|
||||
readonly remote: IStringDictionary<IStorageValue> | null;
|
||||
}
|
||||
|
||||
export interface IGlobalStateResourcePreview extends IResourcePreview {
|
||||
readonly skippedStorageKeys: string[];
|
||||
readonly localUserData: IGlobalState;
|
||||
readonly previewResult: IGlobalStateResourceMergeResult;
|
||||
}
|
||||
|
||||
interface ILastSyncUserData extends IRemoteUserData {
|
||||
skippedStorageKeys: string[] | undefined;
|
||||
}
|
||||
|
||||
export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
private static readonly GLOBAL_STATE_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'globalState', path: `/globalState.json` });
|
||||
protected readonly version: number = 1;
|
||||
private readonly previewResource: URI = this.extUri.joinPath(this.syncPreviewFolder, 'globalState.json');
|
||||
private readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' });
|
||||
private readonly remoteResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' });
|
||||
private readonly acceptedResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' });
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
@IEnvironmentService readonly environmentService: IEnvironmentService,
|
||||
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
|
||||
) {
|
||||
super(SyncResource.GlobalState, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, configurationService);
|
||||
this._register(this.fileService.watch(this.extUri.dirname(this.environmentService.argvResource)));
|
||||
this._register(
|
||||
Event.any(
|
||||
/* Locale change */
|
||||
Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource)),
|
||||
/* Storage change */
|
||||
Event.filter(this.storageService.onDidChangeStorage, e => storageKeysSyncRegistryService.storageKeys.some(({ key }) => e.key === key)),
|
||||
/* Storage key registered */
|
||||
this.storageKeysSyncRegistryService.onDidChangeStorageKeys
|
||||
)((() => this.triggerLocalChange()))
|
||||
);
|
||||
}
|
||||
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateResourcePreview[]> {
|
||||
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
|
||||
const lastSyncGlobalState: IGlobalState | null = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;
|
||||
|
||||
const localGloablState = await this.getLocalGlobalState();
|
||||
|
||||
if (remoteGlobalState) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote ui state with local ui state...`);
|
||||
} else {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Remote ui state does not exist. Synchronizing ui state for the first time.`);
|
||||
}
|
||||
|
||||
const { local, remote, skipped } = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
|
||||
const previewResult: IGlobalStateResourceMergeResult = {
|
||||
content: null,
|
||||
local,
|
||||
remote,
|
||||
localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
};
|
||||
|
||||
return [{
|
||||
skippedStorageKeys: skipped,
|
||||
localResource: this.localResource,
|
||||
localContent: this.format(localGloablState),
|
||||
localUserData: localGloablState,
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null,
|
||||
previewResource: this.previewResource,
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: this.acceptedResource,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async getMergeResult(resourcePreview: IGlobalStateResourcePreview, token: CancellationToken): Promise<IMergeResult> {
|
||||
return { ...resourcePreview.previewResult, hasConflicts: false };
|
||||
}
|
||||
|
||||
protected async getAcceptResult(resourcePreview: IGlobalStateResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IGlobalStateResourceMergeResult> {
|
||||
|
||||
/* Accept local resource */
|
||||
if (this.extUri.isEqual(resource, this.localResource)) {
|
||||
return this.acceptLocal(resourcePreview);
|
||||
}
|
||||
|
||||
/* Accept remote resource */
|
||||
if (this.extUri.isEqual(resource, this.remoteResource)) {
|
||||
return this.acceptRemote(resourcePreview);
|
||||
}
|
||||
|
||||
/* Accept preview resource */
|
||||
if (this.extUri.isEqual(resource, this.previewResource)) {
|
||||
return resourcePreview.previewResult;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid Resource: ${resource.toString()}`);
|
||||
}
|
||||
|
||||
private async acceptLocal(resourcePreview: IGlobalStateResourcePreview): Promise<IGlobalStateResourceMergeResult> {
|
||||
return {
|
||||
content: resourcePreview.localContent,
|
||||
local: { added: {}, removed: [], updated: {} },
|
||||
remote: resourcePreview.localUserData.storage,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Modified,
|
||||
};
|
||||
}
|
||||
|
||||
private async acceptRemote(resourcePreview: IGlobalStateResourcePreview): Promise<IGlobalStateResourceMergeResult> {
|
||||
if (resourcePreview.remoteContent !== null) {
|
||||
const remoteGlobalState: IGlobalState = JSON.parse(resourcePreview.remoteContent);
|
||||
const { local, remote } = merge(resourcePreview.localUserData.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), resourcePreview.skippedStorageKeys, this.logService);
|
||||
return {
|
||||
content: resourcePreview.remoteContent,
|
||||
local,
|
||||
remote,
|
||||
localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: resourcePreview.remoteContent,
|
||||
local: { added: {}, removed: [], updated: {} },
|
||||
remote: null,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, resourcePreviews: [IGlobalStateResourcePreview, IGlobalStateResourceMergeResult][], force: boolean): Promise<void> {
|
||||
let { localUserData, skippedStorageKeys } = resourcePreviews[0][0];
|
||||
let { local, remote, localChange, remoteChange } = resourcePreviews[0][1];
|
||||
|
||||
if (localChange === Change.None && remoteChange === Change.None) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ui state.`);
|
||||
}
|
||||
|
||||
if (localChange !== Change.None) {
|
||||
// update local
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating local ui state...`);
|
||||
await this.backupLocal(JSON.stringify(localUserData));
|
||||
await this.writeLocalGlobalState(local);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated local ui state`);
|
||||
}
|
||||
|
||||
if (remoteChange !== Change.None) {
|
||||
// update remote
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote ui state...`);
|
||||
const content = JSON.stringify(<IGlobalState>{ storage: remote });
|
||||
remoteUserData = await this.updateRemoteUserData(content, force ? null : remoteUserData.ref);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote ui state`);
|
||||
}
|
||||
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref || !equals(lastSyncUserData.skippedStorageKeys, skippedStorageKeys)) {
|
||||
// update last sync
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized ui state...`);
|
||||
await this.updateLastSyncUserData(remoteUserData, { skippedStorageKeys });
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized ui state`);
|
||||
}
|
||||
}
|
||||
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
|
||||
return [{ resource: this.extUri.joinPath(uri, 'globalState.json'), comparableResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI }];
|
||||
}
|
||||
|
||||
async resolveContent(uri: URI): Promise<string | null> {
|
||||
if (this.extUri.isEqual(uri, GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI)) {
|
||||
const localGlobalState = await this.getLocalGlobalState();
|
||||
return this.format(localGlobalState);
|
||||
}
|
||||
|
||||
if (this.extUri.isEqual(this.remoteResource, uri) || this.extUri.isEqual(this.localResource, uri) || this.extUri.isEqual(this.acceptedResource, uri)) {
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
|
||||
let content = await super.resolveContent(uri);
|
||||
if (content) {
|
||||
return content;
|
||||
}
|
||||
|
||||
content = await super.resolveContent(this.extUri.dirname(uri));
|
||||
if (content) {
|
||||
const syncData = this.parseSyncData(content);
|
||||
if (syncData) {
|
||||
switch (this.extUri.basename(uri)) {
|
||||
case 'globalState.json':
|
||||
return this.format(JSON.parse(syncData.content));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private format(globalState: IGlobalState): string {
|
||||
const storageKeys = globalState.storage ? Object.keys(globalState.storage).sort() : [];
|
||||
const storage: IStringDictionary<IStorageValue> = {};
|
||||
storageKeys.forEach(key => storage[key] = globalState.storage[key]);
|
||||
globalState.storage = storage;
|
||||
const content = JSON.stringify(globalState);
|
||||
const edits = format(content, undefined, {});
|
||||
return applyEdits(content, edits);
|
||||
}
|
||||
|
||||
async hasLocalData(): Promise<boolean> {
|
||||
try {
|
||||
const { storage } = await this.getLocalGlobalState();
|
||||
if (Object.keys(storage).length > 1 || storage[`${argvStoragePrefx}.locale`]?.value !== 'en') {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
/* ignore error */
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async getLocalGlobalState(): Promise<IGlobalState> {
|
||||
const storage: IStringDictionary<IStorageValue> = {};
|
||||
const argvContent: string = await this.getLocalArgvContent();
|
||||
const argvValue: IStringDictionary<any> = parse(argvContent);
|
||||
for (const argvProperty of argvProperties) {
|
||||
if (argvValue[argvProperty] !== undefined) {
|
||||
storage[`${argvStoragePrefx}${argvProperty}`] = { version: 1, value: argvValue[argvProperty] };
|
||||
}
|
||||
}
|
||||
for (const { key, version } of this.storageKeysSyncRegistryService.storageKeys) {
|
||||
const value = this.storageService.get(key, StorageScope.GLOBAL);
|
||||
if (value) {
|
||||
storage[key] = { version, value };
|
||||
}
|
||||
}
|
||||
return { 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({ added, removed, updated }: { added: IStringDictionary<IStorageValue>, updated: IStringDictionary<IStorageValue>, removed: string[] }): Promise<void> {
|
||||
const argv: IStringDictionary<any> = {};
|
||||
const updatedStorage: IStringDictionary<any> = {};
|
||||
const handleUpdatedStorage = (keys: string[], storage?: IStringDictionary<IStorageValue>): void => {
|
||||
for (const key of keys) {
|
||||
if (key.startsWith(argvStoragePrefx)) {
|
||||
argv[key.substring(argvStoragePrefx.length)] = storage ? storage[key].value : undefined;
|
||||
continue;
|
||||
}
|
||||
if (storage) {
|
||||
const storageValue = storage[key];
|
||||
if (storageValue.value !== String(this.storageService.get(key, StorageScope.GLOBAL))) {
|
||||
updatedStorage[key] = storageValue.value;
|
||||
}
|
||||
} else {
|
||||
if (this.storageService.get(key, StorageScope.GLOBAL) !== undefined) {
|
||||
updatedStorage[key] = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
handleUpdatedStorage(Object.keys(added), added);
|
||||
handleUpdatedStorage(Object.keys(updated), updated);
|
||||
handleUpdatedStorage(removed);
|
||||
if (Object.keys(argv).length) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating locale...`);
|
||||
await this.updateArgv(argv);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated locale`);
|
||||
}
|
||||
const updatedStorageKeys: string[] = Object.keys(updatedStorage);
|
||||
if (updatedStorageKeys.length) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating global state...`);
|
||||
for (const key of Object.keys(updatedStorage)) {
|
||||
this.storageService.store(key, updatedStorage[key], StorageScope.GLOBAL);
|
||||
}
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated global state`, Object.keys(updatedStorage));
|
||||
}
|
||||
}
|
||||
|
||||
private async updateArgv(argv: IStringDictionary<any>): Promise<void> {
|
||||
const argvContent = await this.getLocalArgvContent();
|
||||
let content = argvContent;
|
||||
for (const argvProperty of Object.keys(argv)) {
|
||||
content = edit(content, [argvProperty], argv[argvProperty], {});
|
||||
}
|
||||
if (argvContent !== content) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating locale...`);
|
||||
await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(content));
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated locale.`);
|
||||
}
|
||||
}
|
||||
|
||||
private getSyncStorageKeys(): IStorageKey[] {
|
||||
return [...this.storageKeysSyncRegistryService.storageKeys, ...argvProperties.map(argvProprety => (<IStorageKey>{ key: `${argvStoragePrefx}${argvProprety}`, version: 1 }))];
|
||||
}
|
||||
}
|
||||
|
||||
export class GlobalStateInitializer extends AbstractInitializer {
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
) {
|
||||
super(SyncResource.GlobalState, environmentService, logService, fileService);
|
||||
}
|
||||
|
||||
async doInitialize(remoteUserData: IRemoteUserData): Promise<void> {
|
||||
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
|
||||
if (!remoteGlobalState) {
|
||||
this.logService.info('Skipping initializing global state because remote global state does not exist.');
|
||||
return;
|
||||
}
|
||||
|
||||
const argv: IStringDictionary<any> = {};
|
||||
const storage: IStringDictionary<any> = {};
|
||||
for (const key of Object.keys(remoteGlobalState.storage)) {
|
||||
if (key.startsWith(argvStoragePrefx)) {
|
||||
argv[key.substring(argvStoragePrefx.length)] = remoteGlobalState.storage[key].value;
|
||||
} else {
|
||||
if (this.storageService.get(key, StorageScope.GLOBAL) === undefined) {
|
||||
storage[key] = remoteGlobalState.storage[key].value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(argv).length) {
|
||||
let content = '{}';
|
||||
try {
|
||||
const fileContent = await this.fileService.readFile(this.environmentService.argvResource);
|
||||
content = fileContent.value.toString();
|
||||
} catch (error) { }
|
||||
for (const argvProperty of Object.keys(argv)) {
|
||||
content = edit(content, [argvProperty], argv[argvProperty], {});
|
||||
}
|
||||
await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(content));
|
||||
}
|
||||
|
||||
if (Object.keys(storage).length) {
|
||||
for (const key of Object.keys(storage)) {
|
||||
this.storageService.store(key, storage[key], StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { distinct } from 'vs/base/common/arrays';
|
||||
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export const IIgnoredExtensionsManagementService = createDecorator<IIgnoredExtensionsManagementService>('IIgnoredExtensionsManagementService');
|
||||
export interface IIgnoredExtensionsManagementService {
|
||||
readonly _serviceBrand: any;
|
||||
|
||||
getIgnoredExtensions(installed: ILocalExtension[]): string[];
|
||||
|
||||
hasToNeverSyncExtension(extensionId: string): boolean;
|
||||
hasToAlwaysSyncExtension(extensionId: string): boolean;
|
||||
updateIgnoredExtensions(ignoredExtensionId: string, ignore: boolean): Promise<void>;
|
||||
updateSynchronizedExtensions(ignoredExtensionId: string, sync: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
export class IgnoredExtensionsManagementService implements IIgnoredExtensionsManagementService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
) {
|
||||
}
|
||||
|
||||
hasToNeverSyncExtension(extensionId: string): boolean {
|
||||
const configuredIgnoredExtensions = this.getConfiguredIgnoredExtensions();
|
||||
return configuredIgnoredExtensions.includes(extensionId.toLowerCase());
|
||||
}
|
||||
|
||||
hasToAlwaysSyncExtension(extensionId: string): boolean {
|
||||
const configuredIgnoredExtensions = this.getConfiguredIgnoredExtensions();
|
||||
return configuredIgnoredExtensions.includes(`-${extensionId.toLowerCase()}`);
|
||||
}
|
||||
|
||||
updateIgnoredExtensions(ignoredExtensionId: string, ignore: boolean): Promise<void> {
|
||||
// first remove the extension completely from ignored extensions
|
||||
let currentValue = [...this.configurationService.getValue<string[]>('settingsSync.ignoredExtensions')].map(id => id.toLowerCase());
|
||||
currentValue = currentValue.filter(v => v !== ignoredExtensionId && v !== `-${ignoredExtensionId}`);
|
||||
|
||||
// Add only if ignored
|
||||
if (ignore) {
|
||||
currentValue.push(ignoredExtensionId.toLowerCase());
|
||||
}
|
||||
|
||||
return this.configurationService.updateValue('settingsSync.ignoredExtensions', currentValue.length ? currentValue : undefined, ConfigurationTarget.USER);
|
||||
}
|
||||
|
||||
updateSynchronizedExtensions(extensionId: string, sync: boolean): Promise<void> {
|
||||
// first remove the extension completely from ignored extensions
|
||||
let currentValue = [...this.configurationService.getValue<string[]>('settingsSync.ignoredExtensions')].map(id => id.toLowerCase());
|
||||
currentValue = currentValue.filter(v => v !== extensionId && v !== `-${extensionId}`);
|
||||
|
||||
// Add only if synced
|
||||
if (sync) {
|
||||
currentValue.push(`-${extensionId.toLowerCase()}`);
|
||||
}
|
||||
|
||||
return this.configurationService.updateValue('settingsSync.ignoredExtensions', currentValue.length ? currentValue : undefined, ConfigurationTarget.USER);
|
||||
}
|
||||
|
||||
getIgnoredExtensions(installed: ILocalExtension[]): string[] {
|
||||
const defaultIgnoredExtensions = installed.filter(i => i.isMachineScoped).map(i => i.identifier.id.toLowerCase());
|
||||
const value = this.getConfiguredIgnoredExtensions().map(id => id.toLowerCase());
|
||||
const added: string[] = [], removed: string[] = [];
|
||||
if (Array.isArray(value)) {
|
||||
for (const key of value) {
|
||||
if (key.startsWith('-')) {
|
||||
removed.push(key.substring(1));
|
||||
} else {
|
||||
added.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return distinct([...defaultIgnoredExtensions, ...added,].filter(setting => removed.indexOf(setting) === -1));
|
||||
}
|
||||
|
||||
private getConfiguredIgnoredExtensions(): string[] {
|
||||
let userValue = this.configurationService.inspect<string[]>('settingsSync.ignoredExtensions').userValue;
|
||||
if (userValue !== undefined) {
|
||||
return userValue;
|
||||
}
|
||||
userValue = this.configurationService.inspect<string[]>('sync.ignoredExtensions').userValue;
|
||||
if (userValue !== undefined) {
|
||||
return userValue;
|
||||
}
|
||||
return (this.configurationService.getValue<string[]>('settingsSync.ignoredExtensions') || []).map(id => id.toLowerCase());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { parse } from 'vs/base/common/json';
|
||||
import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import * as contentUtil from 'vs/platform/userDataSync/common/content';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
|
||||
interface ICompareResult {
|
||||
added: Set<string>;
|
||||
removed: Set<string>;
|
||||
updated: Set<string>;
|
||||
}
|
||||
|
||||
interface IMergeResult {
|
||||
hasLocalForwarded: boolean;
|
||||
hasRemoteForwarded: boolean;
|
||||
added: Set<string>;
|
||||
removed: Set<string>;
|
||||
updated: Set<string>;
|
||||
conflicts: Set<string>;
|
||||
}
|
||||
|
||||
export function parseKeybindings(content: string): IUserFriendlyKeybinding[] {
|
||||
return parse(content) || [];
|
||||
}
|
||||
|
||||
export async function merge(localContent: string, remoteContent: string, baseContent: string | null, formattingOptions: FormattingOptions, userDataSyncUtilService: IUserDataSyncUtilService): Promise<{ mergeContent: string, hasChanges: boolean, hasConflicts: boolean }> {
|
||||
const local = parseKeybindings(localContent);
|
||||
const remote = parseKeybindings(remoteContent);
|
||||
const base = baseContent ? parseKeybindings(baseContent) : null;
|
||||
|
||||
const userbindings: string[] = [...local, ...remote, ...(base || [])].map(keybinding => keybinding.key);
|
||||
const normalizedKeys = await userDataSyncUtilService.resolveUserBindings(userbindings);
|
||||
let keybindingsMergeResult = computeMergeResultByKeybinding(local, remote, base, normalizedKeys);
|
||||
|
||||
if (!keybindingsMergeResult.hasLocalForwarded && !keybindingsMergeResult.hasRemoteForwarded) {
|
||||
// No changes found between local and remote.
|
||||
return { mergeContent: localContent, hasChanges: false, hasConflicts: false };
|
||||
}
|
||||
|
||||
if (!keybindingsMergeResult.hasLocalForwarded && keybindingsMergeResult.hasRemoteForwarded) {
|
||||
return { mergeContent: remoteContent, hasChanges: true, hasConflicts: false };
|
||||
}
|
||||
|
||||
if (keybindingsMergeResult.hasLocalForwarded && !keybindingsMergeResult.hasRemoteForwarded) {
|
||||
// Local has moved forward and remote has not. Return local.
|
||||
return { mergeContent: localContent, hasChanges: true, hasConflicts: false };
|
||||
}
|
||||
|
||||
// Both local and remote has moved forward.
|
||||
const localByCommand = byCommand(local);
|
||||
const remoteByCommand = byCommand(remote);
|
||||
const baseByCommand = base ? byCommand(base) : null;
|
||||
const localToRemoteByCommand = compareByCommand(localByCommand, remoteByCommand, normalizedKeys);
|
||||
const baseToLocalByCommand = baseByCommand ? compareByCommand(baseByCommand, localByCommand, normalizedKeys) : { added: [...localByCommand.keys()].reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
const baseToRemoteByCommand = baseByCommand ? compareByCommand(baseByCommand, remoteByCommand, normalizedKeys) : { added: [...remoteByCommand.keys()].reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
|
||||
const commandsMergeResult = computeMergeResult(localToRemoteByCommand, baseToLocalByCommand, baseToRemoteByCommand);
|
||||
let mergeContent = localContent;
|
||||
|
||||
// Removed commands in Remote
|
||||
for (const command of commandsMergeResult.removed.values()) {
|
||||
if (commandsMergeResult.conflicts.has(command)) {
|
||||
continue;
|
||||
}
|
||||
mergeContent = removeKeybindings(mergeContent, command, formattingOptions);
|
||||
}
|
||||
|
||||
// Added commands in remote
|
||||
for (const command of commandsMergeResult.added.values()) {
|
||||
if (commandsMergeResult.conflicts.has(command)) {
|
||||
continue;
|
||||
}
|
||||
const keybindings = remoteByCommand.get(command)!;
|
||||
// Ignore negated commands
|
||||
if (keybindings.some(keybinding => keybinding.command !== `-${command}` && keybindingsMergeResult.conflicts.has(normalizedKeys[keybinding.key]))) {
|
||||
commandsMergeResult.conflicts.add(command);
|
||||
continue;
|
||||
}
|
||||
mergeContent = addKeybindings(mergeContent, keybindings, formattingOptions);
|
||||
}
|
||||
|
||||
// Updated commands in Remote
|
||||
for (const command of commandsMergeResult.updated.values()) {
|
||||
if (commandsMergeResult.conflicts.has(command)) {
|
||||
continue;
|
||||
}
|
||||
const keybindings = remoteByCommand.get(command)!;
|
||||
// Ignore negated commands
|
||||
if (keybindings.some(keybinding => keybinding.command !== `-${command}` && keybindingsMergeResult.conflicts.has(normalizedKeys[keybinding.key]))) {
|
||||
commandsMergeResult.conflicts.add(command);
|
||||
continue;
|
||||
}
|
||||
mergeContent = updateKeybindings(mergeContent, command, keybindings, formattingOptions);
|
||||
}
|
||||
|
||||
return { mergeContent, hasChanges: true, hasConflicts: commandsMergeResult.conflicts.size > 0 };
|
||||
}
|
||||
|
||||
function computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompareResult, baseToRemote: ICompareResult): { added: Set<string>, removed: Set<string>, updated: Set<string>, conflicts: Set<string> } {
|
||||
const added: Set<string> = new Set<string>();
|
||||
const removed: Set<string> = new Set<string>();
|
||||
const updated: Set<string> = new Set<string>();
|
||||
const conflicts: Set<string> = new Set<string>();
|
||||
|
||||
// Removed keys in Local
|
||||
for (const key of baseToLocal.removed.values()) {
|
||||
// Got updated in remote
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
conflicts.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Removed keys in Remote
|
||||
for (const key of baseToRemote.removed.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got updated in local
|
||||
if (baseToLocal.updated.has(key)) {
|
||||
conflicts.add(key);
|
||||
} else {
|
||||
// remove the key
|
||||
removed.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Added keys in Local
|
||||
for (const key of baseToLocal.added.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got added in remote
|
||||
if (baseToRemote.added.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
conflicts.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Added keys in remote
|
||||
for (const key of baseToRemote.added.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got added in local
|
||||
if (baseToLocal.added.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
conflicts.add(key);
|
||||
}
|
||||
} else {
|
||||
added.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Updated keys in Local
|
||||
for (const key of baseToLocal.updated.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got updated in remote
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
conflicts.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Updated keys in Remote
|
||||
for (const key of baseToRemote.updated.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got updated in local
|
||||
if (baseToLocal.updated.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
conflicts.add(key);
|
||||
}
|
||||
} else {
|
||||
// updated key
|
||||
updated.add(key);
|
||||
}
|
||||
}
|
||||
return { added, removed, updated, conflicts };
|
||||
}
|
||||
|
||||
function computeMergeResultByKeybinding(local: IUserFriendlyKeybinding[], remote: IUserFriendlyKeybinding[], base: IUserFriendlyKeybinding[] | null, normalizedKeys: IStringDictionary<string>): IMergeResult {
|
||||
const empty = new Set<string>();
|
||||
const localByKeybinding = byKeybinding(local, normalizedKeys);
|
||||
const remoteByKeybinding = byKeybinding(remote, normalizedKeys);
|
||||
const baseByKeybinding = base ? byKeybinding(base, normalizedKeys) : null;
|
||||
|
||||
const localToRemoteByKeybinding = compareByKeybinding(localByKeybinding, remoteByKeybinding);
|
||||
if (localToRemoteByKeybinding.added.size === 0 && localToRemoteByKeybinding.removed.size === 0 && localToRemoteByKeybinding.updated.size === 0) {
|
||||
return { hasLocalForwarded: false, hasRemoteForwarded: false, added: empty, removed: empty, updated: empty, conflicts: empty };
|
||||
}
|
||||
|
||||
const baseToLocalByKeybinding = baseByKeybinding ? compareByKeybinding(baseByKeybinding, localByKeybinding) : { added: [...localByKeybinding.keys()].reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
if (baseToLocalByKeybinding.added.size === 0 && baseToLocalByKeybinding.removed.size === 0 && baseToLocalByKeybinding.updated.size === 0) {
|
||||
// Remote has moved forward and local has not.
|
||||
return { hasLocalForwarded: false, hasRemoteForwarded: true, added: empty, removed: empty, updated: empty, conflicts: empty };
|
||||
}
|
||||
|
||||
const baseToRemoteByKeybinding = baseByKeybinding ? compareByKeybinding(baseByKeybinding, remoteByKeybinding) : { added: [...remoteByKeybinding.keys()].reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
if (baseToRemoteByKeybinding.added.size === 0 && baseToRemoteByKeybinding.removed.size === 0 && baseToRemoteByKeybinding.updated.size === 0) {
|
||||
return { hasLocalForwarded: true, hasRemoteForwarded: false, added: empty, removed: empty, updated: empty, conflicts: empty };
|
||||
}
|
||||
|
||||
const { added, removed, updated, conflicts } = computeMergeResult(localToRemoteByKeybinding, baseToLocalByKeybinding, baseToRemoteByKeybinding);
|
||||
return { hasLocalForwarded: true, hasRemoteForwarded: true, added, removed, updated, conflicts };
|
||||
}
|
||||
|
||||
function byKeybinding(keybindings: IUserFriendlyKeybinding[], keys: IStringDictionary<string>) {
|
||||
const map: Map<string, IUserFriendlyKeybinding[]> = new Map<string, IUserFriendlyKeybinding[]>();
|
||||
for (const keybinding of keybindings) {
|
||||
const key = keys[keybinding.key];
|
||||
let value = map.get(key);
|
||||
if (!value) {
|
||||
value = [];
|
||||
map.set(key, value);
|
||||
}
|
||||
value.push(keybinding);
|
||||
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function byCommand(keybindings: IUserFriendlyKeybinding[]): Map<string, IUserFriendlyKeybinding[]> {
|
||||
const map: Map<string, IUserFriendlyKeybinding[]> = new Map<string, IUserFriendlyKeybinding[]>();
|
||||
for (const keybinding of keybindings) {
|
||||
const command = keybinding.command[0] === '-' ? keybinding.command.substring(1) : keybinding.command;
|
||||
let value = map.get(command);
|
||||
if (!value) {
|
||||
value = [];
|
||||
map.set(command, value);
|
||||
}
|
||||
value.push(keybinding);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
|
||||
function compareByKeybinding(from: Map<string, IUserFriendlyKeybinding[]>, to: Map<string, IUserFriendlyKeybinding[]>): ICompareResult {
|
||||
const fromKeys = [...from.keys()];
|
||||
const toKeys = [...to.keys()];
|
||||
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const updated: Set<string> = new Set<string>();
|
||||
|
||||
for (const key of fromKeys) {
|
||||
if (removed.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const value1: IUserFriendlyKeybinding[] = from.get(key)!.map(keybinding => ({ ...keybinding, ...{ key } }));
|
||||
const value2: IUserFriendlyKeybinding[] = to.get(key)!.map(keybinding => ({ ...keybinding, ...{ key } }));
|
||||
if (!equals(value1, value2, (a, b) => isSameKeybinding(a, b))) {
|
||||
updated.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return { added, removed, updated };
|
||||
}
|
||||
|
||||
function compareByCommand(from: Map<string, IUserFriendlyKeybinding[]>, to: Map<string, IUserFriendlyKeybinding[]>, normalizedKeys: IStringDictionary<string>): ICompareResult {
|
||||
const fromKeys = [...from.keys()];
|
||||
const toKeys = [...to.keys()];
|
||||
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const updated: Set<string> = new Set<string>();
|
||||
|
||||
for (const key of fromKeys) {
|
||||
if (removed.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const value1: IUserFriendlyKeybinding[] = from.get(key)!.map(keybinding => ({ ...keybinding, ...{ key: normalizedKeys[keybinding.key] } }));
|
||||
const value2: IUserFriendlyKeybinding[] = to.get(key)!.map(keybinding => ({ ...keybinding, ...{ key: normalizedKeys[keybinding.key] } }));
|
||||
if (!areSameKeybindingsWithSameCommand(value1, value2)) {
|
||||
updated.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return { added, removed, updated };
|
||||
}
|
||||
|
||||
function areSameKeybindingsWithSameCommand(value1: IUserFriendlyKeybinding[], value2: IUserFriendlyKeybinding[]): boolean {
|
||||
// Compare entries adding keybindings
|
||||
if (!equals(value1.filter(({ command }) => command[0] !== '-'), value2.filter(({ command }) => command[0] !== '-'), (a, b) => isSameKeybinding(a, b))) {
|
||||
return false;
|
||||
}
|
||||
// Compare entries removing keybindings
|
||||
if (!equals(value1.filter(({ command }) => command[0] === '-'), value2.filter(({ command }) => command[0] === '-'), (a, b) => isSameKeybinding(a, b))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isSameKeybinding(a: IUserFriendlyKeybinding, b: IUserFriendlyKeybinding): boolean {
|
||||
if (a.command !== b.command) {
|
||||
return false;
|
||||
}
|
||||
if (a.key !== b.key) {
|
||||
return false;
|
||||
}
|
||||
const whenA = ContextKeyExpr.deserialize(a.when);
|
||||
const whenB = ContextKeyExpr.deserialize(b.when);
|
||||
if ((whenA && !whenB) || (!whenA && whenB)) {
|
||||
return false;
|
||||
}
|
||||
if (whenA && whenB && !whenA.equals(whenB)) {
|
||||
return false;
|
||||
}
|
||||
if (!objects.equals(a.args, b.args)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function addKeybindings(content: string, keybindings: IUserFriendlyKeybinding[], formattingOptions: FormattingOptions): string {
|
||||
for (const keybinding of keybindings) {
|
||||
content = contentUtil.edit(content, [-1], keybinding, formattingOptions);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
function removeKeybindings(content: string, command: string, formattingOptions: FormattingOptions): string {
|
||||
const keybindings = parseKeybindings(content);
|
||||
for (let index = keybindings.length - 1; index >= 0; index--) {
|
||||
if (keybindings[index].command === command || keybindings[index].command === `-${command}`) {
|
||||
content = contentUtil.edit(content, [index], undefined, formattingOptions);
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
function updateKeybindings(content: string, command: string, keybindings: IUserFriendlyKeybinding[], formattingOptions: FormattingOptions): string {
|
||||
const allKeybindings = parseKeybindings(content);
|
||||
const location = allKeybindings.findIndex(keybinding => keybinding.command === command || keybinding.command === `-${command}`);
|
||||
// Remove all entries with this command
|
||||
for (let index = allKeybindings.length - 1; index >= 0; index--) {
|
||||
if (allKeybindings[index].command === command || allKeybindings[index].command === `-${command}`) {
|
||||
content = contentUtil.edit(content, [index], undefined, formattingOptions);
|
||||
}
|
||||
}
|
||||
// add all entries at the same location where the entry with this command was located.
|
||||
for (let index = keybindings.length - 1; index >= 0; index--) {
|
||||
content = contentUtil.edit(content, [location], keybindings[index], formattingOptions);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import {
|
||||
UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncResource,
|
||||
IUserDataSynchroniser, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, ISyncResourceHandle,
|
||||
IRemoteUserData, Change
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge';
|
||||
import { parse } from 'vs/base/common/json';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { OS, OperatingSystem } from 'vs/base/common/platform';
|
||||
import { isUndefined } from 'vs/base/common/types';
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { AbstractInitializer, AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
interface ISyncContent {
|
||||
mac?: string;
|
||||
linux?: string;
|
||||
windows?: string;
|
||||
all?: string;
|
||||
}
|
||||
|
||||
interface IKeybindingsResourcePreview extends IFileResourcePreview {
|
||||
previewResult: IMergeResult;
|
||||
}
|
||||
|
||||
export function getKeybindingsContentFromSyncContent(syncContent: string, platformSpecific: boolean): string | null {
|
||||
const parsed = <ISyncContent>JSON.parse(syncContent);
|
||||
if (!platformSpecific) {
|
||||
return isUndefined(parsed.all) ? null : parsed.all;
|
||||
}
|
||||
switch (OS) {
|
||||
case OperatingSystem.Macintosh:
|
||||
return isUndefined(parsed.mac) ? null : parsed.mac;
|
||||
case OperatingSystem.Linux:
|
||||
return isUndefined(parsed.linux) ? null : parsed.linux;
|
||||
case OperatingSystem.Windows:
|
||||
return isUndefined(parsed.windows) ? null : parsed.windows;
|
||||
}
|
||||
}
|
||||
|
||||
export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
/* Version 2: Change settings from `sync.${setting}` to `settingsSync.{setting}` */
|
||||
protected readonly version: number = 2;
|
||||
private readonly previewResource: URI = this.extUri.joinPath(this.syncPreviewFolder, 'keybindings.json');
|
||||
private readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' });
|
||||
private readonly remoteResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' });
|
||||
private readonly acceptedResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' });
|
||||
|
||||
constructor(
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
) {
|
||||
super(environmentService.keybindingsResource, SyncResource.Keybindings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
|
||||
}
|
||||
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IKeybindingsResourcePreview[]> {
|
||||
const remoteContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
|
||||
const lastSyncContent: string | null = lastSyncUserData && lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null;
|
||||
|
||||
// Get file content last to get the latest
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formattingOptions = await this.getFormattingOptions();
|
||||
|
||||
let mergedContent: string | null = null;
|
||||
let hasLocalChanged: boolean = false;
|
||||
let hasRemoteChanged: boolean = false;
|
||||
let hasConflicts: boolean = false;
|
||||
|
||||
if (remoteContent) {
|
||||
let localContent: string = fileContent ? fileContent.value.toString() : '[]';
|
||||
localContent = localContent || '[]';
|
||||
if (this.hasErrors(localContent)) {
|
||||
throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
|
||||
}
|
||||
|
||||
if (!lastSyncContent // First time sync
|
||||
|| lastSyncContent !== localContent // Local has forwarded
|
||||
|| lastSyncContent !== remoteContent // Remote has forwarded
|
||||
) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote keybindings with local keybindings...`);
|
||||
const result = await merge(localContent, remoteContent, lastSyncContent, formattingOptions, this.userDataSyncUtilService);
|
||||
// Sync only if there are changes
|
||||
if (result.hasChanges) {
|
||||
mergedContent = result.mergeContent;
|
||||
hasConflicts = result.hasConflicts;
|
||||
hasLocalChanged = hasConflicts || result.mergeContent !== localContent;
|
||||
hasRemoteChanged = hasConflicts || result.mergeContent !== remoteContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// First time syncing to remote
|
||||
else if (fileContent) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Remote keybindings does not exist. Synchronizing keybindings for the first time.`);
|
||||
mergedContent = fileContent.value.toString();
|
||||
hasRemoteChanged = true;
|
||||
}
|
||||
|
||||
const previewResult: IMergeResult = {
|
||||
content: mergedContent,
|
||||
localChange: hasLocalChanged ? fileContent ? Change.Modified : Change.Added : Change.None,
|
||||
remoteChange: hasRemoteChanged ? Change.Modified : Change.None,
|
||||
hasConflicts
|
||||
};
|
||||
|
||||
return [{
|
||||
fileContent,
|
||||
localResource: this.localResource,
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
localChange: previewResult.localChange,
|
||||
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
|
||||
previewResource: this.previewResource,
|
||||
previewResult,
|
||||
acceptedResource: this.acceptedResource,
|
||||
}];
|
||||
|
||||
}
|
||||
|
||||
protected async getMergeResult(resourcePreview: IKeybindingsResourcePreview, token: CancellationToken): Promise<IMergeResult> {
|
||||
return resourcePreview.previewResult;
|
||||
}
|
||||
|
||||
protected async getAcceptResult(resourcePreview: IKeybindingsResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult> {
|
||||
|
||||
/* Accept local resource */
|
||||
if (this.extUri.isEqual(resource, this.localResource)) {
|
||||
return {
|
||||
content: resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : null,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Modified,
|
||||
};
|
||||
}
|
||||
|
||||
/* Accept remote resource */
|
||||
if (this.extUri.isEqual(resource, this.remoteResource)) {
|
||||
return {
|
||||
content: resourcePreview.remoteContent,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.None,
|
||||
};
|
||||
}
|
||||
|
||||
/* Accept preview resource */
|
||||
if (this.extUri.isEqual(resource, this.previewResource)) {
|
||||
if (content === undefined) {
|
||||
return {
|
||||
content: resourcePreview.previewResult.content,
|
||||
localChange: resourcePreview.previewResult.localChange,
|
||||
remoteChange: resourcePreview.previewResult.remoteChange,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.Modified,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Invalid Resource: ${resource.toString()}`);
|
||||
}
|
||||
|
||||
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [IKeybindingsResourcePreview, IAcceptResult][], force: boolean): Promise<void> {
|
||||
const { fileContent } = resourcePreviews[0][0];
|
||||
let { content, localChange, remoteChange } = resourcePreviews[0][1];
|
||||
|
||||
if (localChange === Change.None && remoteChange === Change.None) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing keybindings.`);
|
||||
}
|
||||
|
||||
if (content !== null) {
|
||||
content = content.trim();
|
||||
content = content || '[]';
|
||||
if (this.hasErrors(content)) {
|
||||
throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
|
||||
}
|
||||
}
|
||||
|
||||
if (localChange !== Change.None) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating local keybindings...`);
|
||||
if (fileContent) {
|
||||
await this.backupLocal(this.toSyncContent(fileContent.value.toString(), null));
|
||||
}
|
||||
await this.updateLocalFileContent(content || '[]', fileContent, force);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated local keybindings`);
|
||||
}
|
||||
|
||||
if (remoteChange !== Change.None) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote keybindings...`);
|
||||
const remoteContents = this.toSyncContent(content || '[]', remoteUserData.syncData ? remoteUserData.syncData.content : null);
|
||||
remoteUserData = await this.updateRemoteUserData(remoteContents, force ? null : remoteUserData.ref);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote keybindings`);
|
||||
}
|
||||
|
||||
// Delete the preview
|
||||
try {
|
||||
await this.fileService.del(this.previewResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized keybindings...`);
|
||||
const lastSyncContent = content !== null ? this.toSyncContent(content, null) : remoteUserData.syncData?.content;
|
||||
await this.updateLastSyncUserData({
|
||||
ref: remoteUserData.ref,
|
||||
syncData: lastSyncContent ? {
|
||||
version: remoteUserData.syncData ? remoteUserData.syncData.version : this.version,
|
||||
machineId: remoteUserData.syncData!.machineId,
|
||||
content: lastSyncContent
|
||||
} : null
|
||||
});
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized keybindings`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async hasLocalData(): Promise<boolean> {
|
||||
try {
|
||||
const localFileContent = await this.getLocalFileContent();
|
||||
if (localFileContent) {
|
||||
const keybindings = parse(localFileContent.value.toString());
|
||||
if (isNonEmptyArray(keybindings)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if ((<FileOperationError>error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
|
||||
const comparableResource = (await this.fileService.exists(this.file)) ? this.file : this.localResource;
|
||||
return [{ resource: this.extUri.joinPath(uri, 'keybindings.json'), comparableResource }];
|
||||
}
|
||||
|
||||
async resolveContent(uri: URI): Promise<string | null> {
|
||||
if (this.extUri.isEqual(this.remoteResource, uri) || this.extUri.isEqual(this.localResource, uri) || this.extUri.isEqual(this.acceptedResource, uri)) {
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
let content = await super.resolveContent(uri);
|
||||
if (content) {
|
||||
return content;
|
||||
}
|
||||
content = await super.resolveContent(this.extUri.dirname(uri));
|
||||
if (content) {
|
||||
const syncData = this.parseSyncData(content);
|
||||
if (syncData) {
|
||||
switch (this.extUri.basename(uri)) {
|
||||
case 'keybindings.json':
|
||||
return this.getKeybindingsContentFromSyncContent(syncData.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private getKeybindingsContentFromSyncContent(syncContent: string): string | null {
|
||||
try {
|
||||
return getKeybindingsContentFromSyncContent(syncContent, this.syncKeybindingsPerPlatform());
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private toSyncContent(keybindingsContent: string, syncContent: string | null): string {
|
||||
let parsed: ISyncContent = {};
|
||||
try {
|
||||
parsed = JSON.parse(syncContent || '{}');
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
if (!this.syncKeybindingsPerPlatform()) {
|
||||
parsed.all = keybindingsContent;
|
||||
} else {
|
||||
delete parsed.all;
|
||||
}
|
||||
switch (OS) {
|
||||
case OperatingSystem.Macintosh:
|
||||
parsed.mac = keybindingsContent;
|
||||
break;
|
||||
case OperatingSystem.Linux:
|
||||
parsed.linux = keybindingsContent;
|
||||
break;
|
||||
case OperatingSystem.Windows:
|
||||
parsed.windows = keybindingsContent;
|
||||
break;
|
||||
}
|
||||
return JSON.stringify(parsed);
|
||||
}
|
||||
|
||||
private syncKeybindingsPerPlatform(): boolean {
|
||||
let userValue = this.configurationService.inspect<boolean>('settingsSync.keybindingsPerPlatform').userValue;
|
||||
if (userValue !== undefined) {
|
||||
return userValue;
|
||||
}
|
||||
userValue = this.configurationService.inspect<boolean>('sync.keybindingsPerPlatform').userValue;
|
||||
if (userValue !== undefined) {
|
||||
return userValue;
|
||||
}
|
||||
return this.configurationService.getValue<boolean>('settingsSync.keybindingsPerPlatform');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class KeybindingsInitializer extends AbstractInitializer {
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
) {
|
||||
super(SyncResource.Keybindings, environmentService, logService, fileService);
|
||||
}
|
||||
|
||||
async doInitialize(remoteUserData: IRemoteUserData): Promise<void> {
|
||||
const keybindingsContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
|
||||
if (!keybindingsContent) {
|
||||
this.logService.info('Skipping initializing keybindings because remote keybindings does not exist.');
|
||||
return;
|
||||
}
|
||||
|
||||
const isEmpty = await this.isEmpty();
|
||||
if (!isEmpty) {
|
||||
this.logService.info('Skipping initializing keybindings because local keybindings exist.');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.fileService.writeFile(this.environmentService.keybindingsResource, VSBuffer.fromString(keybindingsContent));
|
||||
|
||||
await this.updateLastSyncUserData(remoteUserData);
|
||||
}
|
||||
|
||||
private async isEmpty(): Promise<boolean> {
|
||||
try {
|
||||
const fileContent = await this.fileService.readFile(this.environmentService.settingsResource);
|
||||
const keybindings = parse(fileContent.value.toString());
|
||||
return !isNonEmptyArray(keybindings);
|
||||
} catch (error) {
|
||||
return (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
private getKeybindingsContentFromSyncContent(syncContent: string): string | null {
|
||||
try {
|
||||
return getKeybindingsContentFromSyncContent(syncContent, true);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
643
lib/vscode/src/vs/platform/userDataSync/common/settingsMerge.ts
Normal file
643
lib/vscode/src/vs/platform/userDataSync/common/settingsMerge.ts
Normal file
@@ -0,0 +1,643 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { parse, JSONVisitor, visit } from 'vs/base/common/json';
|
||||
import { setProperty, withFormatting, applyEdits } from 'vs/base/common/jsonEdit';
|
||||
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, getDisallowedIgnoredSettings } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { distinct } from 'vs/base/common/arrays';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
export interface IMergeResult {
|
||||
localContent: string | null;
|
||||
remoteContent: string | null;
|
||||
hasConflicts: boolean;
|
||||
conflictsSettings: IConflictSetting[];
|
||||
}
|
||||
|
||||
export function getIgnoredSettings(defaultIgnoredSettings: string[], configurationService: IConfigurationService, settingsContent?: string): string[] {
|
||||
let value: string[] = [];
|
||||
if (settingsContent) {
|
||||
value = getIgnoredSettingsFromContent(settingsContent);
|
||||
} else {
|
||||
value = getIgnoredSettingsFromConfig(configurationService);
|
||||
}
|
||||
const added: string[] = [], removed: string[] = [...getDisallowedIgnoredSettings()];
|
||||
if (Array.isArray(value)) {
|
||||
for (const key of value) {
|
||||
if (key.startsWith('-')) {
|
||||
removed.push(key.substring(1));
|
||||
} else {
|
||||
added.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return distinct([...defaultIgnoredSettings, ...added,].filter(setting => removed.indexOf(setting) === -1));
|
||||
}
|
||||
|
||||
function getIgnoredSettingsFromConfig(configurationService: IConfigurationService): string[] {
|
||||
let userValue = configurationService.inspect<string[]>('settingsSync.ignoredSettings').userValue;
|
||||
if (userValue !== undefined) {
|
||||
return userValue;
|
||||
}
|
||||
userValue = configurationService.inspect<string[]>('sync.ignoredSettings').userValue;
|
||||
if (userValue !== undefined) {
|
||||
return userValue;
|
||||
}
|
||||
return configurationService.getValue<string[]>('settingsSync.ignoredSettings') || [];
|
||||
}
|
||||
|
||||
function getIgnoredSettingsFromContent(settingsContent: string): string[] {
|
||||
const parsed = parse(settingsContent);
|
||||
return parsed ? parsed['settingsSync.ignoredSettings'] || parsed['sync.ignoredSettings'] || [] : [];
|
||||
}
|
||||
|
||||
export function updateIgnoredSettings(targetContent: string, sourceContent: string, ignoredSettings: string[], formattingOptions: FormattingOptions): string {
|
||||
if (ignoredSettings.length) {
|
||||
const sourceTree = parseSettings(sourceContent);
|
||||
const source = parse(sourceContent);
|
||||
const target = parse(targetContent);
|
||||
const settingsToAdd: INode[] = [];
|
||||
for (const key of ignoredSettings) {
|
||||
const sourceValue = source[key];
|
||||
const targetValue = target[key];
|
||||
|
||||
// Remove in target
|
||||
if (sourceValue === undefined) {
|
||||
targetContent = contentUtil.edit(targetContent, [key], undefined, formattingOptions);
|
||||
}
|
||||
|
||||
// Update in target
|
||||
else if (targetValue !== undefined) {
|
||||
targetContent = contentUtil.edit(targetContent, [key], sourceValue, formattingOptions);
|
||||
}
|
||||
|
||||
else {
|
||||
settingsToAdd.push(findSettingNode(key, sourceTree)!);
|
||||
}
|
||||
}
|
||||
|
||||
settingsToAdd.sort((a, b) => a.startOffset - b.startOffset);
|
||||
settingsToAdd.forEach(s => targetContent = addSetting(s.setting!.key, sourceContent, targetContent, formattingOptions));
|
||||
}
|
||||
return targetContent;
|
||||
}
|
||||
|
||||
export function merge(originalLocalContent: string, originalRemoteContent: string, baseContent: string | null, ignoredSettings: string[], resolvedConflicts: { key: string, value: any | undefined }[], formattingOptions: FormattingOptions): IMergeResult {
|
||||
|
||||
const localContentWithoutIgnoredSettings = updateIgnoredSettings(originalLocalContent, originalRemoteContent, ignoredSettings, formattingOptions);
|
||||
const localForwarded = baseContent !== localContentWithoutIgnoredSettings;
|
||||
const remoteForwarded = baseContent !== originalRemoteContent;
|
||||
|
||||
/* no changes */
|
||||
if (!localForwarded && !remoteForwarded) {
|
||||
return { conflictsSettings: [], localContent: null, remoteContent: null, hasConflicts: false };
|
||||
}
|
||||
|
||||
/* local has changed and remote has not */
|
||||
if (localForwarded && !remoteForwarded) {
|
||||
return { conflictsSettings: [], localContent: null, remoteContent: localContentWithoutIgnoredSettings, hasConflicts: false };
|
||||
}
|
||||
|
||||
/* remote has changed and local has not */
|
||||
if (remoteForwarded && !localForwarded) {
|
||||
return { conflictsSettings: [], localContent: updateIgnoredSettings(originalRemoteContent, originalLocalContent, ignoredSettings, formattingOptions), remoteContent: null, hasConflicts: false };
|
||||
}
|
||||
|
||||
/* local is empty and not synced before */
|
||||
if (baseContent === null && isEmpty(originalLocalContent)) {
|
||||
const localContent = areSame(originalLocalContent, originalRemoteContent, ignoredSettings) ? null : updateIgnoredSettings(originalRemoteContent, originalLocalContent, ignoredSettings, formattingOptions);
|
||||
return { conflictsSettings: [], localContent, remoteContent: null, hasConflicts: false };
|
||||
}
|
||||
|
||||
/* remote and local has changed */
|
||||
let localContent = originalLocalContent;
|
||||
let remoteContent = originalRemoteContent;
|
||||
const local = parse(originalLocalContent);
|
||||
const remote = parse(originalRemoteContent);
|
||||
const base = baseContent ? parse(baseContent) : null;
|
||||
|
||||
const ignored = ignoredSettings.reduce((set, key) => { set.add(key); return set; }, new Set<string>());
|
||||
const localToRemote = compare(local, remote, ignored);
|
||||
const baseToLocal = compare(base, local, ignored);
|
||||
const baseToRemote = compare(base, remote, ignored);
|
||||
|
||||
const conflicts: Map<string, IConflictSetting> = new Map<string, IConflictSetting>();
|
||||
const handledConflicts: Set<string> = new Set<string>();
|
||||
const handleConflict = (conflictKey: string): void => {
|
||||
handledConflicts.add(conflictKey);
|
||||
const resolvedConflict = resolvedConflicts.filter(({ key }) => key === conflictKey)[0];
|
||||
if (resolvedConflict) {
|
||||
localContent = contentUtil.edit(localContent, [conflictKey], resolvedConflict.value, formattingOptions);
|
||||
remoteContent = contentUtil.edit(remoteContent, [conflictKey], resolvedConflict.value, formattingOptions);
|
||||
} else {
|
||||
conflicts.set(conflictKey, { key: conflictKey, localValue: local[conflictKey], remoteValue: remote[conflictKey] });
|
||||
}
|
||||
};
|
||||
|
||||
// Removed settings in Local
|
||||
for (const key of baseToLocal.removed.values()) {
|
||||
// Conflict - Got updated in remote.
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
}
|
||||
// Also remove in remote
|
||||
else {
|
||||
remoteContent = contentUtil.edit(remoteContent, [key], undefined, formattingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
// Removed settings in Remote
|
||||
for (const key of baseToRemote.removed.values()) {
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Conflict - Got updated in local
|
||||
if (baseToLocal.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
}
|
||||
// Also remove in locals
|
||||
else {
|
||||
localContent = contentUtil.edit(localContent, [key], undefined, formattingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
// Updated settings in Local
|
||||
for (const key of baseToLocal.updated.values()) {
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got updated in remote
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
}
|
||||
} else {
|
||||
remoteContent = contentUtil.edit(remoteContent, [key], local[key], formattingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
// Updated settings in Remote
|
||||
for (const key of baseToRemote.updated.values()) {
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got updated in local
|
||||
if (baseToLocal.updated.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
}
|
||||
} else {
|
||||
localContent = contentUtil.edit(localContent, [key], remote[key], formattingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
// Added settings in Local
|
||||
for (const key of baseToLocal.added.values()) {
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got added in remote
|
||||
if (baseToRemote.added.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
}
|
||||
} else {
|
||||
remoteContent = addSetting(key, localContent, remoteContent, formattingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
// Added settings in remote
|
||||
for (const key of baseToRemote.added.values()) {
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got added in local
|
||||
if (baseToLocal.added.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
}
|
||||
} else {
|
||||
localContent = addSetting(key, remoteContent, localContent, formattingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
const hasConflicts = conflicts.size > 0 || !areSame(localContent, remoteContent, ignoredSettings);
|
||||
const hasLocalChanged = hasConflicts || !areSame(localContent, originalLocalContent, []);
|
||||
const hasRemoteChanged = hasConflicts || !areSame(remoteContent, originalRemoteContent, []);
|
||||
return { localContent: hasLocalChanged ? localContent : null, remoteContent: hasRemoteChanged ? remoteContent : null, conflictsSettings: [...conflicts.values()], hasConflicts };
|
||||
}
|
||||
|
||||
export function areSame(localContent: string, remoteContent: string, ignoredSettings: string[]): boolean {
|
||||
if (localContent === remoteContent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const local = parse(localContent);
|
||||
const remote = parse(remoteContent);
|
||||
const ignored = ignoredSettings.reduce((set, key) => { set.add(key); return set; }, new Set<string>());
|
||||
const localTree = parseSettings(localContent).filter(node => !(node.setting && ignored.has(node.setting.key)));
|
||||
const remoteTree = parseSettings(remoteContent).filter(node => !(node.setting && ignored.has(node.setting.key)));
|
||||
|
||||
if (localTree.length !== remoteTree.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let index = 0; index < localTree.length; index++) {
|
||||
const localNode = localTree[index];
|
||||
const remoteNode = remoteTree[index];
|
||||
if (localNode.setting && remoteNode.setting) {
|
||||
if (localNode.setting.key !== remoteNode.setting.key) {
|
||||
return false;
|
||||
}
|
||||
if (!objects.equals(local[localNode.setting.key], remote[localNode.setting.key])) {
|
||||
return false;
|
||||
}
|
||||
} else if (!localNode.setting && !remoteNode.setting) {
|
||||
if (localNode.value !== remoteNode.value) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isEmpty(content: string): boolean {
|
||||
if (content) {
|
||||
const nodes = parseSettings(content);
|
||||
return nodes.length === 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function compare(from: IStringDictionary<any> | null, to: IStringDictionary<any>, ignored: Set<string>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
|
||||
const fromKeys = from ? Object.keys(from).filter(key => !ignored.has(key)) : [];
|
||||
const toKeys = Object.keys(to).filter(key => !ignored.has(key));
|
||||
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const updated: Set<string> = new Set<string>();
|
||||
|
||||
if (from) {
|
||||
for (const key of fromKeys) {
|
||||
if (removed.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const value1 = from[key];
|
||||
const value2 = to[key];
|
||||
if (!objects.equals(value1, value2)) {
|
||||
updated.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { added, removed, updated };
|
||||
}
|
||||
|
||||
export function addSetting(key: string, sourceContent: string, targetContent: string, formattingOptions: FormattingOptions): string {
|
||||
const source = parse(sourceContent);
|
||||
const sourceTree = parseSettings(sourceContent);
|
||||
const targetTree = parseSettings(targetContent);
|
||||
const insertLocation = getInsertLocation(key, sourceTree, targetTree);
|
||||
return insertAtLocation(targetContent, key, source[key], insertLocation, targetTree, formattingOptions);
|
||||
}
|
||||
|
||||
interface InsertLocation {
|
||||
index: number,
|
||||
insertAfter: boolean;
|
||||
}
|
||||
|
||||
function getInsertLocation(key: string, sourceTree: INode[], targetTree: INode[]): InsertLocation {
|
||||
|
||||
const sourceNodeIndex = sourceTree.findIndex(node => node.setting?.key === key);
|
||||
|
||||
const sourcePreviousNode: INode = sourceTree[sourceNodeIndex - 1];
|
||||
if (sourcePreviousNode) {
|
||||
/*
|
||||
Previous node in source is a setting.
|
||||
Find the same setting in the target.
|
||||
Insert it after that setting
|
||||
*/
|
||||
if (sourcePreviousNode.setting) {
|
||||
const targetPreviousSetting = findSettingNode(sourcePreviousNode.setting.key, targetTree);
|
||||
if (targetPreviousSetting) {
|
||||
/* Insert after target's previous setting */
|
||||
return { index: targetTree.indexOf(targetPreviousSetting), insertAfter: true };
|
||||
}
|
||||
}
|
||||
/* Previous node in source is a comment */
|
||||
else {
|
||||
const sourcePreviousSettingNode = findPreviousSettingNode(sourceNodeIndex, sourceTree);
|
||||
/*
|
||||
Source has a setting defined before the setting to be added.
|
||||
Find the same previous setting in the target.
|
||||
If found, insert before its next setting so that comments are retrieved.
|
||||
Otherwise, insert at the end.
|
||||
*/
|
||||
if (sourcePreviousSettingNode) {
|
||||
const targetPreviousSetting = findSettingNode(sourcePreviousSettingNode.setting!.key, targetTree);
|
||||
if (targetPreviousSetting) {
|
||||
const targetNextSetting = findNextSettingNode(targetTree.indexOf(targetPreviousSetting), targetTree);
|
||||
const sourceCommentNodes = findNodesBetween(sourceTree, sourcePreviousSettingNode, sourceTree[sourceNodeIndex]);
|
||||
if (targetNextSetting) {
|
||||
const targetCommentNodes = findNodesBetween(targetTree, targetPreviousSetting, targetNextSetting);
|
||||
const targetCommentNode = findLastMatchingTargetCommentNode(sourceCommentNodes, targetCommentNodes);
|
||||
if (targetCommentNode) {
|
||||
return { index: targetTree.indexOf(targetCommentNode), insertAfter: true }; /* Insert after comment */
|
||||
} else {
|
||||
return { index: targetTree.indexOf(targetNextSetting), insertAfter: false }; /* Insert before target next setting */
|
||||
}
|
||||
} else {
|
||||
const targetCommentNodes = findNodesBetween(targetTree, targetPreviousSetting, targetTree[targetTree.length - 1]);
|
||||
const targetCommentNode = findLastMatchingTargetCommentNode(sourceCommentNodes, targetCommentNodes);
|
||||
if (targetCommentNode) {
|
||||
return { index: targetTree.indexOf(targetCommentNode), insertAfter: true }; /* Insert after comment */
|
||||
} else {
|
||||
return { index: targetTree.length - 1, insertAfter: true }; /* Insert at the end */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sourceNextNode = sourceTree[sourceNodeIndex + 1];
|
||||
if (sourceNextNode) {
|
||||
/*
|
||||
Next node in source is a setting.
|
||||
Find the same setting in the target.
|
||||
Insert it before that setting
|
||||
*/
|
||||
if (sourceNextNode.setting) {
|
||||
const targetNextSetting = findSettingNode(sourceNextNode.setting.key, targetTree);
|
||||
if (targetNextSetting) {
|
||||
/* Insert before target's next setting */
|
||||
return { index: targetTree.indexOf(targetNextSetting), insertAfter: false };
|
||||
}
|
||||
}
|
||||
/* Next node in source is a comment */
|
||||
else {
|
||||
const sourceNextSettingNode = findNextSettingNode(sourceNodeIndex, sourceTree);
|
||||
/*
|
||||
Source has a setting defined after the setting to be added.
|
||||
Find the same next setting in the target.
|
||||
If found, insert after its previous setting so that comments are retrieved.
|
||||
Otherwise, insert at the beginning.
|
||||
*/
|
||||
if (sourceNextSettingNode) {
|
||||
const targetNextSetting = findSettingNode(sourceNextSettingNode.setting!.key, targetTree);
|
||||
if (targetNextSetting) {
|
||||
const targetPreviousSetting = findPreviousSettingNode(targetTree.indexOf(targetNextSetting), targetTree);
|
||||
const sourceCommentNodes = findNodesBetween(sourceTree, sourceTree[sourceNodeIndex], sourceNextSettingNode);
|
||||
if (targetPreviousSetting) {
|
||||
const targetCommentNodes = findNodesBetween(targetTree, targetPreviousSetting, targetNextSetting);
|
||||
const targetCommentNode = findLastMatchingTargetCommentNode(sourceCommentNodes.reverse(), targetCommentNodes.reverse());
|
||||
if (targetCommentNode) {
|
||||
return { index: targetTree.indexOf(targetCommentNode), insertAfter: false }; /* Insert before comment */
|
||||
} else {
|
||||
return { index: targetTree.indexOf(targetPreviousSetting), insertAfter: true }; /* Insert after target previous setting */
|
||||
}
|
||||
} else {
|
||||
const targetCommentNodes = findNodesBetween(targetTree, targetTree[0], targetNextSetting);
|
||||
const targetCommentNode = findLastMatchingTargetCommentNode(sourceCommentNodes.reverse(), targetCommentNodes.reverse());
|
||||
if (targetCommentNode) {
|
||||
return { index: targetTree.indexOf(targetCommentNode), insertAfter: false }; /* Insert before comment */
|
||||
} else {
|
||||
return { index: 0, insertAfter: false }; /* Insert at the beginning */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Insert at the end */
|
||||
return { index: targetTree.length - 1, insertAfter: true };
|
||||
}
|
||||
|
||||
function insertAtLocation(content: string, key: string, value: any, location: InsertLocation, tree: INode[], formattingOptions: FormattingOptions): string {
|
||||
let edits: Edit[];
|
||||
/* Insert at the end */
|
||||
if (location.index === -1) {
|
||||
edits = setProperty(content, [key], value, formattingOptions);
|
||||
} else {
|
||||
edits = getEditToInsertAtLocation(content, key, value, location, tree, formattingOptions).map(edit => withFormatting(content, edit, formattingOptions)[0]);
|
||||
}
|
||||
return applyEdits(content, edits);
|
||||
}
|
||||
|
||||
function getEditToInsertAtLocation(content: string, key: string, value: any, location: InsertLocation, tree: INode[], formattingOptions: FormattingOptions): Edit[] {
|
||||
const newProperty = `${JSON.stringify(key)}: ${JSON.stringify(value)}`;
|
||||
const eol = getEOL(formattingOptions, content);
|
||||
const node = tree[location.index];
|
||||
|
||||
if (location.insertAfter) {
|
||||
|
||||
const edits: Edit[] = [];
|
||||
|
||||
/* Insert after a setting */
|
||||
if (node.setting) {
|
||||
edits.push({ offset: node.endOffset, length: 0, content: ',' + newProperty });
|
||||
}
|
||||
|
||||
/* Insert after a comment */
|
||||
else {
|
||||
|
||||
const nextSettingNode = findNextSettingNode(location.index, tree);
|
||||
const previousSettingNode = findPreviousSettingNode(location.index, tree);
|
||||
const previousSettingCommaOffset = previousSettingNode?.setting?.commaOffset;
|
||||
|
||||
/* If there is a previous setting and it does not has comma then add it */
|
||||
if (previousSettingNode && previousSettingCommaOffset === undefined) {
|
||||
edits.push({ offset: previousSettingNode.endOffset, length: 0, content: ',' });
|
||||
}
|
||||
|
||||
const isPreviouisSettingIncludesComment = previousSettingCommaOffset !== undefined && previousSettingCommaOffset > node.endOffset;
|
||||
edits.push({
|
||||
offset: isPreviouisSettingIncludesComment ? previousSettingCommaOffset! + 1 : node.endOffset,
|
||||
length: 0,
|
||||
content: nextSettingNode ? eol + newProperty + ',' : eol + newProperty
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return edits;
|
||||
}
|
||||
|
||||
else {
|
||||
|
||||
/* Insert before a setting */
|
||||
if (node.setting) {
|
||||
return [{ offset: node.startOffset, length: 0, content: newProperty + ',' }];
|
||||
}
|
||||
|
||||
/* Insert before a comment */
|
||||
const content = (tree[location.index - 1] && !tree[location.index - 1].setting /* previous node is comment */ ? eol : '')
|
||||
+ newProperty
|
||||
+ (findNextSettingNode(location.index, tree) ? ',' : '')
|
||||
+ eol;
|
||||
return [{ offset: node.startOffset, length: 0, content }];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function findSettingNode(key: string, tree: INode[]): INode | undefined {
|
||||
return tree.filter(node => node.setting?.key === key)[0];
|
||||
}
|
||||
|
||||
function findPreviousSettingNode(index: number, tree: INode[]): INode | undefined {
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
if (tree[i].setting) {
|
||||
return tree[i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findNextSettingNode(index: number, tree: INode[]): INode | undefined {
|
||||
for (let i = index + 1; i < tree.length; i++) {
|
||||
if (tree[i].setting) {
|
||||
return tree[i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findNodesBetween(nodes: INode[], from: INode, till: INode): INode[] {
|
||||
const fromIndex = nodes.indexOf(from);
|
||||
const tillIndex = nodes.indexOf(till);
|
||||
return nodes.filter((node, index) => fromIndex < index && index < tillIndex);
|
||||
}
|
||||
|
||||
function findLastMatchingTargetCommentNode(sourceComments: INode[], targetComments: INode[]): INode | undefined {
|
||||
if (sourceComments.length && targetComments.length) {
|
||||
let index = 0;
|
||||
for (; index < targetComments.length && index < sourceComments.length; index++) {
|
||||
if (sourceComments[index].value !== targetComments[index].value) {
|
||||
return targetComments[index - 1];
|
||||
}
|
||||
}
|
||||
return targetComments[index - 1];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
interface INode {
|
||||
readonly startOffset: number;
|
||||
readonly endOffset: number;
|
||||
readonly value: string;
|
||||
readonly setting?: {
|
||||
readonly key: string;
|
||||
readonly commaOffset: number | undefined;
|
||||
};
|
||||
readonly comment?: string;
|
||||
}
|
||||
|
||||
function parseSettings(content: string): INode[] {
|
||||
const nodes: INode[] = [];
|
||||
let hierarchyLevel = -1;
|
||||
let startOffset: number;
|
||||
let key: string;
|
||||
|
||||
const visitor: JSONVisitor = {
|
||||
onObjectBegin: (offset: number) => {
|
||||
hierarchyLevel++;
|
||||
},
|
||||
onObjectProperty: (name: string, offset: number, length: number) => {
|
||||
if (hierarchyLevel === 0) {
|
||||
// this is setting key
|
||||
startOffset = offset;
|
||||
key = name;
|
||||
}
|
||||
},
|
||||
onObjectEnd: (offset: number, length: number) => {
|
||||
hierarchyLevel--;
|
||||
if (hierarchyLevel === 0) {
|
||||
nodes.push({
|
||||
startOffset,
|
||||
endOffset: offset + length,
|
||||
value: content.substring(startOffset, offset + length),
|
||||
setting: {
|
||||
key,
|
||||
commaOffset: undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onArrayBegin: (offset: number, length: number) => {
|
||||
hierarchyLevel++;
|
||||
},
|
||||
onArrayEnd: (offset: number, length: number) => {
|
||||
hierarchyLevel--;
|
||||
if (hierarchyLevel === 0) {
|
||||
nodes.push({
|
||||
startOffset,
|
||||
endOffset: offset + length,
|
||||
value: content.substring(startOffset, offset + length),
|
||||
setting: {
|
||||
key,
|
||||
commaOffset: undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onLiteralValue: (value: any, offset: number, length: number) => {
|
||||
if (hierarchyLevel === 0) {
|
||||
nodes.push({
|
||||
startOffset,
|
||||
endOffset: offset + length,
|
||||
value: content.substring(startOffset, offset + length),
|
||||
setting: {
|
||||
key,
|
||||
commaOffset: undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onSeparator: (sep: string, offset: number, length: number) => {
|
||||
if (hierarchyLevel === 0) {
|
||||
if (sep === ',') {
|
||||
let index = nodes.length - 1;
|
||||
for (; index >= 0; index--) {
|
||||
if (nodes[index].setting) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const node = nodes[index];
|
||||
if (node) {
|
||||
nodes.splice(index, 1, {
|
||||
startOffset: node.startOffset,
|
||||
endOffset: node.endOffset,
|
||||
value: node.value,
|
||||
setting: {
|
||||
key: node.setting!.key,
|
||||
commaOffset: offset
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onComment: (offset: number, length: number) => {
|
||||
if (hierarchyLevel === 0) {
|
||||
nodes.push({
|
||||
startOffset: offset,
|
||||
endOffset: offset + length,
|
||||
value: content.substring(offset, offset + length),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(content, visitor);
|
||||
return nodes;
|
||||
}
|
||||
414
lib/vscode/src/vs/platform/userDataSync/common/settingsSync.ts
Normal file
414
lib/vscode/src/vs/platform/userDataSync/common/settingsSync.ts
Normal file
@@ -0,0 +1,414 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import {
|
||||
UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, CONFIGURATION_SYNC_STORE_KEY,
|
||||
SyncResource, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, ISyncResourceHandle, IUserDataSynchroniser,
|
||||
IRemoteUserData, ISyncData, Change
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { updateIgnoredSettings, merge, getIgnoredSettings, isEmpty } from 'vs/platform/userDataSync/common/settingsMerge';
|
||||
import { edit } from 'vs/platform/userDataSync/common/content';
|
||||
import { AbstractInitializer, AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { Edit } from 'vs/base/common/jsonFormatter';
|
||||
import { setProperty, applyEdits } from 'vs/base/common/jsonEdit';
|
||||
|
||||
interface ISettingsResourcePreview extends IFileResourcePreview {
|
||||
previewResult: IMergeResult;
|
||||
}
|
||||
|
||||
export interface ISettingsSyncContent {
|
||||
settings: string;
|
||||
}
|
||||
|
||||
function isSettingsSyncContent(thing: any): thing is ISettingsSyncContent {
|
||||
return thing
|
||||
&& (thing.settings && typeof thing.settings === 'string')
|
||||
&& Object.keys(thing).length === 1;
|
||||
}
|
||||
|
||||
export function parseSettingsSyncContent(syncContent: string): ISettingsSyncContent {
|
||||
const parsed = <ISettingsSyncContent>JSON.parse(syncContent);
|
||||
return isSettingsSyncContent(parsed) ? parsed : /* migrate */ { settings: syncContent };
|
||||
}
|
||||
|
||||
export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
/* Version 2: Change settings from `sync.${setting}` to `settingsSync.{setting}` */
|
||||
protected readonly version: number = 2;
|
||||
readonly previewResource: URI = this.extUri.joinPath(this.syncPreviewFolder, 'settings.json');
|
||||
readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' });
|
||||
readonly remoteResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' });
|
||||
readonly acceptedResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' });
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
@IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
|
||||
) {
|
||||
super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
|
||||
}
|
||||
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISettingsResourcePreview[]> {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formattingOptions = await this.getFormattingOptions();
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
const lastSettingsSyncContent: ISettingsSyncContent | null = lastSyncUserData ? this.getSettingsSyncContent(lastSyncUserData) : null;
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
|
||||
let mergedContent: string | null = null;
|
||||
let hasLocalChanged: boolean = false;
|
||||
let hasRemoteChanged: boolean = false;
|
||||
let hasConflicts: boolean = false;
|
||||
|
||||
if (remoteSettingsSyncContent) {
|
||||
let localContent: string = fileContent ? fileContent.value.toString().trim() : '{}';
|
||||
localContent = localContent || '{}';
|
||||
this.validateContent(localContent);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote settings with local settings...`);
|
||||
const result = merge(localContent, remoteSettingsSyncContent.settings, lastSettingsSyncContent ? lastSettingsSyncContent.settings : null, ignoredSettings, [], formattingOptions);
|
||||
mergedContent = result.localContent || result.remoteContent;
|
||||
hasLocalChanged = result.localContent !== null;
|
||||
hasRemoteChanged = result.remoteContent !== null;
|
||||
hasConflicts = result.hasConflicts;
|
||||
}
|
||||
|
||||
// First time syncing to remote
|
||||
else if (fileContent) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Remote settings does not exist. Synchronizing settings for the first time.`);
|
||||
mergedContent = fileContent.value.toString();
|
||||
hasRemoteChanged = true;
|
||||
}
|
||||
|
||||
const previewResult = {
|
||||
content: mergedContent,
|
||||
localChange: hasLocalChanged ? Change.Modified : Change.None,
|
||||
remoteChange: hasRemoteChanged ? Change.Modified : Change.None,
|
||||
hasConflicts
|
||||
};
|
||||
|
||||
return [{
|
||||
fileContent,
|
||||
localResource: this.localResource,
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
localChange: previewResult.localChange,
|
||||
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
|
||||
previewResource: this.previewResource,
|
||||
previewResult,
|
||||
acceptedResource: this.acceptedResource,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async getMergeResult(resourcePreview: ISettingsResourcePreview, token: CancellationToken): Promise<IMergeResult> {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
return {
|
||||
...resourcePreview.previewResult,
|
||||
|
||||
// remove ignored settings from the preview content
|
||||
content: resourcePreview.previewResult.content ? updateIgnoredSettings(resourcePreview.previewResult.content, '{}', ignoredSettings, formatUtils) : null
|
||||
};
|
||||
}
|
||||
|
||||
protected async getAcceptResult(resourcePreview: ISettingsResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult> {
|
||||
|
||||
const formattingOptions = await this.getFormattingOptions();
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
|
||||
/* Accept local resource */
|
||||
if (this.extUri.isEqual(resource, this.localResource)) {
|
||||
return {
|
||||
/* Remove ignored settings */
|
||||
content: resourcePreview.fileContent ? updateIgnoredSettings(resourcePreview.fileContent.value.toString(), '{}', ignoredSettings, formattingOptions) : null,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Modified,
|
||||
};
|
||||
}
|
||||
|
||||
/* Accept remote resource */
|
||||
if (this.extUri.isEqual(resource, this.remoteResource)) {
|
||||
return {
|
||||
/* Update ignored settings from local file content */
|
||||
content: resourcePreview.remoteContent !== null ? updateIgnoredSettings(resourcePreview.remoteContent, resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : '{}', ignoredSettings, formattingOptions) : null,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.None,
|
||||
};
|
||||
}
|
||||
|
||||
/* Accept preview resource */
|
||||
if (this.extUri.isEqual(resource, this.previewResource)) {
|
||||
if (content === undefined) {
|
||||
return {
|
||||
content: resourcePreview.previewResult.content,
|
||||
localChange: resourcePreview.previewResult.localChange,
|
||||
remoteChange: resourcePreview.previewResult.remoteChange,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
/* Add ignored settings from local file content */
|
||||
content: content !== null ? updateIgnoredSettings(content, resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : '{}', ignoredSettings, formattingOptions) : null,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.Modified,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Invalid Resource: ${resource.toString()}`);
|
||||
}
|
||||
|
||||
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [ISettingsResourcePreview, IAcceptResult][], force: boolean): Promise<void> {
|
||||
const { fileContent } = resourcePreviews[0][0];
|
||||
let { content, localChange, remoteChange } = resourcePreviews[0][1];
|
||||
|
||||
if (localChange === Change.None && remoteChange === Change.None) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing settings.`);
|
||||
}
|
||||
|
||||
content = content ? content.trim() : '{}';
|
||||
content = content || '{}';
|
||||
this.validateContent(content);
|
||||
|
||||
if (localChange !== Change.None) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating local settings...`);
|
||||
if (fileContent) {
|
||||
await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(fileContent.value.toString())));
|
||||
}
|
||||
await this.updateLocalFileContent(content, fileContent, force);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated local settings`);
|
||||
}
|
||||
|
||||
if (remoteChange !== Change.None) {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// Update ignored settings from remote
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
const ignoredSettings = await this.getIgnoredSettings(content);
|
||||
content = updateIgnoredSettings(content, remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : '{}', ignoredSettings, formatUtils);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote settings...`);
|
||||
remoteUserData = await this.updateRemoteUserData(JSON.stringify(this.toSettingsSyncContent(content)), force ? null : remoteUserData.ref);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote settings`);
|
||||
}
|
||||
|
||||
// Delete the preview
|
||||
try {
|
||||
await this.fileService.del(this.previewResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized settings...`);
|
||||
await this.updateLastSyncUserData(remoteUserData);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized settings`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async hasLocalData(): Promise<boolean> {
|
||||
try {
|
||||
const localFileContent = await this.getLocalFileContent();
|
||||
if (localFileContent) {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
const content = edit(localFileContent.value.toString(), [CONFIGURATION_SYNC_STORE_KEY], undefined, formatUtils);
|
||||
return !isEmpty(content);
|
||||
}
|
||||
} catch (error) {
|
||||
if ((<FileOperationError>error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
|
||||
const comparableResource = (await this.fileService.exists(this.file)) ? this.file : this.localResource;
|
||||
return [{ resource: this.extUri.joinPath(uri, 'settings.json'), comparableResource }];
|
||||
}
|
||||
|
||||
async resolveContent(uri: URI): Promise<string | null> {
|
||||
if (this.extUri.isEqual(this.remoteResource, uri) || this.extUri.isEqual(this.localResource, uri) || this.extUri.isEqual(this.acceptedResource, uri)) {
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
let content = await super.resolveContent(uri);
|
||||
if (content) {
|
||||
return content;
|
||||
}
|
||||
content = await super.resolveContent(this.extUri.dirname(uri));
|
||||
if (content) {
|
||||
const syncData = this.parseSyncData(content);
|
||||
if (syncData) {
|
||||
const settingsSyncContent = this.parseSettingsSyncContent(syncData.content);
|
||||
if (settingsSyncContent) {
|
||||
switch (this.extUri.basename(uri)) {
|
||||
case 'settings.json':
|
||||
return settingsSyncContent.settings;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async resolvePreviewContent(resource: URI): Promise<string | null> {
|
||||
let content = await super.resolvePreviewContent(resource);
|
||||
if (content) {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// remove ignored settings from the preview content
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
content = updateIgnoredSettings(content, '{}', ignoredSettings, formatUtils);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
private getSettingsSyncContent(remoteUserData: IRemoteUserData): ISettingsSyncContent | null {
|
||||
return remoteUserData.syncData ? this.parseSettingsSyncContent(remoteUserData.syncData.content) : null;
|
||||
}
|
||||
|
||||
private parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null {
|
||||
try {
|
||||
return parseSettingsSyncContent(syncContent);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private toSettingsSyncContent(settings: string): ISettingsSyncContent {
|
||||
return { settings };
|
||||
}
|
||||
|
||||
private _defaultIgnoredSettings: Promise<string[]> | undefined = undefined;
|
||||
private async getIgnoredSettings(content?: string): Promise<string[]> {
|
||||
if (!this._defaultIgnoredSettings) {
|
||||
this._defaultIgnoredSettings = this.userDataSyncUtilService.resolveDefaultIgnoredSettings();
|
||||
const disposable = Event.any<any>(
|
||||
Event.filter(this.extensionManagementService.onDidInstallExtension, (e => !!e.gallery)),
|
||||
Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error)))(() => {
|
||||
disposable.dispose();
|
||||
this._defaultIgnoredSettings = undefined;
|
||||
});
|
||||
}
|
||||
const defaultIgnoredSettings = await this._defaultIgnoredSettings;
|
||||
return getIgnoredSettings(defaultIgnoredSettings, this.configurationService, content);
|
||||
}
|
||||
|
||||
private validateContent(content: string): void {
|
||||
if (this.hasErrors(content)) {
|
||||
throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync settings as there are errors/warning in settings file."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
|
||||
}
|
||||
}
|
||||
|
||||
async recoverSettings(): Promise<void> {
|
||||
try {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
if (!fileContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const syncData: ISyncData = JSON.parse(fileContent.value.toString());
|
||||
if (!isSyncData(syncData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.telemetryService.publicLog2('sync/settingsCorrupted');
|
||||
const settingsSyncContent = this.parseSettingsSyncContent(syncData.content);
|
||||
if (!settingsSyncContent || !settingsSyncContent.settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
let settings = settingsSyncContent.settings;
|
||||
const formattingOptions = await this.getFormattingOptions();
|
||||
for (const key in syncData) {
|
||||
if (['version', 'content', 'machineId'].indexOf(key) === -1 && (syncData as any)[key] !== undefined) {
|
||||
const edits: Edit[] = setProperty(settings, [key], (syncData as any)[key], formattingOptions);
|
||||
if (edits.length) {
|
||||
settings = applyEdits(settings, edits);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.fileService.writeFile(this.file, VSBuffer.fromString(settings));
|
||||
} catch (e) {/* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
export class SettingsInitializer extends AbstractInitializer {
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
) {
|
||||
super(SyncResource.Settings, environmentService, logService, fileService);
|
||||
}
|
||||
|
||||
async doInitialize(remoteUserData: IRemoteUserData): Promise<void> {
|
||||
const settingsSyncContent = remoteUserData.syncData ? this.parseSettingsSyncContent(remoteUserData.syncData.content) : null;
|
||||
if (!settingsSyncContent) {
|
||||
this.logService.info('Skipping initializing settings because remote settings does not exist.');
|
||||
return;
|
||||
}
|
||||
|
||||
const isEmpty = await this.isEmpty();
|
||||
if (!isEmpty) {
|
||||
this.logService.info('Skipping initializing settings because local settings exist.');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.fileService.writeFile(this.environmentService.settingsResource, VSBuffer.fromString(settingsSyncContent.settings));
|
||||
|
||||
await this.updateLastSyncUserData(remoteUserData);
|
||||
}
|
||||
|
||||
private async isEmpty(): Promise<boolean> {
|
||||
try {
|
||||
const fileContent = await this.fileService.readFile(this.environmentService.settingsResource);
|
||||
return isEmpty(fileContent.value.toString().trim());
|
||||
} catch (error) {
|
||||
return (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
private parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null {
|
||||
try {
|
||||
return parseSettingsSyncContent(syncContent);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function isSyncData(thing: any): thing is ISyncData {
|
||||
if (thing
|
||||
&& (thing.version !== undefined && typeof thing.version === 'number')
|
||||
&& (thing.content !== undefined && typeof thing.content === 'string')
|
||||
&& (thing.machineId !== undefined && typeof thing.machineId === 'string')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
177
lib/vscode/src/vs/platform/userDataSync/common/snippetsMerge.ts
Normal file
177
lib/vscode/src/vs/platform/userDataSync/common/snippetsMerge.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
|
||||
export interface IMergeResult {
|
||||
local: {
|
||||
added: IStringDictionary<string>;
|
||||
updated: IStringDictionary<string>;
|
||||
removed: string[];
|
||||
};
|
||||
remote: {
|
||||
added: IStringDictionary<string>;
|
||||
updated: IStringDictionary<string>;
|
||||
removed: string[];
|
||||
};
|
||||
conflicts: string[];
|
||||
}
|
||||
|
||||
export function merge(local: IStringDictionary<string>, remote: IStringDictionary<string> | null, base: IStringDictionary<string> | null): IMergeResult {
|
||||
const localAdded: IStringDictionary<string> = {};
|
||||
const localUpdated: IStringDictionary<string> = {};
|
||||
const localRemoved: Set<string> = new Set<string>();
|
||||
|
||||
if (!remote) {
|
||||
return {
|
||||
local: { added: localAdded, updated: localUpdated, removed: [...localRemoved.values()] },
|
||||
remote: { added: local, updated: {}, removed: [] },
|
||||
conflicts: []
|
||||
};
|
||||
}
|
||||
|
||||
const localToRemote = compare(local, remote);
|
||||
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
|
||||
// No changes found between local and remote.
|
||||
return {
|
||||
local: { added: localAdded, updated: localUpdated, removed: [...localRemoved.values()] },
|
||||
remote: { added: {}, updated: {}, removed: [] },
|
||||
conflicts: []
|
||||
};
|
||||
}
|
||||
|
||||
const baseToLocal = compare(base, local);
|
||||
const baseToRemote = compare(base, remote);
|
||||
|
||||
const remoteAdded: IStringDictionary<string> = {};
|
||||
const remoteUpdated: IStringDictionary<string> = {};
|
||||
const remoteRemoved: Set<string> = new Set<string>();
|
||||
|
||||
const conflicts: Set<string> = new Set<string>();
|
||||
|
||||
// Removed snippets in Local
|
||||
for (const key of baseToLocal.removed.values()) {
|
||||
// Conflict - Got updated in remote.
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
// Add to local
|
||||
localAdded[key] = remote[key];
|
||||
}
|
||||
// Remove it in remote
|
||||
else {
|
||||
remoteRemoved.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Removed snippets in Remote
|
||||
for (const key of baseToRemote.removed.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Conflict - Got updated in local
|
||||
if (baseToLocal.updated.has(key)) {
|
||||
conflicts.add(key);
|
||||
}
|
||||
// Also remove in Local
|
||||
else {
|
||||
localRemoved.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Updated snippets in Local
|
||||
for (const key of baseToLocal.updated.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got updated in remote
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
conflicts.add(key);
|
||||
}
|
||||
} else {
|
||||
remoteUpdated[key] = local[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Updated snippets in Remote
|
||||
for (const key of baseToRemote.updated.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got updated in local
|
||||
if (baseToLocal.updated.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
conflicts.add(key);
|
||||
}
|
||||
} else if (local[key] !== undefined) {
|
||||
localUpdated[key] = remote[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Added snippets in Local
|
||||
for (const key of baseToLocal.added.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got added in remote
|
||||
if (baseToRemote.added.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
conflicts.add(key);
|
||||
}
|
||||
} else {
|
||||
remoteAdded[key] = local[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Added snippets in remote
|
||||
for (const key of baseToRemote.added.values()) {
|
||||
if (conflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got added in local
|
||||
if (baseToLocal.added.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
conflicts.add(key);
|
||||
}
|
||||
} else {
|
||||
localAdded[key] = remote[key];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
local: { added: localAdded, removed: [...localRemoved.values()], updated: localUpdated },
|
||||
remote: { added: remoteAdded, removed: [...remoteRemoved.values()], updated: remoteUpdated },
|
||||
conflicts: [...conflicts.values()],
|
||||
};
|
||||
}
|
||||
|
||||
function compare(from: IStringDictionary<string> | null, to: IStringDictionary<string> | null): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
|
||||
const fromKeys = from ? Object.keys(from) : [];
|
||||
const toKeys = to ? Object.keys(to) : [];
|
||||
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
|
||||
const updated: Set<string> = new Set<string>();
|
||||
|
||||
for (const key of fromKeys) {
|
||||
if (removed.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const fromSnippet = from![key]!;
|
||||
const toSnippet = to![key]!;
|
||||
if (fromSnippet !== toSnippet) {
|
||||
updated.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return { added, removed, updated };
|
||||
}
|
||||
|
||||
export function areSame(a: IStringDictionary<string>, b: IStringDictionary<string>): boolean {
|
||||
const { added, removed, updated } = compare(a, b);
|
||||
return added.size === 0 && removed.size === 0 && updated.size === 0;
|
||||
}
|
||||
540
lib/vscode/src/vs/platform/userDataSync/common/snippetsSync.ts
Normal file
540
lib/vscode/src/vs/platform/userDataSync/common/snippetsSync.ts
Normal file
@@ -0,0 +1,540 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import {
|
||||
IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService,
|
||||
USER_DATA_SYNC_SCHEME, ISyncResourceHandle, IRemoteUserData, ISyncData, Change
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IFileService, IFileStat, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { AbstractInitializer, AbstractSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { merge, IMergeResult as ISnippetsMergeResult, areSame } from 'vs/platform/userDataSync/common/snippetsMerge';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { deepClone } from 'vs/base/common/objects';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
|
||||
interface ISnippetsResourcePreview extends IFileResourcePreview {
|
||||
previewResult: IMergeResult;
|
||||
}
|
||||
|
||||
interface ISnippetsAcceptedResourcePreview extends IFileResourcePreview {
|
||||
acceptResult: IAcceptResult;
|
||||
}
|
||||
|
||||
export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
protected readonly version: number = 1;
|
||||
private readonly snippetsFolder: URI;
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
) {
|
||||
super(SyncResource.Snippets, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, configurationService);
|
||||
this.snippetsFolder = environmentService.snippetsHome;
|
||||
this._register(this.fileService.watch(environmentService.userRoamingDataHome));
|
||||
this._register(this.fileService.watch(this.snippetsFolder));
|
||||
this._register(Event.filter(this.fileService.onDidFilesChange, e => e.affects(this.snippetsFolder))(() => this.triggerLocalChange()));
|
||||
}
|
||||
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISnippetsResourcePreview[]> {
|
||||
const local = await this.getSnippetsFileContents();
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const remoteSnippets: IStringDictionary<string> | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null;
|
||||
const lastSyncSnippets: IStringDictionary<string> | null = lastSyncUserData && lastSyncUserData.syncData ? this.parseSnippets(lastSyncUserData.syncData) : null;
|
||||
|
||||
if (remoteSnippets) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote snippets with local snippets...`);
|
||||
} else {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Remote snippets does not exist. Synchronizing snippets for the first time.`);
|
||||
}
|
||||
|
||||
const mergeResult = merge(localSnippets, remoteSnippets, lastSyncSnippets);
|
||||
return this.getResourcePreviews(mergeResult, local, remoteSnippets || {});
|
||||
}
|
||||
|
||||
protected async getMergeResult(resourcePreview: ISnippetsResourcePreview, token: CancellationToken): Promise<IMergeResult> {
|
||||
return resourcePreview.previewResult;
|
||||
}
|
||||
|
||||
protected async getAcceptResult(resourcePreview: ISnippetsResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult> {
|
||||
|
||||
/* Accept local resource */
|
||||
if (this.extUri.isEqualOrParent(resource, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }))) {
|
||||
return {
|
||||
content: resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : null,
|
||||
localChange: Change.None,
|
||||
remoteChange: resourcePreview.fileContent
|
||||
? resourcePreview.remoteContent !== null ? Change.Modified : Change.Added
|
||||
: Change.Deleted
|
||||
};
|
||||
}
|
||||
|
||||
/* Accept remote resource */
|
||||
if (this.extUri.isEqualOrParent(resource, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }))) {
|
||||
return {
|
||||
content: resourcePreview.remoteContent,
|
||||
localChange: resourcePreview.remoteContent !== null
|
||||
? resourcePreview.fileContent ? Change.Modified : Change.Added
|
||||
: Change.Deleted,
|
||||
remoteChange: Change.None,
|
||||
};
|
||||
}
|
||||
|
||||
/* Accept preview resource */
|
||||
if (this.extUri.isEqualOrParent(resource, this.syncPreviewFolder)) {
|
||||
if (content === undefined) {
|
||||
return {
|
||||
content: resourcePreview.previewResult.content,
|
||||
localChange: resourcePreview.previewResult.localChange,
|
||||
remoteChange: resourcePreview.previewResult.remoteChange,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content,
|
||||
localChange: content === null
|
||||
? resourcePreview.fileContent !== null ? Change.Deleted : Change.None
|
||||
: Change.Modified,
|
||||
remoteChange: content === null
|
||||
? resourcePreview.remoteContent !== null ? Change.Deleted : Change.None
|
||||
: Change.Modified
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Invalid Resource: ${resource.toString()}`);
|
||||
}
|
||||
|
||||
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [ISnippetsResourcePreview, IAcceptResult][], force: boolean): Promise<void> {
|
||||
const accptedResourcePreviews: ISnippetsAcceptedResourcePreview[] = resourcePreviews.map(([resourcePreview, acceptResult]) => ({ ...resourcePreview, acceptResult }));
|
||||
if (accptedResourcePreviews.every(({ localChange, remoteChange }) => localChange === Change.None && remoteChange === Change.None)) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing snippets.`);
|
||||
}
|
||||
|
||||
if (accptedResourcePreviews.some(({ localChange }) => localChange !== Change.None)) {
|
||||
// back up all snippets
|
||||
await this.updateLocalBackup(accptedResourcePreviews);
|
||||
await this.updateLocalSnippets(accptedResourcePreviews, force);
|
||||
}
|
||||
|
||||
if (accptedResourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None)) {
|
||||
remoteUserData = await this.updateRemoteSnippets(accptedResourcePreviews, remoteUserData, force);
|
||||
}
|
||||
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref) {
|
||||
// update last sync
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized snippets...`);
|
||||
await this.updateLastSyncUserData(remoteUserData);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized snippets`);
|
||||
}
|
||||
|
||||
for (const { previewResource } of accptedResourcePreviews) {
|
||||
// Delete the preview
|
||||
try {
|
||||
await this.fileService.del(previewResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private getResourcePreviews(snippetsMergeResult: ISnippetsMergeResult, localFileContent: IStringDictionary<IFileContent>, remoteSnippets: IStringDictionary<string>): ISnippetsResourcePreview[] {
|
||||
const resourcePreviews: Map<string, ISnippetsResourcePreview> = new Map<string, ISnippetsResourcePreview>();
|
||||
|
||||
/* Snippets added remotely -> add locally */
|
||||
for (const key of Object.keys(snippetsMergeResult.local.added)) {
|
||||
const previewResult: IMergeResult = {
|
||||
content: snippetsMergeResult.local.added[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.Added,
|
||||
remoteChange: Change.None,
|
||||
};
|
||||
resourcePreviews.set(key, {
|
||||
fileContent: null,
|
||||
localResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
|
||||
localContent: null,
|
||||
remoteResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
|
||||
remoteContent: remoteSnippets[key],
|
||||
previewResource: this.extUri.joinPath(this.syncPreviewFolder, key),
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets updated remotely -> update locally */
|
||||
for (const key of Object.keys(snippetsMergeResult.local.updated)) {
|
||||
const previewResult: IMergeResult = {
|
||||
content: snippetsMergeResult.local.updated[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.None,
|
||||
};
|
||||
resourcePreviews.set(key, {
|
||||
localResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
|
||||
fileContent: localFileContent[key],
|
||||
localContent: localFileContent[key].value.toString(),
|
||||
remoteResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
|
||||
remoteContent: remoteSnippets[key],
|
||||
previewResource: this.extUri.joinPath(this.syncPreviewFolder, key),
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets removed remotely -> remove locally */
|
||||
for (const key of snippetsMergeResult.local.removed) {
|
||||
const previewResult: IMergeResult = {
|
||||
content: null,
|
||||
hasConflicts: false,
|
||||
localChange: Change.Deleted,
|
||||
remoteChange: Change.None,
|
||||
};
|
||||
resourcePreviews.set(key, {
|
||||
localResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
|
||||
fileContent: localFileContent[key],
|
||||
localContent: localFileContent[key].value.toString(),
|
||||
remoteResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
|
||||
remoteContent: null,
|
||||
previewResource: this.extUri.joinPath(this.syncPreviewFolder, key),
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets added locally -> add remotely */
|
||||
for (const key of Object.keys(snippetsMergeResult.remote.added)) {
|
||||
const previewResult: IMergeResult = {
|
||||
content: snippetsMergeResult.remote.added[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Added,
|
||||
};
|
||||
resourcePreviews.set(key, {
|
||||
localResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
|
||||
fileContent: localFileContent[key],
|
||||
localContent: localFileContent[key].value.toString(),
|
||||
remoteResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
|
||||
remoteContent: null,
|
||||
previewResource: this.extUri.joinPath(this.syncPreviewFolder, key),
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets updated locally -> update remotely */
|
||||
for (const key of Object.keys(snippetsMergeResult.remote.updated)) {
|
||||
const previewResult: IMergeResult = {
|
||||
content: snippetsMergeResult.remote.updated[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Modified,
|
||||
};
|
||||
resourcePreviews.set(key, {
|
||||
localResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
|
||||
fileContent: localFileContent[key],
|
||||
localContent: localFileContent[key].value.toString(),
|
||||
remoteResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
|
||||
remoteContent: remoteSnippets[key],
|
||||
previewResource: this.extUri.joinPath(this.syncPreviewFolder, key),
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets removed locally -> remove remotely */
|
||||
for (const key of snippetsMergeResult.remote.removed) {
|
||||
const previewResult: IMergeResult = {
|
||||
content: null,
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Deleted,
|
||||
};
|
||||
resourcePreviews.set(key, {
|
||||
localResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
|
||||
fileContent: null,
|
||||
localContent: null,
|
||||
remoteResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
|
||||
remoteContent: remoteSnippets[key],
|
||||
previewResource: this.extUri.joinPath(this.syncPreviewFolder, key),
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets with conflicts */
|
||||
for (const key of snippetsMergeResult.conflicts) {
|
||||
const previewResult: IMergeResult = {
|
||||
content: localFileContent[key] ? localFileContent[key].value.toString() : null,
|
||||
hasConflicts: true,
|
||||
localChange: localFileContent[key] ? Change.Modified : Change.Added,
|
||||
remoteChange: remoteSnippets[key] ? Change.Modified : Change.Added
|
||||
};
|
||||
resourcePreviews.set(key, {
|
||||
localResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
|
||||
fileContent: localFileContent[key] || null,
|
||||
localContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
|
||||
remoteResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
|
||||
remoteContent: remoteSnippets[key] || null,
|
||||
previewResource: this.extUri.joinPath(this.syncPreviewFolder, key),
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
|
||||
});
|
||||
}
|
||||
|
||||
/* Unmodified Snippets */
|
||||
for (const key of Object.keys(localFileContent)) {
|
||||
if (!resourcePreviews.has(key)) {
|
||||
const previewResult: IMergeResult = {
|
||||
content: localFileContent[key] ? localFileContent[key].value.toString() : null,
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.None
|
||||
};
|
||||
resourcePreviews.set(key, {
|
||||
localResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
|
||||
fileContent: localFileContent[key] || null,
|
||||
localContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
|
||||
remoteResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
|
||||
remoteContent: remoteSnippets[key] || null,
|
||||
previewResource: this.extUri.joinPath(this.syncPreviewFolder, key),
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: this.extUri.joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [...resourcePreviews.values()];
|
||||
}
|
||||
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
|
||||
let content = await super.resolveContent(uri);
|
||||
if (content) {
|
||||
const syncData = this.parseSyncData(content);
|
||||
if (syncData) {
|
||||
const snippets = this.parseSnippets(syncData);
|
||||
const result = [];
|
||||
for (const snippet of Object.keys(snippets)) {
|
||||
const resource = this.extUri.joinPath(uri, snippet);
|
||||
const comparableResource = this.extUri.joinPath(this.snippetsFolder, snippet);
|
||||
const exists = await this.fileService.exists(comparableResource);
|
||||
result.push({ resource, comparableResource: exists ? comparableResource : this.extUri.joinPath(this.syncPreviewFolder, snippet).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }) });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async resolveContent(uri: URI): Promise<string | null> {
|
||||
if (this.extUri.isEqualOrParent(uri, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }))
|
||||
|| this.extUri.isEqualOrParent(uri, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }))
|
||||
|| this.extUri.isEqualOrParent(uri, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }))) {
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
|
||||
let content = await super.resolveContent(uri);
|
||||
if (content) {
|
||||
return content;
|
||||
}
|
||||
|
||||
content = await super.resolveContent(this.extUri.dirname(uri));
|
||||
if (content) {
|
||||
const syncData = this.parseSyncData(content);
|
||||
if (syncData) {
|
||||
const snippets = this.parseSnippets(syncData);
|
||||
return snippets[this.extUri.basename(uri)] || null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async hasLocalData(): Promise<boolean> {
|
||||
try {
|
||||
const localSnippets = await this.getSnippetsFileContents();
|
||||
if (Object.keys(localSnippets).length) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
/* ignore error */
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async updateLocalBackup(resourcePreviews: IFileResourcePreview[]): Promise<void> {
|
||||
const local: IStringDictionary<IFileContent> = {};
|
||||
for (const resourcePreview of resourcePreviews) {
|
||||
if (resourcePreview.fileContent) {
|
||||
local[this.extUri.basename(resourcePreview.localResource!)] = resourcePreview.fileContent;
|
||||
}
|
||||
}
|
||||
await this.backupLocal(JSON.stringify(this.toSnippetsContents(local)));
|
||||
}
|
||||
|
||||
private async updateLocalSnippets(resourcePreviews: ISnippetsAcceptedResourcePreview[], force: boolean): Promise<void> {
|
||||
for (const { fileContent, acceptResult, localResource, remoteResource, localChange } of resourcePreviews) {
|
||||
if (localChange !== Change.None) {
|
||||
const key = remoteResource ? this.extUri.basename(remoteResource) : this.extUri.basename(localResource!);
|
||||
const resource = this.extUri.joinPath(this.snippetsFolder, key);
|
||||
|
||||
// Removed
|
||||
if (localChange === Change.Deleted) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Deleting snippet...`, this.extUri.basename(resource));
|
||||
await this.fileService.del(resource);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Deleted snippet`, this.extUri.basename(resource));
|
||||
}
|
||||
|
||||
// Added
|
||||
else if (localChange === Change.Added) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Creating snippet...`, this.extUri.basename(resource));
|
||||
await this.fileService.createFile(resource, VSBuffer.fromString(acceptResult.content!), { overwrite: force });
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Created snippet`, this.extUri.basename(resource));
|
||||
}
|
||||
|
||||
// Updated
|
||||
else {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating snippet...`, this.extUri.basename(resource));
|
||||
await this.fileService.writeFile(resource, VSBuffer.fromString(acceptResult.content!), force ? undefined : fileContent!);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated snippet`, this.extUri.basename(resource));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateRemoteSnippets(resourcePreviews: ISnippetsAcceptedResourcePreview[], remoteUserData: IRemoteUserData, forcePush: boolean): Promise<IRemoteUserData> {
|
||||
const currentSnippets: IStringDictionary<string> = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : {};
|
||||
const newSnippets: IStringDictionary<string> = deepClone(currentSnippets);
|
||||
|
||||
for (const { acceptResult, localResource, remoteResource, remoteChange } of resourcePreviews) {
|
||||
if (remoteChange !== Change.None) {
|
||||
const key = localResource ? this.extUri.basename(localResource) : this.extUri.basename(remoteResource!);
|
||||
if (remoteChange === Change.Deleted) {
|
||||
delete newSnippets[key];
|
||||
} else {
|
||||
newSnippets[key] = acceptResult.content!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!areSame(currentSnippets, newSnippets)) {
|
||||
// update remote
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote snippets...`);
|
||||
remoteUserData = await this.updateRemoteUserData(JSON.stringify(newSnippets), forcePush ? null : remoteUserData.ref);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote snippets`);
|
||||
}
|
||||
return remoteUserData;
|
||||
}
|
||||
|
||||
private parseSnippets(syncData: ISyncData): IStringDictionary<string> {
|
||||
return JSON.parse(syncData.content);
|
||||
}
|
||||
|
||||
private toSnippetsContents(snippetsFileContents: IStringDictionary<IFileContent>): IStringDictionary<string> {
|
||||
const snippets: IStringDictionary<string> = {};
|
||||
for (const key of Object.keys(snippetsFileContents)) {
|
||||
snippets[key] = snippetsFileContents[key].value.toString();
|
||||
}
|
||||
return snippets;
|
||||
}
|
||||
|
||||
private async getSnippetsFileContents(): Promise<IStringDictionary<IFileContent>> {
|
||||
const snippets: IStringDictionary<IFileContent> = {};
|
||||
let stat: IFileStat;
|
||||
try {
|
||||
stat = await this.fileService.resolve(this.snippetsFolder);
|
||||
} catch (e) {
|
||||
// No snippets
|
||||
if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
|
||||
return snippets;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
for (const entry of stat.children || []) {
|
||||
const resource = entry.resource;
|
||||
const extension = this.extUri.extname(resource);
|
||||
if (extension === '.json' || extension === '.code-snippets') {
|
||||
const key = this.extUri.relativePath(this.snippetsFolder, resource)!;
|
||||
const content = await this.fileService.readFile(resource);
|
||||
snippets[key] = content;
|
||||
}
|
||||
}
|
||||
return snippets;
|
||||
}
|
||||
}
|
||||
|
||||
export class SnippetsInitializer extends AbstractInitializer {
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
) {
|
||||
super(SyncResource.Snippets, environmentService, logService, fileService);
|
||||
}
|
||||
|
||||
async doInitialize(remoteUserData: IRemoteUserData): Promise<void> {
|
||||
const remoteSnippets: IStringDictionary<string> | null = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
|
||||
if (!remoteSnippets) {
|
||||
this.logService.info('Skipping initializing snippets because remote snippets does not exist.');
|
||||
return;
|
||||
}
|
||||
|
||||
const isEmpty = await this.isEmpty();
|
||||
if (!isEmpty) {
|
||||
this.logService.info('Skipping initializing snippets because local snippets exist.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(remoteSnippets)) {
|
||||
const content = remoteSnippets[key];
|
||||
if (content) {
|
||||
const resource = this.extUri.joinPath(this.environmentService.snippetsHome, key);
|
||||
await this.fileService.createFile(resource, VSBuffer.fromString(content));
|
||||
this.logService.info('Created snippet', this.extUri.basename(resource));
|
||||
}
|
||||
}
|
||||
|
||||
await this.updateLastSyncUserData(remoteUserData);
|
||||
}
|
||||
|
||||
private async isEmpty(): Promise<boolean> {
|
||||
try {
|
||||
const stat = await this.fileService.resolve(this.environmentService.snippetsHome);
|
||||
return !stat.children?.length;
|
||||
} catch (error) {
|
||||
return (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
134
lib/vscode/src/vs/platform/userDataSync/common/storageKeys.ts
Normal file
134
lib/vscode/src/vs/platform/userDataSync/common/storageKeys.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IExtensionIdentifierWithVersion } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export interface IStorageKey {
|
||||
|
||||
readonly key: string;
|
||||
readonly version: number;
|
||||
|
||||
}
|
||||
|
||||
export interface IExtensionIdWithVersion {
|
||||
id: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export namespace ExtensionIdWithVersion {
|
||||
|
||||
const EXTENSION_ID_VERSION_REGEX = /^([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/;
|
||||
|
||||
export function toKey(extension: IExtensionIdWithVersion): string {
|
||||
return `${extension.id}@${extension.version}`;
|
||||
}
|
||||
|
||||
export function fromKey(key: string): IExtensionIdWithVersion | undefined {
|
||||
const matches = EXTENSION_ID_VERSION_REGEX.exec(key);
|
||||
if (matches && matches[1]) {
|
||||
return { id: matches[1], version: matches[2] };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export const IStorageKeysSyncRegistryService = createDecorator<IStorageKeysSyncRegistryService>('IStorageKeysSyncRegistryService');
|
||||
|
||||
export interface IStorageKeysSyncRegistryService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
/**
|
||||
* All registered storage keys
|
||||
*/
|
||||
readonly storageKeys: ReadonlyArray<IStorageKey>;
|
||||
|
||||
/**
|
||||
* Event that is triggered when storage keys are changed
|
||||
*/
|
||||
readonly onDidChangeStorageKeys: Event<ReadonlyArray<IStorageKey>>;
|
||||
|
||||
/**
|
||||
* Register a storage key that has to be synchronized during sync.
|
||||
*/
|
||||
registerStorageKey(key: IStorageKey): void;
|
||||
|
||||
/**
|
||||
* All registered extensions storage keys
|
||||
*/
|
||||
readonly extensionsStorageKeys: ReadonlyArray<[IExtensionIdentifierWithVersion, ReadonlyArray<string>]>;
|
||||
|
||||
/**
|
||||
* Event that is triggered when extension storage keys are changed
|
||||
*/
|
||||
onDidChangeExtensionStorageKeys: Event<[IExtensionIdWithVersion, ReadonlyArray<string>]>;
|
||||
|
||||
/**
|
||||
* Register storage keys that has to be synchronized for the given extension.
|
||||
*/
|
||||
registerExtensionStorageKeys(extension: IExtensionIdWithVersion, keys: string[]): void;
|
||||
|
||||
/**
|
||||
* Returns storage keys of the given extension that has to be synchronized.
|
||||
*/
|
||||
getExtensioStorageKeys(extension: IExtensionIdWithVersion): ReadonlyArray<string> | undefined;
|
||||
}
|
||||
|
||||
export abstract class AbstractStorageKeysSyncRegistryService extends Disposable implements IStorageKeysSyncRegistryService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
protected readonly _storageKeys = new Map<string, IStorageKey>();
|
||||
get storageKeys(): ReadonlyArray<IStorageKey> { return [...this._storageKeys.values()]; }
|
||||
|
||||
protected readonly _onDidChangeStorageKeys: Emitter<ReadonlyArray<IStorageKey>> = this._register(new Emitter<ReadonlyArray<IStorageKey>>());
|
||||
readonly onDidChangeStorageKeys = this._onDidChangeStorageKeys.event;
|
||||
|
||||
protected readonly _extensionsStorageKeys = new Map<string, string[]>();
|
||||
get extensionsStorageKeys() {
|
||||
const result: [IExtensionIdWithVersion, ReadonlyArray<string>][] = [];
|
||||
this._extensionsStorageKeys.forEach((keys, extension) => result.push([ExtensionIdWithVersion.fromKey(extension)!, keys]));
|
||||
return result;
|
||||
}
|
||||
protected readonly _onDidChangeExtensionStorageKeys = this._register(new Emitter<[IExtensionIdWithVersion, ReadonlyArray<string>]>());
|
||||
readonly onDidChangeExtensionStorageKeys = this._onDidChangeExtensionStorageKeys.event;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._register(toDisposable(() => this._storageKeys.clear()));
|
||||
}
|
||||
|
||||
getExtensioStorageKeys(extension: IExtensionIdWithVersion): ReadonlyArray<string> | undefined {
|
||||
return this._extensionsStorageKeys.get(ExtensionIdWithVersion.toKey(extension));
|
||||
}
|
||||
|
||||
protected updateExtensionStorageKeys(extension: IExtensionIdWithVersion, keys: string[]): void {
|
||||
this._extensionsStorageKeys.set(ExtensionIdWithVersion.toKey(extension), keys);
|
||||
this._onDidChangeExtensionStorageKeys.fire([extension, keys]);
|
||||
}
|
||||
|
||||
abstract registerStorageKey(key: IStorageKey): void;
|
||||
abstract registerExtensionStorageKeys(extension: IExtensionIdWithVersion, keys: string[]): void;
|
||||
}
|
||||
|
||||
export class StorageKeysSyncRegistryService extends AbstractStorageKeysSyncRegistryService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
registerStorageKey(storageKey: IStorageKey): void {
|
||||
if (!this._storageKeys.has(storageKey.key)) {
|
||||
this._storageKeys.set(storageKey.key, storageKey);
|
||||
this._onDidChangeStorageKeys.fire(this.storageKeys);
|
||||
}
|
||||
}
|
||||
|
||||
registerExtensionStorageKeys(extension: IExtensionIdWithVersion, keys: string[]): void {
|
||||
this.updateExtensionStorageKeys(extension, keys);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Delayer, disposableTimeout, CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { Disposable, toDisposable, MutableDisposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, UserDataAutoSyncError, ISyncTask, IUserDataSyncStoreManagementService, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { isPromiseCanceledError } from 'vs/base/common/errors';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';
|
||||
import { localize } from 'vs/nls';
|
||||
import { toLocalISOString } from 'vs/base/common/date';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
|
||||
type AutoSyncClassification = {
|
||||
sources: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
};
|
||||
|
||||
type AutoSyncEnablementClassification = {
|
||||
enabled?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
};
|
||||
|
||||
type AutoSyncErrorClassification = {
|
||||
code: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
service: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
};
|
||||
|
||||
const enablementKey = 'sync.enable';
|
||||
const disableMachineEventuallyKey = 'sync.disableMachineEventually';
|
||||
const sessionIdKey = 'sync.sessionId';
|
||||
const storeUrlKey = 'sync.storeUrl';
|
||||
|
||||
interface _IUserDataAutoSyncEnablementService extends IUserDataAutoSyncEnablementService {
|
||||
canToggleEnablement(): boolean;
|
||||
setEnablement(enabled: boolean): void;
|
||||
}
|
||||
|
||||
export class UserDataAutoSyncEnablementService extends Disposable implements _IUserDataAutoSyncEnablementService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private _onDidChangeEnablement = new Emitter<boolean>();
|
||||
readonly onDidChangeEnablement: Event<boolean> = this._onDidChangeEnablement.event;
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IEnvironmentService protected readonly environmentService: IEnvironmentService,
|
||||
@IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
|
||||
) {
|
||||
super();
|
||||
this._register(storageService.onDidChangeStorage(e => this.onDidStorageChange(e)));
|
||||
}
|
||||
|
||||
isEnabled(defaultEnablement?: boolean): boolean {
|
||||
switch (this.environmentService.sync) {
|
||||
case 'on':
|
||||
return true;
|
||||
case 'off':
|
||||
return false;
|
||||
}
|
||||
return this.storageService.getBoolean(enablementKey, StorageScope.GLOBAL, !!defaultEnablement);
|
||||
}
|
||||
|
||||
canToggleEnablement(): boolean {
|
||||
return this.userDataSyncStoreManagementService.userDataSyncStore !== undefined && this.environmentService.sync === undefined;
|
||||
}
|
||||
|
||||
setEnablement(enabled: boolean): void {
|
||||
this.storageService.store(enablementKey, enabled, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
private onDidStorageChange(workspaceStorageChangeEvent: IWorkspaceStorageChangeEvent): void {
|
||||
if (workspaceStorageChangeEvent.scope !== StorageScope.GLOBAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (enablementKey === workspaceStorageChangeEvent.key) {
|
||||
this._onDidChangeEnablement.fire(this.isEnabled());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class UserDataAutoSyncService extends Disposable implements IUserDataAutoSyncService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private readonly userDataAutoSyncEnablementService: _IUserDataAutoSyncEnablementService;
|
||||
|
||||
private readonly autoSync = this._register(new MutableDisposable<AutoSync>());
|
||||
private successiveFailures: number = 0;
|
||||
private lastSyncTriggerTime: number | undefined = undefined;
|
||||
private readonly syncTriggerDelayer: Delayer<void>;
|
||||
|
||||
private readonly _onError: Emitter<UserDataSyncError> = this._register(new Emitter<UserDataSyncError>());
|
||||
readonly onError: Event<UserDataSyncError> = this._onError.event;
|
||||
|
||||
private lastSyncUrl: URI | undefined;
|
||||
private get syncUrl(): URI | undefined {
|
||||
const value = this.storageService.get(storeUrlKey, StorageScope.GLOBAL);
|
||||
return value ? URI.parse(value) : undefined;
|
||||
}
|
||||
private set syncUrl(syncUrl: URI | undefined) {
|
||||
if (syncUrl) {
|
||||
this.storageService.store(storeUrlKey, syncUrl.toString(), StorageScope.GLOBAL);
|
||||
} else {
|
||||
this.storageService.remove(storeUrlKey, StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
@IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
|
||||
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
|
||||
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
@IUserDataSyncAccountService private readonly userDataSyncAccountService: IUserDataSyncAccountService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IUserDataSyncMachinesService private readonly userDataSyncMachinesService: IUserDataSyncMachinesService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IUserDataAutoSyncEnablementService userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService
|
||||
) {
|
||||
super();
|
||||
this.userDataAutoSyncEnablementService = userDataAutoSyncEnablementService as _IUserDataAutoSyncEnablementService;
|
||||
this.syncTriggerDelayer = this._register(new Delayer<void>(0));
|
||||
|
||||
this.lastSyncUrl = this.syncUrl;
|
||||
this.syncUrl = userDataSyncStoreManagementService.userDataSyncStore?.url;
|
||||
|
||||
if (this.syncUrl) {
|
||||
|
||||
this.logService.info('Using settings sync service', this.syncUrl.toString());
|
||||
this._register(userDataSyncStoreManagementService.onDidChangeUserDataSyncStore(() => {
|
||||
if (!isEqual(this.syncUrl, userDataSyncStoreManagementService.userDataSyncStore?.url)) {
|
||||
this.lastSyncUrl = this.syncUrl;
|
||||
this.syncUrl = userDataSyncStoreManagementService.userDataSyncStore?.url;
|
||||
if (this.syncUrl) {
|
||||
this.logService.info('Using settings sync service', this.syncUrl.toString());
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
if (this.userDataAutoSyncEnablementService.isEnabled()) {
|
||||
this.logService.info('Auto Sync is enabled.');
|
||||
} else {
|
||||
this.logService.info('Auto Sync is disabled.');
|
||||
}
|
||||
this.updateAutoSync();
|
||||
|
||||
if (this.hasToDisableMachineEventually()) {
|
||||
this.disableMachineEventually();
|
||||
}
|
||||
|
||||
this._register(userDataSyncAccountService.onDidChangeAccount(() => this.updateAutoSync()));
|
||||
this._register(userDataSyncStoreService.onDidChangeDonotMakeRequestsUntil(() => this.updateAutoSync()));
|
||||
this._register(Event.debounce<string, string[]>(userDataSyncService.onDidChangeLocal, (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, false, false)));
|
||||
this._register(Event.filter(this.userDataSyncResourceEnablementService.onDidChangeResourceEnablement, ([, enabled]) => enabled)(() => this.triggerSync(['resourceEnablement'], false, false)));
|
||||
}
|
||||
}
|
||||
|
||||
private updateAutoSync(): void {
|
||||
const { enabled, message } = this.isAutoSyncEnabled();
|
||||
if (enabled) {
|
||||
if (this.autoSync.value === undefined) {
|
||||
this.autoSync.value = new AutoSync(this.lastSyncUrl, 1000 * 60 * 5 /* 5 miutes */, this.userDataSyncStoreManagementService, this.userDataSyncStoreService, this.userDataSyncService, this.userDataSyncMachinesService, this.logService, this.storageService);
|
||||
this.autoSync.value.register(this.autoSync.value.onDidStartSync(() => this.lastSyncTriggerTime = new Date().getTime()));
|
||||
this.autoSync.value.register(this.autoSync.value.onDidFinishSync(e => this.onDidFinishSync(e)));
|
||||
if (this.startAutoSync()) {
|
||||
this.autoSync.value.start();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.syncTriggerDelayer.cancel();
|
||||
if (this.autoSync.value !== undefined) {
|
||||
if (message) {
|
||||
this.logService.info(message);
|
||||
}
|
||||
this.autoSync.clear();
|
||||
}
|
||||
|
||||
/* log message when auto sync is not disabled by user */
|
||||
else if (message && this.userDataAutoSyncEnablementService.isEnabled()) {
|
||||
this.logService.info(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For tests purpose only
|
||||
protected startAutoSync(): boolean { return true; }
|
||||
|
||||
private isAutoSyncEnabled(): { enabled: boolean, message?: string } {
|
||||
if (!this.userDataAutoSyncEnablementService.isEnabled()) {
|
||||
return { enabled: false, message: 'Auto Sync: Disabled.' };
|
||||
}
|
||||
if (!this.userDataSyncAccountService.account) {
|
||||
return { enabled: false, message: 'Auto Sync: Suspended until auth token is available.' };
|
||||
}
|
||||
if (this.userDataSyncStoreService.donotMakeRequestsUntil) {
|
||||
return { enabled: false, message: `Auto Sync: Suspended until ${toLocalISOString(this.userDataSyncStoreService.donotMakeRequestsUntil)} because server is not accepting requests until then.` };
|
||||
}
|
||||
return { enabled: true };
|
||||
}
|
||||
|
||||
async turnOn(): Promise<void> {
|
||||
this.stopDisableMachineEventually();
|
||||
this.lastSyncUrl = this.syncUrl;
|
||||
this.updateEnablement(true);
|
||||
}
|
||||
|
||||
async turnOff(everywhere: boolean, softTurnOffOnError?: boolean, donotRemoveMachine?: boolean): Promise<void> {
|
||||
try {
|
||||
|
||||
// Remove machine
|
||||
if (this.userDataSyncAccountService.account && !donotRemoveMachine) {
|
||||
await this.userDataSyncMachinesService.removeCurrentMachine();
|
||||
}
|
||||
|
||||
// Disable Auto Sync
|
||||
this.updateEnablement(false);
|
||||
|
||||
// Reset Session
|
||||
this.storageService.remove(sessionIdKey, StorageScope.GLOBAL);
|
||||
|
||||
// Reset
|
||||
if (everywhere) {
|
||||
this.telemetryService.publicLog2('sync/turnOffEveryWhere');
|
||||
await this.userDataSyncService.reset();
|
||||
} else {
|
||||
await this.userDataSyncService.resetLocal();
|
||||
}
|
||||
} catch (error) {
|
||||
if (softTurnOffOnError) {
|
||||
this.logService.error(error);
|
||||
this.updateEnablement(false);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateEnablement(enabled: boolean): void {
|
||||
if (this.userDataAutoSyncEnablementService.isEnabled() !== enabled) {
|
||||
this.telemetryService.publicLog2<{ enabled: boolean }, AutoSyncEnablementClassification>(enablementKey, { enabled });
|
||||
this.userDataAutoSyncEnablementService.setEnablement(enabled);
|
||||
this.updateAutoSync();
|
||||
}
|
||||
}
|
||||
|
||||
private async onDidFinishSync(error: Error | undefined): Promise<void> {
|
||||
if (!error) {
|
||||
// Sync finished without errors
|
||||
this.successiveFailures = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Error while syncing
|
||||
const userDataSyncError = UserDataSyncError.toUserDataSyncError(error);
|
||||
|
||||
// Log to telemetry
|
||||
if (userDataSyncError instanceof UserDataAutoSyncError) {
|
||||
this.telemetryService.publicLog2<{ code: string, service: string }, AutoSyncErrorClassification>(`autosync/error`, { code: userDataSyncError.code, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() });
|
||||
}
|
||||
|
||||
// Session got expired
|
||||
if (userDataSyncError.code === UserDataSyncErrorCode.SessionExpired) {
|
||||
await this.turnOff(false, true /* force soft turnoff on error */);
|
||||
this.logService.info('Auto Sync: Turned off sync because current session is expired');
|
||||
}
|
||||
|
||||
// Turned off from another device
|
||||
else if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff) {
|
||||
await this.turnOff(false, true /* force soft turnoff on error */);
|
||||
this.logService.info('Auto Sync: Turned off sync because sync is turned off in the cloud');
|
||||
}
|
||||
|
||||
// Exceeded Rate Limit
|
||||
else if (userDataSyncError.code === UserDataSyncErrorCode.LocalTooManyRequests || userDataSyncError.code === UserDataSyncErrorCode.TooManyRequests) {
|
||||
await this.turnOff(false, true /* force soft turnoff on error */,
|
||||
true /* do not disable machine because disabling a machine makes request to server and can fail with TooManyRequests */);
|
||||
this.disableMachineEventually();
|
||||
this.logService.info('Auto Sync: Turned off sync because of making too many requests to server');
|
||||
}
|
||||
|
||||
// Upgrade Required or Gone
|
||||
else if (userDataSyncError.code === UserDataSyncErrorCode.UpgradeRequired || userDataSyncError.code === UserDataSyncErrorCode.Gone) {
|
||||
await this.turnOff(false, true /* force soft turnoff on error */,
|
||||
true /* do not disable machine because disabling a machine makes request to server and can fail with upgrade required or gone */);
|
||||
this.disableMachineEventually();
|
||||
this.logService.info('Auto Sync: Turned off sync because current client is not compatible with server. Requires client upgrade.');
|
||||
}
|
||||
|
||||
// Incompatible Local Content
|
||||
else if (userDataSyncError.code === UserDataSyncErrorCode.IncompatibleLocalContent) {
|
||||
await this.turnOff(false, true /* force soft turnoff on error */);
|
||||
this.logService.info(`Auto Sync: Turned off sync because server has ${userDataSyncError.resource} content with newer version than of client. Requires client upgrade.`);
|
||||
}
|
||||
|
||||
// Incompatible Remote Content
|
||||
else if (userDataSyncError.code === UserDataSyncErrorCode.IncompatibleRemoteContent) {
|
||||
await this.turnOff(false, true /* force soft turnoff on error */);
|
||||
this.logService.info(`Auto Sync: Turned off sync because server has ${userDataSyncError.resource} content with older version than of client. Requires server reset.`);
|
||||
}
|
||||
|
||||
// Service changed
|
||||
else if (userDataSyncError.code === UserDataSyncErrorCode.ServiceChanged || userDataSyncError.code === UserDataSyncErrorCode.DefaultServiceChanged) {
|
||||
await this.turnOff(false, true /* force soft turnoff on error */, true /* do not disable machine */);
|
||||
await this.turnOn();
|
||||
this.logService.info('Auto Sync: Sync Service changed. Turned off auto sync, reset local state and turned on auto sync.');
|
||||
}
|
||||
|
||||
else {
|
||||
this.logService.error(userDataSyncError);
|
||||
this.successiveFailures++;
|
||||
}
|
||||
|
||||
this._onError.fire(userDataSyncError);
|
||||
}
|
||||
|
||||
private async disableMachineEventually(): Promise<void> {
|
||||
this.storageService.store(disableMachineEventuallyKey, true, StorageScope.GLOBAL);
|
||||
await timeout(1000 * 60 * 10);
|
||||
|
||||
// Return if got stopped meanwhile.
|
||||
if (!this.hasToDisableMachineEventually()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stopDisableMachineEventually();
|
||||
|
||||
// disable only if sync is disabled
|
||||
if (!this.userDataAutoSyncEnablementService.isEnabled() && this.userDataSyncAccountService.account) {
|
||||
await this.userDataSyncMachinesService.removeCurrentMachine();
|
||||
}
|
||||
}
|
||||
|
||||
private hasToDisableMachineEventually(): boolean {
|
||||
return this.storageService.getBoolean(disableMachineEventuallyKey, StorageScope.GLOBAL, false);
|
||||
}
|
||||
|
||||
private stopDisableMachineEventually(): void {
|
||||
this.storageService.remove(disableMachineEventuallyKey, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
private sources: string[] = [];
|
||||
async triggerSync(sources: string[], skipIfSyncedRecently: boolean, disableCache: boolean): Promise<void> {
|
||||
if (this.autoSync.value === undefined) {
|
||||
return this.syncTriggerDelayer.cancel();
|
||||
}
|
||||
|
||||
if (skipIfSyncedRecently && this.lastSyncTriggerTime
|
||||
&& Math.round((new Date().getTime() - this.lastSyncTriggerTime) / 1000) < 10) {
|
||||
this.logService.debug('Auto Sync: Skipped. Limited to once per 10 seconds.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.sources.push(...sources);
|
||||
return this.syncTriggerDelayer.trigger(async () => {
|
||||
this.logService.trace('activity sources', ...this.sources);
|
||||
this.telemetryService.publicLog2<{ sources: string[] }, AutoSyncClassification>('sync/triggered', { sources: this.sources });
|
||||
this.sources = [];
|
||||
if (this.autoSync.value) {
|
||||
await this.autoSync.value.sync('Activity', disableCache);
|
||||
}
|
||||
}, this.successiveFailures
|
||||
? this.getSyncTriggerDelayTime() * 1 * Math.min(Math.pow(2, this.successiveFailures), 60) /* Delay exponentially until max 1 minute */
|
||||
: this.getSyncTriggerDelayTime());
|
||||
|
||||
}
|
||||
|
||||
protected getSyncTriggerDelayTime(): number {
|
||||
return 1000; /* Debounce for a second if there are no failures */
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AutoSync extends Disposable {
|
||||
|
||||
private static readonly INTERVAL_SYNCING = 'Interval';
|
||||
|
||||
private readonly intervalHandler = this._register(new MutableDisposable<IDisposable>());
|
||||
|
||||
private readonly _onDidStartSync = this._register(new Emitter<void>());
|
||||
readonly onDidStartSync = this._onDidStartSync.event;
|
||||
|
||||
private readonly _onDidFinishSync = this._register(new Emitter<Error | undefined>());
|
||||
readonly onDidFinishSync = this._onDidFinishSync.event;
|
||||
|
||||
private syncTask: ISyncTask | undefined;
|
||||
private syncPromise: CancelablePromise<void> | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly lastSyncUrl: URI | undefined,
|
||||
private readonly interval: number /* in milliseconds */,
|
||||
private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
|
||||
private readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
private readonly userDataSyncService: IUserDataSyncService,
|
||||
private readonly userDataSyncMachinesService: IUserDataSyncMachinesService,
|
||||
private readonly logService: IUserDataSyncLogService,
|
||||
private readonly storageService: IStorageService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
start(): void {
|
||||
this._register(this.onDidFinishSync(() => this.waitUntilNextIntervalAndSync()));
|
||||
this._register(toDisposable(() => {
|
||||
if (this.syncPromise) {
|
||||
this.syncPromise.cancel();
|
||||
this.logService.info('Auto sync: Cancelled sync that is in progress');
|
||||
this.syncPromise = undefined;
|
||||
}
|
||||
if (this.syncTask) {
|
||||
this.syncTask.stop();
|
||||
}
|
||||
this.logService.info('Auto Sync: Stopped');
|
||||
}));
|
||||
this.logService.info('Auto Sync: Started');
|
||||
this.sync(AutoSync.INTERVAL_SYNCING, false);
|
||||
}
|
||||
|
||||
private waitUntilNextIntervalAndSync(): void {
|
||||
this.intervalHandler.value = disposableTimeout(() => this.sync(AutoSync.INTERVAL_SYNCING, false), this.interval);
|
||||
}
|
||||
|
||||
sync(reason: string, disableCache: boolean): Promise<void> {
|
||||
const syncPromise = createCancelablePromise(async token => {
|
||||
if (this.syncPromise) {
|
||||
try {
|
||||
// Wait until existing sync is finished
|
||||
this.logService.debug('Auto Sync: Waiting until sync is finished.');
|
||||
await this.syncPromise;
|
||||
} catch (error) {
|
||||
if (isPromiseCanceledError(error)) {
|
||||
// Cancelled => Disposed. Donot continue sync.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.doSync(reason, disableCache, token);
|
||||
});
|
||||
this.syncPromise = syncPromise;
|
||||
this.syncPromise.finally(() => this.syncPromise = undefined);
|
||||
return this.syncPromise;
|
||||
}
|
||||
|
||||
private hasSyncServiceChanged(): boolean {
|
||||
return this.lastSyncUrl !== undefined && !isEqual(this.lastSyncUrl, this.userDataSyncStoreManagementService.userDataSyncStore?.url);
|
||||
}
|
||||
|
||||
private async hasDefaultServiceChanged(): Promise<boolean> {
|
||||
const previous = await this.userDataSyncStoreManagementService.getPreviousUserDataSyncStore();
|
||||
const current = this.userDataSyncStoreManagementService.userDataSyncStore;
|
||||
// check if defaults changed
|
||||
return !!current && !!previous &&
|
||||
(!isEqual(current.defaultUrl, previous.defaultUrl) ||
|
||||
!isEqual(current.insidersUrl, previous.insidersUrl) ||
|
||||
!isEqual(current.stableUrl, previous.stableUrl));
|
||||
}
|
||||
|
||||
private async doSync(reason: string, disableCache: boolean, token: CancellationToken): Promise<void> {
|
||||
this.logService.info(`Auto Sync: Triggered by ${reason}`);
|
||||
this._onDidStartSync.fire();
|
||||
let error: Error | undefined;
|
||||
try {
|
||||
this.syncTask = await this.userDataSyncService.createSyncTask(disableCache);
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
let manifest = this.syncTask.manifest;
|
||||
|
||||
// Server has no data but this machine was synced before
|
||||
if (manifest === null && await this.userDataSyncService.hasPreviouslySynced()) {
|
||||
if (this.hasSyncServiceChanged()) {
|
||||
if (await this.hasDefaultServiceChanged()) {
|
||||
throw new UserDataAutoSyncError(localize('default service changed', "Cannot sync because default service has changed"), UserDataSyncErrorCode.DefaultServiceChanged);
|
||||
} else {
|
||||
throw new UserDataAutoSyncError(localize('service changed', "Cannot sync because sync service has changed"), UserDataSyncErrorCode.ServiceChanged);
|
||||
}
|
||||
} else {
|
||||
// Sync was turned off in the cloud
|
||||
throw new UserDataAutoSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff);
|
||||
}
|
||||
}
|
||||
|
||||
const sessionId = this.storageService.get(sessionIdKey, StorageScope.GLOBAL);
|
||||
// Server session is different from client session
|
||||
if (sessionId && manifest && sessionId !== manifest.session) {
|
||||
if (this.hasSyncServiceChanged()) {
|
||||
if (await this.hasDefaultServiceChanged()) {
|
||||
throw new UserDataAutoSyncError(localize('default service changed', "Cannot sync because default service has changed"), UserDataSyncErrorCode.DefaultServiceChanged);
|
||||
} else {
|
||||
throw new UserDataAutoSyncError(localize('service changed', "Cannot sync because sync service has changed"), UserDataSyncErrorCode.ServiceChanged);
|
||||
}
|
||||
} else {
|
||||
throw new UserDataAutoSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired);
|
||||
}
|
||||
}
|
||||
|
||||
const machines = await this.userDataSyncMachinesService.getMachines(manifest || undefined);
|
||||
// Return if cancellation is requested
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentMachine = machines.find(machine => machine.isCurrent);
|
||||
// Check if sync was turned off from other machine
|
||||
if (currentMachine?.disabled) {
|
||||
// Throw TurnedOff error
|
||||
throw new UserDataAutoSyncError(localize('turned off machine', "Cannot sync because syncing is turned off on this machine from another machine."), UserDataSyncErrorCode.TurnedOff);
|
||||
}
|
||||
|
||||
await this.syncTask.run();
|
||||
|
||||
// After syncing, get the manifest if it was not available before
|
||||
if (manifest === null) {
|
||||
manifest = await this.userDataSyncStoreService.manifest();
|
||||
}
|
||||
|
||||
// Update local session id
|
||||
if (manifest && manifest.session !== sessionId) {
|
||||
this.storageService.store(sessionIdKey, manifest.session, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
// Return if cancellation is requested
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add current machine
|
||||
if (!currentMachine) {
|
||||
await this.userDataSyncMachinesService.addCurrentMachine(manifest || undefined);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
error = e;
|
||||
}
|
||||
|
||||
this._onDidFinishSync.fire(error);
|
||||
}
|
||||
|
||||
register<T extends IDisposable>(t: T): T {
|
||||
return super._register(t);
|
||||
}
|
||||
|
||||
}
|
||||
512
lib/vscode/src/vs/platform/userDataSync/common/userDataSync.ts
Normal file
512
lib/vscode/src/vs/platform/userDataSync/common/userDataSync.ts
Normal file
@@ -0,0 +1,512 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IExtensionIdentifier, EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, allSettings } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath, isEqualOrParent } from 'vs/base/common/resources';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { distinct } from 'vs/base/common/arrays';
|
||||
import { isArray, isString, isObject } from 'vs/base/common/types';
|
||||
import { IHeaders } from 'vs/base/parts/request/common/request';
|
||||
|
||||
export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store';
|
||||
|
||||
export function getDisallowedIgnoredSettings(): string[] {
|
||||
const allSettings = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties();
|
||||
return Object.keys(allSettings).filter(setting => !!allSettings[setting].disallowSyncIgnore);
|
||||
}
|
||||
|
||||
export function getDefaultIgnoredSettings(): string[] {
|
||||
const allSettings = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties();
|
||||
const machineSettings = Object.keys(allSettings).filter(setting => allSettings[setting].scope === ConfigurationScope.MACHINE || allSettings[setting].scope === ConfigurationScope.MACHINE_OVERRIDABLE);
|
||||
const disallowedSettings = getDisallowedIgnoredSettings();
|
||||
return distinct([CONFIGURATION_SYNC_STORE_KEY, ...machineSettings, ...disallowedSettings]);
|
||||
}
|
||||
|
||||
export function registerConfiguration(): IDisposable {
|
||||
const ignoredSettingsSchemaId = 'vscode://schemas/ignoredSettings';
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
|
||||
configurationRegistry.registerConfiguration({
|
||||
id: 'settingsSync',
|
||||
order: 30,
|
||||
title: localize('settings sync', "Settings Sync"),
|
||||
type: 'object',
|
||||
properties: {
|
||||
'settingsSync.keybindingsPerPlatform': {
|
||||
type: 'boolean',
|
||||
description: localize('settingsSync.keybindingsPerPlatform', "Synchronize keybindings for each platform."),
|
||||
default: true,
|
||||
scope: ConfigurationScope.APPLICATION,
|
||||
tags: ['sync', 'usesOnlineServices']
|
||||
},
|
||||
'sync.keybindingsPerPlatform': {
|
||||
type: 'boolean',
|
||||
deprecationMessage: localize('sync.keybindingsPerPlatform.deprecated', "Deprecated, use settingsSync.keybindingsPerPlatform instead"),
|
||||
},
|
||||
'settingsSync.ignoredExtensions': {
|
||||
'type': 'array',
|
||||
markdownDescription: localize('settingsSync.ignoredExtensions', "List of extensions to be ignored while synchronizing. The identifier of an extension is always `${publisher}.${name}`. For example: `vscode.csharp`."),
|
||||
items: [{
|
||||
type: 'string',
|
||||
pattern: EXTENSION_IDENTIFIER_PATTERN,
|
||||
errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.")
|
||||
}],
|
||||
'default': [],
|
||||
'scope': ConfigurationScope.APPLICATION,
|
||||
uniqueItems: true,
|
||||
disallowSyncIgnore: true,
|
||||
tags: ['sync', 'usesOnlineServices']
|
||||
},
|
||||
'sync.ignoredExtensions': {
|
||||
'type': 'array',
|
||||
deprecationMessage: localize('sync.ignoredExtensions.deprecated', "Deprecated, use settingsSync.ignoredExtensions instead"),
|
||||
},
|
||||
'settingsSync.ignoredSettings': {
|
||||
'type': 'array',
|
||||
description: localize('settingsSync.ignoredSettings', "Configure settings to be ignored while synchronizing."),
|
||||
'default': [],
|
||||
'scope': ConfigurationScope.APPLICATION,
|
||||
$ref: ignoredSettingsSchemaId,
|
||||
additionalProperties: true,
|
||||
uniqueItems: true,
|
||||
disallowSyncIgnore: true,
|
||||
tags: ['sync', 'usesOnlineServices']
|
||||
},
|
||||
'sync.ignoredSettings': {
|
||||
'type': 'array',
|
||||
deprecationMessage: localize('sync.ignoredSettings.deprecated', "Deprecated, use settingsSync.ignoredSettings instead"),
|
||||
}
|
||||
}
|
||||
});
|
||||
const jsonRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
|
||||
const registerIgnoredSettingsSchema = () => {
|
||||
const disallowedIgnoredSettings = getDisallowedIgnoredSettings();
|
||||
const defaultIgnoredSettings = getDefaultIgnoredSettings().filter(s => s !== CONFIGURATION_SYNC_STORE_KEY);
|
||||
const settings = Object.keys(allSettings.properties).filter(setting => defaultIgnoredSettings.indexOf(setting) === -1);
|
||||
const ignoredSettings = defaultIgnoredSettings.filter(setting => disallowedIgnoredSettings.indexOf(setting) === -1);
|
||||
const ignoredSettingsSchema: IJSONSchema = {
|
||||
items: {
|
||||
type: 'string',
|
||||
enum: [...settings, ...ignoredSettings.map(setting => `-${setting}`)]
|
||||
},
|
||||
};
|
||||
jsonRegistry.registerSchema(ignoredSettingsSchemaId, ignoredSettingsSchema);
|
||||
};
|
||||
return configurationRegistry.onDidUpdateConfiguration(() => registerIgnoredSettingsSchema());
|
||||
}
|
||||
|
||||
// #region User Data Sync Store
|
||||
|
||||
export interface IUserData {
|
||||
ref: string;
|
||||
content: string | null;
|
||||
}
|
||||
|
||||
export type IAuthenticationProvider = { id: string, scopes: string[] };
|
||||
|
||||
export interface IUserDataSyncStore {
|
||||
readonly url: URI;
|
||||
readonly defaultUrl: URI;
|
||||
readonly stableUrl: URI;
|
||||
readonly insidersUrl: URI;
|
||||
readonly canSwitch: boolean;
|
||||
readonly authenticationProviders: IAuthenticationProvider[];
|
||||
}
|
||||
|
||||
export function isAuthenticationProvider(thing: any): thing is IAuthenticationProvider {
|
||||
return thing
|
||||
&& isObject(thing)
|
||||
&& isString(thing.id)
|
||||
&& isArray(thing.scopes);
|
||||
}
|
||||
|
||||
export const enum SyncResource {
|
||||
Settings = 'settings',
|
||||
Keybindings = 'keybindings',
|
||||
Snippets = 'snippets',
|
||||
Extensions = 'extensions',
|
||||
GlobalState = 'globalState'
|
||||
}
|
||||
export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Snippets, SyncResource.Extensions, SyncResource.GlobalState];
|
||||
|
||||
export interface IUserDataManifest {
|
||||
latest?: Record<ServerResource, string>
|
||||
session: string;
|
||||
}
|
||||
|
||||
export interface IResourceRefHandle {
|
||||
ref: string;
|
||||
created: number;
|
||||
}
|
||||
|
||||
export type ServerResource = SyncResource | 'machines';
|
||||
export type UserDataSyncStoreType = 'insiders' | 'stable';
|
||||
|
||||
export const IUserDataSyncStoreManagementService = createDecorator<IUserDataSyncStoreManagementService>('IUserDataSyncStoreManagementService');
|
||||
export interface IUserDataSyncStoreManagementService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly onDidChangeUserDataSyncStore: Event<void>;
|
||||
readonly userDataSyncStore: IUserDataSyncStore | undefined;
|
||||
switch(type: UserDataSyncStoreType): Promise<void>;
|
||||
getPreviousUserDataSyncStore(): Promise<IUserDataSyncStore | undefined>;
|
||||
}
|
||||
|
||||
export interface IUserDataSyncStoreClient {
|
||||
readonly onDidChangeDonotMakeRequestsUntil: Event<void>;
|
||||
readonly donotMakeRequestsUntil: Date | undefined;
|
||||
|
||||
readonly onTokenFailed: Event<void>;
|
||||
readonly onTokenSucceed: Event<void>;
|
||||
setAuthToken(token: string, type: string): void;
|
||||
|
||||
// Sync requests
|
||||
manifest(headers?: IHeaders): Promise<IUserDataManifest | null>;
|
||||
read(resource: ServerResource, oldValue: IUserData | null, headers?: IHeaders): Promise<IUserData>;
|
||||
write(resource: ServerResource, content: string, ref: string | null, headers?: IHeaders): Promise<string>;
|
||||
clear(): Promise<void>;
|
||||
delete(resource: ServerResource): Promise<void>;
|
||||
|
||||
getAllRefs(resource: ServerResource): Promise<IResourceRefHandle[]>;
|
||||
resolveContent(resource: ServerResource, ref: string): Promise<string | null>;
|
||||
}
|
||||
|
||||
export const IUserDataSyncStoreService = createDecorator<IUserDataSyncStoreService>('IUserDataSyncStoreService');
|
||||
export interface IUserDataSyncStoreService extends IUserDataSyncStoreClient {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export const IUserDataSyncBackupStoreService = createDecorator<IUserDataSyncBackupStoreService>('IUserDataSyncBackupStoreService');
|
||||
export interface IUserDataSyncBackupStoreService {
|
||||
readonly _serviceBrand: undefined;
|
||||
backup(resource: SyncResource, content: string): Promise<void>;
|
||||
getAllRefs(resource: SyncResource): Promise<IResourceRefHandle[]>;
|
||||
resolveContent(resource: SyncResource, ref?: string): Promise<string | null>;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
// #region User Data Sync Headers
|
||||
|
||||
export const HEADER_OPERATION_ID = 'x-operation-id';
|
||||
export const HEADER_EXECUTION_ID = 'X-Execution-Id';
|
||||
|
||||
//#endregion
|
||||
|
||||
// #region User Data Sync Error
|
||||
|
||||
export enum UserDataSyncErrorCode {
|
||||
// Client Errors (>= 400 )
|
||||
Unauthorized = 'Unauthorized', /* 401 */
|
||||
Conflict = 'Conflict', /* 409 */
|
||||
Gone = 'Gone', /* 410 */
|
||||
PreconditionFailed = 'PreconditionFailed', /* 412 */
|
||||
TooLarge = 'TooLarge', /* 413 */
|
||||
UpgradeRequired = 'UpgradeRequired', /* 426 */
|
||||
PreconditionRequired = 'PreconditionRequired', /* 428 */
|
||||
TooManyRequests = 'RemoteTooManyRequests', /* 429 */
|
||||
TooManyRequestsAndRetryAfter = 'TooManyRequestsAndRetryAfter', /* 429 + Retry-After */
|
||||
|
||||
// Local Errors
|
||||
ConnectionRefused = 'ConnectionRefused',
|
||||
NoRef = 'NoRef',
|
||||
TurnedOff = 'TurnedOff',
|
||||
SessionExpired = 'SessionExpired',
|
||||
ServiceChanged = 'ServiceChanged',
|
||||
DefaultServiceChanged = 'DefaultServiceChanged',
|
||||
LocalTooManyRequests = 'LocalTooManyRequests',
|
||||
LocalPreconditionFailed = 'LocalPreconditionFailed',
|
||||
LocalInvalidContent = 'LocalInvalidContent',
|
||||
LocalError = 'LocalError',
|
||||
IncompatibleLocalContent = 'IncompatibleLocalContent',
|
||||
IncompatibleRemoteContent = 'IncompatibleRemoteContent',
|
||||
UnresolvedConflicts = 'UnresolvedConflicts',
|
||||
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
export class UserDataSyncError extends Error {
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
readonly code: UserDataSyncErrorCode,
|
||||
readonly resource?: SyncResource,
|
||||
readonly operationId?: string
|
||||
) {
|
||||
super(message);
|
||||
this.name = `${this.code} (UserDataSyncError) syncResource:${this.resource || 'unknown'} operationId:${this.operationId || 'unknown'}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class UserDataSyncStoreError extends UserDataSyncError {
|
||||
constructor(message: string, code: UserDataSyncErrorCode, readonly operationId: string | undefined) {
|
||||
super(message, code, undefined, operationId);
|
||||
}
|
||||
}
|
||||
|
||||
export class UserDataAutoSyncError extends UserDataSyncError {
|
||||
constructor(message: string, code: UserDataSyncErrorCode) {
|
||||
super(message, code);
|
||||
}
|
||||
}
|
||||
|
||||
export namespace UserDataSyncError {
|
||||
|
||||
export function toUserDataSyncError(error: Error): UserDataSyncError {
|
||||
if (error instanceof UserDataSyncError) {
|
||||
return error;
|
||||
}
|
||||
const match = /^(.+) \(UserDataSyncError\) syncResource:(.+) operationId:(.+)$/.exec(error.name);
|
||||
if (match && match[1]) {
|
||||
const syncResource = match[2] === 'unknown' ? undefined : match[2] as SyncResource;
|
||||
const operationId = match[3] === 'unknown' ? undefined : match[3];
|
||||
return new UserDataSyncError(error.message, <UserDataSyncErrorCode>match[1], syncResource, operationId);
|
||||
}
|
||||
return new UserDataSyncError(error.message, UserDataSyncErrorCode.Unknown);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
// #region User Data Synchroniser
|
||||
|
||||
export interface ISyncExtension {
|
||||
identifier: IExtensionIdentifier;
|
||||
version?: string;
|
||||
disabled?: boolean;
|
||||
installed?: boolean;
|
||||
state?: IStringDictionary<any>;
|
||||
}
|
||||
|
||||
export interface ISyncExtensionWithVersion extends ISyncExtension {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface IStorageValue {
|
||||
version: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface IGlobalState {
|
||||
storage: IStringDictionary<IStorageValue>;
|
||||
}
|
||||
|
||||
export const enum SyncStatus {
|
||||
Uninitialized = 'uninitialized',
|
||||
Idle = 'idle',
|
||||
Syncing = 'syncing',
|
||||
HasConflicts = 'hasConflicts',
|
||||
}
|
||||
|
||||
export interface ISyncResourceHandle {
|
||||
created: number;
|
||||
uri: URI;
|
||||
}
|
||||
|
||||
export interface IRemoteUserData {
|
||||
ref: string;
|
||||
syncData: ISyncData | null;
|
||||
}
|
||||
|
||||
export interface ISyncData {
|
||||
version: number;
|
||||
machineId?: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const enum Change {
|
||||
None,
|
||||
Added,
|
||||
Modified,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
export const enum MergeState {
|
||||
Preview = 'preview',
|
||||
Conflict = 'conflict',
|
||||
Accepted = 'accepted',
|
||||
}
|
||||
|
||||
export interface IResourcePreview {
|
||||
readonly remoteResource: URI;
|
||||
readonly localResource: URI;
|
||||
readonly previewResource: URI;
|
||||
readonly acceptedResource: URI;
|
||||
readonly localChange: Change;
|
||||
readonly remoteChange: Change;
|
||||
readonly mergeState: MergeState;
|
||||
}
|
||||
|
||||
export interface ISyncResourcePreview {
|
||||
readonly isLastSyncFromCurrentMachine: boolean;
|
||||
readonly resourcePreviews: IResourcePreview[];
|
||||
}
|
||||
|
||||
export interface IUserDataInitializer {
|
||||
initialize(userData: IUserData): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IUserDataSynchroniser {
|
||||
|
||||
readonly resource: SyncResource;
|
||||
readonly status: SyncStatus;
|
||||
readonly onDidChangeStatus: Event<SyncStatus>;
|
||||
|
||||
readonly conflicts: IResourcePreview[];
|
||||
readonly onDidChangeConflicts: Event<IResourcePreview[]>;
|
||||
|
||||
readonly onDidChangeLocal: Event<void>;
|
||||
|
||||
sync(manifest: IUserDataManifest | null, headers: IHeaders): Promise<void>;
|
||||
replace(uri: URI): Promise<boolean>;
|
||||
stop(): Promise<void>;
|
||||
|
||||
preview(manifest: IUserDataManifest | null, headers: IHeaders): Promise<ISyncResourcePreview | null>;
|
||||
accept(resource: URI, content?: string | null): Promise<ISyncResourcePreview | null>;
|
||||
merge(resource: URI): Promise<ISyncResourcePreview | null>;
|
||||
discard(resource: URI): Promise<ISyncResourcePreview | null>;
|
||||
apply(force: boolean, headers: IHeaders): Promise<ISyncResourcePreview | null>;
|
||||
|
||||
hasPreviouslySynced(): Promise<boolean>;
|
||||
hasLocalData(): Promise<boolean>;
|
||||
resetLocal(): Promise<void>;
|
||||
|
||||
resolveContent(resource: URI): Promise<string | null>;
|
||||
getRemoteSyncResourceHandles(): Promise<ISyncResourceHandle[]>;
|
||||
getLocalSyncResourceHandles(): Promise<ISyncResourceHandle[]>;
|
||||
getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]>;
|
||||
getMachineId(syncResourceHandle: ISyncResourceHandle): Promise<string | undefined>;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
// #region User Data Sync Services
|
||||
|
||||
export const IUserDataSyncResourceEnablementService = createDecorator<IUserDataSyncResourceEnablementService>('IUserDataSyncResourceEnablementService');
|
||||
export interface IUserDataSyncResourceEnablementService {
|
||||
_serviceBrand: any;
|
||||
|
||||
readonly onDidChangeResourceEnablement: Event<[SyncResource, boolean]>;
|
||||
isResourceEnabled(resource: SyncResource): boolean;
|
||||
setResourceEnablement(resource: SyncResource, enabled: boolean): void;
|
||||
}
|
||||
|
||||
export interface ISyncTask {
|
||||
readonly manifest: IUserDataManifest | null;
|
||||
run(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IManualSyncTask extends IDisposable {
|
||||
readonly id: string;
|
||||
readonly status: SyncStatus;
|
||||
readonly manifest: IUserDataManifest | null;
|
||||
readonly onSynchronizeResources: Event<[SyncResource, URI[]][]>;
|
||||
preview(): Promise<[SyncResource, ISyncResourcePreview][]>;
|
||||
accept(resource: URI, content?: string | null): Promise<[SyncResource, ISyncResourcePreview][]>;
|
||||
merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]>;
|
||||
discard(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]>;
|
||||
discardConflicts(): Promise<[SyncResource, ISyncResourcePreview][]>;
|
||||
apply(): Promise<[SyncResource, ISyncResourcePreview][]>;
|
||||
pull(): Promise<void>;
|
||||
push(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUserDataSyncService');
|
||||
export interface IUserDataSyncService {
|
||||
_serviceBrand: any;
|
||||
|
||||
readonly status: SyncStatus;
|
||||
readonly onDidChangeStatus: Event<SyncStatus>;
|
||||
|
||||
readonly conflicts: [SyncResource, IResourcePreview[]][];
|
||||
readonly onDidChangeConflicts: Event<[SyncResource, IResourcePreview[]][]>;
|
||||
|
||||
readonly onDidChangeLocal: Event<SyncResource>;
|
||||
readonly onSyncErrors: Event<[SyncResource, UserDataSyncError][]>;
|
||||
|
||||
readonly lastSyncTime: number | undefined;
|
||||
readonly onDidChangeLastSyncTime: Event<number>;
|
||||
|
||||
readonly onDidResetRemote: Event<void>;
|
||||
readonly onDidResetLocal: Event<void>;
|
||||
|
||||
createSyncTask(disableCache?: boolean): Promise<ISyncTask>;
|
||||
createManualSyncTask(): Promise<IManualSyncTask>;
|
||||
|
||||
replace(uri: URI): Promise<void>;
|
||||
reset(): Promise<void>;
|
||||
resetRemote(): Promise<void>;
|
||||
resetLocal(): Promise<void>;
|
||||
|
||||
hasLocalData(): Promise<boolean>;
|
||||
hasPreviouslySynced(): Promise<boolean>;
|
||||
resolveContent(resource: URI): Promise<string | null>;
|
||||
accept(resource: SyncResource, conflictResource: URI, content: string | null | undefined, apply: boolean): Promise<void>;
|
||||
|
||||
getLocalSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
|
||||
getRemoteSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
|
||||
getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]>;
|
||||
getMachineId(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<string | undefined>;
|
||||
}
|
||||
|
||||
export const IUserDataAutoSyncEnablementService = createDecorator<IUserDataAutoSyncEnablementService>('IUserDataAutoSyncEnablementService');
|
||||
export interface IUserDataAutoSyncEnablementService {
|
||||
_serviceBrand: any;
|
||||
readonly onDidChangeEnablement: Event<boolean>;
|
||||
isEnabled(): boolean;
|
||||
canToggleEnablement(): boolean;
|
||||
}
|
||||
|
||||
export const IUserDataAutoSyncService = createDecorator<IUserDataAutoSyncService>('IUserDataAutoSyncService');
|
||||
export interface IUserDataAutoSyncService {
|
||||
_serviceBrand: any;
|
||||
readonly onError: Event<UserDataSyncError>;
|
||||
turnOn(): Promise<void>;
|
||||
turnOff(everywhere: boolean): Promise<void>;
|
||||
triggerSync(sources: string[], hasToLimitSync: boolean, disableCache: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
export const IUserDataSyncUtilService = createDecorator<IUserDataSyncUtilService>('IUserDataSyncUtilService');
|
||||
export interface IUserDataSyncUtilService {
|
||||
readonly _serviceBrand: undefined;
|
||||
resolveUserBindings(userbindings: string[]): Promise<IStringDictionary<string>>;
|
||||
resolveFormattingOptions(resource: URI): Promise<FormattingOptions>;
|
||||
resolveDefaultIgnoredSettings(): Promise<string[]>;
|
||||
}
|
||||
|
||||
export const IUserDataSyncLogService = createDecorator<IUserDataSyncLogService>('IUserDataSyncLogService');
|
||||
export interface IUserDataSyncLogService extends ILogService { }
|
||||
|
||||
export interface IConflictSetting {
|
||||
key: string;
|
||||
localValue: any | undefined;
|
||||
remoteValue: any | undefined;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
export const USER_DATA_SYNC_SCHEME = 'vscode-userdata-sync';
|
||||
export const PREVIEW_DIR_NAME = 'preview';
|
||||
export function getSyncResourceFromLocalPreview(localPreview: URI, environmentService: IEnvironmentService): SyncResource | undefined {
|
||||
if (localPreview.scheme === USER_DATA_SYNC_SCHEME) {
|
||||
return undefined;
|
||||
}
|
||||
localPreview = localPreview.with({ scheme: environmentService.userDataSyncHome.scheme });
|
||||
return ALL_SYNC_RESOURCES.filter(syncResource => isEqualOrParent(localPreview, joinPath(environmentService.userDataSyncHome, syncResource, PREVIEW_DIR_NAME)))[0];
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IUserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
|
||||
export interface IUserDataSyncAccount {
|
||||
readonly authenticationProviderId: string;
|
||||
readonly token: string;
|
||||
}
|
||||
|
||||
export const IUserDataSyncAccountService = createDecorator<IUserDataSyncAccountService>('IUserDataSyncAccountService');
|
||||
export interface IUserDataSyncAccountService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly onTokenFailed: Event<boolean>;
|
||||
readonly account: IUserDataSyncAccount | undefined;
|
||||
readonly onDidChangeAccount: Event<IUserDataSyncAccount | undefined>;
|
||||
updateAccount(account: IUserDataSyncAccount | undefined): Promise<void>;
|
||||
|
||||
}
|
||||
|
||||
export class UserDataSyncAccountService extends Disposable implements IUserDataSyncAccountService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private _account: IUserDataSyncAccount | undefined;
|
||||
get account(): IUserDataSyncAccount | undefined { return this._account; }
|
||||
private _onDidChangeAccount = this._register(new Emitter<IUserDataSyncAccount | undefined>());
|
||||
readonly onDidChangeAccount = this._onDidChangeAccount.event;
|
||||
|
||||
private _onTokenFailed: Emitter<boolean> = this._register(new Emitter<boolean>());
|
||||
readonly onTokenFailed: Event<boolean> = this._onTokenFailed.event;
|
||||
|
||||
private wasTokenFailed: boolean = false;
|
||||
|
||||
constructor(
|
||||
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService
|
||||
) {
|
||||
super();
|
||||
this._register(userDataSyncStoreService.onTokenFailed(() => {
|
||||
this.updateAccount(undefined);
|
||||
this._onTokenFailed.fire(this.wasTokenFailed);
|
||||
this.wasTokenFailed = true;
|
||||
}));
|
||||
this._register(userDataSyncStoreService.onTokenSucceed(() => this.wasTokenFailed = false));
|
||||
}
|
||||
|
||||
async updateAccount(account: IUserDataSyncAccount | undefined): Promise<void> {
|
||||
if (account && this._account ? account.token !== this._account.token || account.authenticationProviderId !== this._account.authenticationProviderId : account !== this._account) {
|
||||
this._account = account;
|
||||
if (this._account) {
|
||||
this.userDataSyncStoreService.setAuthToken(this._account.token, this._account.authenticationProviderId);
|
||||
}
|
||||
this._onDidChangeAccount.fire(account);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, } from 'vs/base/common/lifecycle';
|
||||
import { IUserDataSyncLogService, ALL_SYNC_RESOURCES, IUserDataSyncBackupStoreService, IResourceRefHandle, SyncResource } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IFileService, IFileStat } from 'vs/platform/files/common/files';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { toLocalISOString } from 'vs/base/common/date';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
export class UserDataSyncBackupStoreService extends Disposable implements IUserDataSyncBackupStoreService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
) {
|
||||
super();
|
||||
ALL_SYNC_RESOURCES.forEach(resourceKey => this.cleanUpBackup(resourceKey));
|
||||
}
|
||||
|
||||
async getAllRefs(resource: SyncResource): Promise<IResourceRefHandle[]> {
|
||||
const folder = joinPath(this.environmentService.userDataSyncHome, resource);
|
||||
const stat = await this.fileService.resolve(folder);
|
||||
if (stat.children) {
|
||||
const all = stat.children.filter(stat => stat.isFile && /^\d{8}T\d{6}(\.json)?$/.test(stat.name)).sort().reverse();
|
||||
return all.map(stat => ({
|
||||
ref: stat.name,
|
||||
created: this.getCreationTime(stat)
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async resolveContent(resource: SyncResource, ref?: string): Promise<string | null> {
|
||||
if (!ref) {
|
||||
const refs = await this.getAllRefs(resource);
|
||||
if (refs.length) {
|
||||
ref = refs[refs.length - 1].ref;
|
||||
}
|
||||
}
|
||||
if (ref) {
|
||||
const file = joinPath(this.environmentService.userDataSyncHome, resource, ref);
|
||||
const content = await this.fileService.readFile(file);
|
||||
return content.value.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async backup(resourceKey: SyncResource, content: string): Promise<void> {
|
||||
const folder = joinPath(this.environmentService.userDataSyncHome, resourceKey);
|
||||
const resource = joinPath(folder, `${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}.json`);
|
||||
try {
|
||||
await this.fileService.writeFile(resource, VSBuffer.fromString(content));
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
try {
|
||||
this.cleanUpBackup(resourceKey);
|
||||
} catch (e) { /* Ignore */ }
|
||||
}
|
||||
|
||||
private async cleanUpBackup(resource: SyncResource): Promise<void> {
|
||||
const folder = joinPath(this.environmentService.userDataSyncHome, resource);
|
||||
try {
|
||||
try {
|
||||
if (!(await this.fileService.exists(folder))) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
const stat = await this.fileService.resolve(folder);
|
||||
if (stat.children) {
|
||||
const all = stat.children.filter(stat => stat.isFile && /^\d{8}T\d{6}(\.json)?$/.test(stat.name)).sort();
|
||||
const backUpMaxAge = 1000 * 60 * 60 * 24 * (this.configurationService.getValue<number>('sync.localBackupDuration') || 30 /* Default 30 days */);
|
||||
let toDelete = all.filter(stat => Date.now() - this.getCreationTime(stat) > backUpMaxAge);
|
||||
const remaining = all.length - toDelete.length;
|
||||
if (remaining < 10) {
|
||||
toDelete = toDelete.slice(10 - remaining);
|
||||
}
|
||||
await Promise.all(toDelete.map(stat => {
|
||||
this.logService.info('Deleting from backup', stat.resource.path);
|
||||
this.fileService.del(stat.resource);
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private getCreationTime(stat: IFileStat) {
|
||||
return stat.ctime || new Date(
|
||||
parseInt(stat.name.substring(0, 4)),
|
||||
parseInt(stat.name.substring(4, 6)) - 1,
|
||||
parseInt(stat.name.substring(6, 8)),
|
||||
parseInt(stat.name.substring(9, 11)),
|
||||
parseInt(stat.name.substring(11, 13)),
|
||||
parseInt(stat.name.substring(13, 15))
|
||||
).getTime();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IServerChannel, IChannel, IPCServer } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService, IManualSyncTask, IUserDataManifest, IUserDataSyncStoreManagementService, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { IStorageKeysSyncRegistryService, IStorageKey, IExtensionIdWithVersion, AbstractStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';
|
||||
import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount';
|
||||
import { IExtensionIdentifierWithVersion } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
|
||||
export class UserDataSyncChannel implements IServerChannel {
|
||||
|
||||
constructor(private server: IPCServer, private readonly service: IUserDataSyncService, private readonly logService: ILogService) { }
|
||||
|
||||
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;
|
||||
case 'onDidChangeLastSyncTime': return this.service.onDidChangeLastSyncTime;
|
||||
case 'onSyncErrors': return this.service.onSyncErrors;
|
||||
case 'onDidResetLocal': return this.service.onDidResetLocal;
|
||||
case 'onDidResetRemote': return this.service.onDidResetRemote;
|
||||
}
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
async call(context: any, command: string, args?: any): Promise<any> {
|
||||
try {
|
||||
const result = await this._call(context, command, args);
|
||||
return result;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private _call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case '_getInitialData': return Promise.resolve([this.service.status, this.service.conflicts, this.service.lastSyncTime]);
|
||||
|
||||
case 'createManualSyncTask': return this.createManualSyncTask();
|
||||
|
||||
case 'replace': return this.service.replace(URI.revive(args[0]));
|
||||
case 'reset': return this.service.reset();
|
||||
case 'resetRemote': return this.service.resetRemote();
|
||||
case 'resetLocal': return this.service.resetLocal();
|
||||
case 'hasPreviouslySynced': return this.service.hasPreviouslySynced();
|
||||
case 'hasLocalData': return this.service.hasLocalData();
|
||||
case 'accept': return this.service.accept(args[0], URI.revive(args[1]), args[2], args[3]);
|
||||
case 'resolveContent': return this.service.resolveContent(URI.revive(args[0]));
|
||||
case 'getLocalSyncResourceHandles': return this.service.getLocalSyncResourceHandles(args[0]);
|
||||
case 'getRemoteSyncResourceHandles': return this.service.getRemoteSyncResourceHandles(args[0]);
|
||||
case 'getAssociatedResources': return this.service.getAssociatedResources(args[0], { created: args[1].created, uri: URI.revive(args[1].uri) });
|
||||
case 'getMachineId': return this.service.getMachineId(args[0], { created: args[1].created, uri: URI.revive(args[1].uri) });
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
|
||||
private async createManualSyncTask(): Promise<{ id: string, manifest: IUserDataManifest | null, status: SyncStatus }> {
|
||||
const manualSyncTask = await this.service.createManualSyncTask();
|
||||
const manualSyncTaskChannel = new ManualSyncTaskChannel(manualSyncTask, this.logService);
|
||||
this.server.registerChannel(`manualSyncTask-${manualSyncTask.id}`, manualSyncTaskChannel);
|
||||
return { id: manualSyncTask.id, manifest: manualSyncTask.manifest, status: manualSyncTask.status };
|
||||
}
|
||||
}
|
||||
|
||||
class ManualSyncTaskChannel implements IServerChannel {
|
||||
|
||||
constructor(
|
||||
private readonly manualSyncTask: IManualSyncTask,
|
||||
private readonly logService: ILogService
|
||||
) { }
|
||||
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
switch (event) {
|
||||
case 'onSynchronizeResources': return this.manualSyncTask.onSynchronizeResources;
|
||||
}
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
async call(context: any, command: string, args?: any): Promise<any> {
|
||||
try {
|
||||
const result = await this._call(context, command, args);
|
||||
return result;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async _call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'preview': return this.manualSyncTask.preview();
|
||||
case 'accept': return this.manualSyncTask.accept(URI.revive(args[0]), args[1]);
|
||||
case 'merge': return this.manualSyncTask.merge(URI.revive(args[0]));
|
||||
case 'discard': return this.manualSyncTask.discard(URI.revive(args[0]));
|
||||
case 'discardConflicts': return this.manualSyncTask.discardConflicts();
|
||||
case 'apply': return this.manualSyncTask.apply();
|
||||
case 'pull': return this.manualSyncTask.pull();
|
||||
case 'push': return this.manualSyncTask.push();
|
||||
case 'stop': return this.manualSyncTask.stop();
|
||||
case '_getStatus': return this.manualSyncTask.status;
|
||||
case 'dispose': return this.manualSyncTask.dispose();
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class UserDataAutoSyncChannel implements IServerChannel {
|
||||
|
||||
constructor(private readonly service: IUserDataAutoSyncService) { }
|
||||
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
switch (event) {
|
||||
case 'onError': return this.service.onError;
|
||||
}
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'triggerSync': return this.service.triggerSync(args[0], args[1], args[2]);
|
||||
case 'turnOn': return this.service.turnOn();
|
||||
case 'turnOff': return this.service.turnOff(args[0]);
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
}
|
||||
|
||||
export class UserDataSycnUtilServiceChannel implements IServerChannel {
|
||||
|
||||
constructor(private readonly service: IUserDataSyncUtilService) { }
|
||||
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'resolveDefaultIgnoredSettings': return this.service.resolveDefaultIgnoredSettings();
|
||||
case 'resolveUserKeybindings': return this.service.resolveUserBindings(args[0]);
|
||||
case 'resolveFormattingOptions': return this.service.resolveFormattingOptions(URI.revive(args[0]));
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
}
|
||||
|
||||
export class UserDataSyncUtilServiceClient implements IUserDataSyncUtilService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(private readonly channel: IChannel) {
|
||||
}
|
||||
|
||||
async resolveDefaultIgnoredSettings(): Promise<string[]> {
|
||||
return this.channel.call('resolveDefaultIgnoredSettings');
|
||||
}
|
||||
|
||||
async resolveUserBindings(userbindings: string[]): Promise<IStringDictionary<string>> {
|
||||
return this.channel.call('resolveUserKeybindings', [userbindings]);
|
||||
}
|
||||
|
||||
async resolveFormattingOptions(file: URI): Promise<FormattingOptions> {
|
||||
return this.channel.call('resolveFormattingOptions', [file]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type StorageKeysSyncRegistryServiceInitData = { storageKeys: ReadonlyArray<IStorageKey>, extensionsStorageKeys: ReadonlyArray<[IExtensionIdWithVersion, ReadonlyArray<string>]> };
|
||||
|
||||
export class StorageKeysSyncRegistryChannel implements IServerChannel {
|
||||
|
||||
constructor(private readonly service: IStorageKeysSyncRegistryService) { }
|
||||
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
switch (event) {
|
||||
case 'onDidChangeStorageKeys': return this.service.onDidChangeStorageKeys;
|
||||
case 'onDidChangeExtensionStorageKeys': return this.service.onDidChangeExtensionStorageKeys;
|
||||
}
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case '_getInitialData': return Promise.resolve<StorageKeysSyncRegistryServiceInitData>({ storageKeys: this.service.storageKeys, extensionsStorageKeys: this.service.extensionsStorageKeys });
|
||||
case 'registerStorageKey': return Promise.resolve(this.service.registerStorageKey(args[0]));
|
||||
case 'registerExtensionStorageKeys': return Promise.resolve(this.service.registerExtensionStorageKeys(args[0], args[1]));
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
}
|
||||
|
||||
export class StorageKeysSyncRegistryChannelClient extends AbstractStorageKeysSyncRegistryService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(private readonly channel: IChannel) {
|
||||
super();
|
||||
this.channel.call<StorageKeysSyncRegistryServiceInitData>('_getInitialData').then(({ storageKeys, extensionsStorageKeys }) => {
|
||||
this.updateStorageKeys(storageKeys);
|
||||
this.updateExtensionsStorageKeys(extensionsStorageKeys);
|
||||
this._register(this.channel.listen<ReadonlyArray<IStorageKey>>('onDidChangeStorageKeys')(storageKeys => this.updateStorageKeys(storageKeys)));
|
||||
this._register(this.channel.listen<[IExtensionIdentifierWithVersion, string[]]>('onDidChangeExtensionStorageKeys')(e => this.updateExtensionStorageKeys(e[0], e[1])));
|
||||
});
|
||||
}
|
||||
|
||||
private async updateStorageKeys(storageKeys: ReadonlyArray<IStorageKey>): Promise<void> {
|
||||
this._storageKeys.clear();
|
||||
for (const storageKey of storageKeys) {
|
||||
this._storageKeys.set(storageKey.key, storageKey);
|
||||
}
|
||||
this._onDidChangeStorageKeys.fire(this.storageKeys);
|
||||
}
|
||||
|
||||
private async updateExtensionsStorageKeys(extensionStorageKeys: ReadonlyArray<[IExtensionIdentifierWithVersion, ReadonlyArray<string>]>): Promise<void> {
|
||||
for (const [extension, keys] of extensionStorageKeys) {
|
||||
this.updateExtensionStorageKeys(extension, [...keys]);
|
||||
}
|
||||
}
|
||||
|
||||
registerStorageKey(storageKey: IStorageKey): void {
|
||||
this.channel.call('registerStorageKey', [storageKey]);
|
||||
}
|
||||
|
||||
registerExtensionStorageKeys(extension: IExtensionIdentifierWithVersion, keys: string[]): void {
|
||||
this.channel.call('registerExtensionStorageKeys', [extension, keys]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class UserDataSyncMachinesServiceChannel implements IServerChannel {
|
||||
|
||||
constructor(private readonly service: IUserDataSyncMachinesService) { }
|
||||
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
switch (event) {
|
||||
case 'onDidChange': return this.service.onDidChange;
|
||||
}
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
async call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'getMachines': return this.service.getMachines();
|
||||
case 'addCurrentMachine': return this.service.addCurrentMachine();
|
||||
case 'removeCurrentMachine': return this.service.removeCurrentMachine();
|
||||
case 'renameMachine': return this.service.renameMachine(args[0], args[1]);
|
||||
case 'setEnablement': return this.service.setEnablement(args[0], args[1]);
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class UserDataSyncAccountServiceChannel implements IServerChannel {
|
||||
constructor(private readonly service: IUserDataSyncAccountService) { }
|
||||
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
switch (event) {
|
||||
case 'onDidChangeAccount': return this.service.onDidChangeAccount;
|
||||
case 'onTokenFailed': return this.service.onTokenFailed;
|
||||
}
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case '_getInitialData': return Promise.resolve(this.service.account);
|
||||
case 'updateAccount': return this.service.updateAccount(args);
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
}
|
||||
|
||||
export class UserDataSyncStoreManagementServiceChannel implements IServerChannel {
|
||||
constructor(private readonly service: IUserDataSyncStoreManagementService) { }
|
||||
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
switch (event) {
|
||||
case 'onDidChangeUserDataSyncStore': return this.service.onDidChangeUserDataSyncStore;
|
||||
}
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'switch': return this.service.switch(args[0]);
|
||||
case 'getPreviousUserDataSyncStore': return this.service.getPreviousUserDataSyncStore();
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { AbstractLogService, ILoggerService, ILogger } from 'vs/platform/log/common/log';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
|
||||
export class UserDataSyncLogService extends AbstractLogService implements IUserDataSyncLogService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(
|
||||
@ILoggerService loggerService: ILoggerService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService
|
||||
) {
|
||||
super();
|
||||
this.logger = this._register(loggerService.getLogger(environmentService.userDataSyncLogResource));
|
||||
}
|
||||
|
||||
trace(message: string, ...args: any[]): void {
|
||||
this.logger.trace(message, ...args);
|
||||
}
|
||||
|
||||
debug(message: string, ...args: any[]): void {
|
||||
this.logger.debug(message, ...args);
|
||||
}
|
||||
|
||||
info(message: string, ...args: any[]): void {
|
||||
this.logger.info(message, ...args);
|
||||
}
|
||||
|
||||
warn(message: string, ...args: any[]): void {
|
||||
this.logger.warn(message, ...args);
|
||||
}
|
||||
|
||||
error(message: string | Error, ...args: any[]): void {
|
||||
this.logger.error(message, ...args);
|
||||
}
|
||||
|
||||
critical(message: string | Error, ...args: any[]): void {
|
||||
this.logger.critical(message, ...args);
|
||||
}
|
||||
|
||||
flush(): void {
|
||||
this.logger.flush();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { IUserDataSyncStoreService, IUserData, IUserDataSyncLogService, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { PlatformToString, isWeb, Platform, platform } from 'vs/base/common/platform';
|
||||
import { escapeRegExpCharacters } from 'vs/base/common/strings';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
|
||||
interface IMachineData {
|
||||
id: string;
|
||||
name: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface IMachinesData {
|
||||
version: number;
|
||||
machines: IMachineData[];
|
||||
}
|
||||
|
||||
export type IUserDataSyncMachine = Readonly<IMachineData> & { readonly isCurrent: boolean };
|
||||
|
||||
export const IUserDataSyncMachinesService = createDecorator<IUserDataSyncMachinesService>('IUserDataSyncMachinesService');
|
||||
export interface IUserDataSyncMachinesService {
|
||||
_serviceBrand: any;
|
||||
|
||||
readonly onDidChange: Event<void>;
|
||||
|
||||
getMachines(manifest?: IUserDataManifest): Promise<IUserDataSyncMachine[]>;
|
||||
|
||||
addCurrentMachine(manifest?: IUserDataManifest): Promise<void>;
|
||||
removeCurrentMachine(manifest?: IUserDataManifest): Promise<void>;
|
||||
renameMachine(machineId: string, name: string): Promise<void>;
|
||||
setEnablement(machineId: string, enabled: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
const currentMachineNameKey = 'sync.currentMachineName';
|
||||
|
||||
export class UserDataSyncMachinesService extends Disposable implements IUserDataSyncMachinesService {
|
||||
|
||||
private static readonly VERSION = 1;
|
||||
private static readonly RESOURCE = 'machines';
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private readonly _onDidChange = this._register(new Emitter<void>());
|
||||
readonly onDidChange = this._onDidChange.event;
|
||||
|
||||
private readonly currentMachineIdPromise: Promise<string>;
|
||||
private userData: IUserData | null = null;
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
) {
|
||||
super();
|
||||
this.currentMachineIdPromise = getServiceMachineId(environmentService, fileService, storageService);
|
||||
}
|
||||
|
||||
async getMachines(manifest?: IUserDataManifest): Promise<IUserDataSyncMachine[]> {
|
||||
const currentMachineId = await this.currentMachineIdPromise;
|
||||
const machineData = await this.readMachinesData(manifest);
|
||||
return machineData.machines.map<IUserDataSyncMachine>(machine => ({ ...machine, ...{ isCurrent: machine.id === currentMachineId } }));
|
||||
}
|
||||
|
||||
async addCurrentMachine(manifest?: IUserDataManifest): Promise<void> {
|
||||
const currentMachineId = await this.currentMachineIdPromise;
|
||||
const machineData = await this.readMachinesData(manifest);
|
||||
if (!machineData.machines.some(({ id }) => id === currentMachineId)) {
|
||||
machineData.machines.push({ id: currentMachineId, name: this.computeCurrentMachineName(machineData.machines) });
|
||||
await this.writeMachinesData(machineData);
|
||||
}
|
||||
}
|
||||
|
||||
async removeCurrentMachine(manifest?: IUserDataManifest): Promise<void> {
|
||||
const currentMachineId = await this.currentMachineIdPromise;
|
||||
const machineData = await this.readMachinesData(manifest);
|
||||
const updatedMachines = machineData.machines.filter(({ id }) => id !== currentMachineId);
|
||||
if (updatedMachines.length !== machineData.machines.length) {
|
||||
machineData.machines = updatedMachines;
|
||||
await this.writeMachinesData(machineData);
|
||||
}
|
||||
}
|
||||
|
||||
async renameMachine(machineId: string, name: string, manifest?: IUserDataManifest): Promise<void> {
|
||||
const currentMachineId = await this.currentMachineIdPromise;
|
||||
const machineData = await this.readMachinesData(manifest);
|
||||
const machine = machineData.machines.find(({ id }) => id === machineId);
|
||||
if (machine) {
|
||||
machine.name = name;
|
||||
await this.writeMachinesData(machineData);
|
||||
if (machineData.machines.some(({ id }) => id === currentMachineId)) {
|
||||
this.storageService.store(currentMachineNameKey, name, StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async setEnablement(machineId: string, enabled: boolean): Promise<void> {
|
||||
const machineData = await this.readMachinesData();
|
||||
const machine = machineData.machines.find(({ id }) => id === machineId);
|
||||
if (machine) {
|
||||
machine.disabled = enabled ? undefined : true;
|
||||
await this.writeMachinesData(machineData);
|
||||
}
|
||||
}
|
||||
|
||||
private computeCurrentMachineName(machines: IMachineData[]): string {
|
||||
const previousName = this.storageService.get(currentMachineNameKey, StorageScope.GLOBAL);
|
||||
if (previousName) {
|
||||
return previousName;
|
||||
}
|
||||
|
||||
const namePrefix = `${this.productService.nameLong} (${PlatformToString(isWeb ? Platform.Web : platform)})`;
|
||||
const nameRegEx = new RegExp(`${escapeRegExpCharacters(namePrefix)}\\s#(\\d+)`);
|
||||
let nameIndex = 0;
|
||||
for (const machine of machines) {
|
||||
const matches = nameRegEx.exec(machine.name);
|
||||
const index = matches ? parseInt(matches[1]) : 0;
|
||||
nameIndex = index > nameIndex ? index : nameIndex;
|
||||
}
|
||||
return `${namePrefix} #${nameIndex + 1}`;
|
||||
}
|
||||
|
||||
private async readMachinesData(manifest?: IUserDataManifest): Promise<IMachinesData> {
|
||||
this.userData = await this.readUserData(manifest);
|
||||
const machinesData = this.parse(this.userData);
|
||||
if (machinesData.version !== UserDataSyncMachinesService.VERSION) {
|
||||
throw new Error(localize('error incompatible', "Cannot read machines data as the current version is incompatible. Please update {0} and try again.", this.productService.nameLong));
|
||||
}
|
||||
return machinesData;
|
||||
}
|
||||
|
||||
private async writeMachinesData(machinesData: IMachinesData): Promise<void> {
|
||||
const content = JSON.stringify(machinesData);
|
||||
const ref = await this.userDataSyncStoreService.write(UserDataSyncMachinesService.RESOURCE, content, this.userData?.ref || null);
|
||||
this.userData = { ref, content };
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
|
||||
private async readUserData(manifest?: IUserDataManifest): Promise<IUserData> {
|
||||
if (this.userData) {
|
||||
|
||||
const latestRef = manifest && manifest.latest ? manifest.latest[UserDataSyncMachinesService.RESOURCE] : undefined;
|
||||
|
||||
// Last time synced resource and latest resource on server are same
|
||||
if (this.userData.ref === latestRef) {
|
||||
return this.userData;
|
||||
}
|
||||
|
||||
// There is no resource on server and last time it was synced with no resource
|
||||
if (latestRef === undefined && this.userData.content === null) {
|
||||
return this.userData;
|
||||
}
|
||||
}
|
||||
|
||||
return this.userDataSyncStoreService.read(UserDataSyncMachinesService.RESOURCE, this.userData);
|
||||
}
|
||||
|
||||
private parse(userData: IUserData): IMachinesData {
|
||||
if (userData.content !== null) {
|
||||
try {
|
||||
return JSON.parse(userData.content);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
return {
|
||||
version: UserDataSyncMachinesService.VERSION,
|
||||
machines: []
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IUserDataSyncResourceEnablementService, ALL_SYNC_RESOURCES, SyncResource } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IStorageService, IWorkspaceStorageChangeEvent, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
type SyncEnablementClassification = {
|
||||
enabled?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
};
|
||||
|
||||
const enablementKey = 'sync.enable';
|
||||
function getEnablementKey(resource: SyncResource) { return `${enablementKey}.${resource}`; }
|
||||
|
||||
export class UserDataSyncResourceEnablementService extends Disposable implements IUserDataSyncResourceEnablementService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private _onDidChangeResourceEnablement = new Emitter<[SyncResource, boolean]>();
|
||||
readonly onDidChangeResourceEnablement: Event<[SyncResource, boolean]> = this._onDidChangeResourceEnablement.event;
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
) {
|
||||
super();
|
||||
this._register(storageService.onDidChangeStorage(e => this.onDidStorageChange(e)));
|
||||
}
|
||||
|
||||
isResourceEnabled(resource: SyncResource): boolean {
|
||||
return this.storageService.getBoolean(getEnablementKey(resource), StorageScope.GLOBAL, true);
|
||||
}
|
||||
|
||||
setResourceEnablement(resource: SyncResource, enabled: boolean): void {
|
||||
if (this.isResourceEnabled(resource) !== enabled) {
|
||||
const resourceEnablementKey = getEnablementKey(resource);
|
||||
this.telemetryService.publicLog2<{ enabled: boolean }, SyncEnablementClassification>(resourceEnablementKey, { enabled });
|
||||
this.storageService.store(resourceEnablementKey, enabled, StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
|
||||
private onDidStorageChange(workspaceStorageChangeEvent: IWorkspaceStorageChangeEvent): void {
|
||||
if (workspaceStorageChangeEvent.scope === StorageScope.GLOBAL) {
|
||||
const resourceKey = ALL_SYNC_RESOURCES.filter(resourceKey => getEnablementKey(resourceKey) === workspaceStorageChangeEvent.key)[0];
|
||||
if (resourceKey) {
|
||||
this._onDidChangeResourceEnablement.fire([resourceKey, this.isResourceEnabled(resourceKey)]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,706 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import {
|
||||
IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode,
|
||||
UserDataSyncError, ISyncResourceHandle, IUserDataManifest, ISyncTask, IResourcePreview, IManualSyncTask, ISyncResourcePreview, HEADER_EXECUTION_ID, MergeState, Change, IUserDataSyncStoreManagementService
|
||||
} 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';
|
||||
import { ExtensionsSynchroniser } from 'vs/platform/userDataSync/common/extensionsSync';
|
||||
import { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync';
|
||||
import { GlobalStateSynchroniser } from 'vs/platform/userDataSync/common/globalStateSync';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IHeaders } from 'vs/base/parts/request/common/request';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async';
|
||||
import { isPromiseCanceledError } from 'vs/base/common/errors';
|
||||
|
||||
type SyncErrorClassification = {
|
||||
code: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
service: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
resource?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
executionId?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
};
|
||||
|
||||
const LAST_SYNC_TIME_KEY = 'sync.lastSyncTime';
|
||||
|
||||
function createSyncHeaders(executionId: string): IHeaders {
|
||||
const headers: IHeaders = {};
|
||||
headers[HEADER_EXECUTION_ID] = executionId;
|
||||
return headers;
|
||||
}
|
||||
|
||||
export class UserDataSyncService extends Disposable implements IUserDataSyncService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private readonly synchronisers: IUserDataSynchroniser[];
|
||||
|
||||
private _status: SyncStatus = SyncStatus.Uninitialized;
|
||||
get status(): SyncStatus { return this._status; }
|
||||
private _onDidChangeStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
|
||||
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangeStatus.event;
|
||||
|
||||
readonly onDidChangeLocal: Event<SyncResource>;
|
||||
|
||||
private _conflicts: [SyncResource, IResourcePreview[]][] = [];
|
||||
get conflicts(): [SyncResource, IResourcePreview[]][] { return this._conflicts; }
|
||||
private _onDidChangeConflicts: Emitter<[SyncResource, IResourcePreview[]][]> = this._register(new Emitter<[SyncResource, IResourcePreview[]][]>());
|
||||
readonly onDidChangeConflicts: Event<[SyncResource, IResourcePreview[]][]> = this._onDidChangeConflicts.event;
|
||||
|
||||
private _syncErrors: [SyncResource, UserDataSyncError][] = [];
|
||||
private _onSyncErrors: Emitter<[SyncResource, UserDataSyncError][]> = this._register(new Emitter<[SyncResource, UserDataSyncError][]>());
|
||||
readonly onSyncErrors: Event<[SyncResource, UserDataSyncError][]> = this._onSyncErrors.event;
|
||||
|
||||
private _lastSyncTime: number | undefined = undefined;
|
||||
get lastSyncTime(): number | undefined { return this._lastSyncTime; }
|
||||
private _onDidChangeLastSyncTime: Emitter<number> = this._register(new Emitter<number>());
|
||||
readonly onDidChangeLastSyncTime: Event<number> = this._onDidChangeLastSyncTime.event;
|
||||
|
||||
private _onDidResetLocal = this._register(new Emitter<void>());
|
||||
readonly onDidResetLocal = this._onDidResetLocal.event;
|
||||
private _onDidResetRemote = this._register(new Emitter<void>());
|
||||
readonly onDidResetRemote = this._onDidResetRemote.event;
|
||||
|
||||
private readonly settingsSynchroniser: SettingsSynchroniser;
|
||||
private readonly keybindingsSynchroniser: KeybindingsSynchroniser;
|
||||
private readonly snippetsSynchroniser: SnippetsSynchroniser;
|
||||
private readonly extensionsSynchroniser: ExtensionsSynchroniser;
|
||||
private readonly globalStateSynchroniser: GlobalStateSynchroniser;
|
||||
|
||||
constructor(
|
||||
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
) {
|
||||
super();
|
||||
this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser));
|
||||
this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser));
|
||||
this.snippetsSynchroniser = this._register(this.instantiationService.createInstance(SnippetsSynchroniser));
|
||||
this.globalStateSynchroniser = this._register(this.instantiationService.createInstance(GlobalStateSynchroniser));
|
||||
this.extensionsSynchroniser = this._register(this.instantiationService.createInstance(ExtensionsSynchroniser));
|
||||
this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.globalStateSynchroniser, this.extensionsSynchroniser];
|
||||
this.updateStatus();
|
||||
|
||||
if (this.userDataSyncStoreManagementService.userDataSyncStore) {
|
||||
this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus()));
|
||||
this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeConflicts, () => undefined)))(() => this.updateConflicts()));
|
||||
}
|
||||
|
||||
this._lastSyncTime = this.storageService.getNumber(LAST_SYNC_TIME_KEY, StorageScope.GLOBAL, undefined);
|
||||
this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeLocal, () => s.resource)));
|
||||
}
|
||||
|
||||
async createSyncTask(disableCache?: boolean): Promise<ISyncTask> {
|
||||
await this.checkEnablement();
|
||||
|
||||
const executionId = generateUuid();
|
||||
let manifest: IUserDataManifest | null;
|
||||
try {
|
||||
const syncHeaders = createSyncHeaders(executionId);
|
||||
if (disableCache) {
|
||||
syncHeaders['Cache-Control'] = 'no-cache';
|
||||
}
|
||||
manifest = await this.userDataSyncStoreService.manifest(syncHeaders);
|
||||
} catch (error) {
|
||||
error = UserDataSyncError.toUserDataSyncError(error);
|
||||
this.telemetryService.publicLog2<{ code: string, service: string, resource?: string, executionId?: string }, SyncErrorClassification>('sync/error', { code: error.code, resource: error.resource, executionId, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() });
|
||||
throw error;
|
||||
}
|
||||
|
||||
let executed = false;
|
||||
const that = this;
|
||||
let cancellablePromise: CancelablePromise<void> | undefined;
|
||||
return {
|
||||
manifest,
|
||||
run(): Promise<void> {
|
||||
if (executed) {
|
||||
throw new Error('Can run a task only once');
|
||||
}
|
||||
cancellablePromise = createCancelablePromise(token => that.sync(manifest, executionId, token));
|
||||
return cancellablePromise.finally(() => cancellablePromise = undefined);
|
||||
},
|
||||
async stop(): Promise<void> {
|
||||
if (cancellablePromise) {
|
||||
cancellablePromise.cancel();
|
||||
}
|
||||
if (that.status !== SyncStatus.Idle) {
|
||||
return that.stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async createManualSyncTask(): Promise<IManualSyncTask> {
|
||||
await this.checkEnablement();
|
||||
|
||||
const executionId = generateUuid();
|
||||
const syncHeaders = createSyncHeaders(executionId);
|
||||
|
||||
let manifest: IUserDataManifest | null;
|
||||
try {
|
||||
manifest = await this.userDataSyncStoreService.manifest(syncHeaders);
|
||||
} catch (error) {
|
||||
error = UserDataSyncError.toUserDataSyncError(error);
|
||||
this.telemetryService.publicLog2<{ code: string, service: string, resource?: string, executionId?: string }, SyncErrorClassification>('sync/error', { code: error.code, resource: error.resource, executionId, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() });
|
||||
throw error;
|
||||
}
|
||||
|
||||
return new ManualSyncTask(executionId, manifest, syncHeaders, this.synchronisers, this.logService);
|
||||
}
|
||||
|
||||
private recoveredSettings: boolean = false;
|
||||
private async sync(manifest: IUserDataManifest | null, executionId: string, token: CancellationToken): Promise<void> {
|
||||
if (!this.recoveredSettings) {
|
||||
await this.settingsSynchroniser.recoverSettings();
|
||||
this.recoveredSettings = true;
|
||||
}
|
||||
|
||||
// Return if cancellation is requested
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = new Date().getTime();
|
||||
this._syncErrors = [];
|
||||
try {
|
||||
this.logService.trace('Sync started.');
|
||||
if (this.status !== SyncStatus.HasConflicts) {
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
}
|
||||
|
||||
const syncHeaders = createSyncHeaders(executionId);
|
||||
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
// Return if cancellation is requested
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await synchroniser.sync(manifest, syncHeaders);
|
||||
} catch (e) {
|
||||
this.handleSynchronizerError(e, synchroniser.resource);
|
||||
this._syncErrors.push([synchroniser.resource, UserDataSyncError.toUserDataSyncError(e)]);
|
||||
}
|
||||
}
|
||||
|
||||
this.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`);
|
||||
this.updateLastSyncTime();
|
||||
} catch (error) {
|
||||
error = UserDataSyncError.toUserDataSyncError(error);
|
||||
this.telemetryService.publicLog2<{ code: string, service: string, resource?: string, executionId?: string }, SyncErrorClassification>('sync/error', { code: error.code, resource: error.resource, executionId, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() });
|
||||
throw error;
|
||||
} finally {
|
||||
this.updateStatus();
|
||||
this._onSyncErrors.fire(this._syncErrors);
|
||||
}
|
||||
}
|
||||
|
||||
private async stop(): Promise<void> {
|
||||
if (this.status === SyncStatus.Idle) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
try {
|
||||
if (synchroniser.status !== SyncStatus.Idle) {
|
||||
await synchroniser.stop();
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async replace(uri: URI): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
if (await synchroniser.replace(uri)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async accept(syncResource: SyncResource, resource: URI, content: string | null | undefined, apply: boolean): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
const synchroniser = this.getSynchroniser(syncResource);
|
||||
await synchroniser.accept(resource, content);
|
||||
if (apply) {
|
||||
await synchroniser.apply(false, createSyncHeaders(generateUuid()));
|
||||
}
|
||||
}
|
||||
|
||||
async resolveContent(resource: URI): Promise<string | null> {
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
const content = await synchroniser.resolveContent(resource);
|
||||
if (content) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getRemoteSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]> {
|
||||
return this.getSynchroniser(resource).getRemoteSyncResourceHandles();
|
||||
}
|
||||
|
||||
getLocalSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]> {
|
||||
return this.getSynchroniser(resource).getLocalSyncResourceHandles();
|
||||
}
|
||||
|
||||
getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
|
||||
return this.getSynchroniser(resource).getAssociatedResources(syncResourceHandle);
|
||||
}
|
||||
|
||||
getMachineId(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<string | undefined> {
|
||||
return this.getSynchroniser(resource).getMachineId(syncResourceHandle);
|
||||
}
|
||||
|
||||
async hasLocalData(): Promise<boolean> {
|
||||
// skip global state synchronizer
|
||||
const synchronizers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.extensionsSynchroniser];
|
||||
for (const synchroniser of synchronizers) {
|
||||
if (await synchroniser.hasLocalData()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async reset(): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
await this.resetRemote();
|
||||
await this.resetLocal();
|
||||
}
|
||||
|
||||
async resetRemote(): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
try {
|
||||
await this.userDataSyncStoreService.clear();
|
||||
this.logService.info('Cleared data on server');
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
this._onDidResetRemote.fire();
|
||||
}
|
||||
|
||||
async resetLocal(): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
this.storageService.remove(LAST_SYNC_TIME_KEY, StorageScope.GLOBAL);
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
try {
|
||||
await synchroniser.resetLocal();
|
||||
} catch (e) {
|
||||
this.logService.error(`${synchroniser.resource}: ${toErrorMessage(e)}`);
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
this._onDidResetLocal.fire();
|
||||
this.logService.info('Did reset the local sync state.');
|
||||
}
|
||||
|
||||
async hasPreviouslySynced(): Promise<boolean> {
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
if (await synchroniser.hasPreviouslySynced()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private setStatus(status: SyncStatus): void {
|
||||
const oldStatus = this._status;
|
||||
if (this._status !== status) {
|
||||
this._status = status;
|
||||
this._onDidChangeStatus.fire(status);
|
||||
if (oldStatus === SyncStatus.HasConflicts) {
|
||||
this.updateLastSyncTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateStatus(): void {
|
||||
this.updateConflicts();
|
||||
const status = this.computeStatus();
|
||||
this.setStatus(status);
|
||||
}
|
||||
|
||||
private updateConflicts(): void {
|
||||
const conflicts = this.computeConflicts();
|
||||
if (!equals(this._conflicts, conflicts, ([syncResourceA, conflictsA], [syncResourceB, conflictsB]) => syncResourceA === syncResourceA && equals(conflictsA, conflictsB, (a, b) => isEqual(a.previewResource, b.previewResource)))) {
|
||||
this._conflicts = this.computeConflicts();
|
||||
this._onDidChangeConflicts.fire(conflicts);
|
||||
}
|
||||
}
|
||||
|
||||
private computeStatus(): SyncStatus {
|
||||
if (!this.userDataSyncStoreManagementService.userDataSyncStore) {
|
||||
return SyncStatus.Uninitialized;
|
||||
}
|
||||
if (this.synchronisers.some(s => s.status === SyncStatus.HasConflicts)) {
|
||||
return SyncStatus.HasConflicts;
|
||||
}
|
||||
if (this.synchronisers.some(s => s.status === SyncStatus.Syncing)) {
|
||||
return SyncStatus.Syncing;
|
||||
}
|
||||
return SyncStatus.Idle;
|
||||
}
|
||||
|
||||
private updateLastSyncTime(): void {
|
||||
if (this.status === SyncStatus.Idle) {
|
||||
this._lastSyncTime = new Date().getTime();
|
||||
this.storageService.store(LAST_SYNC_TIME_KEY, this._lastSyncTime, StorageScope.GLOBAL);
|
||||
this._onDidChangeLastSyncTime.fire(this._lastSyncTime);
|
||||
}
|
||||
}
|
||||
|
||||
private handleSynchronizerError(e: Error, source: SyncResource): void {
|
||||
if (e instanceof UserDataSyncError) {
|
||||
switch (e.code) {
|
||||
case UserDataSyncErrorCode.TooLarge:
|
||||
throw new UserDataSyncError(e.message, e.code, source);
|
||||
|
||||
case UserDataSyncErrorCode.TooManyRequests:
|
||||
case UserDataSyncErrorCode.TooManyRequestsAndRetryAfter:
|
||||
case UserDataSyncErrorCode.LocalTooManyRequests:
|
||||
case UserDataSyncErrorCode.Gone:
|
||||
case UserDataSyncErrorCode.UpgradeRequired:
|
||||
case UserDataSyncErrorCode.IncompatibleRemoteContent:
|
||||
case UserDataSyncErrorCode.IncompatibleLocalContent:
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
this.logService.error(e);
|
||||
this.logService.error(`${source}: ${toErrorMessage(e)}`);
|
||||
}
|
||||
|
||||
private computeConflicts(): [SyncResource, IResourcePreview[]][] {
|
||||
return this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)
|
||||
.map(s => ([s.resource, s.conflicts.map(toStrictResourcePreview)]));
|
||||
}
|
||||
|
||||
getSynchroniser(source: SyncResource): IUserDataSynchroniser {
|
||||
return this.synchronisers.find(s => s.resource === source)!;
|
||||
}
|
||||
|
||||
private async checkEnablement(): Promise<void> {
|
||||
if (!this.userDataSyncStoreManagementService.userDataSyncStore) {
|
||||
throw new Error('Not enabled');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ManualSyncTask extends Disposable implements IManualSyncTask {
|
||||
|
||||
private previewsPromise: CancelablePromise<[SyncResource, ISyncResourcePreview][]> | undefined;
|
||||
private previews: [SyncResource, ISyncResourcePreview][] | undefined;
|
||||
|
||||
private synchronizingResources: [SyncResource, URI[]][] = [];
|
||||
private _onSynchronizeResources = this._register(new Emitter<[SyncResource, URI[]][]>());
|
||||
readonly onSynchronizeResources = this._onSynchronizeResources.event;
|
||||
|
||||
private isDisposed: boolean = false;
|
||||
|
||||
get status(): SyncStatus {
|
||||
if (this.synchronisers.some(s => s.status === SyncStatus.HasConflicts)) {
|
||||
return SyncStatus.HasConflicts;
|
||||
}
|
||||
if (this.synchronisers.some(s => s.status === SyncStatus.Syncing)) {
|
||||
return SyncStatus.Syncing;
|
||||
}
|
||||
return SyncStatus.Idle;
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly id: string,
|
||||
readonly manifest: IUserDataManifest | null,
|
||||
private readonly syncHeaders: IHeaders,
|
||||
private readonly synchronisers: IUserDataSynchroniser[],
|
||||
private readonly logService: IUserDataSyncLogService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async preview(): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
if (this.isDisposed) {
|
||||
throw new Error('Disposed');
|
||||
}
|
||||
if (!this.previewsPromise) {
|
||||
this.previewsPromise = createCancelablePromise(token => this.getPreviews(token));
|
||||
}
|
||||
if (!this.previews) {
|
||||
this.previews = await this.previewsPromise;
|
||||
}
|
||||
return this.previews;
|
||||
}
|
||||
|
||||
async accept(resource: URI, content?: string | null): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
return this.performAction(resource, sychronizer => sychronizer.accept(resource, content));
|
||||
}
|
||||
|
||||
async merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
if (resource) {
|
||||
return this.performAction(resource, sychronizer => sychronizer.merge(resource));
|
||||
} else {
|
||||
return this.mergeAll();
|
||||
}
|
||||
}
|
||||
|
||||
async discard(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
return this.performAction(resource, sychronizer => sychronizer.discard(resource));
|
||||
}
|
||||
|
||||
async discardConflicts(): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
if (!this.previews) {
|
||||
throw new Error('Missing preview. Create preview and try again.');
|
||||
}
|
||||
if (this.synchronizingResources.length) {
|
||||
throw new Error('Cannot discard while synchronizing resources');
|
||||
}
|
||||
|
||||
const conflictResources: URI[] = [];
|
||||
for (const [, syncResourcePreview] of this.previews) {
|
||||
for (const resourcePreview of syncResourcePreview.resourcePreviews) {
|
||||
if (resourcePreview.mergeState === MergeState.Conflict) {
|
||||
conflictResources.push(resourcePreview.previewResource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const resource of conflictResources) {
|
||||
await this.discard(resource);
|
||||
}
|
||||
return this.previews;
|
||||
}
|
||||
|
||||
async apply(): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
if (!this.previews) {
|
||||
throw new Error('You need to create preview before applying');
|
||||
}
|
||||
if (this.synchronizingResources.length) {
|
||||
throw new Error('Cannot pull while synchronizing resources');
|
||||
}
|
||||
const previews: [SyncResource, ISyncResourcePreview][] = [];
|
||||
for (const [syncResource, preview] of this.previews) {
|
||||
this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]);
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
|
||||
const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!;
|
||||
|
||||
/* merge those which are not yet merged */
|
||||
for (const resourcePreview of preview.resourcePreviews) {
|
||||
if ((resourcePreview.localChange !== Change.None || resourcePreview.remoteChange !== Change.None) && resourcePreview.mergeState === MergeState.Preview) {
|
||||
await synchroniser.merge(resourcePreview.previewResource);
|
||||
}
|
||||
}
|
||||
|
||||
/* apply */
|
||||
const newPreview = await synchroniser.apply(false, this.syncHeaders);
|
||||
if (newPreview) {
|
||||
previews.push(this.toSyncResourcePreview(synchroniser.resource, newPreview));
|
||||
}
|
||||
|
||||
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
}
|
||||
this.previews = previews;
|
||||
return this.previews;
|
||||
}
|
||||
|
||||
async pull(): Promise<void> {
|
||||
if (!this.previews) {
|
||||
throw new Error('You need to create preview before applying');
|
||||
}
|
||||
if (this.synchronizingResources.length) {
|
||||
throw new Error('Cannot pull while synchronizing resources');
|
||||
}
|
||||
for (const [syncResource, preview] of this.previews) {
|
||||
this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]);
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!;
|
||||
for (const resourcePreview of preview.resourcePreviews) {
|
||||
await synchroniser.accept(resourcePreview.remoteResource);
|
||||
}
|
||||
await synchroniser.apply(true, this.syncHeaders);
|
||||
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
}
|
||||
this.previews = [];
|
||||
}
|
||||
|
||||
async push(): Promise<void> {
|
||||
if (!this.previews) {
|
||||
throw new Error('You need to create preview before applying');
|
||||
}
|
||||
if (this.synchronizingResources.length) {
|
||||
throw new Error('Cannot pull while synchronizing resources');
|
||||
}
|
||||
for (const [syncResource, preview] of this.previews) {
|
||||
this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]);
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!;
|
||||
for (const resourcePreview of preview.resourcePreviews) {
|
||||
await synchroniser.accept(resourcePreview.localResource);
|
||||
}
|
||||
await synchroniser.apply(true, this.syncHeaders);
|
||||
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
}
|
||||
this.previews = [];
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
try {
|
||||
await synchroniser.stop();
|
||||
} catch (error) {
|
||||
if (!isPromiseCanceledError(error)) {
|
||||
this.logService.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.reset();
|
||||
}
|
||||
|
||||
private async performAction(resource: URI, action: (synchroniser: IUserDataSynchroniser) => Promise<ISyncResourcePreview | null>): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
if (!this.previews) {
|
||||
throw new Error('Missing preview. Create preview and try again.');
|
||||
}
|
||||
|
||||
const index = this.previews.findIndex(([, preview]) => preview.resourcePreviews.some(({ localResource, previewResource, remoteResource }) =>
|
||||
isEqual(resource, localResource) || isEqual(resource, previewResource) || isEqual(resource, remoteResource)));
|
||||
if (index === -1) {
|
||||
return this.previews;
|
||||
}
|
||||
|
||||
const [syncResource, previews] = this.previews[index];
|
||||
const resourcePreview = previews.resourcePreviews.find(({ localResource, remoteResource, previewResource }) => isEqual(localResource, resource) || isEqual(remoteResource, resource) || isEqual(previewResource, resource));
|
||||
if (!resourcePreview) {
|
||||
return this.previews;
|
||||
}
|
||||
|
||||
let synchronizingResources = this.synchronizingResources.find(s => s[0] === syncResource);
|
||||
if (!synchronizingResources) {
|
||||
synchronizingResources = [syncResource, []];
|
||||
this.synchronizingResources.push(synchronizingResources);
|
||||
}
|
||||
if (!synchronizingResources[1].some(s => isEqual(s, resourcePreview.localResource))) {
|
||||
synchronizingResources[1].push(resourcePreview.localResource);
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
}
|
||||
|
||||
const synchroniser = this.synchronisers.find(s => s.resource === this.previews![index][0])!;
|
||||
const preview = await action(synchroniser);
|
||||
preview ? this.previews.splice(index, 1, this.toSyncResourcePreview(synchroniser.resource, preview)) : this.previews.splice(index, 1);
|
||||
|
||||
const i = this.synchronizingResources.findIndex(s => s[0] === syncResource);
|
||||
this.synchronizingResources[i][1].splice(synchronizingResources[1].findIndex(r => isEqual(r, resourcePreview.localResource)), 1);
|
||||
if (!synchronizingResources[1].length) {
|
||||
this.synchronizingResources.splice(i, 1);
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
}
|
||||
|
||||
return this.previews;
|
||||
}
|
||||
|
||||
private async mergeAll(): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
if (!this.previews) {
|
||||
throw new Error('You need to create preview before merging or applying');
|
||||
}
|
||||
if (this.synchronizingResources.length) {
|
||||
throw new Error('Cannot merge or apply while synchronizing resources');
|
||||
}
|
||||
const previews: [SyncResource, ISyncResourcePreview][] = [];
|
||||
for (const [syncResource, preview] of this.previews) {
|
||||
this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]);
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
|
||||
const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!;
|
||||
|
||||
/* merge those which are not yet merged */
|
||||
let newPreview: ISyncResourcePreview | null = preview;
|
||||
for (const resourcePreview of preview.resourcePreviews) {
|
||||
if ((resourcePreview.localChange !== Change.None || resourcePreview.remoteChange !== Change.None) && resourcePreview.mergeState === MergeState.Preview) {
|
||||
newPreview = await synchroniser.merge(resourcePreview.previewResource);
|
||||
}
|
||||
}
|
||||
|
||||
if (newPreview) {
|
||||
previews.push(this.toSyncResourcePreview(synchroniser.resource, newPreview));
|
||||
}
|
||||
|
||||
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
}
|
||||
this.previews = previews;
|
||||
return this.previews;
|
||||
}
|
||||
|
||||
private async getPreviews(token: CancellationToken): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
const result: [SyncResource, ISyncResourcePreview][] = [];
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
const preview = await synchroniser.preview(this.manifest, this.syncHeaders);
|
||||
if (preview) {
|
||||
result.push(this.toSyncResourcePreview(synchroniser.resource, preview));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private toSyncResourcePreview(syncResource: SyncResource, preview: ISyncResourcePreview): [SyncResource, ISyncResourcePreview] {
|
||||
return [
|
||||
syncResource,
|
||||
{
|
||||
isLastSyncFromCurrentMachine: preview.isLastSyncFromCurrentMachine,
|
||||
resourcePreviews: preview.resourcePreviews.map(toStrictResourcePreview)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private reset(): void {
|
||||
if (this.previewsPromise) {
|
||||
this.previewsPromise.cancel();
|
||||
this.previewsPromise = undefined;
|
||||
}
|
||||
this.previews = undefined;
|
||||
this.synchronizingResources = [];
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.reset();
|
||||
this.isDisposed = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function toStrictResourcePreview(resourcePreview: IResourcePreview): IResourcePreview {
|
||||
return {
|
||||
localResource: resourcePreview.localResource,
|
||||
previewResource: resourcePreview.previewResource,
|
||||
remoteResource: resourcePreview.remoteResource,
|
||||
acceptedResource: resourcePreview.acceptedResource,
|
||||
localChange: resourcePreview.localChange,
|
||||
remoteChange: resourcePreview.remoteChange,
|
||||
mergeState: resourcePreview.mergeState,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, } from 'vs/base/common/lifecycle';
|
||||
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle, HEADER_OPERATION_ID, HEADER_EXECUTION_ID, CONFIGURATION_SYNC_STORE_KEY, IAuthenticationProvider, IUserDataSyncStoreManagementService, UserDataSyncStoreType, IUserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IRequestService, asText, isSuccess as isSuccessContext, asJson } from 'vs/platform/request/common/request';
|
||||
import { joinPath, relativePath } from 'vs/base/common/resources';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IProductService, ConfigurationSyncStore } from 'vs/platform/product/common/productService';
|
||||
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { isWeb } from 'vs/base/common/platform';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { createCancelablePromise, timeout, CancelablePromise } from 'vs/base/common/async';
|
||||
import { isString, isObject, isArray } from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
const SYNC_SERVICE_URL_TYPE = 'sync.store.url.type';
|
||||
const SYNC_PREVIOUS_STORE = 'sync.previous.store';
|
||||
const DONOT_MAKE_REQUESTS_UNTIL_KEY = 'sync.donot-make-requests-until';
|
||||
const USER_SESSION_ID_KEY = 'sync.user-session-id';
|
||||
const MACHINE_SESSION_ID_KEY = 'sync.machine-session-id';
|
||||
const REQUEST_SESSION_LIMIT = 100;
|
||||
const REQUEST_SESSION_INTERVAL = 1000 * 60 * 5; /* 5 minutes */
|
||||
|
||||
type UserDataSyncStore = IUserDataSyncStore & { defaultType?: UserDataSyncStoreType; type?: UserDataSyncStoreType };
|
||||
|
||||
export abstract class AbstractUserDataSyncStoreManagementService extends Disposable implements IUserDataSyncStoreManagementService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private readonly _onDidChangeUserDataSyncStore = this._register(new Emitter<void>());
|
||||
readonly onDidChangeUserDataSyncStore = this._onDidChangeUserDataSyncStore.event;
|
||||
private _userDataSyncStore: UserDataSyncStore | undefined;
|
||||
get userDataSyncStore(): UserDataSyncStore | undefined { return this._userDataSyncStore; }
|
||||
|
||||
constructor(
|
||||
@IProductService protected readonly productService: IProductService,
|
||||
@IConfigurationService protected readonly configurationService: IConfigurationService,
|
||||
@IStorageService protected readonly storageService: IStorageService,
|
||||
) {
|
||||
super();
|
||||
this.updateUserDataSyncStore();
|
||||
}
|
||||
|
||||
protected updateUserDataSyncStore(): void {
|
||||
this._userDataSyncStore = this.toUserDataSyncStore(this.productService[CONFIGURATION_SYNC_STORE_KEY], this.configurationService.getValue<ConfigurationSyncStore>(CONFIGURATION_SYNC_STORE_KEY));
|
||||
this._onDidChangeUserDataSyncStore.fire();
|
||||
}
|
||||
|
||||
protected toUserDataSyncStore(productStore: ConfigurationSyncStore | undefined, configuredStore?: ConfigurationSyncStore): UserDataSyncStore | undefined {
|
||||
// Web overrides
|
||||
productStore = isWeb && productStore?.web ? { ...productStore, ...productStore.web } : productStore;
|
||||
const value: Partial<ConfigurationSyncStore> = { ...(productStore || {}), ...(configuredStore || {}) };
|
||||
if (value
|
||||
&& isString(value.url)
|
||||
&& isObject(value.authenticationProviders)
|
||||
&& Object.keys(value.authenticationProviders).every(authenticationProviderId => isArray(value!.authenticationProviders![authenticationProviderId].scopes))
|
||||
) {
|
||||
const syncStore = value as ConfigurationSyncStore;
|
||||
const canSwitch = !!syncStore.canSwitch && !configuredStore?.url;
|
||||
const type: UserDataSyncStoreType | undefined = canSwitch ? this.storageService.get(SYNC_SERVICE_URL_TYPE, StorageScope.GLOBAL) as UserDataSyncStoreType : undefined;
|
||||
const url = configuredStore?.url
|
||||
|| type === 'insiders' ? syncStore.insidersUrl
|
||||
: type === 'stable' ? syncStore.stableUrl
|
||||
: syncStore.url;
|
||||
return {
|
||||
url: URI.parse(url),
|
||||
type,
|
||||
defaultType: syncStore.url === syncStore.insidersUrl ? 'insiders' : syncStore.url === syncStore.stableUrl ? 'stable' : undefined,
|
||||
defaultUrl: URI.parse(syncStore.url),
|
||||
stableUrl: URI.parse(syncStore.stableUrl),
|
||||
insidersUrl: URI.parse(syncStore.insidersUrl),
|
||||
canSwitch: !!syncStore.canSwitch && !configuredStore?.url,
|
||||
authenticationProviders: Object.keys(syncStore.authenticationProviders).reduce<IAuthenticationProvider[]>((result, id) => {
|
||||
result.push({ id, scopes: syncStore!.authenticationProviders[id].scopes });
|
||||
return result;
|
||||
}, [])
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
abstract switch(type: UserDataSyncStoreType): Promise<void>;
|
||||
abstract getPreviousUserDataSyncStore(): Promise<IUserDataSyncStore | undefined>;
|
||||
|
||||
}
|
||||
|
||||
export class UserDataSyncStoreManagementService extends AbstractUserDataSyncStoreManagementService implements IUserDataSyncStoreManagementService {
|
||||
|
||||
private readonly previousConfigurationSyncStore: ConfigurationSyncStore | undefined;
|
||||
|
||||
constructor(
|
||||
@IProductService productService: IProductService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
) {
|
||||
super(productService, configurationService, storageService);
|
||||
|
||||
const previousConfigurationSyncStore = this.storageService.get(SYNC_PREVIOUS_STORE, StorageScope.GLOBAL);
|
||||
if (previousConfigurationSyncStore) {
|
||||
this.previousConfigurationSyncStore = JSON.parse(previousConfigurationSyncStore);
|
||||
}
|
||||
|
||||
const syncStore = this.productService[CONFIGURATION_SYNC_STORE_KEY];
|
||||
if (syncStore) {
|
||||
this.storageService.store(SYNC_PREVIOUS_STORE, JSON.stringify(syncStore), StorageScope.GLOBAL);
|
||||
} else {
|
||||
this.storageService.remove(SYNC_PREVIOUS_STORE, StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
|
||||
async switch(type: UserDataSyncStoreType): Promise<void> {
|
||||
if (this.userDataSyncStore?.canSwitch && type !== this.userDataSyncStore.type) {
|
||||
if (type === this.userDataSyncStore.defaultType) {
|
||||
this.storageService.remove(SYNC_SERVICE_URL_TYPE, StorageScope.GLOBAL);
|
||||
} else {
|
||||
this.storageService.store(SYNC_SERVICE_URL_TYPE, type, StorageScope.GLOBAL);
|
||||
}
|
||||
this.updateUserDataSyncStore();
|
||||
}
|
||||
}
|
||||
|
||||
async getPreviousUserDataSyncStore(): Promise<IUserDataSyncStore | undefined> {
|
||||
return this.toUserDataSyncStore(this.previousConfigurationSyncStore);
|
||||
}
|
||||
}
|
||||
|
||||
export class UserDataSyncStoreClient extends Disposable implements IUserDataSyncStoreClient {
|
||||
|
||||
private userDataSyncStoreUrl: URI | undefined;
|
||||
|
||||
private authToken: { token: string, type: string } | undefined;
|
||||
private readonly commonHeadersPromise: Promise<{ [key: string]: string; }>;
|
||||
private readonly session: RequestsSession;
|
||||
|
||||
private _onTokenFailed: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onTokenFailed: Event<void> = this._onTokenFailed.event;
|
||||
|
||||
private _onTokenSucceed: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onTokenSucceed: Event<void> = this._onTokenSucceed.event;
|
||||
|
||||
private _donotMakeRequestsUntil: Date | undefined = undefined;
|
||||
get donotMakeRequestsUntil() { return this._donotMakeRequestsUntil; }
|
||||
private _onDidChangeDonotMakeRequestsUntil = this._register(new Emitter<void>());
|
||||
readonly onDidChangeDonotMakeRequestsUntil = this._onDidChangeDonotMakeRequestsUntil.event;
|
||||
|
||||
constructor(
|
||||
userDataSyncStoreUrl: URI | undefined,
|
||||
@IProductService productService: IProductService,
|
||||
@IRequestService private readonly requestService: IRequestService,
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
) {
|
||||
super();
|
||||
this.updateUserDataSyncStoreUrl(userDataSyncStoreUrl);
|
||||
this.commonHeadersPromise = getServiceMachineId(environmentService, fileService, storageService)
|
||||
.then(uuid => {
|
||||
const headers: IHeaders = {
|
||||
'X-Client-Name': `${productService.applicationName}${isWeb ? '-web' : ''}`,
|
||||
'X-Client-Version': productService.version,
|
||||
'X-Machine-Id': uuid
|
||||
};
|
||||
if (productService.commit) {
|
||||
headers['X-Client-Commit'] = productService.commit;
|
||||
}
|
||||
return headers;
|
||||
});
|
||||
|
||||
/* A requests session that limits requests per sessions */
|
||||
this.session = new RequestsSession(REQUEST_SESSION_LIMIT, REQUEST_SESSION_INTERVAL, this.requestService, this.logService);
|
||||
this.initDonotMakeRequestsUntil();
|
||||
}
|
||||
|
||||
setAuthToken(token: string, type: string): void {
|
||||
this.authToken = { token, type };
|
||||
}
|
||||
|
||||
protected updateUserDataSyncStoreUrl(userDataSyncStoreUrl: URI | undefined): void {
|
||||
this.userDataSyncStoreUrl = userDataSyncStoreUrl ? joinPath(userDataSyncStoreUrl, 'v1') : undefined;
|
||||
}
|
||||
|
||||
private initDonotMakeRequestsUntil(): void {
|
||||
const donotMakeRequestsUntil = this.storageService.getNumber(DONOT_MAKE_REQUESTS_UNTIL_KEY, StorageScope.GLOBAL);
|
||||
if (donotMakeRequestsUntil && Date.now() < donotMakeRequestsUntil) {
|
||||
this.setDonotMakeRequestsUntil(new Date(donotMakeRequestsUntil));
|
||||
}
|
||||
}
|
||||
|
||||
private resetDonotMakeRequestsUntilPromise: CancelablePromise<void> | undefined = undefined;
|
||||
private setDonotMakeRequestsUntil(donotMakeRequestsUntil: Date | undefined): void {
|
||||
if (this._donotMakeRequestsUntil?.getTime() !== donotMakeRequestsUntil?.getTime()) {
|
||||
this._donotMakeRequestsUntil = donotMakeRequestsUntil;
|
||||
|
||||
if (this.resetDonotMakeRequestsUntilPromise) {
|
||||
this.resetDonotMakeRequestsUntilPromise.cancel();
|
||||
this.resetDonotMakeRequestsUntilPromise = undefined;
|
||||
}
|
||||
|
||||
if (this._donotMakeRequestsUntil) {
|
||||
this.storageService.store(DONOT_MAKE_REQUESTS_UNTIL_KEY, this._donotMakeRequestsUntil.getTime(), StorageScope.GLOBAL);
|
||||
this.resetDonotMakeRequestsUntilPromise = createCancelablePromise(token => timeout(this._donotMakeRequestsUntil!.getTime() - Date.now(), token).then(() => this.setDonotMakeRequestsUntil(undefined)));
|
||||
} else {
|
||||
this.storageService.remove(DONOT_MAKE_REQUESTS_UNTIL_KEY, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
this._onDidChangeDonotMakeRequestsUntil.fire();
|
||||
}
|
||||
}
|
||||
|
||||
async getAllRefs(resource: ServerResource): Promise<IResourceRefHandle[]> {
|
||||
if (!this.userDataSyncStoreUrl) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const uri = joinPath(this.userDataSyncStoreUrl, 'resource', resource);
|
||||
const headers: IHeaders = {};
|
||||
|
||||
const context = await this.request({ type: 'GET', url: uri.toString(), headers }, [], CancellationToken.None);
|
||||
|
||||
const result = await asJson<{ url: string, created: number }[]>(context) || [];
|
||||
return result.map(({ url, created }) => ({ ref: relativePath(uri, uri.with({ path: url }))!, created: created * 1000 /* Server returns in seconds */ }));
|
||||
}
|
||||
|
||||
async resolveContent(resource: ServerResource, ref: string): Promise<string | null> {
|
||||
if (!this.userDataSyncStoreUrl) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource, ref).toString();
|
||||
const headers: IHeaders = {};
|
||||
headers['Cache-Control'] = 'no-cache';
|
||||
|
||||
const context = await this.request({ type: 'GET', url, headers }, [], CancellationToken.None);
|
||||
const content = await asText(context);
|
||||
return content;
|
||||
}
|
||||
|
||||
async delete(resource: ServerResource): Promise<void> {
|
||||
if (!this.userDataSyncStoreUrl) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource).toString();
|
||||
const headers: IHeaders = {};
|
||||
|
||||
await this.request({ type: 'DELETE', url, headers }, [], CancellationToken.None);
|
||||
}
|
||||
|
||||
async read(resource: ServerResource, oldValue: IUserData | null, headers: IHeaders = {}): Promise<IUserData> {
|
||||
if (!this.userDataSyncStoreUrl) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource, 'latest').toString();
|
||||
headers = { ...headers };
|
||||
// Disable caching as they are cached by synchronisers
|
||||
headers['Cache-Control'] = 'no-cache';
|
||||
if (oldValue) {
|
||||
headers['If-None-Match'] = oldValue.ref;
|
||||
}
|
||||
|
||||
const context = await this.request({ type: 'GET', url, headers }, [304], CancellationToken.None);
|
||||
|
||||
if (context.res.statusCode === 304) {
|
||||
// There is no new value. Hence return the old value.
|
||||
return oldValue!;
|
||||
}
|
||||
|
||||
const ref = context.res.headers['etag'];
|
||||
if (!ref) {
|
||||
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, context.res.headers[HEADER_OPERATION_ID]);
|
||||
}
|
||||
const content = await asText(context);
|
||||
return { ref, content };
|
||||
}
|
||||
|
||||
async write(resource: ServerResource, data: string, ref: string | null, headers: IHeaders = {}): Promise<string> {
|
||||
if (!this.userDataSyncStoreUrl) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource).toString();
|
||||
headers = { ...headers };
|
||||
headers['Content-Type'] = 'text/plain';
|
||||
if (ref) {
|
||||
headers['If-Match'] = ref;
|
||||
}
|
||||
|
||||
const context = await this.request({ type: 'POST', url, data, headers }, [], CancellationToken.None);
|
||||
|
||||
const newRef = context.res.headers['etag'];
|
||||
if (!newRef) {
|
||||
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, context.res.headers[HEADER_OPERATION_ID]);
|
||||
}
|
||||
return newRef;
|
||||
}
|
||||
|
||||
async manifest(headers: IHeaders = {}): Promise<IUserDataManifest | null> {
|
||||
if (!this.userDataSyncStoreUrl) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(this.userDataSyncStoreUrl, 'manifest').toString();
|
||||
headers = { ...headers };
|
||||
headers['Content-Type'] = 'application/json';
|
||||
|
||||
const context = await this.request({ type: 'GET', url, headers }, [], CancellationToken.None);
|
||||
|
||||
const manifest = await asJson<IUserDataManifest>(context);
|
||||
const currentSessionId = this.storageService.get(USER_SESSION_ID_KEY, StorageScope.GLOBAL);
|
||||
|
||||
if (currentSessionId && manifest && currentSessionId !== manifest.session) {
|
||||
// Server session is different from client session so clear cached session.
|
||||
this.clearSession();
|
||||
}
|
||||
|
||||
if (manifest === null && currentSessionId) {
|
||||
// server session is cleared so clear cached session.
|
||||
this.clearSession();
|
||||
}
|
||||
|
||||
if (manifest) {
|
||||
// update session
|
||||
this.storageService.store(USER_SESSION_ID_KEY, manifest.session, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
return manifest;
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
if (!this.userDataSyncStoreUrl) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(this.userDataSyncStoreUrl, 'resource').toString();
|
||||
const headers: IHeaders = { 'Content-Type': 'text/plain' };
|
||||
|
||||
await this.request({ type: 'DELETE', url, headers }, [], CancellationToken.None);
|
||||
|
||||
// clear cached session.
|
||||
this.clearSession();
|
||||
}
|
||||
|
||||
private clearSession(): void {
|
||||
this.storageService.remove(USER_SESSION_ID_KEY, StorageScope.GLOBAL);
|
||||
this.storageService.remove(MACHINE_SESSION_ID_KEY, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
private async request(options: IRequestOptions, successCodes: number[], token: CancellationToken): Promise<IRequestContext> {
|
||||
if (!this.authToken) {
|
||||
throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized, undefined);
|
||||
}
|
||||
|
||||
if (this._donotMakeRequestsUntil && Date.now() < this._donotMakeRequestsUntil.getTime()) {
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too many requests (429).`, UserDataSyncErrorCode.TooManyRequestsAndRetryAfter, undefined);
|
||||
}
|
||||
this.setDonotMakeRequestsUntil(undefined);
|
||||
|
||||
const commonHeaders = await this.commonHeadersPromise;
|
||||
options.headers = {
|
||||
...(options.headers || {}),
|
||||
...commonHeaders,
|
||||
'X-Account-Type': this.authToken.type,
|
||||
'authorization': `Bearer ${this.authToken.token}`,
|
||||
};
|
||||
|
||||
// Add session headers
|
||||
this.addSessionHeaders(options.headers);
|
||||
|
||||
this.logService.trace('Sending request to server', { url: options.url, type: options.type, headers: { ...options.headers, ...{ authorization: undefined } } });
|
||||
|
||||
let context;
|
||||
try {
|
||||
context = await this.session.request(options, token);
|
||||
} catch (e) {
|
||||
if (!(e instanceof UserDataSyncStoreError)) {
|
||||
e = new UserDataSyncStoreError(`Connection refused for the request '${options.url?.toString()}'.`, UserDataSyncErrorCode.ConnectionRefused, undefined);
|
||||
}
|
||||
this.logService.info('Request failed', options.url);
|
||||
throw e;
|
||||
}
|
||||
|
||||
const operationId = context.res.headers[HEADER_OPERATION_ID];
|
||||
const requestInfo = { url: options.url, status: context.res.statusCode, 'execution-id': options.headers[HEADER_EXECUTION_ID], 'operation-id': operationId };
|
||||
const isSuccess = isSuccessContext(context) || (context.res.statusCode && successCodes.indexOf(context.res.statusCode) !== -1);
|
||||
if (isSuccess) {
|
||||
this.logService.trace('Request succeeded', requestInfo);
|
||||
} else {
|
||||
this.logService.info('Request failed', requestInfo);
|
||||
}
|
||||
|
||||
if (context.res.statusCode === 401) {
|
||||
this.authToken = undefined;
|
||||
this._onTokenFailed.fire();
|
||||
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized, operationId);
|
||||
}
|
||||
|
||||
this._onTokenSucceed.fire();
|
||||
|
||||
if (context.res.statusCode === 409) {
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Conflict (409). There is new data for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.Conflict, operationId);
|
||||
}
|
||||
|
||||
if (context.res.statusCode === 410) {
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because the requested resource is not longer available (410).`, UserDataSyncErrorCode.Gone, operationId);
|
||||
}
|
||||
|
||||
if (context.res.statusCode === 412) {
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.PreconditionFailed, operationId);
|
||||
}
|
||||
|
||||
if (context.res.statusCode === 413) {
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too large payload (413).`, UserDataSyncErrorCode.TooLarge, operationId);
|
||||
}
|
||||
|
||||
if (context.res.statusCode === 426) {
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed with status Upgrade Required (426). Please upgrade the client and try again.`, UserDataSyncErrorCode.UpgradeRequired, operationId);
|
||||
}
|
||||
|
||||
if (context.res.statusCode === 429) {
|
||||
const retryAfter = context.res.headers['retry-after'];
|
||||
if (retryAfter) {
|
||||
this.setDonotMakeRequestsUntil(new Date(Date.now() + (parseInt(retryAfter) * 1000)));
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too many requests (429).`, UserDataSyncErrorCode.TooManyRequestsAndRetryAfter, operationId);
|
||||
} else {
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too many requests (429).`, UserDataSyncErrorCode.TooManyRequests, operationId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSuccess) {
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, operationId);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private addSessionHeaders(headers: IHeaders): void {
|
||||
let machineSessionId = this.storageService.get(MACHINE_SESSION_ID_KEY, StorageScope.GLOBAL);
|
||||
if (machineSessionId === undefined) {
|
||||
machineSessionId = generateUuid();
|
||||
this.storageService.store(MACHINE_SESSION_ID_KEY, machineSessionId, StorageScope.GLOBAL);
|
||||
}
|
||||
headers['X-Machine-Session-Id'] = machineSessionId;
|
||||
|
||||
const userSessionId = this.storageService.get(USER_SESSION_ID_KEY, StorageScope.GLOBAL);
|
||||
if (userSessionId !== undefined) {
|
||||
headers['X-User-Session-Id'] = userSessionId;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class UserDataSyncStoreService extends UserDataSyncStoreClient implements IUserDataSyncStoreService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
constructor(
|
||||
@IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
|
||||
@IProductService productService: IProductService,
|
||||
@IRequestService requestService: IRequestService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
) {
|
||||
super(userDataSyncStoreManagementService.userDataSyncStore?.url, productService, requestService, logService, environmentService, fileService, storageService);
|
||||
this._register(userDataSyncStoreManagementService.onDidChangeUserDataSyncStore(() => this.updateUserDataSyncStoreUrl(userDataSyncStoreManagementService.userDataSyncStore?.url)));
|
||||
}
|
||||
}
|
||||
|
||||
export class RequestsSession {
|
||||
|
||||
private requests: string[] = [];
|
||||
private startTime: Date | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
private readonly limit: number,
|
||||
private readonly interval: number, /* in ms */
|
||||
private readonly requestService: IRequestService,
|
||||
private readonly logService: IUserDataSyncLogService,
|
||||
) { }
|
||||
|
||||
request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
|
||||
if (this.isExpired()) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
if (this.requests.length >= this.limit) {
|
||||
this.logService.info('Too many requests', ...this.requests);
|
||||
throw new UserDataSyncStoreError(`Too many requests. Only ${this.limit} requests allowed in ${this.interval / (1000 * 60)} minutes.`, UserDataSyncErrorCode.LocalTooManyRequests, undefined);
|
||||
}
|
||||
|
||||
this.startTime = this.startTime || new Date();
|
||||
this.requests.push(options.url!);
|
||||
|
||||
return this.requestService.request(options, token);
|
||||
}
|
||||
|
||||
private isExpired(): boolean {
|
||||
return this.startTime !== undefined && new Date().getTime() - this.startTime.getTime() > this.interval;
|
||||
}
|
||||
|
||||
private reset(): void {
|
||||
this.requests = [];
|
||||
this.startTime = undefined;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
//
|
||||
import { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, IUserDataSyncStoreManagementService, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
|
||||
import { UserDataAutoSyncService as BaseUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService';
|
||||
import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';
|
||||
|
||||
export class UserDataAutoSyncService extends BaseUserDataAutoSyncService {
|
||||
|
||||
constructor(
|
||||
@IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
|
||||
@IUserDataSyncService userDataSyncService: IUserDataSyncService,
|
||||
@INativeHostService nativeHostService: INativeHostService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
@IUserDataSyncAccountService authTokenService: IUserDataSyncAccountService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IUserDataSyncMachinesService userDataSyncMachinesService: IUserDataSyncMachinesService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IUserDataAutoSyncEnablementService userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService,
|
||||
) {
|
||||
super(userDataSyncStoreManagementService, userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, userDataSyncMachinesService, storageService, userDataAutoSyncEnablementService);
|
||||
|
||||
this._register(Event.debounce<string, string[]>(Event.any<string>(
|
||||
Event.map(nativeHostService.onDidFocusWindow, () => 'windowFocus'),
|
||||
Event.map(nativeHostService.onDidOpenWindow, () => 'windowOpen'),
|
||||
), (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, true, false)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,650 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { ISyncExtension, ISyncExtensionWithVersion } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { merge } from 'vs/platform/userDataSync/common/extensionsMerge';
|
||||
|
||||
suite('ExtensionsMerge', () => {
|
||||
|
||||
test('merge returns local extension if remote does not exist', () => {
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, null, null, [], []);
|
||||
|
||||
assert.deepEqual(actual.added, []);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, localExtensions);
|
||||
});
|
||||
|
||||
test('merge returns local extension if remote does not exist with ignored extensions', () => {
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const expected: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, null, null, [], ['a']);
|
||||
|
||||
assert.deepEqual(actual.added, []);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, expected);
|
||||
});
|
||||
|
||||
test('merge returns local extension if remote does not exist with ignored extensions (ignore case)', () => {
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const expected: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, null, null, [], ['A']);
|
||||
|
||||
assert.deepEqual(actual.added, []);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, expected);
|
||||
});
|
||||
|
||||
test('merge returns local extension if remote does not exist with skipped extensions', () => {
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const skippedExtension: ISyncExtension[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
|
||||
];
|
||||
const expected: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, null, null, skippedExtension, []);
|
||||
|
||||
assert.deepEqual(actual.added, []);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, expected);
|
||||
});
|
||||
|
||||
test('merge returns local extension if remote does not exist with skipped and ignored extensions', () => {
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const skippedExtension: ISyncExtension[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
|
||||
];
|
||||
const expected: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, null, null, skippedExtension, ['a']);
|
||||
|
||||
assert.deepEqual(actual.added, []);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, expected);
|
||||
});
|
||||
|
||||
test('merge local and remote extensions when there is no base', () => {
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const expected: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, null, [], []);
|
||||
|
||||
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, expected);
|
||||
});
|
||||
|
||||
test('merge local and remote extensions when there is no base and with ignored extensions', () => {
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const expected: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, null, [], ['a']);
|
||||
|
||||
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, expected);
|
||||
});
|
||||
|
||||
test('merge local and remote extensions when remote is moved forwarded', () => {
|
||||
const baseExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
|
||||
];
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []);
|
||||
|
||||
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]);
|
||||
assert.deepEqual(actual.removed, [{ id: 'a', uuid: 'a' }, { id: 'd', uuid: 'd' }]);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.equal(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge local and remote extensions when remote is moved forwarded with disabled extension', () => {
|
||||
const baseExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
|
||||
];
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, disabled: true, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []);
|
||||
|
||||
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]);
|
||||
assert.deepEqual(actual.removed, [{ id: 'a', uuid: 'a' }]);
|
||||
assert.deepEqual(actual.updated, [{ identifier: { id: 'd', uuid: 'd' }, disabled: true, installed: true, version: '1.0.0' }]);
|
||||
assert.equal(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge local and remote extensions when remote moved forwarded with ignored extensions', () => {
|
||||
const baseExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
|
||||
];
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['a']);
|
||||
|
||||
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]);
|
||||
assert.deepEqual(actual.removed, [{ id: 'd', uuid: 'd' }]);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.equal(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge local and remote extensions when remote is moved forwarded with skipped extensions', () => {
|
||||
const baseExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
|
||||
];
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const skippedExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []);
|
||||
|
||||
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]);
|
||||
assert.deepEqual(actual.removed, [{ id: 'd', uuid: 'd' }]);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.equal(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge local and remote extensions when remote is moved forwarded with skipped and ignored extensions', () => {
|
||||
const baseExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
|
||||
];
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const skippedExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['b']);
|
||||
|
||||
assert.deepEqual(actual.added, [{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]);
|
||||
assert.deepEqual(actual.removed, [{ id: 'd', uuid: 'd' }]);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.equal(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge local and remote extensions when local is moved forwarded', () => {
|
||||
const baseExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
|
||||
];
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []);
|
||||
|
||||
assert.deepEqual(actual.added, []);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, localExtensions);
|
||||
});
|
||||
|
||||
test('merge local and remote extensions when local is moved forwarded with disabled extensions', () => {
|
||||
const baseExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
|
||||
];
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, disabled: true, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []);
|
||||
|
||||
assert.deepEqual(actual.added, []);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, localExtensions);
|
||||
});
|
||||
|
||||
test('merge local and remote extensions when local is moved forwarded with ignored settings', () => {
|
||||
const baseExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
|
||||
];
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['b']);
|
||||
|
||||
assert.deepEqual(actual.added, []);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, [
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('merge local and remote extensions when local is moved forwarded with skipped extensions', () => {
|
||||
const baseExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
|
||||
];
|
||||
const skippedExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
|
||||
];
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const expected: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []);
|
||||
|
||||
assert.deepEqual(actual.added, []);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, expected);
|
||||
});
|
||||
|
||||
test('merge local and remote extensions when local is moved forwarded with skipped and ignored extensions', () => {
|
||||
const baseExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
|
||||
];
|
||||
const skippedExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
|
||||
];
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const expected: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['c']);
|
||||
|
||||
assert.deepEqual(actual.added, []);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, expected);
|
||||
});
|
||||
|
||||
test('merge local and remote extensions when both moved forwarded', () => {
|
||||
const baseExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
|
||||
];
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const expected: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []);
|
||||
|
||||
assert.deepEqual(actual.added, [{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' }]);
|
||||
assert.deepEqual(actual.removed, [{ id: 'a', uuid: 'a' }]);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, expected);
|
||||
});
|
||||
|
||||
test('merge local and remote extensions when both moved forwarded with ignored extensions', () => {
|
||||
const baseExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
|
||||
];
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const expected: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['a', 'e']);
|
||||
|
||||
assert.deepEqual(actual.added, []);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, expected);
|
||||
});
|
||||
|
||||
test('merge local and remote extensions when both moved forwarded with skipped extensions', () => {
|
||||
const baseExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
|
||||
];
|
||||
const skippedExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
|
||||
];
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const expected: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []);
|
||||
|
||||
assert.deepEqual(actual.added, [{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' }]);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, expected);
|
||||
});
|
||||
|
||||
test('merge local and remote extensions when both moved forwarded with skipped and ignoredextensions', () => {
|
||||
const baseExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
|
||||
];
|
||||
const skippedExtensions: ISyncExtension[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
|
||||
];
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const expected: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['e']);
|
||||
|
||||
assert.deepEqual(actual.added, []);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, expected);
|
||||
});
|
||||
|
||||
test('merge when remote extension has no uuid and different extension id case', () => {
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'A' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const expected: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'A', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, null, [], []);
|
||||
|
||||
assert.deepEqual(actual.added, [{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' }]);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, expected);
|
||||
});
|
||||
|
||||
test('merge when remote extension is not an installed extension', () => {
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
{ identifier: { id: 'b', uuid: 'b' }, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, null, [], []);
|
||||
|
||||
assert.deepEqual(actual.added, []);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when remote extension is not an installed extension but is an installed extension locally', () => {
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, null, [], []);
|
||||
|
||||
assert.deepEqual(actual.added, []);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, localExtensions);
|
||||
});
|
||||
|
||||
test('merge when an extension is not an installed extension remotely and does not exist locally', () => {
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0' },
|
||||
{ identifier: { id: 'b', uuid: 'b' }, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, remoteExtensions, [], []);
|
||||
|
||||
assert.deepEqual(actual.added, []);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when an extension is an installed extension remotely but not locally and updated locally', () => {
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, disabled: true, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
|
||||
];
|
||||
const expected: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, disabled: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, remoteExtensions, [], []);
|
||||
|
||||
assert.deepEqual(actual.added, []);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, expected);
|
||||
});
|
||||
|
||||
test('merge when an extension is an installed extension remotely but not locally and updated remotely', () => {
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, installed: true, disabled: true, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, localExtensions, [], []);
|
||||
|
||||
assert.deepEqual(actual.added, []);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, remoteExtensions);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge not installed extensions', () => {
|
||||
const localExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0' },
|
||||
];
|
||||
const remoteExtensions: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, version: '1.0.0' },
|
||||
];
|
||||
const expected: ISyncExtensionWithVersion[] = [
|
||||
{ identifier: { id: 'b', uuid: 'b' }, version: '1.0.0' },
|
||||
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0' },
|
||||
];
|
||||
|
||||
const actual = merge(localExtensions, remoteExtensions, null, [], []);
|
||||
|
||||
assert.deepEqual(actual.added, []);
|
||||
assert.deepEqual(actual.removed, []);
|
||||
assert.deepEqual(actual.updated, []);
|
||||
assert.deepEqual(actual.remote, expected);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,380 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { merge } from 'vs/platform/userDataSync/common/globalStateMerge';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
|
||||
suite('GlobalStateMerge', () => {
|
||||
|
||||
test('merge when local and remote are same with one value', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with multiple entries', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with multiple entries in different order', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with different base content', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
const base = { 'b': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when a new entry is added to remote', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'b': { version: 1, value: 'b' } });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when multiple new entries are added to remote', async () => {
|
||||
const local = {};
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when new entry is added to remote from base and local has not changed', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'b': { version: 1, value: 'b' } });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when an entry is removed from remote from base and local has not changed', async () => {
|
||||
const local = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, ['b']);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when all entries are removed from base and local has not changed', async () => {
|
||||
const local = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
const remote = {};
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, ['b', 'a']);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when an entry is updated in remote from base and local has not changed', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } });
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when remote has moved forwarded with multiple changes and local stays with base', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'd' }, 'c': { version: 1, value: 'c' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'c': { version: 1, value: 'c' } });
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'd' } });
|
||||
assert.deepEqual(actual.local.removed, ['b']);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when new entries are added to local', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
test('merge when multiple new entries are added to local from base and remote is not changed', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' }, 'c': { version: 1, value: 'c' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
test('merge when an entry is removed from local from base and remote has not changed', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
test('merge when an entry is updated in local from base and remote has not changed', async () => {
|
||||
const local = { 'a': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
test('merge when local has moved forwarded with multiple changes and remote stays with base', async () => {
|
||||
const local = { 'a': { version: 1, value: 'd' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' }, 'c': { version: 1, value: 'c' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
test('merge when local and remote with one entry but different value', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } });
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when the entry is removed in remote but updated in local and a new entry is added in remote', async () => {
|
||||
const base = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'd' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' }, 'c': { version: 1, value: 'c' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'c': { version: 1, value: 'c' } });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, ['b']);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge with single entry and local is empty', async () => {
|
||||
const base = { 'a': { version: 1, value: 'a' } };
|
||||
const local = {};
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } });
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with conflicts', async () => {
|
||||
const base = { 'a': { version: 1, value: 'a' } };
|
||||
const local = { 'a': { version: 1, value: 'd' } };
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } });
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when a new entry is added to remote but not a registered key', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when a new entry is added to remote but different version', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when an entry is updated to remote but not a registered key', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, local, [], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when a new entry is updated to remote but different version', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when a local value is update with lower version', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'c' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when a local value is update with higher version', async () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 2, value: 'c' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 2 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
test('merge when a local value is removed but not registered', async () => {
|
||||
const base = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when a local value is removed with lower version', async () => {
|
||||
const base = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
test('merge when a local value is removed with higher version', async () => {
|
||||
const base = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 2 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
test('merge when a local value is not yet registered', async () => {
|
||||
const base = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,243 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, SyncStatus, IGlobalState, ISyncData } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
|
||||
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { GlobalStateSynchroniser } from 'vs/platform/userDataSync/common/globalStateSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
|
||||
|
||||
suite('GlobalStateSync', () => {
|
||||
|
||||
const disposableStore = new DisposableStore();
|
||||
const server = new UserDataSyncTestServer();
|
||||
let testClient: UserDataSyncClient;
|
||||
let client2: UserDataSyncClient;
|
||||
|
||||
let testObject: GlobalStateSynchroniser;
|
||||
|
||||
setup(async () => {
|
||||
testClient = disposableStore.add(new UserDataSyncClient(server));
|
||||
await testClient.setUp(true);
|
||||
let storageKeysSyncRegistryService = testClient.instantiationService.get(IStorageKeysSyncRegistryService);
|
||||
storageKeysSyncRegistryService.registerStorageKey({ key: 'a', version: 1 });
|
||||
storageKeysSyncRegistryService.registerStorageKey({ key: 'b', version: 1 });
|
||||
testObject = (testClient.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.GlobalState) as GlobalStateSynchroniser;
|
||||
disposableStore.add(toDisposable(() => testClient.instantiationService.get(IUserDataSyncStoreService).clear()));
|
||||
|
||||
client2 = disposableStore.add(new UserDataSyncClient(server));
|
||||
await client2.setUp(true);
|
||||
storageKeysSyncRegistryService = client2.instantiationService.get(IStorageKeysSyncRegistryService);
|
||||
storageKeysSyncRegistryService.registerStorageKey({ key: 'a', version: 1 });
|
||||
storageKeysSyncRegistryService.registerStorageKey({ key: 'b', version: 1 });
|
||||
});
|
||||
|
||||
teardown(() => disposableStore.clear());
|
||||
|
||||
test('when global state does not exist', async () => {
|
||||
assert.deepEqual(await testObject.getLastSyncUserData(), null);
|
||||
let manifest = await testClient.manifest();
|
||||
server.reset();
|
||||
await testObject.sync(manifest);
|
||||
|
||||
assert.deepEqual(server.requests, [
|
||||
{ type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
const lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const remoteUserData = await testObject.getRemoteUserData(null);
|
||||
assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref);
|
||||
assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData);
|
||||
assert.equal(lastSyncUserData!.syncData, null);
|
||||
|
||||
manifest = await testClient.manifest();
|
||||
server.reset();
|
||||
await testObject.sync(manifest);
|
||||
assert.deepEqual(server.requests, []);
|
||||
|
||||
manifest = await testClient.manifest();
|
||||
server.reset();
|
||||
await testObject.sync(manifest);
|
||||
assert.deepEqual(server.requests, []);
|
||||
});
|
||||
|
||||
test('when global state is created after first sync', async () => {
|
||||
await testObject.sync(await testClient.manifest());
|
||||
updateStorage('a', 'value1', testClient);
|
||||
|
||||
let lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const manifest = await testClient.manifest();
|
||||
server.reset();
|
||||
await testObject.sync(manifest);
|
||||
|
||||
assert.deepEqual(server.requests, [
|
||||
{ type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } },
|
||||
]);
|
||||
|
||||
lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const remoteUserData = await testObject.getRemoteUserData(null);
|
||||
assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref);
|
||||
assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData);
|
||||
assert.deepEqual(JSON.parse(lastSyncUserData!.syncData!.content).storage, { 'a': { version: 1, value: 'value1' } });
|
||||
});
|
||||
|
||||
test('first time sync - outgoing to server (no state)', async () => {
|
||||
updateStorage('a', 'value1', testClient);
|
||||
await updateLocale(testClient);
|
||||
|
||||
await testObject.sync(await testClient.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseGlobalState(content!);
|
||||
assert.deepEqual(actual.storage, { 'globalState.argv.locale': { version: 1, value: 'en' }, 'a': { version: 1, value: 'value1' } });
|
||||
});
|
||||
|
||||
test('first time sync - incoming from server (no state)', async () => {
|
||||
updateStorage('a', 'value1', client2);
|
||||
await updateLocale(client2);
|
||||
await client2.sync();
|
||||
|
||||
await testObject.sync(await testClient.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
assert.equal(readStorage('a', testClient), 'value1');
|
||||
assert.equal(await readLocale(testClient), 'en');
|
||||
});
|
||||
|
||||
test('first time sync when storage exists', async () => {
|
||||
updateStorage('a', 'value1', client2);
|
||||
await client2.sync();
|
||||
|
||||
updateStorage('b', 'value2', testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
assert.equal(readStorage('a', testClient), 'value1');
|
||||
assert.equal(readStorage('b', testClient), 'value2');
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseGlobalState(content!);
|
||||
assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' }, 'b': { version: 1, value: 'value2' } });
|
||||
});
|
||||
|
||||
test('first time sync when storage exists - has conflicts', async () => {
|
||||
updateStorage('a', 'value1', client2);
|
||||
await client2.sync();
|
||||
|
||||
updateStorage('a', 'value2', client2);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
assert.equal(readStorage('a', testClient), 'value1');
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseGlobalState(content!);
|
||||
assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' } });
|
||||
});
|
||||
|
||||
test('sync adding a storage value', async () => {
|
||||
updateStorage('a', 'value1', testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
updateStorage('b', 'value2', testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
assert.equal(readStorage('a', testClient), 'value1');
|
||||
assert.equal(readStorage('b', testClient), 'value2');
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseGlobalState(content!);
|
||||
assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' }, 'b': { version: 1, value: 'value2' } });
|
||||
});
|
||||
|
||||
test('sync updating a storage value', async () => {
|
||||
updateStorage('a', 'value1', testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
updateStorage('a', 'value2', testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
assert.equal(readStorage('a', testClient), 'value2');
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseGlobalState(content!);
|
||||
assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value2' } });
|
||||
});
|
||||
|
||||
test('sync removing a storage value', async () => {
|
||||
updateStorage('a', 'value1', testClient);
|
||||
updateStorage('b', 'value2', testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
removeStorage('b', testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
assert.equal(readStorage('a', testClient), 'value1');
|
||||
assert.equal(readStorage('b', testClient), undefined);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseGlobalState(content!);
|
||||
assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' } });
|
||||
});
|
||||
|
||||
function parseGlobalState(content: string): IGlobalState {
|
||||
const syncData: ISyncData = JSON.parse(content);
|
||||
return JSON.parse(syncData.content);
|
||||
}
|
||||
|
||||
async function updateLocale(client: UserDataSyncClient): Promise<void> {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'en' })));
|
||||
}
|
||||
|
||||
function updateStorage(key: string, value: string, client: UserDataSyncClient): void {
|
||||
const storageService = client.instantiationService.get(IStorageService);
|
||||
storageService.store(key, value, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
function removeStorage(key: string, client: UserDataSyncClient): void {
|
||||
const storageService = client.instantiationService.get(IStorageService);
|
||||
storageService.remove(key, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
function readStorage(key: string, client: UserDataSyncClient): string | undefined {
|
||||
const storageService = client.instantiationService.get(IStorageService);
|
||||
return storageService.get(key, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
async function readLocale(client: UserDataSyncClient): Promise<string | undefined> {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
const content = await fileService.readFile(environmentService.argvResource);
|
||||
return JSON.parse(content.value.toString()).locale;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,619 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge';
|
||||
import { TestUserDataSyncUtilService } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
|
||||
|
||||
suite('KeybindingsMerge - No Conflicts', () => {
|
||||
|
||||
test('merge when local and remote are same with one entry', async () => {
|
||||
const localContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
|
||||
const remoteContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, null);
|
||||
assert.ok(!actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, localContent);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with similar when contexts', async () => {
|
||||
const localContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
|
||||
const remoteContent = stringify([{ key: 'alt+c', command: 'a', when: '!editorReadonly && editorTextFocus' }]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, null);
|
||||
assert.ok(!actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, localContent);
|
||||
});
|
||||
|
||||
test('merge when local and remote has entries in different order', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+a', command: 'a', when: 'editorTextFocus' }
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+a', command: 'a', when: 'editorTextFocus' },
|
||||
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, null);
|
||||
assert.ok(!actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, localContent);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with multiple entries', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'cmd+c', command: 'b', args: { text: '`' } }
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'cmd+c', command: 'b', args: { text: '`' } }
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, null);
|
||||
assert.ok(!actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, localContent);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with different base content', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'cmd+c', command: 'b', args: { text: '`' } }
|
||||
]);
|
||||
const baseContent = stringify([
|
||||
{ key: 'ctrl+c', command: 'e' },
|
||||
{ key: 'shift+d', command: 'd', args: { text: '`' } }
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'cmd+c', command: 'b', args: { text: '`' } }
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, baseContent);
|
||||
assert.ok(!actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, localContent);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with multiple entries in different order', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'cmd+c', command: 'b', args: { text: '`' } }
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, null);
|
||||
assert.ok(!actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, localContent);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same when remove entry is in different order', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'cmd+c', command: 'b', args: { text: '`' } }
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, null);
|
||||
assert.ok(!actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, localContent);
|
||||
});
|
||||
|
||||
test('merge when a new entry is added to remote', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, null);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, remoteContent);
|
||||
});
|
||||
|
||||
test('merge when multiple new entries are added to remote', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
|
||||
{ key: 'cmd+d', command: 'c' },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, null);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, remoteContent);
|
||||
});
|
||||
|
||||
test('merge when multiple new entries are added to remote from base and local has not changed', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
|
||||
{ key: 'cmd+d', command: 'c' },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, localContent);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, remoteContent);
|
||||
});
|
||||
|
||||
test('merge when an entry is removed from remote from base and local has not changed', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, localContent);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, remoteContent);
|
||||
});
|
||||
|
||||
test('merge when an entry (same command) is removed from remote from base and local has not changed', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, localContent);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, remoteContent);
|
||||
});
|
||||
|
||||
test('merge when an entry is updated in remote from base and local has not changed', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, localContent);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, remoteContent);
|
||||
});
|
||||
|
||||
test('merge when a command with multiple entries is updated from remote from base and local has not changed', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'shift+c', command: 'c' },
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: 'b' },
|
||||
{ key: 'cmd+c', command: 'a' },
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'shift+c', command: 'c' },
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: 'b' },
|
||||
{ key: 'cmd+d', command: 'a' },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, localContent);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, remoteContent);
|
||||
});
|
||||
|
||||
test('merge when remote has moved forwareded with multiple changes and local stays with base', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'cmd+e', command: 'd' },
|
||||
{ key: 'cmd+d', command: 'c', when: 'context1' },
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'cmd+e', command: 'd' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'alt+f', command: 'f' },
|
||||
{ key: 'alt+d', command: '-f' },
|
||||
{ key: 'cmd+d', command: 'c', when: 'context1' },
|
||||
{ key: 'cmd+c', command: '-c' },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, localContent);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, remoteContent);
|
||||
});
|
||||
|
||||
test('merge when a new entry is added to local', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, null);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, localContent);
|
||||
});
|
||||
|
||||
test('merge when multiple new entries are added to local', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
|
||||
{ key: 'cmd+d', command: 'c' },
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, null);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, localContent);
|
||||
});
|
||||
|
||||
test('merge when multiple new entries are added to local from base and remote is not changed', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
|
||||
{ key: 'cmd+d', command: 'c' },
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, remoteContent);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, localContent);
|
||||
});
|
||||
|
||||
test('merge when an entry is removed from local from base and remote has not changed', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, remoteContent);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, localContent);
|
||||
});
|
||||
|
||||
test('merge when an entry (with same command) is removed from local from base and remote has not changed', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, remoteContent);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, localContent);
|
||||
});
|
||||
|
||||
test('merge when an entry is updated in local from base and remote has not changed', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+d', command: 'a', when: 'editorTextFocus' },
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, remoteContent);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, localContent);
|
||||
});
|
||||
|
||||
test('merge when a command with multiple entries is updated from local from base and remote has not changed', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'shift+c', command: 'c' },
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: 'b' },
|
||||
{ key: 'cmd+c', command: 'a' },
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'shift+c', command: 'c' },
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+d', command: 'b' },
|
||||
{ key: 'cmd+d', command: 'a' },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, remoteContent);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, localContent);
|
||||
});
|
||||
|
||||
test('merge when local has moved forwareded with multiple changes and remote stays with base', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'cmd+e', command: 'd' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'alt+f', command: 'f' },
|
||||
{ key: 'alt+d', command: '-f' },
|
||||
{ key: 'cmd+d', command: 'c', when: 'context1' },
|
||||
{ key: 'cmd+c', command: '-c' },
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'cmd+e', command: 'd' },
|
||||
{ key: 'cmd+d', command: 'c', when: 'context1' },
|
||||
]);
|
||||
const expected = stringify([
|
||||
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'cmd+e', command: 'd' },
|
||||
{ key: 'alt+d', command: '-a' },
|
||||
{ key: 'alt+f', command: 'f' },
|
||||
{ key: 'alt+d', command: '-f' },
|
||||
{ key: 'cmd+d', command: 'c', when: 'context1' },
|
||||
{ key: 'cmd+c', command: '-c' },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, remoteContent);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, expected);
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with conflicts', async () => {
|
||||
const baseContent = stringify([
|
||||
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'ctrl+c', command: '-a' },
|
||||
{ key: 'cmd+e', command: 'd' },
|
||||
{ key: 'alt+a', command: 'f' },
|
||||
{ key: 'alt+d', command: '-f' },
|
||||
{ key: 'cmd+d', command: 'c', when: 'context1' },
|
||||
{ key: 'cmd+c', command: '-c' },
|
||||
]);
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+d', command: '-f' },
|
||||
{ key: 'cmd+e', command: 'd' },
|
||||
{ key: 'cmd+c', command: '-c' },
|
||||
{ key: 'cmd+d', command: 'c', when: 'context1' },
|
||||
{ key: 'alt+a', command: 'f' },
|
||||
{ key: 'alt+e', command: 'e' },
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+a', command: 'f' },
|
||||
{ key: 'cmd+c', command: '-c' },
|
||||
{ key: 'cmd+d', command: 'd' },
|
||||
{ key: 'alt+d', command: '-f' },
|
||||
{ key: 'alt+c', command: 'c', when: 'context1' },
|
||||
{ key: 'alt+g', command: 'g', when: 'context2' },
|
||||
]);
|
||||
const expected = stringify([
|
||||
{ key: 'alt+d', command: '-f' },
|
||||
{ key: 'cmd+d', command: 'd' },
|
||||
{ key: 'cmd+c', command: '-c' },
|
||||
{ key: 'alt+c', command: 'c', when: 'context1' },
|
||||
{ key: 'alt+a', command: 'f' },
|
||||
{ key: 'alt+e', command: 'e' },
|
||||
{ key: 'alt+g', command: 'g', when: 'context2' },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, baseContent);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(!actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent, expected);
|
||||
});
|
||||
|
||||
test('merge when local and remote with one entry but different value', async () => {
|
||||
const localContent = stringify([{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
|
||||
const remoteContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, null);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent,
|
||||
`[
|
||||
{
|
||||
"key": "alt+d",
|
||||
"command": "a",
|
||||
"when": "editorTextFocus && !editorReadonly"
|
||||
}
|
||||
]`);
|
||||
});
|
||||
|
||||
test('merge when local and remote with different keybinding', async () => {
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+a', command: '-a', when: 'editorTextFocus && !editorReadonly' }
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+a', command: '-a', when: 'editorTextFocus && !editorReadonly' }
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, null);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent,
|
||||
`[
|
||||
{
|
||||
"key": "alt+d",
|
||||
"command": "a",
|
||||
"when": "editorTextFocus && !editorReadonly"
|
||||
},
|
||||
{
|
||||
"key": "alt+a",
|
||||
"command": "-a",
|
||||
"when": "editorTextFocus && !editorReadonly"
|
||||
}
|
||||
]`);
|
||||
});
|
||||
|
||||
test('merge when the entry is removed in local but updated in remote', async () => {
|
||||
const baseContent = stringify([{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
|
||||
const localContent = stringify([]);
|
||||
const remoteContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, baseContent);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent,
|
||||
`[]`);
|
||||
});
|
||||
|
||||
test('merge when the entry is removed in local but updated in remote and a new entry is added in local', async () => {
|
||||
const baseContent = stringify([{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
|
||||
const localContent = stringify([{ key: 'alt+b', command: 'b' }]);
|
||||
const remoteContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, baseContent);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent,
|
||||
`[
|
||||
{
|
||||
"key": "alt+b",
|
||||
"command": "b"
|
||||
}
|
||||
]`);
|
||||
});
|
||||
|
||||
test('merge when the entry is removed in remote but updated in local', async () => {
|
||||
const baseContent = stringify([{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
|
||||
const localContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
|
||||
const remoteContent = stringify([]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, baseContent);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent,
|
||||
`[
|
||||
{
|
||||
"key": "alt+c",
|
||||
"command": "a",
|
||||
"when": "editorTextFocus && !editorReadonly"
|
||||
}
|
||||
]`);
|
||||
});
|
||||
|
||||
test('merge when the entry is removed in remote but updated in local and a new entry is added in remote', async () => {
|
||||
const baseContent = stringify([{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
|
||||
const localContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
|
||||
const remoteContent = stringify([{ key: 'alt+b', command: 'b' }]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, baseContent);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent,
|
||||
`[
|
||||
{
|
||||
"key": "alt+c",
|
||||
"command": "a",
|
||||
"when": "editorTextFocus && !editorReadonly"
|
||||
},
|
||||
{
|
||||
"key": "alt+b",
|
||||
"command": "b"
|
||||
}
|
||||
]`);
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with conflicts', async () => {
|
||||
const baseContent = stringify([
|
||||
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
|
||||
{ key: 'alt+c', command: '-a' },
|
||||
{ key: 'cmd+e', command: 'd' },
|
||||
{ key: 'alt+a', command: 'f' },
|
||||
{ key: 'alt+d', command: '-f' },
|
||||
{ key: 'cmd+d', command: 'c', when: 'context1' },
|
||||
{ key: 'cmd+c', command: '-c' },
|
||||
]);
|
||||
const localContent = stringify([
|
||||
{ key: 'alt+d', command: '-f' },
|
||||
{ key: 'cmd+e', command: 'd' },
|
||||
{ key: 'cmd+c', command: '-c' },
|
||||
{ key: 'cmd+d', command: 'c', when: 'context1' },
|
||||
{ key: 'alt+a', command: 'f' },
|
||||
{ key: 'alt+e', command: 'e' },
|
||||
]);
|
||||
const remoteContent = stringify([
|
||||
{ key: 'alt+a', command: 'f' },
|
||||
{ key: 'cmd+c', command: '-c' },
|
||||
{ key: 'cmd+d', command: 'd' },
|
||||
{ key: 'alt+d', command: '-f' },
|
||||
{ key: 'alt+c', command: 'c', when: 'context1' },
|
||||
{ key: 'alt+g', command: 'g', when: 'context2' },
|
||||
]);
|
||||
const actual = await mergeKeybindings(localContent, remoteContent, baseContent);
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent,
|
||||
`[
|
||||
{
|
||||
"key": "alt+d",
|
||||
"command": "-f"
|
||||
},
|
||||
{
|
||||
"key": "cmd+d",
|
||||
"command": "d"
|
||||
},
|
||||
{
|
||||
"key": "cmd+c",
|
||||
"command": "-c"
|
||||
},
|
||||
{
|
||||
"key": "cmd+d",
|
||||
"command": "c",
|
||||
"when": "context1"
|
||||
},
|
||||
{
|
||||
"key": "alt+a",
|
||||
"command": "f"
|
||||
},
|
||||
{
|
||||
"key": "alt+e",
|
||||
"command": "e"
|
||||
},
|
||||
{
|
||||
"key": "alt+g",
|
||||
"command": "g",
|
||||
"when": "context2"
|
||||
}
|
||||
]`);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
async function mergeKeybindings(localContent: string, remoteContent: string, baseContent: string | null) {
|
||||
const userDataSyncUtilService = new TestUserDataSyncUtilService();
|
||||
const formattingOptions = await userDataSyncUtilService.resolveFormattingOptions();
|
||||
return merge(localContent, remoteContent, baseContent, formattingOptions, userDataSyncUtilService);
|
||||
}
|
||||
|
||||
function stringify(value: any): string {
|
||||
return JSON.stringify(value, null, '\t');
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
|
||||
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { getKeybindingsContentFromSyncContent, KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
suite('KeybindingsSync', () => {
|
||||
|
||||
const disposableStore = new DisposableStore();
|
||||
const server = new UserDataSyncTestServer();
|
||||
let client: UserDataSyncClient;
|
||||
|
||||
let testObject: KeybindingsSynchroniser;
|
||||
|
||||
setup(async () => {
|
||||
client = disposableStore.add(new UserDataSyncClient(server));
|
||||
await client.setUp(true);
|
||||
testObject = (client.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Keybindings) as KeybindingsSynchroniser;
|
||||
disposableStore.add(toDisposable(() => client.instantiationService.get(IUserDataSyncStoreService).clear()));
|
||||
});
|
||||
|
||||
teardown(() => disposableStore.clear());
|
||||
|
||||
test('when keybindings file does not exist', async () => {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource;
|
||||
|
||||
assert.deepEqual(await testObject.getLastSyncUserData(), null);
|
||||
let manifest = await client.manifest();
|
||||
server.reset();
|
||||
await testObject.sync(manifest);
|
||||
|
||||
assert.deepEqual(server.requests, [
|
||||
{ type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} },
|
||||
]);
|
||||
assert.ok(!await fileService.exists(keybindingsResource));
|
||||
|
||||
const lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const remoteUserData = await testObject.getRemoteUserData(null);
|
||||
assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref);
|
||||
assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData);
|
||||
assert.equal(lastSyncUserData!.syncData, null);
|
||||
|
||||
manifest = await client.manifest();
|
||||
server.reset();
|
||||
await testObject.sync(manifest);
|
||||
assert.deepEqual(server.requests, []);
|
||||
|
||||
manifest = await client.manifest();
|
||||
server.reset();
|
||||
await testObject.sync(manifest);
|
||||
assert.deepEqual(server.requests, []);
|
||||
});
|
||||
|
||||
test('when keybindings file is empty and remote has no changes', async () => {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource;
|
||||
await fileService.writeFile(keybindingsResource, VSBuffer.fromString(''));
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const remoteUserData = await testObject.getRemoteUserData(null);
|
||||
assert.equal(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), '[]');
|
||||
assert.equal(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), '[]');
|
||||
assert.equal((await fileService.readFile(keybindingsResource)).value.toString(), '');
|
||||
});
|
||||
|
||||
test('when keybindings file is empty and remote has changes', async () => {
|
||||
const client2 = disposableStore.add(new UserDataSyncClient(server));
|
||||
await client2.setUp(true);
|
||||
const content = JSON.stringify([
|
||||
{
|
||||
'key': 'shift+cmd+w',
|
||||
'command': 'workbench.action.closeAllEditors',
|
||||
}
|
||||
]);
|
||||
await client2.instantiationService.get(IFileService).writeFile(client2.instantiationService.get(IEnvironmentService).keybindingsResource, VSBuffer.fromString(content));
|
||||
await client2.sync();
|
||||
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource;
|
||||
await fileService.writeFile(keybindingsResource, VSBuffer.fromString(''));
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const remoteUserData = await testObject.getRemoteUserData(null);
|
||||
assert.equal(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), content);
|
||||
assert.equal(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), content);
|
||||
assert.equal((await fileService.readFile(keybindingsResource)).value.toString(), content);
|
||||
});
|
||||
|
||||
test('when keybindings file is empty with comment and remote has no changes', async () => {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource;
|
||||
const expectedContent = '// Empty Keybindings';
|
||||
await fileService.writeFile(keybindingsResource, VSBuffer.fromString(expectedContent));
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const remoteUserData = await testObject.getRemoteUserData(null);
|
||||
assert.equal(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), expectedContent);
|
||||
assert.equal(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), expectedContent);
|
||||
assert.equal((await fileService.readFile(keybindingsResource)).value.toString(), expectedContent);
|
||||
});
|
||||
|
||||
test('when keybindings file is empty and remote has keybindings', async () => {
|
||||
const client2 = disposableStore.add(new UserDataSyncClient(server));
|
||||
await client2.setUp(true);
|
||||
const content = JSON.stringify([
|
||||
{
|
||||
'key': 'shift+cmd+w',
|
||||
'command': 'workbench.action.closeAllEditors',
|
||||
}
|
||||
]);
|
||||
await client2.instantiationService.get(IFileService).writeFile(client2.instantiationService.get(IEnvironmentService).keybindingsResource, VSBuffer.fromString(content));
|
||||
await client2.sync();
|
||||
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource;
|
||||
await fileService.writeFile(keybindingsResource, VSBuffer.fromString('// Empty Keybindings'));
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const remoteUserData = await testObject.getRemoteUserData(null);
|
||||
assert.equal(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), content);
|
||||
assert.equal(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), content);
|
||||
assert.equal((await fileService.readFile(keybindingsResource)).value.toString(), content);
|
||||
});
|
||||
|
||||
test('when keybindings file is empty and remote has empty array', async () => {
|
||||
const client2 = disposableStore.add(new UserDataSyncClient(server));
|
||||
await client2.setUp(true);
|
||||
const content =
|
||||
`// Place your key bindings in this file to override the defaults
|
||||
[
|
||||
]`;
|
||||
await client2.instantiationService.get(IFileService).writeFile(client2.instantiationService.get(IEnvironmentService).keybindingsResource, VSBuffer.fromString(content));
|
||||
await client2.sync();
|
||||
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource;
|
||||
const expectedLocalContent = '// Empty Keybindings';
|
||||
await fileService.writeFile(keybindingsResource, VSBuffer.fromString(expectedLocalContent));
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const remoteUserData = await testObject.getRemoteUserData(null);
|
||||
assert.equal(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), content);
|
||||
assert.equal(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), content);
|
||||
assert.equal((await fileService.readFile(keybindingsResource)).value.toString(), expectedLocalContent);
|
||||
});
|
||||
|
||||
test('when keybindings file is created after first sync', async () => {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource;
|
||||
await testObject.sync(await client.manifest());
|
||||
await fileService.createFile(keybindingsResource, VSBuffer.fromString('[]'));
|
||||
|
||||
let lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const manifest = await client.manifest();
|
||||
server.reset();
|
||||
await testObject.sync(manifest);
|
||||
|
||||
assert.deepEqual(server.requests, [
|
||||
{ type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } },
|
||||
]);
|
||||
|
||||
lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const remoteUserData = await testObject.getRemoteUserData(null);
|
||||
assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref);
|
||||
assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData);
|
||||
assert.equal(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), '[]');
|
||||
});
|
||||
|
||||
test('test apply remote when keybindings file does not exist', async () => {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource;
|
||||
if (await fileService.exists(keybindingsResource)) {
|
||||
await fileService.del(keybindingsResource);
|
||||
}
|
||||
|
||||
const preview = (await testObject.preview(await client.manifest()))!;
|
||||
|
||||
server.reset();
|
||||
const content = await testObject.resolveContent(preview.resourcePreviews[0].remoteResource);
|
||||
await testObject.accept(preview.resourcePreviews[0].remoteResource, content);
|
||||
await testObject.apply(false);
|
||||
assert.deepEqual(server.requests, []);
|
||||
});
|
||||
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,574 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, ISyncData, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
|
||||
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { SettingsSynchroniser, ISettingsSyncContent, parseSettingsSyncContent } from 'vs/platform/userDataSync/common/settingsSync';
|
||||
import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IConfigurationRegistry, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
|
||||
'id': 'settingsSync',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'settingsSync.machine': {
|
||||
'type': 'string',
|
||||
'scope': ConfigurationScope.MACHINE
|
||||
},
|
||||
'settingsSync.machineOverridable': {
|
||||
'type': 'string',
|
||||
'scope': ConfigurationScope.MACHINE_OVERRIDABLE
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
suite('SettingsSync - Auto', () => {
|
||||
|
||||
const disposableStore = new DisposableStore();
|
||||
const server = new UserDataSyncTestServer();
|
||||
let client: UserDataSyncClient;
|
||||
let testObject: SettingsSynchroniser;
|
||||
|
||||
setup(async () => {
|
||||
client = disposableStore.add(new UserDataSyncClient(server));
|
||||
await client.setUp(true);
|
||||
testObject = (client.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Settings) as SettingsSynchroniser;
|
||||
disposableStore.add(toDisposable(() => client.instantiationService.get(IUserDataSyncStoreService).clear()));
|
||||
});
|
||||
|
||||
teardown(() => disposableStore.clear());
|
||||
|
||||
test('when settings file does not exist', async () => {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const settingResource = client.instantiationService.get(IEnvironmentService).settingsResource;
|
||||
|
||||
assert.deepEqual(await testObject.getLastSyncUserData(), null);
|
||||
let manifest = await client.manifest();
|
||||
server.reset();
|
||||
await testObject.sync(manifest);
|
||||
|
||||
assert.deepEqual(server.requests, [
|
||||
{ type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} },
|
||||
]);
|
||||
assert.ok(!await fileService.exists(settingResource));
|
||||
|
||||
const lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const remoteUserData = await testObject.getRemoteUserData(null);
|
||||
assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref);
|
||||
assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData);
|
||||
assert.equal(lastSyncUserData!.syncData, null);
|
||||
|
||||
manifest = await client.manifest();
|
||||
server.reset();
|
||||
await testObject.sync(manifest);
|
||||
assert.deepEqual(server.requests, []);
|
||||
|
||||
manifest = await client.manifest();
|
||||
server.reset();
|
||||
await testObject.sync(manifest);
|
||||
assert.deepEqual(server.requests, []);
|
||||
});
|
||||
|
||||
test('when settings file is empty and remote has no changes', async () => {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const settingsResource = client.instantiationService.get(IEnvironmentService).settingsResource;
|
||||
await fileService.writeFile(settingsResource, VSBuffer.fromString(''));
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const remoteUserData = await testObject.getRemoteUserData(null);
|
||||
assert.equal(parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, '{}');
|
||||
assert.equal(parseSettingsSyncContent(remoteUserData!.syncData!.content!)?.settings, '{}');
|
||||
assert.equal((await fileService.readFile(settingsResource)).value.toString(), '');
|
||||
});
|
||||
|
||||
test('when settings file is empty and remote has changes', async () => {
|
||||
const client2 = disposableStore.add(new UserDataSyncClient(server));
|
||||
await client2.setUp(true);
|
||||
const content =
|
||||
`{
|
||||
// Always
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.simpleDialog.enable": true,
|
||||
|
||||
// Workbench
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
"workbench.tree.indent": 20,
|
||||
"workbench.colorCustomizations": {
|
||||
"editorLineNumber.activeForeground": "#ff0000",
|
||||
"[GitHub Sharp]": {
|
||||
"statusBarItem.remoteBackground": "#24292E",
|
||||
"editorPane.background": "#f3f1f11a"
|
||||
}
|
||||
},
|
||||
|
||||
"gitBranch.base": "remote-repo/master",
|
||||
|
||||
// Experimental
|
||||
"workbench.view.experimental.allowMovingToNewContainer": true,
|
||||
}`;
|
||||
await client2.instantiationService.get(IFileService).writeFile(client2.instantiationService.get(IEnvironmentService).settingsResource, VSBuffer.fromString(content));
|
||||
await client2.sync();
|
||||
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const settingsResource = client.instantiationService.get(IEnvironmentService).settingsResource;
|
||||
await fileService.writeFile(settingsResource, VSBuffer.fromString(''));
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const remoteUserData = await testObject.getRemoteUserData(null);
|
||||
assert.equal(parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, content);
|
||||
assert.equal(parseSettingsSyncContent(remoteUserData!.syncData!.content!)?.settings, content);
|
||||
assert.equal((await fileService.readFile(settingsResource)).value.toString(), content);
|
||||
});
|
||||
|
||||
test('when settings file is created after first sync', async () => {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
|
||||
const settingsResource = client.instantiationService.get(IEnvironmentService).settingsResource;
|
||||
await testObject.sync(await client.manifest());
|
||||
await fileService.createFile(settingsResource, VSBuffer.fromString('{}'));
|
||||
|
||||
let lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const manifest = await client.manifest();
|
||||
server.reset();
|
||||
await testObject.sync(manifest);
|
||||
|
||||
assert.deepEqual(server.requests, [
|
||||
{ type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } },
|
||||
]);
|
||||
|
||||
lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const remoteUserData = await testObject.getRemoteUserData(null);
|
||||
assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref);
|
||||
assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData);
|
||||
assert.equal(parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, '{}');
|
||||
});
|
||||
|
||||
test('sync for first time to the server', async () => {
|
||||
const expected =
|
||||
`{
|
||||
// Always
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.simpleDialog.enable": true,
|
||||
|
||||
// Workbench
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
"workbench.tree.indent": 20,
|
||||
"workbench.colorCustomizations": {
|
||||
"editorLineNumber.activeForeground": "#ff0000",
|
||||
"[GitHub Sharp]": {
|
||||
"statusBarItem.remoteBackground": "#24292E",
|
||||
"editorPane.background": "#f3f1f11a"
|
||||
}
|
||||
},
|
||||
|
||||
"gitBranch.base": "remote-repo/master",
|
||||
|
||||
// Experimental
|
||||
"workbench.view.experimental.allowMovingToNewContainer": true,
|
||||
}`;
|
||||
|
||||
await updateSettings(expected, client);
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const { content } = await client.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSettings(content!);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
test('do not sync machine settings', async () => {
|
||||
const settingsContent =
|
||||
`{
|
||||
// Always
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.simpleDialog.enable": true,
|
||||
|
||||
// Workbench
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
|
||||
// Machine
|
||||
"settingsSync.machine": "someValue",
|
||||
"settingsSync.machineOverridable": "someValue"
|
||||
}`;
|
||||
await updateSettings(settingsContent, client);
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const { content } = await client.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSettings(content!);
|
||||
assert.deepEqual(actual, `{
|
||||
// Always
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.simpleDialog.enable": true,
|
||||
|
||||
// Workbench
|
||||
"workbench.colorTheme": "GitHub Sharp"
|
||||
}`);
|
||||
});
|
||||
|
||||
test('do not sync machine settings when spread across file', async () => {
|
||||
const settingsContent =
|
||||
`{
|
||||
// Always
|
||||
"files.autoSave": "afterDelay",
|
||||
"settingsSync.machine": "someValue",
|
||||
"files.simpleDialog.enable": true,
|
||||
|
||||
// Workbench
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
|
||||
// Machine
|
||||
"settingsSync.machineOverridable": "someValue"
|
||||
}`;
|
||||
await updateSettings(settingsContent, client);
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const { content } = await client.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSettings(content!);
|
||||
assert.deepEqual(actual, `{
|
||||
// Always
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.simpleDialog.enable": true,
|
||||
|
||||
// Workbench
|
||||
"workbench.colorTheme": "GitHub Sharp"
|
||||
}`);
|
||||
});
|
||||
|
||||
test('do not sync machine settings when spread across file - 2', async () => {
|
||||
const settingsContent =
|
||||
`{
|
||||
// Always
|
||||
"files.autoSave": "afterDelay",
|
||||
"settingsSync.machine": "someValue",
|
||||
|
||||
// Workbench
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
|
||||
// Machine
|
||||
"settingsSync.machineOverridable": "someValue",
|
||||
"files.simpleDialog.enable": true,
|
||||
}`;
|
||||
await updateSettings(settingsContent, client);
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const { content } = await client.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSettings(content!);
|
||||
assert.deepEqual(actual, `{
|
||||
// Always
|
||||
"files.autoSave": "afterDelay",
|
||||
|
||||
// Workbench
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
"files.simpleDialog.enable": true,
|
||||
}`);
|
||||
});
|
||||
|
||||
test('sync when all settings are machine settings', async () => {
|
||||
const settingsContent =
|
||||
`{
|
||||
// Machine
|
||||
"settingsSync.machine": "someValue",
|
||||
"settingsSync.machineOverridable": "someValue"
|
||||
}`;
|
||||
await updateSettings(settingsContent, client);
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const { content } = await client.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSettings(content!);
|
||||
assert.deepEqual(actual, `{
|
||||
}`);
|
||||
});
|
||||
|
||||
test('sync when all settings are machine settings with trailing comma', async () => {
|
||||
const settingsContent =
|
||||
`{
|
||||
// Machine
|
||||
"settingsSync.machine": "someValue",
|
||||
"settingsSync.machineOverridable": "someValue",
|
||||
}`;
|
||||
await updateSettings(settingsContent, client);
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const { content } = await client.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSettings(content!);
|
||||
assert.deepEqual(actual, `{
|
||||
,
|
||||
}`);
|
||||
});
|
||||
|
||||
test('local change event is triggered when settings are changed', async () => {
|
||||
const content =
|
||||
`{
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.simpleDialog.enable": true,
|
||||
}`;
|
||||
|
||||
await updateSettings(content, client);
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const promise = Event.toPromise(testObject.onDidChangeLocal);
|
||||
await updateSettings(`{
|
||||
"files.autoSave": "off",
|
||||
"files.simpleDialog.enable": true,
|
||||
}`, client);
|
||||
await promise;
|
||||
});
|
||||
|
||||
test('do not sync ignored settings', async () => {
|
||||
const settingsContent =
|
||||
`{
|
||||
// Always
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.simpleDialog.enable": true,
|
||||
|
||||
// Editor
|
||||
"editor.fontFamily": "Fira Code",
|
||||
|
||||
// Terminal
|
||||
"terminal.integrated.shell.osx": "some path",
|
||||
|
||||
// Workbench
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
|
||||
// Ignored
|
||||
"settingsSync.ignoredSettings": [
|
||||
"editor.fontFamily",
|
||||
"terminal.integrated.shell.osx"
|
||||
]
|
||||
}`;
|
||||
await updateSettings(settingsContent, client);
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const { content } = await client.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSettings(content!);
|
||||
assert.deepEqual(actual, `{
|
||||
// Always
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.simpleDialog.enable": true,
|
||||
|
||||
// Workbench
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
|
||||
// Ignored
|
||||
"settingsSync.ignoredSettings": [
|
||||
"editor.fontFamily",
|
||||
"terminal.integrated.shell.osx"
|
||||
]
|
||||
}`);
|
||||
});
|
||||
|
||||
test('do not sync ignored and machine settings', async () => {
|
||||
const settingsContent =
|
||||
`{
|
||||
// Always
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.simpleDialog.enable": true,
|
||||
|
||||
// Editor
|
||||
"editor.fontFamily": "Fira Code",
|
||||
|
||||
// Terminal
|
||||
"terminal.integrated.shell.osx": "some path",
|
||||
|
||||
// Workbench
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
|
||||
// Ignored
|
||||
"settingsSync.ignoredSettings": [
|
||||
"editor.fontFamily",
|
||||
"terminal.integrated.shell.osx"
|
||||
],
|
||||
|
||||
// Machine
|
||||
"settingsSync.machine": "someValue",
|
||||
}`;
|
||||
await updateSettings(settingsContent, client);
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const { content } = await client.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSettings(content!);
|
||||
assert.deepEqual(actual, `{
|
||||
// Always
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.simpleDialog.enable": true,
|
||||
|
||||
// Workbench
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
|
||||
// Ignored
|
||||
"settingsSync.ignoredSettings": [
|
||||
"editor.fontFamily",
|
||||
"terminal.integrated.shell.osx"
|
||||
],
|
||||
}`);
|
||||
});
|
||||
|
||||
test('sync throws invalid content error', async () => {
|
||||
const expected =
|
||||
`{
|
||||
// Always
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.simpleDialog.enable": true,
|
||||
|
||||
// Workbench
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
"workbench.tree.indent": 20,
|
||||
"workbench.colorCustomizations": {
|
||||
"editorLineNumber.activeForeground": "#ff0000",
|
||||
"[GitHub Sharp]": {
|
||||
"statusBarItem.remoteBackground": "#24292E",
|
||||
"editorPane.background": "#f3f1f11a"
|
||||
}
|
||||
}
|
||||
|
||||
"gitBranch.base": "remote-repo/master",
|
||||
|
||||
// Experimental
|
||||
"workbench.view.experimental.allowMovingToNewContainer": true,
|
||||
}`;
|
||||
|
||||
await updateSettings(expected, client);
|
||||
|
||||
try {
|
||||
await testObject.sync(await client.manifest());
|
||||
assert.fail('should fail with invalid content error');
|
||||
} catch (e) {
|
||||
assert.ok(e instanceof UserDataSyncError);
|
||||
assert.deepEqual((<UserDataSyncError>e).code, UserDataSyncErrorCode.LocalInvalidContent);
|
||||
}
|
||||
});
|
||||
|
||||
test('sync when there are conflicts', async () => {
|
||||
const client2 = disposableStore.add(new UserDataSyncClient(server));
|
||||
await client2.setUp(true);
|
||||
await updateSettings(JSON.stringify({
|
||||
'a': 1,
|
||||
'b': 2,
|
||||
'settingsSync.ignoredSettings': ['a']
|
||||
}), client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSettings(JSON.stringify({
|
||||
'a': 2,
|
||||
'b': 1,
|
||||
'settingsSync.ignoredSettings': ['a']
|
||||
}), client);
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
assert.equal(testObject.conflicts[0].localResource.toString(), testObject.localResource);
|
||||
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const mergeContent = (await fileService.readFile(testObject.conflicts[0].previewResource)).value.toString();
|
||||
assert.deepEqual(JSON.parse(mergeContent), {
|
||||
'b': 1,
|
||||
'settingsSync.ignoredSettings': ['a']
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('SettingsSync - Manual', () => {
|
||||
|
||||
const disposableStore = new DisposableStore();
|
||||
const server = new UserDataSyncTestServer();
|
||||
let client: UserDataSyncClient;
|
||||
let testObject: SettingsSynchroniser;
|
||||
|
||||
setup(async () => {
|
||||
client = disposableStore.add(new UserDataSyncClient(server));
|
||||
await client.setUp(true);
|
||||
testObject = (client.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Settings) as SettingsSynchroniser;
|
||||
disposableStore.add(toDisposable(() => client.instantiationService.get(IUserDataSyncStoreService).clear()));
|
||||
});
|
||||
|
||||
teardown(() => disposableStore.clear());
|
||||
|
||||
test('do not sync ignored settings', async () => {
|
||||
const settingsContent =
|
||||
`{
|
||||
// Always
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.simpleDialog.enable": true,
|
||||
|
||||
// Editor
|
||||
"editor.fontFamily": "Fira Code",
|
||||
|
||||
// Terminal
|
||||
"terminal.integrated.shell.osx": "some path",
|
||||
|
||||
// Workbench
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
|
||||
// Ignored
|
||||
"settingsSync.ignoredSettings": [
|
||||
"editor.fontFamily",
|
||||
"terminal.integrated.shell.osx"
|
||||
]
|
||||
}`;
|
||||
await updateSettings(settingsContent, client);
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.apply(false);
|
||||
|
||||
const { content } = await client.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSettings(content!);
|
||||
assert.deepEqual(actual, `{
|
||||
// Always
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.simpleDialog.enable": true,
|
||||
|
||||
// Workbench
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
|
||||
// Ignored
|
||||
"settingsSync.ignoredSettings": [
|
||||
"editor.fontFamily",
|
||||
"terminal.integrated.shell.osx"
|
||||
]
|
||||
}`);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function parseSettings(content: string): string {
|
||||
const syncData: ISyncData = JSON.parse(content);
|
||||
const settingsSyncContent: ISettingsSyncContent = JSON.parse(syncData.content);
|
||||
return settingsSyncContent.settings;
|
||||
}
|
||||
|
||||
async function updateSettings(content: string, client: UserDataSyncClient): Promise<void> {
|
||||
await client.instantiationService.get(IFileService).writeFile(client.instantiationService.get(IEnvironmentService).settingsResource, VSBuffer.fromString(content));
|
||||
await client.instantiationService.get(IConfigurationService).reloadConfiguration();
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { merge } from 'vs/platform/userDataSync/common/snippetsMerge';
|
||||
|
||||
const tsSnippet1 = `{
|
||||
|
||||
// Place your snippets for TypeScript here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, Placeholders with the
|
||||
// same ids are connected.
|
||||
"Print to console": {
|
||||
// Example:
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console",
|
||||
}
|
||||
|
||||
}`;
|
||||
|
||||
const tsSnippet2 = `{
|
||||
|
||||
// Place your snippets for TypeScript here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, Placeholders with the
|
||||
// same ids are connected.
|
||||
"Print to console": {
|
||||
// Example:
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console always",
|
||||
}
|
||||
|
||||
}`;
|
||||
|
||||
const htmlSnippet1 = `{
|
||||
/*
|
||||
// Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted.
|
||||
// Example:
|
||||
"Print to console": {
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console"
|
||||
}
|
||||
*/
|
||||
"Div": {
|
||||
"prefix": "div",
|
||||
"body": [
|
||||
"<div>",
|
||||
"",
|
||||
"</div>"
|
||||
],
|
||||
"description": "New div"
|
||||
}
|
||||
}`;
|
||||
|
||||
const htmlSnippet2 = `{
|
||||
/*
|
||||
// Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted.
|
||||
// Example:
|
||||
"Print to console": {
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console"
|
||||
}
|
||||
*/
|
||||
"Div": {
|
||||
"prefix": "div",
|
||||
"body": [
|
||||
"<div>",
|
||||
"",
|
||||
"</div>"
|
||||
],
|
||||
"description": "New div changed"
|
||||
}
|
||||
}`;
|
||||
|
||||
const cSnippet = `{
|
||||
// Place your snippets for c here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position.Placeholders with the
|
||||
// same ids are connected.
|
||||
// Example:
|
||||
"Print to console": {
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console"
|
||||
}
|
||||
}`;
|
||||
|
||||
suite('SnippetsMerge', () => {
|
||||
|
||||
test('merge when local and remote are same with one snippet', async () => {
|
||||
const local = { 'html.json': htmlSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with multiple entries', async () => {
|
||||
const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with multiple entries in different order', async () => {
|
||||
const local = { 'typescript.json': tsSnippet1, 'html.json': htmlSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when local and remote are same with different base content', async () => {
|
||||
const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const base = { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet2 };
|
||||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when a new entry is added to remote', async () => {
|
||||
const local = { 'html.json': htmlSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet1 });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when multiple new entries are added to remote', async () => {
|
||||
const local = {};
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.local.added, remote);
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when new entry is added to remote from base and local has not changed', async () => {
|
||||
const local = { 'html.json': htmlSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet1 });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when an entry is removed from remote from base and local has not changed', async () => {
|
||||
const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, ['typescript.json']);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when all entries are removed from base and local has not changed', async () => {
|
||||
const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const remote = {};
|
||||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, ['html.json', 'typescript.json']);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when an entry is updated in remote from base and local has not changed', async () => {
|
||||
const local = { 'html.json': htmlSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet2 };
|
||||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when remote has moved forwarded with multiple changes and local stays with base', async () => {
|
||||
const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet2, 'c.json': cSnippet };
|
||||
|
||||
const actual = merge(local, remote, local);
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.local.updated, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.local.removed, ['typescript.json']);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when a new entries are added to local', async () => {
|
||||
const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1, 'c.json': cSnippet };
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote.added, { 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when multiple new entries are added to local from base and remote is not changed', async () => {
|
||||
const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1, 'c.json': cSnippet };
|
||||
const remote = { 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, remote);
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote.added, { 'html.json': htmlSnippet1, 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when an entry is removed from local from base and remote has not changed', async () => {
|
||||
const local = { 'html.json': htmlSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, remote);
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, ['typescript.json']);
|
||||
});
|
||||
|
||||
test('merge when an entry is updated in local from base and remote has not changed', async () => {
|
||||
const local = { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, remote);
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when local has moved forwarded with multiple changes and remote stays with base', async () => {
|
||||
const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet };
|
||||
const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, remote);
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote.added, { 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.remote.updated, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.remote.removed, ['typescript.json']);
|
||||
});
|
||||
|
||||
test('merge when local and remote with one entry but different value', async () => {
|
||||
const local = { 'html.json': htmlSnippet1 };
|
||||
const remote = { 'html.json': htmlSnippet2 };
|
||||
|
||||
const actual = merge(local, remote, null);
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, ['html.json']);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when the entry is removed in remote but updated in local and a new entry is added in remote', async () => {
|
||||
const base = { 'html.json': htmlSnippet1 };
|
||||
const local = { 'html.json': htmlSnippet2 };
|
||||
const remote = { 'typescript.json': tsSnippet1 };
|
||||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet1 });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, ['html.json']);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge with single entry and local is empty', async () => {
|
||||
const base = { 'html.json': htmlSnippet1 };
|
||||
const local = {};
|
||||
const remote = { 'html.json': htmlSnippet2 };
|
||||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'html.json': htmlSnippet2 });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, []);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with conflicts', async () => {
|
||||
const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet };
|
||||
const remote = { 'typescript.json': tsSnippet2 };
|
||||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'typescript.json': tsSnippet2 });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, ['html.json']);
|
||||
assert.deepEqual(actual.remote.added, { 'c.json': cSnippet });
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with multiple conflicts', async () => {
|
||||
const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 };
|
||||
const local = { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet2, 'c.json': cSnippet };
|
||||
const remote = { 'c.json': cSnippet };
|
||||
|
||||
const actual = merge(local, remote, base);
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.conflicts, ['html.json', 'typescript.json']);
|
||||
assert.deepEqual(actual.remote.added, {});
|
||||
assert.deepEqual(actual.remote.updated, {});
|
||||
assert.deepEqual(actual.remote.removed, []);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,998 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, SyncStatus, PREVIEW_DIR_NAME, ISyncData, IResourcePreview } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
|
||||
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync';
|
||||
import { joinPath, dirname } from 'vs/base/common/resources';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
const tsSnippet1 = `{
|
||||
|
||||
// Place your snippets for TypeScript here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, Placeholders with the
|
||||
// same ids are connected.
|
||||
"Print to console": {
|
||||
// Example:
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console",
|
||||
}
|
||||
|
||||
}`;
|
||||
|
||||
const tsSnippet2 = `{
|
||||
|
||||
// Place your snippets for TypeScript here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, Placeholders with the
|
||||
// same ids are connected.
|
||||
"Print to console": {
|
||||
// Example:
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console always",
|
||||
}
|
||||
|
||||
}`;
|
||||
|
||||
const htmlSnippet1 = `{
|
||||
/*
|
||||
// Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted.
|
||||
// Example:
|
||||
"Print to console": {
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console"
|
||||
}
|
||||
*/
|
||||
"Div": {
|
||||
"prefix": "div",
|
||||
"body": [
|
||||
"<div>",
|
||||
"",
|
||||
"</div>"
|
||||
],
|
||||
"description": "New div"
|
||||
}
|
||||
}`;
|
||||
|
||||
const htmlSnippet2 = `{
|
||||
/*
|
||||
// Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted.
|
||||
// Example:
|
||||
"Print to console": {
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console"
|
||||
}
|
||||
*/
|
||||
"Div": {
|
||||
"prefix": "div",
|
||||
"body": [
|
||||
"<div>",
|
||||
"",
|
||||
"</div>"
|
||||
],
|
||||
"description": "New div changed"
|
||||
}
|
||||
}`;
|
||||
|
||||
const htmlSnippet3 = `{
|
||||
/*
|
||||
// Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted.
|
||||
// Example:
|
||||
"Print to console": {
|
||||
"prefix": "log",
|
||||
"body": [
|
||||
"console.log('$1');",
|
||||
"$2"
|
||||
],
|
||||
"description": "Log output to console"
|
||||
}
|
||||
*/
|
||||
"Div": {
|
||||
"prefix": "div",
|
||||
"body": [
|
||||
"<div>",
|
||||
"",
|
||||
"</div>"
|
||||
],
|
||||
"description": "New div changed again"
|
||||
}
|
||||
}`;
|
||||
|
||||
const globalSnippet = `{
|
||||
// Place your global snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
||||
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
||||
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
||||
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, and {1: label}, { 2: another } for placeholders.
|
||||
// Placeholders with the same ids are connected.
|
||||
// Example:
|
||||
// "Print to console": {
|
||||
// "scope": "javascript,typescript",
|
||||
// "prefix": "log",
|
||||
// "body": [
|
||||
// "console.log('$1');",
|
||||
// "$2"
|
||||
// ],
|
||||
// "description": "Log output to console"
|
||||
// }
|
||||
}`;
|
||||
|
||||
suite('SnippetsSync', () => {
|
||||
|
||||
const disposableStore = new DisposableStore();
|
||||
const server = new UserDataSyncTestServer();
|
||||
let testClient: UserDataSyncClient;
|
||||
let client2: UserDataSyncClient;
|
||||
|
||||
let testObject: SnippetsSynchroniser;
|
||||
|
||||
setup(async () => {
|
||||
testClient = disposableStore.add(new UserDataSyncClient(server));
|
||||
await testClient.setUp(true);
|
||||
testObject = (testClient.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Snippets) as SnippetsSynchroniser;
|
||||
disposableStore.add(toDisposable(() => testClient.instantiationService.get(IUserDataSyncStoreService).clear()));
|
||||
|
||||
client2 = disposableStore.add(new UserDataSyncClient(server));
|
||||
await client2.setUp(true);
|
||||
});
|
||||
|
||||
teardown(() => disposableStore.clear());
|
||||
|
||||
test('when snippets does not exist', async () => {
|
||||
const fileService = testClient.instantiationService.get(IFileService);
|
||||
const snippetsResource = testClient.instantiationService.get(IEnvironmentService).snippetsHome;
|
||||
|
||||
assert.deepEqual(await testObject.getLastSyncUserData(), null);
|
||||
let manifest = await testClient.manifest();
|
||||
server.reset();
|
||||
await testObject.sync(manifest);
|
||||
|
||||
assert.deepEqual(server.requests, [
|
||||
{ type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} },
|
||||
]);
|
||||
assert.ok(!await fileService.exists(snippetsResource));
|
||||
|
||||
const lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const remoteUserData = await testObject.getRemoteUserData(null);
|
||||
assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref);
|
||||
assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData);
|
||||
assert.equal(lastSyncUserData!.syncData, null);
|
||||
|
||||
manifest = await testClient.manifest();
|
||||
server.reset();
|
||||
await testObject.sync(manifest);
|
||||
assert.deepEqual(server.requests, []);
|
||||
|
||||
manifest = await testClient.manifest();
|
||||
server.reset();
|
||||
await testObject.sync(manifest);
|
||||
assert.deepEqual(server.requests, []);
|
||||
});
|
||||
|
||||
test('when snippet is created after first sync', async () => {
|
||||
await testObject.sync(await testClient.manifest());
|
||||
await updateSnippet('html.json', htmlSnippet1, testClient);
|
||||
|
||||
let lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const manifest = await testClient.manifest();
|
||||
server.reset();
|
||||
await testObject.sync(manifest);
|
||||
|
||||
assert.deepEqual(server.requests, [
|
||||
{ type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } },
|
||||
]);
|
||||
|
||||
lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const remoteUserData = await testObject.getRemoteUserData(null);
|
||||
assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref);
|
||||
assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData);
|
||||
assert.deepEqual(lastSyncUserData!.syncData!.content, JSON.stringify({ 'html.json': htmlSnippet1 }));
|
||||
});
|
||||
|
||||
test('first time sync - outgoing to server (no snippets)', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet1, testClient);
|
||||
|
||||
await testObject.sync(await testClient.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 });
|
||||
});
|
||||
|
||||
test('first time sync - incoming from server (no snippets)', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await testObject.sync(await testClient.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet1);
|
||||
const actual2 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual2, tsSnippet1);
|
||||
});
|
||||
|
||||
test('first time sync when snippets exists', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('typescript.json', tsSnippet1, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet1);
|
||||
const actual2 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual2, tsSnippet1);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 });
|
||||
});
|
||||
|
||||
test('first time sync when snippets exists - has conflicts', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
|
||||
assertPreviews(testObject.conflicts, [local]);
|
||||
});
|
||||
|
||||
test('first time sync when snippets exists - has conflicts and accept conflicts', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
const conflicts = testObject.conflicts;
|
||||
await testObject.accept(conflicts[0].previewResource, htmlSnippet1);
|
||||
await testObject.apply(false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet1);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'html.json': htmlSnippet1 });
|
||||
});
|
||||
|
||||
test('first time sync when snippets exists - has multiple conflicts', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
const local1 = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
|
||||
const local2 = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json');
|
||||
assertPreviews(testObject.conflicts, [local1, local2]);
|
||||
});
|
||||
|
||||
test('first time sync when snippets exists - has multiple conflicts and accept one conflict', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
let conflicts = testObject.conflicts;
|
||||
await testObject.accept(conflicts[0].previewResource, htmlSnippet2);
|
||||
|
||||
conflicts = testObject.conflicts;
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json');
|
||||
assertPreviews(testObject.conflicts, [local]);
|
||||
});
|
||||
|
||||
test('first time sync when snippets exists - has multiple conflicts and accept all conflicts', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
const conflicts = testObject.conflicts;
|
||||
await testObject.accept(conflicts[0].previewResource, htmlSnippet2);
|
||||
await testObject.accept(conflicts[1].previewResource, tsSnippet1);
|
||||
await testObject.apply(false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet2);
|
||||
const actual2 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual2, tsSnippet1);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet1 });
|
||||
});
|
||||
|
||||
test('sync adding a snippet', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
await updateSnippet('typescript.json', tsSnippet1, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet1);
|
||||
const actual2 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual2, tsSnippet1);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 });
|
||||
});
|
||||
|
||||
test('sync adding a snippet - accept', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await client2.sync();
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await testObject.sync(await testClient.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet1);
|
||||
const actual2 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual2, tsSnippet1);
|
||||
});
|
||||
|
||||
test('sync updating a snippet', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet2);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'html.json': htmlSnippet2 });
|
||||
});
|
||||
|
||||
test('sync updating a snippet - accept', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await client2.sync();
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, client2);
|
||||
await client2.sync();
|
||||
|
||||
await testObject.sync(await testClient.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet2);
|
||||
});
|
||||
|
||||
test('sync updating a snippet - conflict', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await client2.sync();
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet3, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
|
||||
assertPreviews(testObject.conflicts, [local]);
|
||||
});
|
||||
|
||||
test('sync updating a snippet - resolve conflict', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await client2.sync();
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet3, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
await testObject.accept(testObject.conflicts[0].previewResource, htmlSnippet2);
|
||||
await testObject.apply(false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet2);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'html.json': htmlSnippet2 });
|
||||
});
|
||||
|
||||
test('sync removing a snippet', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet1, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
await removeSnippet('html.json', testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual1, tsSnippet1);
|
||||
const actual2 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual2, null);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'typescript.json': tsSnippet1 });
|
||||
});
|
||||
|
||||
test('sync removing a snippet - accept', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
await removeSnippet('html.json', client2);
|
||||
await client2.sync();
|
||||
|
||||
await testObject.sync(await testClient.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual1, tsSnippet1);
|
||||
const actual2 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual2, null);
|
||||
});
|
||||
|
||||
test('sync removing a snippet locally and updating it remotely', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, client2);
|
||||
await client2.sync();
|
||||
|
||||
await removeSnippet('html.json', testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual1, tsSnippet1);
|
||||
const actual2 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual2, htmlSnippet2);
|
||||
});
|
||||
|
||||
test('sync removing a snippet - conflict', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
await removeSnippet('html.json', client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
|
||||
assertPreviews(testObject.conflicts, [local]);
|
||||
});
|
||||
|
||||
test('sync removing a snippet - resolve conflict', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
await removeSnippet('html.json', client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
await testObject.accept(testObject.conflicts[0].previewResource, htmlSnippet3);
|
||||
await testObject.apply(false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual1, tsSnippet1);
|
||||
const actual2 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual2, htmlSnippet3);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'typescript.json': tsSnippet1, 'html.json': htmlSnippet3 });
|
||||
});
|
||||
|
||||
test('sync removing a snippet - resolve conflict by removing', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
await removeSnippet('html.json', client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
await testObject.accept(testObject.conflicts[0].previewResource, null);
|
||||
await testObject.apply(false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual1, tsSnippet1);
|
||||
const actual2 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual2, null);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'typescript.json': tsSnippet1 });
|
||||
});
|
||||
|
||||
test('sync global and language snippet', async () => {
|
||||
await updateSnippet('global.code-snippets', globalSnippet, client2);
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await testObject.sync(await testClient.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet1);
|
||||
const actual2 = await readSnippet('global.code-snippets', testClient);
|
||||
assert.equal(actual2, globalSnippet);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'global.code-snippets': globalSnippet });
|
||||
});
|
||||
|
||||
test('sync should ignore non snippets', async () => {
|
||||
await updateSnippet('global.code-snippets', globalSnippet, client2);
|
||||
await updateSnippet('html.html', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await testObject.sync(await testClient.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual1, tsSnippet1);
|
||||
const actual2 = await readSnippet('global.code-snippets', testClient);
|
||||
assert.equal(actual2, globalSnippet);
|
||||
const actual3 = await readSnippet('html.html', testClient);
|
||||
assert.equal(actual3, null);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'typescript.json': tsSnippet1, 'global.code-snippets': globalSnippet });
|
||||
});
|
||||
|
||||
test('previews are reset after all conflicts resolved', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await testObject.sync(await testClient.manifest());
|
||||
|
||||
let conflicts = testObject.conflicts;
|
||||
await testObject.accept(conflicts[0].previewResource, htmlSnippet2);
|
||||
await testObject.apply(false);
|
||||
|
||||
const fileService = testClient.instantiationService.get(IFileService);
|
||||
assert.ok(!await fileService.exists(dirname(conflicts[0].previewResource)));
|
||||
});
|
||||
|
||||
test('merge when there are multiple snippets and only one snippet is merged', async () => {
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
let preview = await testObject.preview(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].localResource);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('merge when there are multiple snippets and all snippets are merged', async () => {
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
let preview = await testObject.preview(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].localResource);
|
||||
preview = await testObject.merge(preview!.resourcePreviews[1].localResource);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('merge when there are multiple snippets and all snippets are merged and applied', async () => {
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
let preview = await testObject.preview(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].localResource);
|
||||
preview = await testObject.merge(preview!.resourcePreviews[1].localResource);
|
||||
preview = await testObject.apply(false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('merge when there are multiple snippets and one snippet has no changes and one snippet is merged', async () => {
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet1, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
let preview = await testObject.preview(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].localResource);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('merge when there are multiple snippets and one snippet has no changes and one snippet is merged and applied', async () => {
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet1, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
let preview = await testObject.preview(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].localResource);
|
||||
preview = await testObject.apply(false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('merge when there are multiple snippets with conflicts and only one snippet is merged', async () => {
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
let preview = await testObject.preview(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assertPreviews(testObject.conflicts,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('merge when there are multiple snippets with conflicts and all snippets are merged', async () => {
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
let preview = await testObject.preview(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.merge(preview!.resourcePreviews[1].previewResource);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assertPreviews(testObject.conflicts,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('accept when there are multiple snippets with conflicts and only one snippet is accepted', async () => {
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
let preview = await testObject.preview(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, htmlSnippet2);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('accept when there are multiple snippets with conflicts and all snippets are accepted', async () => {
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
let preview = await testObject.preview(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, htmlSnippet2);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[1].previewResource, tsSnippet2);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('accept when there are multiple snippets with conflicts and all snippets are accepted and applied', async () => {
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await updateSnippet('html.json', htmlSnippet2, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet2, testClient);
|
||||
let preview = await testObject.preview(await testClient.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews,
|
||||
[
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'),
|
||||
joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'),
|
||||
]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, htmlSnippet2);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[1].previewResource, tsSnippet2);
|
||||
preview = await testObject.apply(false);
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
function parseSnippets(content: string): IStringDictionary<string> {
|
||||
const syncData: ISyncData = JSON.parse(content);
|
||||
return JSON.parse(syncData.content);
|
||||
}
|
||||
|
||||
async function updateSnippet(name: string, content: string, client: UserDataSyncClient): Promise<void> {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
const snippetsResource = joinPath(environmentService.snippetsHome, name);
|
||||
await fileService.writeFile(snippetsResource, VSBuffer.fromString(content));
|
||||
}
|
||||
|
||||
async function removeSnippet(name: string, client: UserDataSyncClient): Promise<void> {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
const snippetsResource = joinPath(environmentService.snippetsHome, name);
|
||||
await fileService.del(snippetsResource);
|
||||
}
|
||||
|
||||
async function readSnippet(name: string, client: UserDataSyncClient): Promise<string | null> {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
const snippetsResource = joinPath(environmentService.snippetsHome, name);
|
||||
if (await fileService.exists(snippetsResource)) {
|
||||
const content = await fileService.readFile(snippetsResource);
|
||||
return content.value.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function assertPreviews(actual: IResourcePreview[], expected: URI[]) {
|
||||
assert.deepEqual(actual.map(({ previewResource }) => previewResource.toString()), expected.map(uri => uri.toString()));
|
||||
}
|
||||
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,428 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { UserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService';
|
||||
import { IUserDataSyncService, SyncResource, UserDataAutoSyncError, UserDataSyncErrorCode, UserDataSyncStoreError } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';
|
||||
|
||||
class TestUserDataAutoSyncService extends UserDataAutoSyncService {
|
||||
protected startAutoSync(): boolean { return false; }
|
||||
protected getSyncTriggerDelayTime(): number { return 50; }
|
||||
|
||||
sync(): Promise<void> {
|
||||
return this.triggerSync(['sync'], false, false);
|
||||
}
|
||||
}
|
||||
|
||||
suite('UserDataAutoSyncService', () => {
|
||||
|
||||
const disposableStore = new DisposableStore();
|
||||
|
||||
teardown(() => disposableStore.clear());
|
||||
|
||||
test('test auto sync with sync resource change triggers sync', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
|
||||
// Sync once and reset requests
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
target.reset();
|
||||
|
||||
const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
|
||||
// Trigger auto sync with settings change
|
||||
await testObject.triggerSync([SyncResource.Settings], false, false);
|
||||
|
||||
// Filter out machine requests
|
||||
const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`));
|
||||
|
||||
// Make sure only one manifest request is made
|
||||
assert.deepEqual(actual, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]);
|
||||
});
|
||||
|
||||
test('test auto sync with sync resource change triggers sync for every change', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
|
||||
// Sync once and reset requests
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
target.reset();
|
||||
|
||||
const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
|
||||
// Trigger auto sync with settings change multiple times
|
||||
for (let counter = 0; counter < 2; counter++) {
|
||||
await testObject.triggerSync([SyncResource.Settings], false, false);
|
||||
}
|
||||
|
||||
// Filter out machine requests
|
||||
const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`));
|
||||
|
||||
assert.deepEqual(actual, [
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }
|
||||
]);
|
||||
});
|
||||
|
||||
test('test auto sync with non sync resource change triggers sync', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
|
||||
// Sync once and reset requests
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
target.reset();
|
||||
|
||||
const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
|
||||
// Trigger auto sync with window focus once
|
||||
await testObject.triggerSync(['windowFocus'], true, false);
|
||||
|
||||
// Filter out machine requests
|
||||
const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`));
|
||||
|
||||
// Make sure only one manifest request is made
|
||||
assert.deepEqual(actual, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]);
|
||||
});
|
||||
|
||||
test('test auto sync with non sync resource change does not trigger continuous syncs', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
|
||||
// Sync once and reset requests
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
target.reset();
|
||||
|
||||
const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
|
||||
// Trigger auto sync with window focus multiple times
|
||||
for (let counter = 0; counter < 2; counter++) {
|
||||
await testObject.triggerSync(['windowFocus'], true, false);
|
||||
}
|
||||
|
||||
// Filter out machine requests
|
||||
const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`));
|
||||
|
||||
// Make sure only one manifest request is made
|
||||
assert.deepEqual(actual, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]);
|
||||
});
|
||||
|
||||
test('test first auto sync requests', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject: TestUserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
|
||||
await testObject.sync();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Machines
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '0' } },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '0' } },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },
|
||||
// Extensions
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Machines
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '0' } }
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test further auto sync requests without changes', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject: TestUserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
|
||||
// Sync once and reset requests
|
||||
await testObject.sync();
|
||||
target.reset();
|
||||
|
||||
await testObject.sync();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test further auto sync requests with changes', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject: TestUserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
|
||||
// Sync once and reset requests
|
||||
await testObject.sync();
|
||||
target.reset();
|
||||
|
||||
// Do changes in the client
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
|
||||
await fileService.writeFile(joinPath(environmentService.snippetsHome, 'html.json'), VSBuffer.fromString(`{}`));
|
||||
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
|
||||
await testObject.sync();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } },
|
||||
// Keybindings
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } },
|
||||
// Snippets
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } },
|
||||
// Global state
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test auto sync send execution id header', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject: TestUserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
|
||||
// Sync once and reset requests
|
||||
await testObject.sync();
|
||||
target.reset();
|
||||
|
||||
await testObject.sync();
|
||||
|
||||
for (const request of target.requestsWithAllHeaders) {
|
||||
const hasExecutionIdHeader = request.headers && request.headers['X-Execution-Id'] && request.headers['X-Execution-Id'].length > 0;
|
||||
if (request.url.startsWith(`${target.url}/v1/resource/machines`)) {
|
||||
assert.ok(!hasExecutionIdHeader, `Should not have execution header: ${request.url}`);
|
||||
} else {
|
||||
assert.ok(hasExecutionIdHeader, `Should have execution header: ${request.url}`);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
test('test delete on one client throws turned off error on other client while syncing', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Set up and sync from the client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Set up and sync from the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
await testObject.sync();
|
||||
|
||||
// Reset from the first client
|
||||
await client.instantiationService.get(IUserDataSyncService).reset();
|
||||
|
||||
// Sync from the test client
|
||||
target.reset();
|
||||
|
||||
const errorPromise = Event.toPromise(testObject.onError);
|
||||
await testObject.sync();
|
||||
|
||||
const e = await errorPromise;
|
||||
assert.ok(e instanceof UserDataAutoSyncError);
|
||||
assert.deepEqual((<UserDataAutoSyncError>e).code, UserDataSyncErrorCode.TurnedOff);
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Machine
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: { 'If-None-Match': '1' } },
|
||||
]);
|
||||
});
|
||||
|
||||
test('test disabling the machine turns off sync', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Set up and sync from the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
await testObject.sync();
|
||||
|
||||
// Disable current machine
|
||||
const userDataSyncMachinesService = testClient.instantiationService.get(IUserDataSyncMachinesService);
|
||||
const machines = await userDataSyncMachinesService.getMachines();
|
||||
const currentMachine = machines.find(m => m.isCurrent)!;
|
||||
await userDataSyncMachinesService.setEnablement(currentMachine.id, false);
|
||||
|
||||
target.reset();
|
||||
|
||||
const errorPromise = Event.toPromise(testObject.onError);
|
||||
await testObject.sync();
|
||||
|
||||
const e = await errorPromise;
|
||||
assert.ok(e instanceof UserDataAutoSyncError);
|
||||
assert.deepEqual((<UserDataAutoSyncError>e).code, UserDataSyncErrorCode.TurnedOff);
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Machine
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: { 'If-None-Match': '2' } },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '2' } },
|
||||
]);
|
||||
});
|
||||
|
||||
test('test removing the machine adds machine back', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Set up and sync from the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
await testObject.sync();
|
||||
|
||||
// Remove current machine
|
||||
await testClient.instantiationService.get(IUserDataSyncMachinesService).removeCurrentMachine();
|
||||
|
||||
target.reset();
|
||||
|
||||
await testObject.sync();
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Machine
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '2' } },
|
||||
]);
|
||||
});
|
||||
|
||||
test('test creating new session from one client throws session expired error on another client while syncing', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Set up and sync from the client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Set up and sync from the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
await testObject.sync();
|
||||
|
||||
// Reset from the first client
|
||||
await client.instantiationService.get(IUserDataSyncService).reset();
|
||||
|
||||
// Sync again from the first client to create new session
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Sync from the test client
|
||||
target.reset();
|
||||
|
||||
const errorPromise = Event.toPromise(testObject.onError);
|
||||
await testObject.sync();
|
||||
|
||||
const e = await errorPromise;
|
||||
assert.ok(e instanceof UserDataAutoSyncError);
|
||||
assert.deepEqual((<UserDataAutoSyncError>e).code, UserDataSyncErrorCode.SessionExpired);
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Machine
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: { 'If-None-Match': '1' } },
|
||||
]);
|
||||
});
|
||||
|
||||
test('test rate limit on server', async () => {
|
||||
const target = new UserDataSyncTestServer(5);
|
||||
|
||||
// Set up and sync from the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
|
||||
const errorPromise = Event.toPromise(testObject.onError);
|
||||
while (target.requests.length < 5) {
|
||||
await testObject.sync();
|
||||
}
|
||||
|
||||
const e = await errorPromise;
|
||||
assert.ok(e instanceof UserDataSyncStoreError);
|
||||
assert.deepEqual((<UserDataSyncStoreError>e).code, UserDataSyncErrorCode.TooManyRequests);
|
||||
});
|
||||
|
||||
test('test auto sync is suspended when server donot accepts requests', async () => {
|
||||
const target = new UserDataSyncTestServer(5, 1);
|
||||
|
||||
// Set up and sync from the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
|
||||
while (target.requests.length < 5) {
|
||||
await testObject.sync();
|
||||
}
|
||||
|
||||
target.reset();
|
||||
await testObject.sync();
|
||||
|
||||
assert.deepEqual(target.requests, []);
|
||||
});
|
||||
|
||||
test('test cache control header with no cache is sent when triggered with disable cache option', async () => {
|
||||
const target = new UserDataSyncTestServer(5, 1);
|
||||
|
||||
// Set up and sync from the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
|
||||
await testObject.triggerSync(['some reason'], true, true);
|
||||
assert.equal(target.requestsWithAllHeaders[0].headers!['Cache-Control'], 'no-cache');
|
||||
});
|
||||
|
||||
test('test cache control header is not sent when triggered without disable cache option', async () => {
|
||||
const target = new UserDataSyncTestServer(5, 1);
|
||||
|
||||
// Set up and sync from the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService);
|
||||
|
||||
await testObject.triggerSync(['some reason'], true, false);
|
||||
assert.equal(target.requestsWithAllHeaders[0].headers!['Cache-Control'], undefined);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,290 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IRequestService } from 'vs/platform/request/common/request';
|
||||
import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IUserData, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource, ServerResource, IUserDataSyncStoreManagementService, registerConfiguration, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { bufferToStream, VSBuffer } from 'vs/base/common/buffer';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { NullLogService, ILogService } from 'vs/platform/log/common/log';
|
||||
import { UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
|
||||
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { UserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSyncResourceEnablementService';
|
||||
import { IGlobalExtensionEnablementService, IExtensionManagementService, IExtensionGalleryService, DidInstallExtensionEvent, DidUninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService';
|
||||
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
|
||||
import { ConfigurationService } from 'vs/platform/configuration/common/configurationService';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { IUserDataSyncAccountService, UserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService';
|
||||
import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
import { IUserDataSyncMachinesService, UserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';
|
||||
import { UserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataAutoSyncService';
|
||||
import { IgnoredExtensionsManagementService, IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions';
|
||||
|
||||
export class UserDataSyncClient extends Disposable {
|
||||
|
||||
readonly instantiationService: TestInstantiationService;
|
||||
|
||||
constructor(readonly testServer: UserDataSyncTestServer = new UserDataSyncTestServer()) {
|
||||
super();
|
||||
this.instantiationService = new TestInstantiationService();
|
||||
}
|
||||
|
||||
async setUp(empty: boolean = false): Promise<void> {
|
||||
registerConfiguration();
|
||||
const userRoamingDataHome = URI.file('userdata').with({ scheme: Schemas.inMemory });
|
||||
const userDataSyncHome = joinPath(userRoamingDataHome, '.sync');
|
||||
const environmentService = this.instantiationService.stub(IEnvironmentService, <Partial<IEnvironmentService>>{
|
||||
userDataSyncHome,
|
||||
userRoamingDataHome,
|
||||
settingsResource: joinPath(userRoamingDataHome, 'settings.json'),
|
||||
keybindingsResource: joinPath(userRoamingDataHome, 'keybindings.json'),
|
||||
snippetsHome: joinPath(userRoamingDataHome, 'snippets'),
|
||||
argvResource: joinPath(userRoamingDataHome, 'argv.json'),
|
||||
sync: 'on',
|
||||
});
|
||||
|
||||
const logService = new NullLogService();
|
||||
this.instantiationService.stub(ILogService, logService);
|
||||
|
||||
this.instantiationService.stub(IProductService, {
|
||||
_serviceBrand: undefined, ...product, ...{
|
||||
'configurationSync.store': {
|
||||
url: this.testServer.url,
|
||||
stableUrl: this.testServer.url,
|
||||
insidersUrl: this.testServer.url,
|
||||
canSwitch: false,
|
||||
authenticationProviders: { 'test': { scopes: [] } }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const fileService = this._register(new FileService(logService));
|
||||
fileService.registerProvider(Schemas.inMemory, new InMemoryFileSystemProvider());
|
||||
this.instantiationService.stub(IFileService, fileService);
|
||||
|
||||
this.instantiationService.stub(IStorageService, new InMemoryStorageService());
|
||||
|
||||
const configurationService = new ConfigurationService(environmentService.settingsResource, fileService);
|
||||
await configurationService.initialize();
|
||||
this.instantiationService.stub(IConfigurationService, configurationService);
|
||||
|
||||
this.instantiationService.stub(IRequestService, this.testServer);
|
||||
|
||||
this.instantiationService.stub(IUserDataSyncLogService, logService);
|
||||
this.instantiationService.stub(ITelemetryService, NullTelemetryService);
|
||||
this.instantiationService.stub(IUserDataSyncStoreManagementService, this.instantiationService.createInstance(UserDataSyncStoreManagementService));
|
||||
this.instantiationService.stub(IUserDataSyncStoreService, this.instantiationService.createInstance(UserDataSyncStoreService));
|
||||
|
||||
const userDataSyncAccountService: IUserDataSyncAccountService = this.instantiationService.createInstance(UserDataSyncAccountService);
|
||||
await userDataSyncAccountService.updateAccount({ authenticationProviderId: 'authenticationProviderId', token: 'token' });
|
||||
this.instantiationService.stub(IUserDataSyncAccountService, userDataSyncAccountService);
|
||||
|
||||
this.instantiationService.stub(IUserDataSyncMachinesService, this.instantiationService.createInstance(UserDataSyncMachinesService));
|
||||
this.instantiationService.stub(IUserDataSyncBackupStoreService, this.instantiationService.createInstance(UserDataSyncBackupStoreService));
|
||||
this.instantiationService.stub(IUserDataSyncUtilService, new TestUserDataSyncUtilService());
|
||||
this.instantiationService.stub(IUserDataSyncResourceEnablementService, this.instantiationService.createInstance(UserDataSyncResourceEnablementService));
|
||||
this.instantiationService.stub(IStorageKeysSyncRegistryService, this.instantiationService.createInstance(StorageKeysSyncRegistryService));
|
||||
|
||||
this.instantiationService.stub(IGlobalExtensionEnablementService, this.instantiationService.createInstance(GlobalExtensionEnablementService));
|
||||
this.instantiationService.stub(IIgnoredExtensionsManagementService, this.instantiationService.createInstance(IgnoredExtensionsManagementService));
|
||||
this.instantiationService.stub(IExtensionManagementService, <Partial<IExtensionManagementService>>{
|
||||
async getInstalled() { return []; },
|
||||
onDidInstallExtension: new Emitter<DidInstallExtensionEvent>().event,
|
||||
onDidUninstallExtension: new Emitter<DidUninstallExtensionEvent>().event,
|
||||
});
|
||||
this.instantiationService.stub(IExtensionGalleryService, <Partial<IExtensionGalleryService>>{
|
||||
isEnabled() { return true; },
|
||||
async getCompatibleExtension() { return null; }
|
||||
});
|
||||
|
||||
this.instantiationService.stub(IUserDataAutoSyncEnablementService, this.instantiationService.createInstance(UserDataAutoSyncEnablementService));
|
||||
this.instantiationService.stub(IUserDataSyncService, this.instantiationService.createInstance(UserDataSyncService));
|
||||
|
||||
if (!empty) {
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({})));
|
||||
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([])));
|
||||
await fileService.writeFile(joinPath(environmentService.snippetsHome, 'c.json'), VSBuffer.fromString(`{}`));
|
||||
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'en' })));
|
||||
}
|
||||
await configurationService.reloadConfiguration();
|
||||
}
|
||||
|
||||
async sync(): Promise<void> {
|
||||
await (await this.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
}
|
||||
|
||||
read(resource: SyncResource): Promise<IUserData> {
|
||||
return this.instantiationService.get(IUserDataSyncStoreService).read(resource, null);
|
||||
}
|
||||
|
||||
manifest(): Promise<IUserDataManifest | null> {
|
||||
return this.instantiationService.get(IUserDataSyncStoreService).manifest();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const ALL_SERVER_RESOURCES: ServerResource[] = [...ALL_SYNC_RESOURCES, 'machines'];
|
||||
|
||||
export class UserDataSyncTestServer implements IRequestService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
readonly url: string = 'http://host:3000';
|
||||
private session: string | null = null;
|
||||
private readonly data: Map<ServerResource, IUserData> = new Map<SyncResource, IUserData>();
|
||||
|
||||
private _requests: { url: string, type: string, headers?: IHeaders }[] = [];
|
||||
get requests(): { url: string, type: string, headers?: IHeaders }[] { return this._requests; }
|
||||
|
||||
private _requestsWithAllHeaders: { url: string, type: string, headers?: IHeaders }[] = [];
|
||||
get requestsWithAllHeaders(): { url: string, type: string, headers?: IHeaders }[] { return this._requestsWithAllHeaders; }
|
||||
|
||||
private _responses: { status: number }[] = [];
|
||||
get responses(): { status: number }[] { return this._responses; }
|
||||
reset(): void { this._requests = []; this._responses = []; this._requestsWithAllHeaders = []; }
|
||||
|
||||
constructor(private readonly rateLimit = Number.MAX_SAFE_INTEGER, private readonly retryAfter?: number) { }
|
||||
|
||||
async resolveProxy(url: string): Promise<string | undefined> { return url; }
|
||||
|
||||
async request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
|
||||
if (this._requests.length === this.rateLimit) {
|
||||
return this.toResponse(429, this.retryAfter ? { 'retry-after': `${this.retryAfter}` } : undefined);
|
||||
}
|
||||
const headers: IHeaders = {};
|
||||
if (options.headers) {
|
||||
if (options.headers['If-None-Match']) {
|
||||
headers['If-None-Match'] = options.headers['If-None-Match'];
|
||||
}
|
||||
if (options.headers['If-Match']) {
|
||||
headers['If-Match'] = options.headers['If-Match'];
|
||||
}
|
||||
}
|
||||
this._requests.push({ url: options.url!, type: options.type!, headers });
|
||||
this._requestsWithAllHeaders.push({ url: options.url!, type: options.type!, headers: options.headers });
|
||||
const requestContext = await this.doRequest(options);
|
||||
this._responses.push({ status: requestContext.res.statusCode! });
|
||||
return requestContext;
|
||||
}
|
||||
|
||||
private async doRequest(options: IRequestOptions): Promise<IRequestContext> {
|
||||
const versionUrl = `${this.url}/v1/`;
|
||||
const relativePath = options.url!.indexOf(versionUrl) === 0 ? options.url!.substring(versionUrl.length) : undefined;
|
||||
const segments = relativePath ? relativePath.split('/') : [];
|
||||
if (options.type === 'GET' && segments.length === 1 && segments[0] === 'manifest') {
|
||||
return this.getManifest(options.headers);
|
||||
}
|
||||
if (options.type === 'GET' && segments.length === 3 && segments[0] === 'resource' && segments[2] === 'latest') {
|
||||
return this.getLatestData(segments[1], options.headers);
|
||||
}
|
||||
if (options.type === 'POST' && segments.length === 2 && segments[0] === 'resource') {
|
||||
return this.writeData(segments[1], options.data, options.headers);
|
||||
}
|
||||
if (options.type === 'DELETE' && segments.length === 1 && segments[0] === 'resource') {
|
||||
return this.clear(options.headers);
|
||||
}
|
||||
return this.toResponse(501);
|
||||
}
|
||||
|
||||
private async getManifest(headers?: IHeaders): Promise<IRequestContext> {
|
||||
if (this.session) {
|
||||
const latest: Record<ServerResource, string> = Object.create({});
|
||||
const manifest: IUserDataManifest = { session: this.session, latest };
|
||||
this.data.forEach((value, key) => latest[key] = value.ref);
|
||||
return this.toResponse(200, { 'Content-Type': 'application/json' }, JSON.stringify(manifest));
|
||||
}
|
||||
return this.toResponse(204);
|
||||
}
|
||||
|
||||
private async getLatestData(resource: string, headers: IHeaders = {}): Promise<IRequestContext> {
|
||||
const resourceKey = ALL_SERVER_RESOURCES.find(key => key === resource);
|
||||
if (resourceKey) {
|
||||
const data = this.data.get(resourceKey);
|
||||
if (!data) {
|
||||
return this.toResponse(204, { etag: '0' });
|
||||
}
|
||||
if (headers['If-None-Match'] === data.ref) {
|
||||
return this.toResponse(304);
|
||||
}
|
||||
return this.toResponse(200, { etag: data.ref }, data.content || '');
|
||||
}
|
||||
return this.toResponse(204);
|
||||
}
|
||||
|
||||
private async writeData(resource: string, content: string = '', headers: IHeaders = {}): Promise<IRequestContext> {
|
||||
if (!this.session) {
|
||||
this.session = generateUuid();
|
||||
}
|
||||
const resourceKey = ALL_SERVER_RESOURCES.find(key => key === resource);
|
||||
if (resourceKey) {
|
||||
const data = this.data.get(resourceKey);
|
||||
if (headers['If-Match'] !== undefined && headers['If-Match'] !== (data ? data.ref : '0')) {
|
||||
return this.toResponse(412);
|
||||
}
|
||||
const ref = `${parseInt(data?.ref || '0') + 1}`;
|
||||
this.data.set(resourceKey, { ref, content });
|
||||
return this.toResponse(200, { etag: ref });
|
||||
}
|
||||
return this.toResponse(204);
|
||||
}
|
||||
|
||||
async clear(headers?: IHeaders): Promise<IRequestContext> {
|
||||
this.data.clear();
|
||||
this.session = null;
|
||||
return this.toResponse(204);
|
||||
}
|
||||
|
||||
private toResponse(statusCode: number, headers?: IHeaders, data?: string): IRequestContext {
|
||||
return {
|
||||
res: {
|
||||
headers: headers || {},
|
||||
statusCode
|
||||
},
|
||||
stream: bufferToStream(VSBuffer.fromString(data || ''))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class TestUserDataSyncUtilService implements IUserDataSyncUtilService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
async resolveDefaultIgnoredSettings(): Promise<string[]> {
|
||||
return getDefaultIgnoredSettings();
|
||||
}
|
||||
|
||||
async resolveUserBindings(userbindings: string[]): Promise<IStringDictionary<string>> {
|
||||
const keys: IStringDictionary<string> = {};
|
||||
for (const keybinding of userbindings) {
|
||||
keys[keybinding] = keybinding;
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
async resolveFormattingOptions(file?: URI): Promise<FormattingOptions> {
|
||||
return { eol: '\n', insertSpaces: false, tabSize: 4 };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,444 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IUserDataSyncService, SyncStatus, SyncResource } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
|
||||
suite('UserDataSyncService', () => {
|
||||
|
||||
const disposableStore = new DisposableStore();
|
||||
|
||||
teardown(() => disposableStore.clear());
|
||||
|
||||
test('test first time sync ever', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
// Sync for first time
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '0' } },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '0' } },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },
|
||||
// Extensions
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test first time sync ever with no data', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp(true);
|
||||
const testObject = client.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
// Sync for first time
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
// Extensions
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test first time sync from the client with no changes - merge', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup and sync from the first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Setup the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
// Sync (merge) from the test client
|
||||
target.reset();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test first time sync from the client with changes - merge', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup and sync from the first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Setup the test client with changes
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const fileService = testClient.instantiationService.get(IFileService);
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
|
||||
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
|
||||
await fileService.writeFile(joinPath(environmentService.snippetsHome, 'html.json'), VSBuffer.fromString(`{}`));
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
// Sync (merge) from the test client
|
||||
target.reset();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test sync when there are no changes', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup and sync from the client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncService);
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
// sync from the client again
|
||||
target.reset();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
]);
|
||||
});
|
||||
|
||||
test('test sync when there are local changes', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup and sync from the client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncService);
|
||||
await (await testObject.createSyncTask()).run();
|
||||
target.reset();
|
||||
|
||||
// Do changes in the client
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
|
||||
await fileService.writeFile(joinPath(environmentService.snippetsHome, 'html.json'), VSBuffer.fromString(`{}`));
|
||||
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
|
||||
|
||||
// Sync from the client
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } },
|
||||
// Keybindings
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } },
|
||||
// Snippets
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } },
|
||||
// Global state
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } },
|
||||
]);
|
||||
});
|
||||
|
||||
test('test sync when there are remote changes', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Sync from first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Sync from test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
// Do changes in first client and sync
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
|
||||
await fileService.writeFile(joinPath(environmentService.snippetsHome, 'html.json'), VSBuffer.fromString(`{ "a": "changed" }`));
|
||||
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Sync from test client
|
||||
target.reset();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: { 'If-None-Match': '1' } },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: { 'If-None-Match': '1' } },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: { 'If-None-Match': '1' } },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: { 'If-None-Match': '1' } },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test delete', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Sync from the client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
// Reset from the client
|
||||
target.reset();
|
||||
await testObject.reset();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'DELETE', url: `${target.url}/v1/resource`, headers: {} },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test delete and sync', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Sync from the client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
// Reset from the client
|
||||
await testObject.reset();
|
||||
|
||||
// Sync again
|
||||
target.reset();
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '0' } },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '0' } },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },
|
||||
// Extensions
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test sync status', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup the client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
// sync from the client
|
||||
const actualStatuses: SyncStatus[] = [];
|
||||
const disposable = testObject.onDidChangeStatus(status => actualStatuses.push(status));
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
disposable.dispose();
|
||||
assert.deepEqual(actualStatuses, [SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle]);
|
||||
});
|
||||
|
||||
test('test sync conflicts status', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup and sync from the first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
let fileService = client.instantiationService.get(IFileService);
|
||||
let environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Setup the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
fileService = testClient.instantiationService.get(IFileService);
|
||||
environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 16 })));
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
// sync from the client
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
assert.deepEqual(testObject.conflicts.map(([syncResource]) => syncResource), [SyncResource.Settings]);
|
||||
});
|
||||
|
||||
test('test sync will sync other non conflicted areas', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup and sync from the first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
let fileService = client.instantiationService.get(IFileService);
|
||||
let environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Setup the test client and get conflicts in settings
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
let testFileService = testClient.instantiationService.get(IFileService);
|
||||
let testEnvironmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
await testFileService.writeFile(testEnvironmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 16 })));
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
// sync from the first client with changes in keybindings
|
||||
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// sync from the test client
|
||||
target.reset();
|
||||
const actualStatuses: SyncStatus[] = [];
|
||||
const disposable = testObject.onDidChangeStatus(status => actualStatuses.push(status));
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
disposable.dispose();
|
||||
assert.deepEqual(actualStatuses, []);
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: { 'If-None-Match': '1' } },
|
||||
]);
|
||||
});
|
||||
|
||||
test('test stop sync reset status', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup and sync from the first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
let fileService = client.instantiationService.get(IFileService);
|
||||
let environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Setup the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
fileService = testClient.instantiationService.get(IFileService);
|
||||
environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 16 })));
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
|
||||
const syncTask = (await testObject.createSyncTask());
|
||||
syncTask.run();
|
||||
await syncTask.stop();
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('test sync send execution id header', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
await (await testObject.createSyncTask()).run();
|
||||
|
||||
for (const request of target.requestsWithAllHeaders) {
|
||||
const hasExecutionIdHeader = request.headers && request.headers['X-Execution-Id'] && request.headers['X-Execution-Id'].length > 0;
|
||||
assert.ok(hasExecutionIdHeader, `Should have execution header: ${request.url}`);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
test('test can run sync taks only once', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
const syncTask = await testObject.createSyncTask();
|
||||
await syncTask.run();
|
||||
|
||||
try {
|
||||
await syncTask.run();
|
||||
assert.fail('Should fail running the task again');
|
||||
} catch (error) {
|
||||
/* expected */
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,502 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IUserDataSyncStoreService, SyncResource, UserDataSyncErrorCode, UserDataSyncStoreError, IUserDataSyncStoreManagementService, IUserDataSyncStore } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IProductService, ConfigurationSyncStore } from 'vs/platform/product/common/productService';
|
||||
import { isWeb } from 'vs/base/common/platform';
|
||||
import { RequestsSession, UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IRequestService } from 'vs/platform/request/common/request';
|
||||
import { newWriteableBufferStream, VSBuffer } from 'vs/base/common/buffer';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
suite('UserDataSyncStoreManagementService', () => {
|
||||
const disposableStore = new DisposableStore();
|
||||
|
||||
teardown(() => disposableStore.clear());
|
||||
|
||||
test('test sync store is read from settings', async () => {
|
||||
const client = disposableStore.add(new UserDataSyncClient(new UserDataSyncTestServer()));
|
||||
await client.setUp();
|
||||
|
||||
client.instantiationService.stub(IProductService, {
|
||||
_serviceBrand: undefined, ...product, ...{
|
||||
'configurationSync.store': undefined
|
||||
}
|
||||
});
|
||||
|
||||
const configuredStore: ConfigurationSyncStore = {
|
||||
url: 'http://configureHost:3000',
|
||||
stableUrl: 'http://configureHost:3000',
|
||||
insidersUrl: 'http://configureHost:3000',
|
||||
canSwitch: false,
|
||||
authenticationProviders: { 'configuredAuthProvider': { scopes: [] } }
|
||||
};
|
||||
await client.instantiationService.get(IFileService).writeFile(client.instantiationService.get(IEnvironmentService).settingsResource, VSBuffer.fromString(JSON.stringify({
|
||||
'configurationSync.store': configuredStore
|
||||
})));
|
||||
await client.instantiationService.get(IConfigurationService).reloadConfiguration();
|
||||
|
||||
const expected: IUserDataSyncStore = {
|
||||
url: URI.parse('http://configureHost:3000'),
|
||||
defaultUrl: URI.parse('http://configureHost:3000'),
|
||||
stableUrl: URI.parse('http://configureHost:3000'),
|
||||
insidersUrl: URI.parse('http://configureHost:3000'),
|
||||
canSwitch: false,
|
||||
authenticationProviders: [{ id: 'configuredAuthProvider', scopes: [] }]
|
||||
};
|
||||
|
||||
const testObject: IUserDataSyncStoreManagementService = client.instantiationService.createInstance(UserDataSyncStoreManagementService);
|
||||
|
||||
assert.equal(testObject.userDataSyncStore?.url.toString(), expected.url.toString());
|
||||
assert.equal(testObject.userDataSyncStore?.defaultUrl.toString(), expected.defaultUrl.toString());
|
||||
assert.deepEqual(testObject.userDataSyncStore?.authenticationProviders, expected.authenticationProviders);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('UserDataSyncStoreService', () => {
|
||||
|
||||
const disposableStore = new DisposableStore();
|
||||
|
||||
teardown(() => disposableStore.clear());
|
||||
|
||||
test('test read manifest for the first time', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
|
||||
const productService = client.instantiationService.get(IProductService);
|
||||
|
||||
await testObject.manifest();
|
||||
|
||||
assert.equal(target.requestsWithAllHeaders.length, 1);
|
||||
assert.equal(target.requestsWithAllHeaders[0].headers!['X-Client-Name'], `${productService.applicationName}${isWeb ? '-web' : ''}`);
|
||||
assert.equal(target.requestsWithAllHeaders[0].headers!['X-Client-Version'], productService.version);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Id'], undefined);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined);
|
||||
assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
|
||||
});
|
||||
|
||||
test('test read manifest for the second time when session is not yet created', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
|
||||
|
||||
await testObject.manifest();
|
||||
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
|
||||
|
||||
target.reset();
|
||||
await testObject.manifest();
|
||||
|
||||
assert.equal(target.requestsWithAllHeaders.length, 1);
|
||||
assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
|
||||
assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
|
||||
});
|
||||
|
||||
test('test session id header is not set in the first manifest request after session is created', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
|
||||
|
||||
await testObject.manifest();
|
||||
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
|
||||
await testObject.write(SyncResource.Settings, 'some content', null);
|
||||
|
||||
target.reset();
|
||||
await testObject.manifest();
|
||||
|
||||
assert.equal(target.requestsWithAllHeaders.length, 1);
|
||||
assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
|
||||
assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
|
||||
});
|
||||
|
||||
test('test session id header is set from the second manifest request after session is created', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
|
||||
|
||||
await testObject.manifest();
|
||||
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
|
||||
await testObject.write(SyncResource.Settings, 'some content', null);
|
||||
await testObject.manifest();
|
||||
|
||||
target.reset();
|
||||
await testObject.manifest();
|
||||
|
||||
assert.equal(target.requestsWithAllHeaders.length, 1);
|
||||
assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
|
||||
});
|
||||
|
||||
test('test headers are send for write request', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
|
||||
|
||||
await testObject.manifest();
|
||||
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
|
||||
await testObject.write(SyncResource.Settings, 'some content', null);
|
||||
await testObject.manifest();
|
||||
await testObject.manifest();
|
||||
|
||||
target.reset();
|
||||
await testObject.write(SyncResource.Settings, 'some content', null);
|
||||
|
||||
assert.equal(target.requestsWithAllHeaders.length, 1);
|
||||
assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
|
||||
});
|
||||
|
||||
test('test headers are send for read request', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
|
||||
|
||||
await testObject.manifest();
|
||||
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
|
||||
await testObject.write(SyncResource.Settings, 'some content', null);
|
||||
await testObject.manifest();
|
||||
await testObject.manifest();
|
||||
|
||||
target.reset();
|
||||
await testObject.read(SyncResource.Settings, null);
|
||||
|
||||
assert.equal(target.requestsWithAllHeaders.length, 1);
|
||||
assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
|
||||
});
|
||||
|
||||
test('test headers are reset after session is cleared ', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
|
||||
|
||||
await testObject.manifest();
|
||||
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
|
||||
await testObject.write(SyncResource.Settings, 'some content', null);
|
||||
await testObject.manifest();
|
||||
await testObject.manifest();
|
||||
await testObject.clear();
|
||||
|
||||
target.reset();
|
||||
await testObject.manifest();
|
||||
|
||||
assert.equal(target.requestsWithAllHeaders.length, 1);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
|
||||
assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
|
||||
});
|
||||
|
||||
test('test old headers are sent after session is changed on server ', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
|
||||
|
||||
await testObject.manifest();
|
||||
await testObject.write(SyncResource.Settings, 'some content', null);
|
||||
await testObject.manifest();
|
||||
target.reset();
|
||||
await testObject.manifest();
|
||||
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
|
||||
const userSessionId = target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'];
|
||||
await target.clear();
|
||||
|
||||
// client 2
|
||||
const client2 = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client2.setUp();
|
||||
const testObject2 = client2.instantiationService.get(IUserDataSyncStoreService);
|
||||
await testObject2.write(SyncResource.Settings, 'some content', null);
|
||||
|
||||
target.reset();
|
||||
await testObject.manifest();
|
||||
|
||||
assert.equal(target.requestsWithAllHeaders.length, 1);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined);
|
||||
assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
|
||||
assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], userSessionId);
|
||||
});
|
||||
|
||||
test('test old headers are reset from second request after session is changed on server ', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
|
||||
|
||||
await testObject.manifest();
|
||||
await testObject.write(SyncResource.Settings, 'some content', null);
|
||||
await testObject.manifest();
|
||||
target.reset();
|
||||
await testObject.manifest();
|
||||
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
|
||||
const userSessionId = target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'];
|
||||
await target.clear();
|
||||
|
||||
// client 2
|
||||
const client2 = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client2.setUp();
|
||||
const testObject2 = client2.instantiationService.get(IUserDataSyncStoreService);
|
||||
await testObject2.write(SyncResource.Settings, 'some content', null);
|
||||
|
||||
await testObject.manifest();
|
||||
target.reset();
|
||||
await testObject.manifest();
|
||||
|
||||
assert.equal(target.requestsWithAllHeaders.length, 1);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], userSessionId);
|
||||
});
|
||||
|
||||
test('test old headers are sent after session is cleared from another server ', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
|
||||
|
||||
await testObject.manifest();
|
||||
await testObject.write(SyncResource.Settings, 'some content', null);
|
||||
await testObject.manifest();
|
||||
target.reset();
|
||||
await testObject.manifest();
|
||||
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
|
||||
const userSessionId = target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'];
|
||||
|
||||
// client 2
|
||||
const client2 = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client2.setUp();
|
||||
const testObject2 = client2.instantiationService.get(IUserDataSyncStoreService);
|
||||
await testObject2.clear();
|
||||
|
||||
target.reset();
|
||||
await testObject.manifest();
|
||||
|
||||
assert.equal(target.requestsWithAllHeaders.length, 1);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined);
|
||||
assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
|
||||
assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], userSessionId);
|
||||
});
|
||||
|
||||
test('test headers are reset after session is cleared from another server ', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
|
||||
|
||||
await testObject.manifest();
|
||||
await testObject.write(SyncResource.Settings, 'some content', null);
|
||||
await testObject.manifest();
|
||||
target.reset();
|
||||
await testObject.manifest();
|
||||
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
|
||||
|
||||
// client 2
|
||||
const client2 = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client2.setUp();
|
||||
const testObject2 = client2.instantiationService.get(IUserDataSyncStoreService);
|
||||
await testObject2.clear();
|
||||
|
||||
await testObject.manifest();
|
||||
target.reset();
|
||||
await testObject.manifest();
|
||||
|
||||
assert.equal(target.requestsWithAllHeaders.length, 1);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
|
||||
assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
|
||||
});
|
||||
|
||||
test('test headers are reset after session is cleared from another server - started syncing again', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
|
||||
|
||||
await testObject.manifest();
|
||||
await testObject.write(SyncResource.Settings, 'some content', null);
|
||||
await testObject.manifest();
|
||||
target.reset();
|
||||
await testObject.manifest();
|
||||
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
|
||||
const userSessionId = target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'];
|
||||
|
||||
// client 2
|
||||
const client2 = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client2.setUp();
|
||||
const testObject2 = client2.instantiationService.get(IUserDataSyncStoreService);
|
||||
await testObject2.clear();
|
||||
|
||||
await testObject.manifest();
|
||||
await testObject.write(SyncResource.Settings, 'some content', null);
|
||||
await testObject.manifest();
|
||||
target.reset();
|
||||
await testObject.manifest();
|
||||
|
||||
assert.equal(target.requestsWithAllHeaders.length, 1);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], userSessionId);
|
||||
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
|
||||
});
|
||||
|
||||
test('test rate limit on server with retry after', async () => {
|
||||
const target = new UserDataSyncTestServer(1, 1);
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
|
||||
|
||||
await testObject.manifest();
|
||||
|
||||
const promise = Event.toPromise(testObject.onDidChangeDonotMakeRequestsUntil);
|
||||
try {
|
||||
await testObject.manifest();
|
||||
assert.fail('should fail');
|
||||
} catch (e) {
|
||||
assert.ok(e instanceof UserDataSyncStoreError);
|
||||
assert.deepEqual((<UserDataSyncStoreError>e).code, UserDataSyncErrorCode.TooManyRequestsAndRetryAfter);
|
||||
await promise;
|
||||
assert.ok(!!testObject.donotMakeRequestsUntil);
|
||||
}
|
||||
});
|
||||
|
||||
test('test donotMakeRequestsUntil is reset after retry time is finished', async () => {
|
||||
const client = disposableStore.add(new UserDataSyncClient(new UserDataSyncTestServer(1, 0.25)));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
|
||||
|
||||
await testObject.manifest();
|
||||
try {
|
||||
await testObject.manifest();
|
||||
} catch (e) { }
|
||||
|
||||
const promise = Event.toPromise(testObject.onDidChangeDonotMakeRequestsUntil);
|
||||
await timeout(300);
|
||||
await promise;
|
||||
assert.ok(!testObject.donotMakeRequestsUntil);
|
||||
});
|
||||
|
||||
test('test donotMakeRequestsUntil is retrieved', async () => {
|
||||
const client = disposableStore.add(new UserDataSyncClient(new UserDataSyncTestServer(1, 1)));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
|
||||
|
||||
await testObject.manifest();
|
||||
try {
|
||||
await testObject.manifest();
|
||||
} catch (e) { }
|
||||
|
||||
const target = client.instantiationService.createInstance(UserDataSyncStoreService);
|
||||
assert.equal(target.donotMakeRequestsUntil?.getTime(), testObject.donotMakeRequestsUntil?.getTime());
|
||||
});
|
||||
|
||||
test('test donotMakeRequestsUntil is checked and reset after retreived', async () => {
|
||||
const client = disposableStore.add(new UserDataSyncClient(new UserDataSyncTestServer(1, 0.25)));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
|
||||
|
||||
await testObject.manifest();
|
||||
try {
|
||||
await testObject.manifest();
|
||||
} catch (e) { }
|
||||
|
||||
await timeout(300);
|
||||
const target = client.instantiationService.createInstance(UserDataSyncStoreService);
|
||||
assert.ok(!target.donotMakeRequestsUntil);
|
||||
});
|
||||
|
||||
test('test read resource request handles 304', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await client.sync();
|
||||
|
||||
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
|
||||
const expected = await testObject.read(SyncResource.Settings, null);
|
||||
const actual = await testObject.read(SyncResource.Settings, expected);
|
||||
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('UserDataSyncRequestsSession', () => {
|
||||
|
||||
const requestService: IRequestService = {
|
||||
_serviceBrand: undefined,
|
||||
async request() { return { res: { headers: {} }, stream: newWriteableBufferStream() }; },
|
||||
async resolveProxy() { return undefined; }
|
||||
};
|
||||
|
||||
test('too many requests are thrown when limit exceeded', async () => {
|
||||
const testObject = new RequestsSession(1, 500, requestService, new NullLogService());
|
||||
await testObject.request({}, CancellationToken.None);
|
||||
|
||||
try {
|
||||
await testObject.request({}, CancellationToken.None);
|
||||
} catch (error) {
|
||||
assert.ok(error instanceof UserDataSyncStoreError);
|
||||
assert.equal((<UserDataSyncStoreError>error).code, UserDataSyncErrorCode.LocalTooManyRequests);
|
||||
return;
|
||||
}
|
||||
assert.fail('Should fail with limit exceeded');
|
||||
});
|
||||
|
||||
test('requests are handled after session is expired', async () => {
|
||||
const testObject = new RequestsSession(1, 500, requestService, new NullLogService());
|
||||
await testObject.request({}, CancellationToken.None);
|
||||
await timeout(600);
|
||||
await testObject.request({}, CancellationToken.None);
|
||||
});
|
||||
|
||||
test('too many requests are thrown after session is expired', async () => {
|
||||
const testObject = new RequestsSession(1, 500, requestService, new NullLogService());
|
||||
await testObject.request({}, CancellationToken.None);
|
||||
await timeout(600);
|
||||
await testObject.request({}, CancellationToken.None);
|
||||
|
||||
try {
|
||||
await testObject.request({}, CancellationToken.None);
|
||||
} catch (error) {
|
||||
assert.ok(error instanceof UserDataSyncStoreError);
|
||||
assert.equal((<UserDataSyncStoreError>error).code, UserDataSyncErrorCode.LocalTooManyRequests);
|
||||
return;
|
||||
}
|
||||
assert.fail('Should fail with limit exceeded');
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user