Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/microsoft/vscode.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSandeep Somavarapu <sasomava@microsoft.com>2022-11-11 22:56:58 +0300
committerGitHub <noreply@github.com>2022-11-11 22:56:58 +0300
commitf8995e0b1aae1e6cafe677eb76fbae047034a8c8 (patch)
treef2392b10c30f7796c1d0356f2ee52139199f5dd3
parentb982536f83376e6266b3e0aaca6211699167b6cf (diff)
store last sync data in state (#166133)
fall back to server if last sync content does not exist
-rw-r--r--src/vs/platform/userDataSync/common/abstractSynchronizer.ts146
-rw-r--r--src/vs/platform/userDataSync/common/userDataSync.ts4
-rw-r--r--src/vs/platform/userDataSync/common/userDataSyncService.ts29
-rw-r--r--src/vs/platform/userDataSync/common/userDataSyncServiceIpc.ts27
-rw-r--r--src/vs/platform/userDataSync/common/userDataSyncStoreService.ts4
-rw-r--r--src/vs/platform/userDataSync/test/common/synchronizer.test.ts201
-rw-r--r--src/vs/platform/userDataSync/test/common/userDataSyncClient.ts31
-rw-r--r--src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts2
8 files changed, 365 insertions, 79 deletions
diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts
index 88e5a11f829..7cde80bda84 100644
--- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts
+++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts
@@ -23,7 +23,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
import { FileChangesEvent, FileOperationError, FileOperationResult, IFileContent, IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { getServiceMachineId } from 'vs/platform/externalServices/common/serviceMachineId';
-import { IStorageService } from 'vs/platform/storage/common/storage';
+import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { Change, getLastSyncResourceUri, IRemoteUserData, IResourcePreview as IBaseResourcePreview, ISyncData, IUserDataSyncResourcePreview as IBaseSyncResourcePreview, IUserData, IUserDataInitializer, IUserDataSyncBackupStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, MergeState, PREVIEW_DIR_NAME, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_CONFIGURATION_SCOPE, USER_DATA_SYNC_SCHEME, IUserDataResourceManifest, getPathSegments, IUserDataSyncResourceConflicts, IUserDataSyncResource } from 'vs/platform/userDataSync/common/userDataSync';
@@ -98,6 +98,12 @@ interface ISyncResourcePreview extends IBaseSyncResourcePreview {
readonly resourcePreviews: IEditableResourcePreview[];
}
+interface ILastSyncUserDataState {
+ readonly ref: string;
+ readonly version: string | undefined;
+ [key: string]: any;
+}
+
export abstract class AbstractSynchroniser extends Disposable implements IUserDataSynchroniser {
private syncPreviewPromise: CancelablePromise<ISyncResourcePreview> | null = null;
@@ -105,7 +111,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa
protected readonly syncFolder: URI;
protected readonly syncPreviewFolder: URI;
protected readonly extUri: IExtUri;
- private readonly currentMachineIdPromise: Promise<string>;
+ protected readonly currentMachineIdPromise: Promise<string>;
private _status: SyncStatus = SyncStatus.Idle;
get status(): SyncStatus { return this._status; }
@@ -122,6 +128,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa
readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;
protected readonly lastSyncResource: URI;
+ private readonly lastSyncUserDataStateKey = `${this.collection ? `${this.collection}.` : ''}${this.syncResource.syncResource}.lastSyncUserData`;
private hasSyncResourceStateVersionChanged: boolean = false;
protected readonly syncResourceLogLabel: string;
@@ -134,7 +141,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa
readonly collection: string | undefined,
@IFileService protected readonly fileService: IFileService,
@IEnvironmentService protected readonly environmentService: IEnvironmentService,
- @IStorageService storageService: IStorageService,
+ @IStorageService protected readonly storageService: IStorageService,
@IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncBackupStoreService protected readonly userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IUserDataSyncEnablementService protected readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
@@ -327,7 +334,7 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa
// 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
+ // Get the latest last sync user data. Because multiple 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();
@@ -507,9 +514,12 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa
}
async resetLocal(): Promise<void> {
+ this.storageService.remove(this.lastSyncUserDataStateKey, StorageScope.APPLICATION);
try {
await this.fileService.del(this.lastSyncResource);
- } catch (e) { /* ignore */ }
+ } catch (e) {
+ this.logService.error(e);
+ }
}
private async doGenerateSyncResourcePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, apply: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration, token: CancellationToken): Promise<ISyncResourcePreview> {
@@ -558,48 +568,116 @@ export abstract class AbstractSynchroniser extends Disposable implements IUserDa
return { syncResource: this.resource, profile: this.syncResource.profile, remoteUserData, lastSyncUserData, resourcePreviews, isLastSyncFromCurrentMachine: isRemoteDataFromCurrentMachine };
}
- 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 resourceSyncStateVersion = this.userDataSyncEnablementService.getResourceSyncStateVersion(this.resource);
- this.hasSyncResourceStateVersionChanged = parsed.version && resourceSyncStateVersion && parsed.version !== resourceSyncStateVersion;
- if (this.hasSyncResourceStateVersionChanged) {
- this.logService.info(`${this.syncResourceLogLabel}: Reset last sync state because last sync state version ${parsed.version} is not compatible with current sync state version ${resourceSyncStateVersion}.`);
- await this.resetLocal();
- return null;
- }
+ async getLastSyncUserData<T = IRemoteUserData & { [key: string]: any }>(): Promise<T | null> {
+ let storedLastSyncUserDataStateContent = this.storageService.get(this.lastSyncUserDataStateKey, StorageScope.APPLICATION);
+ if (!storedLastSyncUserDataStateContent) {
+ storedLastSyncUserDataStateContent = await this.migrateLastSyncUserData();
+ }
- 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);
+ // Last Sync Data state does not exist
+ if (!storedLastSyncUserDataStateContent) {
+ this.logService.info(`${this.syncResourceLogLabel}: Last sync data state does not exist.`);
+ return null;
+ }
- /* Check if syncData is of expected type. Return only if matches */
- if (isSyncData(syncData)) {
- return { ...parsed, ...{ syncData, content: undefined } };
+ const lastSyncUserDataState: ILastSyncUserDataState = JSON.parse(storedLastSyncUserDataStateContent);
+ const resourceSyncStateVersion = this.userDataSyncEnablementService.getResourceSyncStateVersion(this.resource);
+ this.hasSyncResourceStateVersionChanged = !!lastSyncUserDataState.version && !!resourceSyncStateVersion && lastSyncUserDataState.version !== resourceSyncStateVersion;
+ if (this.hasSyncResourceStateVersionChanged) {
+ this.logService.info(`${this.syncResourceLogLabel}: Reset last sync state because last sync state version ${lastSyncUserDataState.version} is not compatible with current sync state version ${resourceSyncStateVersion}.`);
+ await this.resetLocal();
+ return null;
+ }
+
+ let syncData: ISyncData | null | undefined = undefined;
+
+ // Get Last Sync Data from Local
+ let retrial = 1;
+ while (syncData === undefined && retrial++ < 6 /* Retry 5 times */) {
+ try {
+ const content = (await this.fileService.readFile(this.lastSyncResource)).value.toString();
+ try { syncData = content ? JSON.parse(content) : null; } catch (e) { /* Ignore */ }
+ if (syncData && !isSyncData(syncData)) {
+ this.logService.info(`${this.syncResourceLogLabel}: Last sync data stored locally is invalid.`);
+ syncData = undefined;
+ }
+ break;
+ } catch (error) {
+ if (error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
+ this.logService.info(`${this.syncResourceLogLabel}: Last sync resource does not exist locally.`);
+ break;
+ } else if (error instanceof UserDataSyncError) {
+ throw error;
+ } else {
+ // log and retry
+ this.logService.error(error, retrial);
+ }
}
+ }
- } catch (error) {
- if (error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
- this.logService.info(`${this.syncResourceLogLabel}: Not synced yet. Last sync resource does not exist.`);
- } else {
- // log error always except when file does not exist
- this.logService.error(error);
+ // Get Last Sync Data from Remote
+ if (syncData === undefined) {
+ try {
+ const content = await this.userDataSyncStoreService.resolveResourceContent(this.resource, lastSyncUserDataState.ref, this.collection, this.syncHeaders);
+ syncData = content === null ? null : this.parseSyncData(content);
+ await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(syncData ? JSON.stringify(syncData) : ''));
+ } catch (error) {
+ if (error instanceof UserDataSyncError && error.code === UserDataSyncErrorCode.NotFound) {
+ this.logService.info(`${this.syncResourceLogLabel}: Last sync resource does not exist on the server.`);
+ } else {
+ throw error;
+ }
}
}
- return null;
+
+ // Last Sync Data Not Found
+ if (syncData === undefined) {
+ return null;
+ }
+
+ return {
+ ...lastSyncUserDataState,
+ syncData,
+ } as T;
}
protected async updateLastSyncUserData(lastSyncRemoteUserData: IRemoteUserData, additionalProps: IStringDictionary<any> = {}): Promise<void> {
- if (additionalProps['ref'] || additionalProps['content'] || additionalProps['version']) {
+ if (additionalProps['ref'] || additionalProps['version']) {
throw new Error('Cannot have core properties as additional');
}
const version = this.userDataSyncEnablementService.getResourceSyncStateVersion(this.resource);
- const lastSyncUserData = { ref: lastSyncRemoteUserData.ref, content: lastSyncRemoteUserData.syncData ? JSON.stringify(lastSyncRemoteUserData.syncData) : null, version, ...additionalProps };
- await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData)));
+ const lastSyncUserDataState: ILastSyncUserDataState = {
+ ref: lastSyncRemoteUserData.ref,
+ version,
+ ...additionalProps
+ };
+
+ this.storageService.store(this.lastSyncUserDataStateKey, JSON.stringify(lastSyncUserDataState), StorageScope.APPLICATION, StorageTarget.MACHINE);
+ await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(lastSyncRemoteUserData.syncData ? JSON.stringify(lastSyncRemoteUserData.syncData) : ''));
+ }
+
+ private async migrateLastSyncUserData(): Promise<string | undefined> {
+ try {
+ const content = await this.fileService.readFile(this.lastSyncResource);
+ const userData = JSON.parse(content.value.toString());
+ await this.fileService.del(this.lastSyncResource);
+ if (userData.ref) {
+ this.storageService.store(this.lastSyncUserDataStateKey, JSON.stringify({
+ ...userData,
+ content: undefined,
+ }), StorageScope.APPLICATION, StorageTarget.MACHINE);
+ await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(userData.content || ''));
+ this.logService.info(`${this.syncResourceLogLabel}: Migrated data from last sync resource to last sync state.`);
+ }
+ } catch (error) {
+ if (error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
+ this.logService.debug(`${this.syncResourceLogLabel}: Migrating last sync user data. Resource does not exist.`);
+ } else {
+ this.logService.error(error);
+ }
+ }
+ return this.storageService.get(this.lastSyncUserDataStateKey, StorageScope.APPLICATION);
}
async getRemoteUserData(lastSyncData: IRemoteUserData | null): Promise<IRemoteUserData> {
diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts
index c1d0e4508c3..f9c2aa24687 100644
--- a/src/vs/platform/userDataSync/common/userDataSync.ts
+++ b/src/vs/platform/userDataSync/common/userDataSync.ts
@@ -237,6 +237,7 @@ export function createSyncHeaders(executionId: string): IHeaders {
export const enum UserDataSyncErrorCode {
// Client Errors (>= 400 )
Unauthorized = 'Unauthorized', /* 401 */
+ NotFound = 'NotFound', /* 404 */
Conflict = 'Conflict', /* 409 */
Gone = 'Gone', /* 410 */
PreconditionFailed = 'PreconditionFailed', /* 412 */
@@ -266,7 +267,6 @@ export const enum UserDataSyncErrorCode {
LocalError = 'LocalError',
IncompatibleLocalContent = 'IncompatibleLocalContent',
IncompatibleRemoteContent = 'IncompatibleRemoteContent',
- UnresolvedConflicts = 'UnresolvedConflicts',
Unknown = 'Unknown',
}
@@ -476,7 +476,7 @@ export interface IUserDataSyncTask {
stop(): Promise<void>;
}
-export interface IUserDataManualSyncTask extends IDisposable {
+export interface IUserDataManualSyncTask {
readonly id: string;
merge(): Promise<void>;
apply(): Promise<void>;
diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts
index f55c9ac957a..6ded0fde8eb 100644
--- a/src/vs/platform/userDataSync/common/userDataSyncService.ts
+++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts
@@ -175,13 +175,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
that.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`);
that.updateLastSyncTime();
},
- stop(): Promise<void> {
- cancellableToken.cancel();
- return that.stop();
- },
- dispose(): void {
+ async stop(): Promise<void> {
cancellableToken.cancel();
- that.stop();
+ await that.stop();
+ await that.resetLocal();
}
};
}
@@ -388,6 +385,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
async resetLocal(): Promise<void> {
this.checkEnablement();
+ this._lastSyncTime = undefined;
this.storageService.remove(LAST_SYNC_TIME_KEY, StorageScope.APPLICATION);
for (const [synchronizer] of this.activeProfileSynchronizers.values()) {
try {
@@ -600,21 +598,14 @@ class ProfileSynchronizer extends Disposable {
this._enabled.push([synchronizer, order, disposables]);
}
- protected deRegisterSynchronizer(syncResource: SyncResource): void {
+ private deRegisterSynchronizer(syncResource: SyncResource): void {
const index = this._enabled.findIndex(([synchronizer]) => synchronizer.resource === syncResource);
if (index !== -1) {
- const removed = this._enabled.splice(index, 1);
- for (const [synchronizer, , disposable] of removed) {
- if (synchronizer.status !== SyncStatus.Idle) {
- const hasConflicts = synchronizer.conflicts.conflicts.length > 0;
- synchronizer.stop();
- if (hasConflicts) {
- this.updateConflicts();
- }
- this.updateStatus();
- }
- disposable.dispose();
- }
+ const [[synchronizer, , disposable]] = this._enabled.splice(index, 1);
+ disposable.dispose();
+ this.updateStatus();
+ Promise.allSettled([synchronizer.stop(), synchronizer.resetLocal()])
+ .then(null, error => this.logService.error(error));
}
}
diff --git a/src/vs/platform/userDataSync/common/userDataSyncServiceIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncServiceIpc.ts
index edd7ba8159a..7f5cd49c958 100644
--- a/src/vs/platform/userDataSync/common/userDataSyncServiceIpc.ts
+++ b/src/vs/platform/userDataSync/common/userDataSyncServiceIpc.ts
@@ -5,7 +5,7 @@
import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
-import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
+import { Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { ILogService } from 'vs/platform/log/common/log';
@@ -27,7 +27,7 @@ function reviewSyncResourceHandle(syncResourceHandle: ISyncResourceHandle): ISyn
export class UserDataSyncChannel implements IServerChannel {
- private readonly manualSyncTasks = new Map<string, { manualSyncTask: IUserDataManualSyncTask; disposables: DisposableStore }>();
+ private readonly manualSyncTasks = new Map<string, IUserDataManualSyncTask>();
private readonly onManualSynchronizeResources = new Emitter<ManualSyncTaskEvent<[SyncResource, URI[]][]>>();
constructor(
@@ -95,9 +95,8 @@ export class UserDataSyncChannel implements IServerChannel {
switch (manualSyncTaskCommand) {
case 'merge': return manualSyncTask.merge();
- case 'apply': return manualSyncTask.apply();
- case 'stop': return manualSyncTask.stop();
- case 'dispose': return this.disposeManualSyncTask(manualSyncTask);
+ case 'apply': return manualSyncTask.apply().finally(() => this.manualSyncTasks.delete(this.createKey(manualSyncTask.id)));
+ case 'stop': return manualSyncTask.stop().finally(() => this.manualSyncTasks.delete(this.createKey(manualSyncTask.id)));
}
}
@@ -105,27 +104,19 @@ export class UserDataSyncChannel implements IServerChannel {
}
private getManualSyncTask(manualSyncTaskId: string): IUserDataManualSyncTask {
- const value = this.manualSyncTasks.get(this.createKey(manualSyncTaskId));
- if (!value) {
+ const manualSyncTask = this.manualSyncTasks.get(this.createKey(manualSyncTaskId));
+ if (!manualSyncTask) {
throw new Error(`Manual sync taks not found: ${manualSyncTaskId}`);
}
- return value.manualSyncTask;
+ return manualSyncTask;
}
private async createManualSyncTask(): Promise<string> {
- const disposables = new DisposableStore();
- const manualSyncTask = disposables.add(await this.service.createManualSyncTask());
- this.manualSyncTasks.set(this.createKey(manualSyncTask.id), { manualSyncTask, disposables });
+ const manualSyncTask = await this.service.createManualSyncTask();
+ this.manualSyncTasks.set(this.createKey(manualSyncTask.id), manualSyncTask);
return manualSyncTask.id;
}
- private disposeManualSyncTask(manualSyncTask: IUserDataManualSyncTask): void {
- manualSyncTask.dispose();
- const key = this.createKey(manualSyncTask.id);
- this.manualSyncTasks.get(key)?.disposables.dispose();
- this.manualSyncTasks.delete(key);
- }
-
private createKey(manualSyncTaskId: string): string { return `manualSyncTask-${manualSyncTaskId}`; }
}
diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts
index 61e2b0df33d..8c3ef72ade5 100644
--- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts
+++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts
@@ -543,6 +543,10 @@ export class UserDataSyncStoreClient extends Disposable {
this._onTokenSucceed.fire();
+ if (context.res.statusCode === 404) {
+ throw new UserDataSyncStoreError(`${options.type} request '${url}' failed because the requested resource is not found (404).`, url, UserDataSyncErrorCode.NotFound, context.res.statusCode, operationId);
+ }
+
if (context.res.statusCode === 409) {
throw new UserDataSyncStoreError(`${options.type} request '${url}' failed because of Conflict (409). There is new data for this resource. Make the request again with latest data.`, url, UserDataSyncErrorCode.Conflict, context.res.statusCode, operationId);
}
diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts
index 3386bda9ba5..081cab1358c 100644
--- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts
+++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts
@@ -14,6 +14,7 @@ import { URI } from 'vs/base/common/uri';
import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler';
import { IFileService } from 'vs/platform/files/common/files';
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
+import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
import { AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { Change, IRemoteUserData, IResourcePreview as IBaseResourcePreview, IUserDataResourceManifest, IUserDataSyncConfiguration, IUserDataSyncStoreService, MergeState, SyncResource, SyncStatus, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync';
@@ -35,6 +36,9 @@ class TestSynchroniser extends AbstractSynchroniser {
private cancelled: boolean = false;
readonly localResource = joinPath(this.environmentService.userRoamingDataHome, 'testResource.json');
+ getMachineId(): Promise<string> { return this.currentMachineIdPromise; }
+ getLastSyncResource(): URI { return this.lastSyncResource; }
+
protected override getLatestRemoteUserData(manifest: IUserDataResourceManifest | null, lastSyncUserData: IRemoteUserData | null): Promise<IRemoteUserData> {
if (this.failWhenGettingLatestRemoteUserData) {
throw new Error();
@@ -1071,6 +1075,203 @@ suite('TestSynchronizer - Manual Sync', () => {
});
+suite('TestSynchronizer - Last Sync Data', () => {
+ const disposableStore = new DisposableStore();
+ const server = new UserDataSyncTestServer();
+ let client: UserDataSyncClient;
+ let userDataSyncStoreService: IUserDataSyncStoreService;
+
+ setup(async () => {
+ client = disposableStore.add(new UserDataSyncClient(server));
+ await client.setUp();
+ userDataSyncStoreService = client.instantiationService.get(IUserDataSyncStoreService);
+ disposableStore.add(toDisposable(() => userDataSyncStoreService.clear()));
+ client.instantiationService.get(IFileService).registerProvider(USER_DATA_SYNC_SCHEME, new InMemoryFileSystemProvider());
+ });
+
+ teardown(() => disposableStore.clear());
+
+ test('last sync data is null when not synced before', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
+ const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined));
+
+ const actual = await testObject.getLastSyncUserData();
+
+ assert.strictEqual(actual, null);
+ }));
+
+ test('last sync data is set after sync', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
+ const storageService = client.instantiationService.get(IStorageService);
+ const fileService = client.instantiationService.get(IFileService);
+ const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined));
+ testObject.syncBarrier.open();
+
+ await testObject.sync(await client.getResourceManifest());
+ const machineId = await testObject.getMachineId();
+ const actual = await testObject.getLastSyncUserData();
+
+ assert.deepStrictEqual(storageService.get('settings.lastSyncUserData', StorageScope.APPLICATION), JSON.stringify({ ref: '1' }));
+ assert.deepStrictEqual(JSON.parse((await fileService.readFile(testObject.getLastSyncResource())).value.toString()), { content: '0', machineId, version: 1 });
+ assert.deepStrictEqual(actual, {
+ ref: '1',
+ syncData: {
+ content: '0',
+ machineId,
+ version: 1
+ },
+ });
+ }));
+
+ test('last sync data is read from server after sync if last sync resource is deleted', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
+ const storageService = client.instantiationService.get(IStorageService);
+ const fileService = client.instantiationService.get(IFileService);
+ const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined));
+ testObject.syncBarrier.open();
+
+ await testObject.sync(await client.getResourceManifest());
+ const machineId = await testObject.getMachineId();
+ await fileService.del(testObject.getLastSyncResource());
+ const actual = await testObject.getLastSyncUserData();
+
+ assert.deepStrictEqual(storageService.get('settings.lastSyncUserData', StorageScope.APPLICATION), JSON.stringify({ ref: '1' }));
+ assert.deepStrictEqual(actual, {
+ ref: '1',
+ syncData: {
+ content: '0',
+ machineId,
+ version: 1
+ },
+ });
+ }));
+
+ test('last sync data is read from server after sync and sync data is invalid', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
+ const storageService = client.instantiationService.get(IStorageService);
+ const fileService = client.instantiationService.get(IFileService);
+ const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined));
+ testObject.syncBarrier.open();
+
+ await testObject.sync(await client.getResourceManifest());
+ const machineId = await testObject.getMachineId();
+ await fileService.writeFile(testObject.getLastSyncResource(), VSBuffer.fromString(JSON.stringify({
+ ref: '1',
+ version: 1,
+ content: JSON.stringify({
+ content: '0',
+ machineId,
+ version: 1
+ }),
+ additionalData: {
+ foo: 'bar'
+ }
+ })));
+ const actual = await testObject.getLastSyncUserData();
+
+ assert.deepStrictEqual(storageService.get('settings.lastSyncUserData', StorageScope.APPLICATION), JSON.stringify({ ref: '1' }));
+ assert.deepStrictEqual(actual, {
+ ref: '1',
+ syncData: {
+ content: '0',
+ machineId,
+ version: 1
+ },
+ });
+ }));
+
+ test('reading last sync data: no requests are made to server when sync data is invalid', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
+ const fileService = client.instantiationService.get(IFileService);
+ const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined));
+ testObject.syncBarrier.open();
+
+ await testObject.sync(await client.getResourceManifest());
+ const machineId = await testObject.getMachineId();
+ await fileService.writeFile(testObject.getLastSyncResource(), VSBuffer.fromString(JSON.stringify({
+ ref: '1',
+ version: 1,
+ content: JSON.stringify({
+ content: '0',
+ machineId,
+ version: 1
+ }),
+ additionalData: {
+ foo: 'bar'
+ }
+ })));
+ await testObject.getLastSyncUserData();
+ server.reset();
+
+ await testObject.getLastSyncUserData();
+ assert.deepStrictEqual(server.requests, []);
+ }));
+
+ test('last sync data is null after sync if last sync state is deleted', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
+ const storageService = client.instantiationService.get(IStorageService);
+ const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined));
+ testObject.syncBarrier.open();
+
+ await testObject.sync(await client.getResourceManifest());
+ storageService.remove('settings.lastSyncUserData', StorageScope.APPLICATION);
+ const actual = await testObject.getLastSyncUserData();
+
+ assert.strictEqual(actual, null);
+ }));
+
+ test('last sync data is null after sync if last sync content is deleted everywhere', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
+ const storageService = client.instantiationService.get(IStorageService);
+ const fileService = client.instantiationService.get(IFileService);
+ const userDataSyncStoreService = client.instantiationService.get(IUserDataSyncStoreService);
+ const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined));
+ testObject.syncBarrier.open();
+
+ await testObject.sync(await client.getResourceManifest());
+ await fileService.del(testObject.getLastSyncResource());
+ await userDataSyncStoreService.deleteResource(testObject.syncResource.syncResource, null);
+ const actual = await testObject.getLastSyncUserData();
+
+ assert.deepStrictEqual(storageService.get('settings.lastSyncUserData', StorageScope.APPLICATION), JSON.stringify({ ref: '1' }));
+ assert.strictEqual(actual, null);
+ }));
+
+ test('last sync data is migrated', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
+ const storageService = client.instantiationService.get(IStorageService);
+ const fileService = client.instantiationService.get(IFileService);
+ const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, { syncResource: SyncResource.Settings, profile: client.instantiationService.get(IUserDataProfilesService).defaultProfile }, undefined));
+ const machineId = await testObject.getMachineId();
+ await fileService.writeFile(testObject.getLastSyncResource(), VSBuffer.fromString(JSON.stringify({
+ ref: '1',
+ version: 1,
+ content: JSON.stringify({
+ content: '0',
+ machineId,
+ version: 1
+ }),
+ additionalData: {
+ foo: 'bar'
+ }
+ })));
+
+ const actual = await testObject.getLastSyncUserData();
+
+ assert.deepStrictEqual(storageService.get('settings.lastSyncUserData', StorageScope.APPLICATION), JSON.stringify({
+ ref: '1',
+ version: 1,
+ additionalData: {
+ foo: 'bar'
+ }
+ }));
+ assert.deepStrictEqual(actual, {
+ ref: '1',
+ version: 1,
+ syncData: {
+ content: '0',
+ machineId,
+ version: 1
+ },
+ additionalData: {
+ foo: 'bar'
+ }
+ });
+ }));
+});
+
function assertConflicts(actual: IBaseResourcePreview[], expected: URI[]) {
assert.deepStrictEqual(actual.map(({ localResource }) => localResource.toString()), expected.map(uri => uri.toString()));
}
diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts
index a0638ba7618..dbbccba5373 100644
--- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts
+++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts
@@ -215,19 +215,22 @@ export class UserDataSyncTestServer implements IRequestService {
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(undefined, segments[1], options.headers);
+ if (options.type === 'GET' && segments.length === 3 && segments[0] === 'resource') {
+ return this.getResourceData(undefined, segments[1], segments[2] === 'latest' ? undefined : segments[2], options.headers);
}
if (options.type === 'POST' && segments.length === 2 && segments[0] === 'resource') {
return this.writeData(undefined, segments[1], options.data, options.headers);
}
// resources in collection
- if (options.type === 'GET' && segments.length === 5 && segments[0] === 'collection' && segments[2] === 'resource' && segments[4] === 'latest') {
- return this.getLatestData(segments[1], segments[3], options.headers);
+ if (options.type === 'GET' && segments.length === 5 && segments[0] === 'collection' && segments[2] === 'resource') {
+ return this.getResourceData(segments[1], segments[3], segments[4] === 'latest' ? undefined : segments[4], options.headers);
}
if (options.type === 'POST' && segments.length === 4 && segments[0] === 'collection' && segments[2] === 'resource') {
return this.writeData(segments[1], segments[3], options.data, options.headers);
}
+ if (options.type === 'DELETE' && segments.length === 2 && segments[0] === 'resource') {
+ return this.deleteResourceData(undefined, segments[1]);
+ }
if (options.type === 'DELETE' && segments.length === 1 && segments[0] === 'resource') {
return this.clear(options.headers);
}
@@ -262,7 +265,7 @@ export class UserDataSyncTestServer implements IRequestService {
return this.toResponse(204, { etag: `${this.manifestRef++}` });
}
- private async getLatestData(collection: string | undefined, resource: string, headers: IHeaders = {}): Promise<IRequestContext> {
+ private async getResourceData(collection: string | undefined, resource: string, ref?: string, headers: IHeaders = {}): Promise<IRequestContext> {
const collectionData = collection ? this.collections.get(collection) : this.data;
if (!collectionData) {
return this.toResponse(501);
@@ -271,6 +274,9 @@ export class UserDataSyncTestServer implements IRequestService {
const resourceKey = ALL_SERVER_RESOURCES.find(key => key === resource);
if (resourceKey) {
const data = collectionData.get(resourceKey);
+ if (ref && data?.ref !== ref) {
+ return this.toResponse(404);
+ }
if (!data) {
return this.toResponse(204, { etag: '0' });
}
@@ -303,6 +309,21 @@ export class UserDataSyncTestServer implements IRequestService {
return this.toResponse(204);
}
+ private async deleteResourceData(collection: string | undefined, resource: string, headers: IHeaders = {}): Promise<IRequestContext> {
+ const collectionData = collection ? this.collections.get(collection) : this.data;
+ if (!collectionData) {
+ return this.toResponse(501);
+ }
+
+ const resourceKey = ALL_SERVER_RESOURCES.find(key => key === resource);
+ if (resourceKey) {
+ collectionData.delete(resourceKey);
+ return this.toResponse(200);
+ }
+
+ return this.toResponse(404);
+ }
+
private async createCollection(): Promise<IRequestContext> {
const collectionId = `${++this.collectionCounter}`;
this.collections.set(collectionId, new Map());
diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts
index d8b5db2f759..87c9f728a4b 100644
--- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts
+++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts
@@ -368,7 +368,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
private async doTurnOnSync(token: CancellationToken): Promise<void> {
const disposables = new DisposableStore();
- const manualSyncTask = disposables.add(await this.userDataSyncService.createManualSyncTask());
+ const manualSyncTask = await this.userDataSyncService.createManualSyncTask();
try {
await this.progressService.withProgress({
location: ProgressLocation.Window,