diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index 591d7cd230b8e8a5de0d64e0ade86eaa72f1f87e..9dd666804304599f3d5ee8e8272c52f83cc7c102 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -6,7 +6,7 @@ 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 } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, UserDataAutoSyncError } 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'; @@ -17,8 +17,6 @@ import { IUserDataSyncMachine, IUserDataSyncMachinesService } from 'vs/platform/ import { PlatformToString, isWeb, Platform, platform } from 'vs/base/common/platform'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IHeaders } from 'vs/base/parts/request/common/request'; -import { generateUuid } from 'vs/base/common/uuid'; import { localize } from 'vs/nls'; type AutoSyncClassification = { @@ -29,6 +27,10 @@ type AutoSyncEnablementClassification = { enabled?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; }; +type AutoSyncErrorClassification = { + code: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; +}; + const enablementKey = 'sync.enable'; const disableMachineEventuallyKey = 'sync.disableMachineEventually'; const SESSION_ID_KEY = 'sync.sessionId'; @@ -83,11 +85,11 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i readonly onError: Event = this._onError.event; constructor( - @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, - @IUserDataSyncAccountService private readonly authTokenService: IUserDataSyncAccountService, + @IUserDataSyncAccountService private readonly userDataSyncAccountService: IUserDataSyncAccountService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IUserDataSyncMachinesService private readonly userDataSyncMachinesService: IUserDataSyncMachinesService, @IProductService private readonly productService: IProductService, @@ -99,15 +101,10 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i if (userDataSyncStoreService.userDataSyncStore) { this.updateAutoSync(); - - // Update machine if sync is enabled - if (this.isEnabled()) { - this.updateMachine(true); - } else if (this.hasToDisableMachineEventually()) { + if (this.hasToDisableMachineEventually()) { this.disableMachineEventually(); } - - this._register(authTokenService.onDidChangeAccount(() => this.updateAutoSync())); + this._register(userDataSyncAccountService.onDidChangeAccount(() => this.updateAutoSync())); this._register(Event.debounce(userDataSyncService.onDidChangeLocal, (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, false))); this._register(Event.filter(this.userDataSyncResourceEnablementService.onDidChangeResourceEnablement, ([, enabled]) => enabled)(() => this.triggerSync(['resourceEnablement'], false))); } @@ -117,7 +114,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i const { enabled, reason } = this.isAutoSyncEnabled(); if (enabled) { if (this.autoSync.value === undefined) { - this.autoSync.value = new AutoSync(1000 * 60 * 5 /* 5 miutes */, this.userDataSyncService, this.logService); + this.autoSync.value = new AutoSync(1000 * 60 * 5 /* 5 miutes */, this.userDataSyncStoreService, this.userDataSyncService, this.userDataSyncMachinesService, this.logService, this.storageService, this.productService); 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()) { @@ -140,31 +137,39 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i if (!this.isEnabled()) { return { enabled: false, reason: 'sync is disabled' }; } - if (!this.authTokenService.account) { + if (!this.userDataSyncAccountService.account) { return { enabled: false, reason: 'token is not avaialable' }; } return { enabled: true }; } async turnOn(pullFirst: boolean): Promise { - await this.updateMachine(true); + this.stopDisableMachineEventually(); if (pullFirst) { await this.userDataSyncService.pull(); } else { - await this.userDataSyncService.sync(CancellationToken.None); + await this.userDataSyncService.sync(); } this.setEnablement(true); } - async turnOff(everywhere: boolean, softTurnOffOnError?: boolean, donotDisableMachine?: boolean): Promise { + async turnOff(everywhere: boolean, softTurnOffOnError?: boolean, donotRemoveMachine?: boolean): Promise { try { - if (!donotDisableMachine) { - await this.updateMachine(false); + + // Remove machine + if (!donotRemoveMachine) { + await this.userDataSyncMachinesService.removeCurrentMachine(); } + + // Disable Auto Sync this.setEnablement(false); + // Reset Session + this.storageService.remove(SESSION_ID_KEY, StorageScope.GLOBAL); + + // Reset if (everywhere) { this.telemetryService.publicLog2('sync/turnOffEveryWhere'); await this.userDataSyncService.reset(); @@ -189,44 +194,6 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i } } - private async updateMachine(enable: boolean): Promise { - if (!this.authTokenService.account) { - return; - } - - const machines = await this.userDataSyncMachinesService.getMachines(); - const currentMachine = machines.find(machine => machine.isCurrent); - if (enable) { - this.stopDisableMachineEventually(); - // Add or enable current machine - if (!currentMachine) { - const name = this.computeDefaultMachineName(machines); - await this.userDataSyncMachinesService.addCurrentMachine(name); - this.logService.debug('Auto Sync: Added current machine to sync'); - } else if (currentMachine.disabled) { - await this.userDataSyncMachinesService.setEnablement(currentMachine.id, true); - this.logService.debug('Auto Sync: Enabled current machine to sync'); - } - } else if (currentMachine && !currentMachine.disabled) { - await this.userDataSyncMachinesService.setEnablement(currentMachine.id, false); - this.logService.debug('Auto Sync: Disabled current machine to sync'); - } - } - - private computeDefaultMachineName(machines: IUserDataSyncMachine[]): string { - 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 onDidFinishSync(error: Error | undefined): Promise { if (!error) { // Sync finished without errors @@ -236,18 +203,31 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i // Error while syncing const userDataSyncError = UserDataSyncError.toUserDataSyncError(error); + + // Log to telemetry + if (userDataSyncError instanceof UserDataAutoSyncError) { + this.telemetryService.publicLog2<{ code: string }, AutoSyncErrorClassification>(`autosync/error`, { code: userDataSyncError.code }); + } + + // Turned off from another device or session got expired if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff || userDataSyncError.code === UserDataSyncErrorCode.SessionExpired) { 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'); - } else if (userDataSyncError.code === UserDataSyncErrorCode.LocalTooManyRequests || userDataSyncError.code === UserDataSyncErrorCode.TooManyRequests) { + } + + // 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'); - } else { + } + + else { this.logService.error(userDataSyncError); this.successiveFailures++; } + this._onError.fire(userDataSyncError); } @@ -263,8 +243,8 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i this.stopDisableMachineEventually(); // disable only if sync is disabled - if (!this.isEnabled()) { - return this.updateMachine(false); + if (!this.isEnabled() && this.userDataSyncAccountService.account) { + await this.userDataSyncMachinesService.removeCurrentMachine(); } } @@ -324,12 +304,12 @@ class AutoSync extends Disposable { constructor( private readonly interval: number /* in milliseconds */, + private readonly userDataSyncStoreService: IUserDataSyncStoreService, private readonly userDataSyncService: IUserDataSyncService, + private readonly userDataSyncMachinesService: IUserDataSyncMachinesService, private readonly logService: IUserDataSyncLogService, - private readonly userDataSyncStoreService: IUserDataSyncStoreService, - private readonly telemetryService: ITelemetryService, private readonly storageService: IStorageService, - private readonly userDataSyncMachinesService: IUserDataSyncMachinesService, + private readonly productService: IProductService, ) { super(); } @@ -379,30 +359,19 @@ class AutoSync extends Disposable { this._onDidStartSync.fire(); let error: Error | undefined; try { - await this.userDataSyncService.sync(token); - } catch (e) { - this.logService.error(e); - error = e; - } - this._onDidFinishSync.fire(error); - } - - private async doSync(reason: string, token: CancellationToken): Promise { - try { - this.telemetryService.publicLog2('sync/getmanifest'); - const syncHeaders: IHeaders = { 'X-Execution-Id': generateUuid() }; - let manifest = await this.userDataSyncStoreService.manifest(syncHeaders); + const syncTask = await this.userDataSyncService.createSyncTask(); + let manifest = syncTask.manifest; // Server has no data but this machine was synced before if (manifest === null && await this.userDataSyncService.hasPreviouslySynced()) { // Sync was turned off in the cloud - throw new UserDataSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff); + throw new UserDataAutoSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff); } const sessionId = this.storageService.get(SESSION_ID_KEY, StorageScope.GLOBAL); // Server session is different from client session if (sessionId && manifest && sessionId !== manifest.session) { - throw new UserDataSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired); + throw new UserDataAutoSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired); } const machines = await this.userDataSyncMachinesService.getMachines(manifest || undefined); @@ -415,14 +384,19 @@ class AutoSync extends Disposable { // Check if sync was turned off from other machine if (currentMachine?.disabled) { // Throw TurnedOff error - throw new UserDataSyncError(localize('turned off machine', "Cannot sync because syncing is turned off on this machine from another machine."), UserDataSyncErrorCode.TurnedOff); + throw new UserDataAutoSyncError(localize('turned off machine', "Cannot sync because syncing is turned off on this machine from another machine."), UserDataSyncErrorCode.TurnedOff); } - await this.userDataSyncService.sync(manifest, headers, token); + await syncTask.run(token); // After syncing, get the manifest if it was not available before if (manifest === null) { - manifest = await this.userDataSyncStoreService.manifest(syncHeaders); + manifest = await this.userDataSyncStoreService.manifest(); + } + + // Update local session id + if (manifest && manifest.session !== sessionId) { + this.storageService.store(SESSION_ID_KEY, manifest.session, StorageScope.GLOBAL); } // Return if cancellation is requested @@ -430,20 +404,32 @@ class AutoSync extends Disposable { return; } - // Update local session id - if (manifest && manifest.session !== sessionId) { - this.storageService.store(SESSION_ID_KEY, manifest.session, StorageScope.GLOBAL); + // Add current machine + if (!currentMachine) { + const name = this.computeDefaultMachineName(machines); + await this.userDataSyncMachinesService.addCurrentMachine(name, manifest || undefined); } - } catch (error) { - if (error instanceof UserDataSyncError) { - this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${error.code}`, { resource: error.resource }); - } - throw error; - } finally { - this.updateStatus(); - this._onSyncErrors.fire(this._syncErrors); + } catch (e) { + this.logService.error(e); + error = e; } + + this._onDidFinishSync.fire(error); + } + + private computeDefaultMachineName(machines: IUserDataSyncMachine[]): string { + 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}`; } register(t: T): T { diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 11dfeac2d4872943e7aa4c73eac31e162d455862..a79be90663016934eb71d5687d91dd3ca6f1d2e8 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -224,7 +224,7 @@ export class UserDataSyncError extends Error { } static toUserDataSyncError(error: Error): UserDataSyncError { - if (error instanceof UserDataSyncStoreError) { + if (error instanceof UserDataSyncError) { return error; } const match = /^(.+) \(UserDataSyncError\) (.+)?$/.exec(error.name); @@ -242,6 +242,12 @@ export class UserDataSyncStoreError extends UserDataSyncError { } } +export class UserDataAutoSyncError extends UserDataSyncError { + constructor(message: string, code: UserDataSyncErrorCode) { + super(message, code); + } +} + //#endregion // #region User Data Synchroniser @@ -340,6 +346,11 @@ export interface IUserDataSyncResourceEnablementService { export type SyncResourceConflicts = { syncResource: SyncResource, conflicts: Conflict[] }; +export interface ISyncTask { + manifest: IUserDataManifest | null; + run(token: CancellationToken): Promise; +} + export const IUserDataSyncService = createDecorator('IUserDataSyncService'); export interface IUserDataSyncService { _serviceBrand: any; @@ -357,12 +368,14 @@ export interface IUserDataSyncService { readonly onDidChangeLastSyncTime: Event; pull(): Promise; - sync(token: CancellationToken): Promise; + sync(): Promise; stop(): Promise; replace(uri: URI): Promise; reset(): Promise; resetLocal(): Promise; + createSyncTask(): Promise + isFirstTimeSyncingWithAnotherMachine(): Promise; hasPreviouslySynced(): Promise; resolveContent(resource: URI): Promise; diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index c3d1ccdcf854d4ce3f93a013c102cfb2fa31e771..85c0898902cb5c4f785df85c8bb9efd167df4fec 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -14,7 +14,6 @@ import { Disposable } from 'vs/base/common/lifecycle'; 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 { CancellationToken } from 'vs/base/common/cancellation'; export class UserDataSyncChannel implements IServerChannel { @@ -45,11 +44,12 @@ export class UserDataSyncChannel implements IServerChannel { switch (command) { case '_getInitialData': return Promise.resolve([this.service.status, this.service.conflicts, this.service.lastSyncTime]); case 'pull': return this.service.pull(); - case 'sync': return this.service.sync(CancellationToken.None); + case 'sync': return this.service.sync(); case 'stop': this.service.stop(); return Promise.resolve(); case 'replace': return this.service.replace(URI.revive(args[0])); case 'reset': return this.service.reset(); case 'resetLocal': return this.service.resetLocal(); + case 'hasPreviouslySynced': return this.service.hasPreviouslySynced(); case 'isFirstTimeSyncingWithAnotherMachine': return this.service.isFirstTimeSyncingWithAnotherMachine(); case 'acceptConflict': return this.service.acceptConflict(URI.revive(args[0]), args[1]); case 'resolveContent': return this.service.resolveContent(URI.revive(args[0])); diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index fc8d18f0a4217f4175af55e23dd8d748f8c0fa7f..a1e9c9592c93216623d234815c7c12ccc7bebd72 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle, IUserDataManifest, ISyncTask } 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'; @@ -13,16 +13,14 @@ import { GlobalStateSynchroniser } from 'vs/platform/userDataSync/common/globalS import { toErrorMessage } from 'vs/base/common/errorMessage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { equals } from 'vs/base/common/arrays'; -import { localize } from 'vs/nls'; 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 { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { generateUuid } from 'vs/base/common/uuid'; import { IHeaders } from 'vs/base/parts/request/common/request'; +import { generateUuid } from 'vs/base/common/uuid'; type SyncClassification = { resource?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -69,7 +67,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IStorageService private readonly storageService: IStorageService, - @IUserDataSyncMachinesService private readonly userDataSyncMachinesService: IUserDataSyncMachinesService, ) { super(); this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser)); @@ -128,7 +125,30 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } private recoveredSettings: boolean = false; - async sync(token: CancellationToken): Promise { + async sync(): Promise { + const syncTask = await this.createSyncTask(); + return syncTask.run(CancellationToken.None); + } + + async createSyncTask(): Promise { + this.telemetryService.publicLog2('sync/getmanifest'); + const syncHeaders: IHeaders = { 'X-Execution-Id': generateUuid() }; + const manifest = await this.userDataSyncStoreService.manifest(syncHeaders); + + let executed = false; + const that = this; + return { + manifest, + run(token: CancellationToken): Promise { + if (executed) { + throw new Error('Can run a task only once'); + } + return that.doSync(manifest, syncHeaders, token); + } + }; + } + + private async doSync(manifest: IUserDataManifest | null, syncHeaders: IHeaders, token: CancellationToken): Promise { await this.checkEnablement(); if (!this.recoveredSettings) { @@ -141,10 +161,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return; } - return this.doSync(token); - } - - private async doSync(token: CancellationToken): Promise { const startTime = new Date().getTime(); this._syncErrors = []; try { @@ -153,35 +169,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.setStatus(SyncStatus.Syncing); } - this.telemetryService.publicLog2('sync/getmanifest'); - const syncHeaders: IHeaders = { 'X-Execution-Id': generateUuid() }; - let manifest = await this.userDataSyncStoreService.manifest(syncHeaders); - - // Server has no data but this machine was synced before - if (manifest === null && await this.hasPreviouslySynced()) { - // Sync was turned off in the cloud - throw new UserDataSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff); - } - - const sessionId = this.storageService.get(SESSION_ID_KEY, StorageScope.GLOBAL); - // Server session is different from client session - if (sessionId && manifest && sessionId !== manifest.session) { - throw new UserDataSyncError(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 UserDataSyncError(localize('turned off machine', "Cannot sync because syncing is turned off on this machine from another machine."), UserDataSyncErrorCode.TurnedOff); - } - for (const synchroniser of this.synchronisers) { // Return if cancellation is requested if (token.isCancellationRequested) { @@ -195,34 +182,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } } - // After syncing, get the manifest if it was not available before - if (manifest === null) { - manifest = await this.userDataSyncStoreService.manifest(syncHeaders); - } - - // Return if cancellation is requested - if (token.isCancellationRequested) { - return; - } - - // Update local session id - if (manifest && manifest.session !== sessionId) { - this.storageService.store(SESSION_ID_KEY, manifest.session, StorageScope.GLOBAL); - } - - // Return if cancellation is requested - if (token.isCancellationRequested) { - return; - } - - // Return if cancellation is requested - if (token.isCancellationRequested) { - return; - } - this.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`); this.updateLastSyncTime(); - } catch (error) { if (error instanceof UserDataSyncError) { this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${error.code}`, { resource: error.resource }); @@ -337,7 +298,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ async resetLocal(): Promise { await this.checkEnablement(); - this.storageService.remove(SESSION_ID_KEY, StorageScope.GLOBAL); this.storageService.remove(LAST_SYNC_TIME_KEY, StorageScope.GLOBAL); for (const synchroniser of this.synchronisers) { try { diff --git a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts index db0606bb8d14ef4267242074234f9af1cc021b09..83f34007ea447b5c8176a147dcc165f76b653fc1 100644 --- a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts @@ -7,12 +7,20 @@ 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 } from 'vs/platform/userDataSync/common/userDataSync'; -import { CancellationToken } from 'vs/base/common/cancellation'; +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'; class TestUserDataAutoSyncService extends UserDataAutoSyncService { protected startAutoSync(): boolean { return false; } protected getSyncTriggerDelayTime(): number { return 50; } + + sync(): Promise { + return this.triggerSync(['sync'], false); + } } suite('UserDataAutoSyncService', () => { @@ -28,7 +36,7 @@ suite('UserDataAutoSyncService', () => { await client.setUp(); // Sync once and reset requests - await client.instantiationService.get(IUserDataSyncService).sync(CancellationToken.None); + await client.instantiationService.get(IUserDataSyncService).sync(); target.reset(); const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); @@ -50,7 +58,7 @@ suite('UserDataAutoSyncService', () => { await client.setUp(); // Sync once and reset requests - await client.instantiationService.get(IUserDataSyncService).sync(CancellationToken.None); + await client.instantiationService.get(IUserDataSyncService).sync(); target.reset(); const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); @@ -76,7 +84,7 @@ suite('UserDataAutoSyncService', () => { await client.setUp(); // Sync once and reset requests - await client.instantiationService.get(IUserDataSyncService).sync(CancellationToken.None); + await client.instantiationService.get(IUserDataSyncService).sync(); target.reset(); const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); @@ -98,7 +106,7 @@ suite('UserDataAutoSyncService', () => { await client.setUp(); // Sync once and reset requests - await client.instantiationService.get(IUserDataSyncService).sync(CancellationToken.None); + await client.instantiationService.get(IUserDataSyncService).sync(); target.reset(); const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); @@ -115,5 +123,209 @@ suite('UserDataAutoSyncService', () => { 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 client.instantiationService.get(IUserDataSyncService).sync(); + + // 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((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 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 client.instantiationService.get(IUserDataSyncService).sync(); + + // 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 client.instantiationService.get(IUserDataSyncService).sync(); + + // 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((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((e).code, UserDataSyncErrorCode.TooManyRequests); + }); + }); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index c2e6008fca72a021b40ee724411082a0e3130c9d..3cb12370bcf8376ebd05b025913de46ede4d78ef 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -121,7 +121,7 @@ export class UserDataSyncClient extends Disposable { } sync(): Promise { - return this.instantiationService.get(IUserDataSyncService).sync(CancellationToken.None); + return this.instantiationService.get(IUserDataSyncService).sync(); } read(resource: SyncResource): Promise { @@ -154,9 +154,14 @@ export class UserDataSyncTestServer implements IRequestService { get responses(): { status: number }[] { return this._responses; } reset(): void { this._requests = []; this._responses = []; this._requestsWithAllHeaders = []; } + constructor(private readonly rateLimit = Number.MAX_SAFE_INTEGER) { } + async resolveProxy(url: string): Promise { return url; } async request(options: IRequestOptions, token: CancellationToken): Promise { + if (this._requests.length === this.rateLimit) { + return this.toResponse(429); + } const headers: IHeaders = {}; if (options.headers) { if (options.headers['If-None-Match']) { diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts index 9625a6ab0e6a5894c95d69cf932c9ad424843042..5c60bf19758bb4ea98db7035e6a394e9a9d10f02 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts @@ -4,14 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IUserDataSyncService, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; +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'; -import { CancellationToken } from 'vs/base/common/cancellation'; suite('UserDataSyncService', () => { @@ -27,13 +26,11 @@ suite('UserDataSyncService', () => { const testObject = client.instantiationService.get(IUserDataSyncService); // Sync for first time - await testObject.sync(CancellationToken.None); + 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' } }, @@ -48,8 +45,6 @@ suite('UserDataSyncService', () => { { 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: {} }, ]); }); @@ -62,13 +57,11 @@ suite('UserDataSyncService', () => { const testObject = client.instantiationService.get(IUserDataSyncService); // Sync for first time - await testObject.sync(CancellationToken.None); + 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: {} }, // Keybindings @@ -79,8 +72,6 @@ suite('UserDataSyncService', () => { { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, // Extensions { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, - // Manifest - { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, ]); }); @@ -91,7 +82,7 @@ suite('UserDataSyncService', () => { // Setup and sync from the first client const client = disposableStore.add(new UserDataSyncClient(target)); await client.setUp(); - await client.instantiationService.get(IUserDataSyncService).sync(CancellationToken.None); + await client.instantiationService.get(IUserDataSyncService).sync(); // Setup the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); @@ -126,7 +117,7 @@ suite('UserDataSyncService', () => { // Setup and sync from the first client const client = disposableStore.add(new UserDataSyncClient(target)); await client.setUp(); - await client.instantiationService.get(IUserDataSyncService).sync(CancellationToken.None); + await client.instantiationService.get(IUserDataSyncService).sync(); // Setup the test client with changes const testClient = disposableStore.add(new UserDataSyncClient(target)); @@ -163,7 +154,7 @@ suite('UserDataSyncService', () => { // Setup and sync from the first client const client = disposableStore.add(new UserDataSyncClient(target)); await client.setUp(); - await client.instantiationService.get(IUserDataSyncService).sync(CancellationToken.None); + await client.instantiationService.get(IUserDataSyncService).sync(); // Setup the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); @@ -173,7 +164,7 @@ suite('UserDataSyncService', () => { // Sync (merge) from the test client target.reset(); await testObject.isFirstTimeSyncingWithAnotherMachine(); - await testObject.sync(CancellationToken.None); + await testObject.sync(); assert.deepEqual(target.requests, [ /* first time sync */ @@ -184,7 +175,6 @@ suite('UserDataSyncService', () => { { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, /* sync */ { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/machines/latest`, 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: {} }, @@ -200,7 +190,7 @@ suite('UserDataSyncService', () => { // Setup and sync from the first client const client = disposableStore.add(new UserDataSyncClient(target)); await client.setUp(); - await client.instantiationService.get(IUserDataSyncService).sync(CancellationToken.None); + await client.instantiationService.get(IUserDataSyncService).sync(); // Setup the test client with changes const testClient = disposableStore.add(new UserDataSyncClient(target)); @@ -216,7 +206,7 @@ suite('UserDataSyncService', () => { // Sync (merge) from the test client target.reset(); await testObject.isFirstTimeSyncingWithAnotherMachine(); - await testObject.sync(CancellationToken.None); + await testObject.sync(); assert.deepEqual(target.requests, [ /* first time sync */ @@ -225,7 +215,6 @@ suite('UserDataSyncService', () => { /* first time sync */ { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/machines/latest`, 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: {} }, @@ -245,11 +234,11 @@ suite('UserDataSyncService', () => { const client = disposableStore.add(new UserDataSyncClient(target)); await client.setUp(); const testObject = client.instantiationService.get(IUserDataSyncService); - await testObject.sync(CancellationToken.None); + await testObject.sync(); // sync from the client again target.reset(); - await testObject.sync(CancellationToken.None); + await testObject.sync(); assert.deepEqual(target.requests, [ // Manifest @@ -264,7 +253,7 @@ suite('UserDataSyncService', () => { const client = disposableStore.add(new UserDataSyncClient(target)); await client.setUp(); const testObject = client.instantiationService.get(IUserDataSyncService); - await testObject.sync(CancellationToken.None); + await testObject.sync(); target.reset(); // Do changes in the client @@ -276,7 +265,7 @@ suite('UserDataSyncService', () => { await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' }))); // Sync from the client - await testObject.sync(CancellationToken.None); + await testObject.sync(); assert.deepEqual(target.requests, [ // Manifest @@ -298,13 +287,13 @@ suite('UserDataSyncService', () => { // Sync from first client const client = disposableStore.add(new UserDataSyncClient(target)); await client.setUp(); - await client.instantiationService.get(IUserDataSyncService).sync(CancellationToken.None); + await client.instantiationService.get(IUserDataSyncService).sync(); // Sync from test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); const testObject = testClient.instantiationService.get(IUserDataSyncService); - await testObject.sync(CancellationToken.None); + await testObject.sync(); // Do changes in first client and sync const fileService = client.instantiationService.get(IFileService); @@ -313,11 +302,11 @@ suite('UserDataSyncService', () => { 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 client.instantiationService.get(IUserDataSyncService).sync(CancellationToken.None); + await client.instantiationService.get(IUserDataSyncService).sync(); // Sync from test client target.reset(); - await testObject.sync(CancellationToken.None); + await testObject.sync(); assert.deepEqual(target.requests, [ // Manifest @@ -341,7 +330,7 @@ suite('UserDataSyncService', () => { const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); const testObject = testClient.instantiationService.get(IUserDataSyncService); - await testObject.sync(CancellationToken.None); + await testObject.sync(); // Reset from the client target.reset(); @@ -361,14 +350,14 @@ suite('UserDataSyncService', () => { const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); const testObject = testClient.instantiationService.get(IUserDataSyncService); - await testObject.sync(CancellationToken.None); + await testObject.sync(); // Reset from the client await testObject.reset(); // Sync again target.reset(); - await testObject.sync(CancellationToken.None); + await testObject.sync(); assert.deepEqual(target.requests, [ // Manifest @@ -387,81 +376,10 @@ suite('UserDataSyncService', () => { { 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: {} }, ]); }); - 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 client.instantiationService.get(IUserDataSyncService).sync(CancellationToken.None); - - // Set up and sync from the test client - const testClient = disposableStore.add(new UserDataSyncClient(target)); - await testClient.setUp(); - const testObject = testClient.instantiationService.get(IUserDataSyncService); - await testObject.sync(CancellationToken.None); - - // Reset from the first client - await client.instantiationService.get(IUserDataSyncService).reset(); - - // Sync from the test client - target.reset(); - try { - await testObject.sync(CancellationToken.None); - } catch (e) { - assert.ok(e instanceof UserDataSyncError); - assert.deepEqual((e).code, UserDataSyncErrorCode.TurnedOff); - assert.deepEqual(target.requests, [ - // Manifest - { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, - ]); - return; - } - throw assert.fail('Should fail with turned off error'); - }); - - 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 client.instantiationService.get(IUserDataSyncService).sync(CancellationToken.None); - - // Set up and sync from the test client - const testClient = disposableStore.add(new UserDataSyncClient(target)); - await testClient.setUp(); - const testObject = testClient.instantiationService.get(IUserDataSyncService); - await testObject.sync(CancellationToken.None); - - // Reset from the first client - await client.instantiationService.get(IUserDataSyncService).reset(); - - // Sync again from the first client to create new session - await client.instantiationService.get(IUserDataSyncService).sync(CancellationToken.None); - - // Sync from the test client - target.reset(); - try { - await testObject.sync(CancellationToken.None); - } catch (e) { - assert.ok(e instanceof UserDataSyncError); - assert.deepEqual((e).code, UserDataSyncErrorCode.SessionExpired); - assert.deepEqual(target.requests, [ - // Manifest - { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, - ]); - return; - } - throw assert.fail('Should fail with turned off error'); - }); - test('test sync status', async () => { const target = new UserDataSyncTestServer(); @@ -473,7 +391,7 @@ suite('UserDataSyncService', () => { // sync from the client const actualStatuses: SyncStatus[] = []; const disposable = testObject.onDidChangeStatus(status => actualStatuses.push(status)); - await testObject.sync(CancellationToken.None); + await testObject.sync(); 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]); @@ -488,7 +406,7 @@ suite('UserDataSyncService', () => { 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 client.instantiationService.get(IUserDataSyncService).sync(CancellationToken.None); + await client.instantiationService.get(IUserDataSyncService).sync(); // Setup the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); @@ -499,7 +417,7 @@ suite('UserDataSyncService', () => { const testObject = testClient.instantiationService.get(IUserDataSyncService); // sync from the client - await testObject.sync(CancellationToken.None); + await testObject.sync(); assert.deepEqual(testObject.status, SyncStatus.HasConflicts); assert.deepEqual(testObject.conflicts.map(({ syncResource }) => syncResource), [SyncResource.Settings]); @@ -514,7 +432,7 @@ suite('UserDataSyncService', () => { 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 client.instantiationService.get(IUserDataSyncService).sync(CancellationToken.None); + await client.instantiationService.get(IUserDataSyncService).sync(); // Setup the test client and get conflicts in settings const testClient = disposableStore.add(new UserDataSyncClient(target)); @@ -523,17 +441,17 @@ suite('UserDataSyncService', () => { 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 testObject.sync(CancellationToken.None); + await testObject.sync(); // sync from the first client with changes in keybindings await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }]))); - await client.instantiationService.get(IUserDataSyncService).sync(CancellationToken.None); + await client.instantiationService.get(IUserDataSyncService).sync(); // sync from the test client target.reset(); const actualStatuses: SyncStatus[] = []; const disposable = testObject.onDidChangeStatus(status => actualStatuses.push(status)); - await testObject.sync(CancellationToken.None); + await testObject.sync(); disposable.dispose(); assert.deepEqual(actualStatuses, []); @@ -556,7 +474,7 @@ suite('UserDataSyncService', () => { 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 client.instantiationService.get(IUserDataSyncService).sync(CancellationToken.None); + await client.instantiationService.get(IUserDataSyncService).sync(); // Setup the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); @@ -565,7 +483,7 @@ suite('UserDataSyncService', () => { environmentService = testClient.instantiationService.get(IEnvironmentService); await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 16 }))); const testObject = testClient.instantiationService.get(IUserDataSyncService); - await testObject.sync(CancellationToken.None); + await testObject.sync(); // sync from the client await testObject.stop(); @@ -581,15 +499,11 @@ suite('UserDataSyncService', () => { await client.setUp(); const testObject = client.instantiationService.get(IUserDataSyncService); - await testObject.sync(CancellationToken.None); + 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}`); - } + assert.ok(hasExecutionIdHeader, `Should have execution header: ${request.url}`); } }); diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts index 805fe71574dfc32813d9683122ee49ef1ca993a0..a562eb0c66ff2d88574486c2d4ca5995a47f1c08 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SyncStatus, SyncResource, IUserDataSyncService, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncStatus, SyncResource, IUserDataSyncService, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle, ISyncTask } from 'vs/platform/userDataSync/common/userDataSync'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; @@ -73,6 +73,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return this.channel.call('sync'); } + createSyncTask(): Promise { + throw new Error('not supported'); + } + stop(): Promise { return this.channel.call('stop'); } @@ -89,6 +93,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return this.channel.call('resetLocal'); } + hasPreviouslySynced(): Promise { + return this.channel.call('hasPreviouslySynced'); + } + isFirstTimeSyncingWithAnotherMachine(): Promise { return this.channel.call('isFirstTimeSyncingWithAnotherMachine'); }