diff options
author | Sandeep Somavarapu <sasomava@microsoft.com> | 2022-11-11 22:56:58 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-11 22:56:58 +0300 |
commit | f8995e0b1aae1e6cafe677eb76fbae047034a8c8 (patch) | |
tree | f2392b10c30f7796c1d0356f2ee52139199f5dd3 | |
parent | b982536f83376e6266b3e0aaca6211699167b6cf (diff) |
store last sync data in state (#166133)
fall back to server if last sync content does not exist
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, |