Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@@ -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>;
}

View 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;
}

View File

@@ -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;
}

View 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);
}
}
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}
}
}

View 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);
}
}

View File

@@ -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);
}
}

View 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];
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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');
}
}

View File

@@ -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();
}
}

View File

@@ -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: []
};
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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,
};
}

View File

@@ -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;
}
}

View File

@@ -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)));
}
}

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -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;
}
});

View File

@@ -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');
}

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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, []);
});
});

View File

@@ -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

View File

@@ -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);
});
});

View File

@@ -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 };
}
}

View File

@@ -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 */
}
});
});

View File

@@ -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');
});
});