diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index 8d0f6434fa2..96500ad38c4 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -81,12 +81,6 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i private readonly _onError: Emitter = this._register(new Emitter()); readonly onError: Event = this._onError.event; - private readonly _onTurnOnSync: Emitter = this._register(new Emitter()); - readonly onTurnOnSync: Event = this._onTurnOnSync.event; - - private readonly _onDidTurnOnSync: Emitter = this._register(new Emitter()); - readonly onDidTurnOnSync: Event = this._onDidTurnOnSync.event; - constructor( @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @@ -145,24 +139,9 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i return { enabled: true }; } - async turnOn(pullFirst: boolean): Promise { - this._onTurnOnSync.fire(); - - try { - this.stopDisableMachineEventually(); - - if (pullFirst) { - await this.userDataSyncService.pull(); - } else { - await (await this.userDataSyncService.createSyncTask()).run(); - } - - this.setEnablement(true); - this._onDidTurnOnSync.fire(undefined); - } catch (error) { - this._onDidTurnOnSync.fire(error); - throw error; - } + async turnOn(): Promise { + this.stopDisableMachineEventually(); + this.setEnablement(true); } async turnOff(everywhere: boolean, softTurnOffOnError?: boolean, donotRemoveMachine?: boolean): Promise { diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 8d6f6d3754c..e43b0dadac1 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -304,7 +304,6 @@ export interface IResourcePreview { readonly previewResource: URI; readonly localChange: Change; readonly remoteChange: Change; - } export interface ISyncResourcePreview { @@ -364,11 +363,13 @@ export interface ISyncTask { stop(): Promise; } -export interface IManualSyncTask { +export interface IManualSyncTask extends IDisposable { + readonly id: string; readonly manifest: IUserDataManifest | null; + readonly onSynchronizeResources: Event<[SyncResource, URI[]][]>; preview(): Promise<[SyncResource, ISyncResourcePreview][]>; accept(uri: URI, content: string): Promise<[SyncResource, ISyncResourcePreview][]>; - merge(uri: URI): Promise<[SyncResource, ISyncResourcePreview][]>; + merge(uri?: URI): Promise<[SyncResource, ISyncResourcePreview][]>; pull(): Promise; push(): Promise; stop(): Promise; @@ -380,7 +381,6 @@ export interface IUserDataSyncService { readonly status: SyncStatus; readonly onDidChangeStatus: Event; - readonly onSynchronizeResource: Event; readonly conflicts: [SyncResource, IResourcePreview[]][]; readonly onDidChangeConflicts: Event<[SyncResource, IResourcePreview[]][]>; @@ -400,7 +400,6 @@ export interface IUserDataSyncService { resetLocal(): Promise; hasLocalData(): Promise; - isFirstTimeSyncingWithAnotherMachine(): Promise; hasPreviouslySynced(): Promise; resolveContent(resource: URI): Promise; acceptPreviewContent(resource: SyncResource, conflictResource: URI, content: string): Promise; @@ -414,13 +413,11 @@ export interface IUserDataSyncService { export const IUserDataAutoSyncService = createDecorator('IUserDataAutoSyncService'); export interface IUserDataAutoSyncService { _serviceBrand: any; - readonly onTurnOnSync: Event - readonly onDidTurnOnSync: Event readonly onError: Event; readonly onDidChangeEnablement: Event; isEnabled(): boolean; canToggleEnablement(): boolean; - turnOn(pullFirst: boolean): Promise; + turnOn(): Promise; turnOff(everywhere: boolean): Promise; triggerSync(sources: string[], hasToLimitSync: boolean): Promise; } diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index a06e412596e..4edcf255f35 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -22,7 +22,6 @@ export class UserDataSyncChannel implements IServerChannel { listen(_: unknown, event: string): Event { switch (event) { case 'onDidChangeStatus': return this.service.onDidChangeStatus; - case 'onSynchronizeResource': return this.service.onSynchronizeResource; case 'onDidChangeConflicts': return this.service.onDidChangeConflicts; case 'onDidChangeLocal': return this.service.onDidChangeLocal; case 'onDidChangeLastSyncTime': return this.service.onDidChangeLastSyncTime; @@ -53,7 +52,6 @@ export class UserDataSyncChannel implements IServerChannel { case 'resetLocal': return this.service.resetLocal(); case 'hasPreviouslySynced': return this.service.hasPreviouslySynced(); case 'hasLocalData': return this.service.hasLocalData(); - case 'isFirstTimeSyncingWithAnotherMachine': return this.service.isFirstTimeSyncingWithAnotherMachine(); case 'acceptPreviewContent': return this.service.acceptPreviewContent(args[0], URI.revive(args[1]), args[2]); case 'resolveContent': return this.service.resolveContent(URI.revive(args[0])); case 'getLocalSyncResourceHandles': return this.service.getLocalSyncResourceHandles(args[0]); @@ -64,13 +62,11 @@ export class UserDataSyncChannel implements IServerChannel { throw new Error('Invalid call'); } - private taskCounter = 1; - private async createManualSyncTask(): Promise<{ initialData: { manifest: IUserDataManifest | null }, channelName: string }> { + private async createManualSyncTask(): Promise<{ id: string, manifest: IUserDataManifest | null }> { const manualSyncTask = await this.service.createManualSyncTask(); const manualSyncTaskChannel = new ManualSyncTaskChannel(manualSyncTask); - const channelName = `manualSyncTask-${this.taskCounter++}`; - this.server.registerChannel(channelName, manualSyncTaskChannel); - return { initialData: { manifest: manualSyncTask.manifest }, channelName }; + this.server.registerChannel(`manualSyncTask-${manualSyncTask.id}`, manualSyncTaskChannel); + return { id: manualSyncTask.id, manifest: manualSyncTask.manifest }; } } @@ -79,6 +75,9 @@ class ManualSyncTaskChannel implements IServerChannel { constructor(private readonly manualSyncTask: IManualSyncTask) { } listen(_: unknown, event: string): Event { + switch (event) { + case 'onSynchronizeResources': return this.manualSyncTask.onSynchronizeResources; + } throw new Error(`Event not found: ${event}`); } @@ -90,6 +89,7 @@ class ManualSyncTaskChannel implements IServerChannel { case 'pull': return this.manualSyncTask.pull(); case 'push': return this.manualSyncTask.push(); case 'stop': return this.manualSyncTask.stop(); + case 'dispose': return this.manualSyncTask.dispose(); } throw new Error('Invalid call'); } @@ -102,8 +102,6 @@ export class UserDataAutoSyncChannel implements IServerChannel { listen(_: unknown, event: string): Event { switch (event) { - case 'onTurnOnSync': return this.service.onTurnOnSync; - case 'onDidTurnOnSync': return this.service.onDidTurnOnSync; case 'onError': return this.service.onError; } throw new Error(`Event not found: ${event}`); @@ -112,7 +110,7 @@ export class UserDataAutoSyncChannel implements IServerChannel { call(context: any, command: string, args?: any): Promise { switch (command) { case 'triggerSync': return this.service.triggerSync(args[0], args[1]); - case 'turnOn': return this.service.turnOn(args[0]); + case 'turnOn': return this.service.turnOn(); case 'turnOff': return this.service.turnOff(args[0]); } throw new Error('Invalid call'); diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 132f3d06f48..eb8a69d8b0e 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, ISyncResourceHandle, IUserDataManifest, ISyncTask, Change, IResourcePreview, IManualSyncTask, ISyncResourcePreview } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, UserDataSyncError, ISyncResourceHandle, IUserDataManifest, ISyncTask, IResourcePreview, IManualSyncTask, ISyncResourcePreview } 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'; @@ -42,9 +42,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ private _onDidChangeStatus: Emitter = this._register(new Emitter()); readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; - private _onSynchronizeResource: Emitter = this._register(new Emitter()); - readonly onSynchronizeResource: Event = this._onSynchronizeResource.event; - readonly onDidChangeLocal: Event; private _conflicts: [SyncResource, IResourcePreview[]][] = []; @@ -97,7 +94,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ try { for (const synchroniser of this.synchronisers) { try { - this._onSynchronizeResource.fire(synchroniser.resource); await synchroniser.pull(); } catch (e) { this.handleSynchronizerError(e, synchroniser.resource); @@ -181,7 +177,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ throw error; } - return new ManualSyncTask(manifest, syncHeaders, this.synchronisers, this.logService); + return new ManualSyncTask(executionId, manifest, syncHeaders, this.synchronisers, this.logService); } private recoveredSettings: boolean = false; @@ -212,7 +208,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return; } try { - this._onSynchronizeResource.fire(synchroniser.resource); await synchroniser.sync(manifest, syncHeaders); } catch (e) { this.handleSynchronizerError(e, synchroniser.resource); @@ -302,39 +297,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return false; } - async isFirstTimeSyncingWithAnotherMachine(): Promise { - await this.checkEnablement(); - - if (!await this.userDataSyncStoreService.manifest()) { - return false; - } - - // skip global state synchronizer - const synchronizers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.extensionsSynchroniser]; - - let hasLocalData: boolean = false; - for (const synchroniser of synchronizers) { - if (await synchroniser.hasLocalData()) { - hasLocalData = true; - break; - } - } - - if (!hasLocalData) { - return false; - } - - for (const synchroniser of synchronizers) { - const preview = await synchroniser.generateSyncResourcePreview(); - if (preview && !preview.isLastSyncFromCurrentMachine - && (preview.resourcePreviews.some(({ localChange }) => localChange !== Change.None) || preview.resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None))) { - return true; - } - } - - return false; - } - async reset(): Promise { await this.checkEnablement(); await this.resetRemote(); @@ -455,18 +417,31 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } -class ManualSyncTask implements IManualSyncTask { +class ManualSyncTask extends Disposable implements IManualSyncTask { private previewsPromise: CancelablePromise<[SyncResource, ISyncResourcePreview][]> | undefined; private previews: [SyncResource, ISyncResourcePreview][] | undefined; - constructor(readonly manifest: IUserDataManifest | null, + private synchronizingResources: [SyncResource, URI[]][] = []; + private _onSynchronizeResources = this._register(new Emitter<[SyncResource, URI[]][]>()); + readonly onSynchronizeResources = this._onSynchronizeResources.event; + + private isDisposed: boolean = false; + + constructor( + readonly id: string, + readonly manifest: IUserDataManifest | null, private readonly syncHeaders: IHeaders, private readonly synchronisers: IUserDataSynchroniser[], private readonly logService: IUserDataSyncLogService, - ) { } + ) { + super(); + } async preview(): Promise<[SyncResource, ISyncResourcePreview][]> { + if (this.isDisposed) { + throw new Error('Disposed'); + } if (!this.previewsPromise) { this.previewsPromise = createCancelablePromise(token => this.getPreviews(token)); } @@ -475,34 +450,83 @@ class ManualSyncTask implements IManualSyncTask { } async accept(resource: URI, content: string): Promise<[SyncResource, ISyncResourcePreview][]> { - if (!this.previews) { - throw new Error('You need to create preview before applying'); + return this.mergeOrAccept(resource, (sychronizer, force) => sychronizer.acceptPreviewContent(resource, content, force, this.syncHeaders)); + } + + async merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]> { + if (resource) { + return this.mergeOrAccept(resource, (sychronizer, force) => sychronizer.merge(resource, force, this.syncHeaders)); + } else { + return this.mergeAll(); } + } + + private async mergeOrAccept(resource: URI, mergeOrAccept: (synchroniser: IUserDataSynchroniser, force: boolean) => Promise): Promise<[SyncResource, ISyncResourcePreview][]> { + if (!this.previews) { + throw new Error('You need to create preview before merging or accepting'); + } + const index = this.previews.findIndex(([, preview]) => preview.resourcePreviews.some(({ localResource, previewResource, remoteResource }) => isEqual(resource, localResource) || isEqual(resource, previewResource) || isEqual(resource, remoteResource))); - if (index !== -1) { - const synchroniser = this.synchronisers.find(s => s.resource === this.previews![index][0])!; - /* force only if the resource is local or remote resource */ - const force = this.previews![index][1].resourcePreviews.some(({ localResource, remoteResource }) => isEqual(resource, localResource) || isEqual(resource, remoteResource)); - const preview = await synchroniser.acceptPreviewContent(resource, content, force, this.syncHeaders); - preview ? this.previews.splice(index, 1, this.toSyncResourcePreview(synchroniser.resource, preview)) : this.previews.splice(index, 1); + if (index === -1) { + return this.previews; } + + const [syncResource, previews] = this.previews[index]; + const resourcePreview = previews.resourcePreviews.find(({ localResource, remoteResource, previewResource }) => isEqual(localResource, resource) || isEqual(remoteResource, resource) || isEqual(previewResource, resource)); + if (!resourcePreview) { + return this.previews; + } + + let synchronizingResources = this.synchronizingResources.find(s => s[0] === syncResource); + if (!synchronizingResources) { + synchronizingResources = [syncResource, []]; + this.synchronizingResources.push(synchronizingResources); + } + if (!synchronizingResources[1].some(s => isEqual(s, resourcePreview.localResource))) { + synchronizingResources[1].push(resourcePreview.localResource); + this._onSynchronizeResources.fire(this.synchronizingResources); + } + + const synchroniser = this.synchronisers.find(s => s.resource === this.previews![index][0])!; + /* force only if the resource is local or remote resource */ + const force = isEqual(resource, resourcePreview.localResource) || isEqual(resource, resourcePreview.remoteResource); + const preview = await mergeOrAccept(synchroniser, force); + preview ? this.previews.splice(index, 1, this.toSyncResourcePreview(synchroniser.resource, preview)) : this.previews.splice(index, 1); + + const i = this.synchronizingResources.findIndex(s => s[0] === syncResource); + this.synchronizingResources[i][1].splice(synchronizingResources[1].findIndex(r => isEqual(r, resourcePreview.localResource)), 1); + if (!synchronizingResources[1].length) { + this.synchronizingResources.splice(i, 1); + this._onSynchronizeResources.fire(this.synchronizingResources); + } + return this.previews; } - async merge(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]> { + private async mergeAll(): Promise<[SyncResource, ISyncResourcePreview][]> { if (!this.previews) { - throw new Error('You need to create preview before applying'); + throw new Error('You need to create preview before merging'); } - const index = this.previews.findIndex(([, preview]) => preview.resourcePreviews.some(({ localResource, previewResource, remoteResource }) => - isEqual(resource, localResource) || isEqual(resource, previewResource) || isEqual(resource, remoteResource))); - if (index !== -1) { - const synchroniser = this.synchronisers.find(s => s.resource === this.previews![index][0])!; - /* force only if the resource is local or remote resource */ - const force = this.previews![index][1].resourcePreviews.some(({ localResource, remoteResource }) => isEqual(resource, localResource) || isEqual(resource, remoteResource)); - const preview = await synchroniser.merge(resource, force, this.syncHeaders); - preview ? this.previews.splice(index, 1, this.toSyncResourcePreview(synchroniser.resource, preview)) : this.previews.splice(index, 1); + if (this.synchronizingResources.length) { + throw new Error('Cannot merge while synchronizing resources'); } + const previews: [SyncResource, ISyncResourcePreview][] = []; + for (const [syncResource, preview] of this.previews) { + this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); + this._onSynchronizeResources.fire(this.synchronizingResources); + const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; + let syncResourcePreview = null; + for (const resourcePreview of preview.resourcePreviews) { + syncResourcePreview = await synchroniser.merge(resourcePreview.remoteResource, false, this.syncHeaders); + } + if (syncResourcePreview) { + previews.push([syncResource, syncResourcePreview]); + } + this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); + this._onSynchronizeResources.fire(this.synchronizingResources); + } + this.previews = previews; return this.previews; } @@ -510,12 +534,19 @@ class ManualSyncTask implements IManualSyncTask { if (!this.previews) { throw new Error('You need to create preview before applying'); } + if (this.synchronizingResources.length) { + throw new Error('Cannot pull while synchronizing resources'); + } for (const [syncResource, preview] of this.previews) { + this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); + this._onSynchronizeResources.fire(this.synchronizingResources); const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; for (const resourcePreview of preview.resourcePreviews) { const content = await synchroniser.resolveContent(resourcePreview.remoteResource) || ''; await synchroniser.acceptPreviewContent(resourcePreview.remoteResource, content, true, this.syncHeaders); } + this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); + this._onSynchronizeResources.fire(this.synchronizingResources); } this.previews = []; } @@ -524,22 +555,24 @@ class ManualSyncTask implements IManualSyncTask { if (!this.previews) { throw new Error('You need to create preview before applying'); } + if (this.synchronizingResources.length) { + throw new Error('Cannot pull while synchronizing resources'); + } for (const [syncResource, preview] of this.previews) { + this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); + this._onSynchronizeResources.fire(this.synchronizingResources); const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; for (const resourcePreview of preview.resourcePreviews) { const content = await synchroniser.resolveContent(resourcePreview.localResource) || ''; await synchroniser.acceptPreviewContent(resourcePreview.localResource, content, true, this.syncHeaders); } + this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); + this._onSynchronizeResources.fire(this.synchronizingResources); } this.previews = []; } async stop(): Promise { - if (this.previewsPromise) { - this.previewsPromise.cancel(); - this.previewsPromise = undefined; - } - this.previews = undefined; for (const synchroniser of this.synchronisers) { try { await synchroniser.stop(); @@ -549,6 +582,7 @@ class ManualSyncTask implements IManualSyncTask { } } } + this.reset(); } private async getPreviews(token: CancellationToken): Promise<[SyncResource, ISyncResourcePreview][]> { @@ -575,6 +609,20 @@ class ManualSyncTask implements IManualSyncTask { ]; } + private reset(): void { + if (this.previewsPromise) { + this.previewsPromise.cancel(); + this.previewsPromise = undefined; + } + this.previews = undefined; + this.synchronizingResources = []; + } + + dispose(): void { + this.reset(); + this.isDisposed = true; + } + } function toStrictResourcePreview(resourcePreview: IResourcePreview): IResourcePreview { diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts index 0fa55f6be9c..def897833ec 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts @@ -91,17 +91,9 @@ suite('UserDataSyncService', () => { // Sync (pull) from the test client target.reset(); - await testObject.isFirstTimeSyncingWithAnotherMachine(); await testObject.pull(); assert.deepEqual(target.requests, [ - /* first time sync */ - { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, - /* pull */ { 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: {} }, @@ -131,14 +123,9 @@ suite('UserDataSyncService', () => { // Sync (pull) from the test client target.reset(); - await testObject.isFirstTimeSyncingWithAnotherMachine(); await testObject.pull(); assert.deepEqual(target.requests, [ - /* first time sync */ - { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, - /* pull */ { 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: {} }, @@ -163,17 +150,9 @@ suite('UserDataSyncService', () => { // Sync (merge) from the test client target.reset(); - await testObject.isFirstTimeSyncingWithAnotherMachine(); await (await testObject.createSyncTask()).run(); assert.deepEqual(target.requests, [ - /* first time sync */ - { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, - /* sync */ { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, @@ -205,15 +184,9 @@ suite('UserDataSyncService', () => { // Sync (merge) from the test client target.reset(); - await testObject.isFirstTimeSyncingWithAnotherMachine(); await (await testObject.createSyncTask()).run(); assert.deepEqual(target.requests, [ - /* first time sync */ - { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, - - /* first time sync */ { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } }, diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index b58d587c678..43c61b65a15 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -143,8 +143,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this._register(userDataSyncService.onDidChangeConflicts(() => this.onDidChangeConflicts(this.userDataSyncService.conflicts))); this._register(userDataSyncService.onSyncErrors(errors => this.onSynchronizerErrors(errors))); this._register(userDataAutoSyncService.onError(error => this.onAutoSyncError(error))); - this._register(userDataAutoSyncService.onTurnOnSync(() => this.turningOnSync = true)); - this._register(userDataAutoSyncService.onDidTurnOnSync(() => this.turningOnSync = false)); this.registerActions(); this.registerViews(); @@ -412,6 +410,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private async turnOn(): Promise { + this.turningOnSync = true; try { if (!this.storageService.getBoolean('sync.donotAskPreviewConfirmation', StorageScope.GLOBAL, false)) { if (!await this.askForConfirmation()) { @@ -447,6 +446,8 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } this.notificationService.error(localize('turn on failed', "Error while starting Sync: {0}", toErrorMessage(e))); + } finally { + this.turningOnSync = false; } } diff --git a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts index bf841f93119..daef57314c0 100644 --- a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts +++ b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts @@ -18,8 +18,6 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i declare readonly _serviceBrand: undefined; private readonly channel: IChannel; - get onTurnOnSync(): Event { return this.channel.listen('onTurnOnSync'); } - get onDidTurnOnSync(): Event { return Event.map(this.channel.listen('onDidTurnOnSync'), e => e ? UserDataSyncError.toUserDataSyncError(e) : undefined); } get onError(): Event { return Event.map(this.channel.listen('onError'), e => UserDataSyncError.toUserDataSyncError(e)); } constructor( @@ -37,8 +35,8 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i return this.channel.call('triggerSync', [sources, hasToLimitSync]); } - turnOn(pullFirst: boolean): Promise { - return this.channel.call('turnOn', [pullFirst]); + turnOn(): Promise { + return this.channel.call('turnOn'); } turnOff(everywhere: boolean): Promise { diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 1f7767b9423..9b9f3a4a1d2 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, IAuthenticationProvider, getUserDataSyncStore, isAuthenticationProvider, IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IAuthenticationProvider, getUserDataSyncStore, isAuthenticationProvider, IUserDataAutoSyncService, Change } from 'vs/platform/userDataSync/common/userDataSync'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IUserDataSyncWorkbenchService, IUserDataSyncAccount, AccountStatus, CONTEXT_SYNC_ENABLEMENT, CONTEXT_SYNC_STATE, CONTEXT_ACCOUNT_STATE, SHOW_SYNCED_DATA_COMMAND_ID, SHOW_SYNC_LOG_COMMAND_ID, getSyncAreaLabel } from 'vs/workbench/services/userDataSync/common/userDataSync'; @@ -21,13 +21,13 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { localize } from 'vs/nls'; -import { canceled } from 'vs/base/common/errors'; +import { canceled, isPromiseCanceledError } from 'vs/base/common/errors'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { Action } from 'vs/base/common/actions'; -import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IProgressService, ProgressLocation, IProgressStep, IProgress } from 'vs/platform/progress/common/progress'; type UserAccountClassification = { id: { classification: 'EndUserPseudonymizedInformation', purpose: 'BusinessInsight' }; @@ -226,17 +226,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat location: ProgressLocation.Notification, title, delay: 500, - }, async (progress) => { - progress.report({ message: localize('turning on', "Turning on...") }); - const pullFirst = await this.isSyncingWithAnotherMachine(); - const disposable = this.userDataSyncService.onSynchronizeResource(resource => - progress.report({ message: localize('syncing resource', "Syncing {0}...", getSyncAreaLabel(resource)) })); - try { - await this.userDataAutoSyncService.turnOn(pullFirst); - } finally { - disposable.dispose(); - } - }); + }, (progress) => this.turnOnWithProgress(progress)); this.notificationService.info(localize('sync turned on', "{0} is turned on", title)); } @@ -245,12 +235,48 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat return this.userDataAutoSyncService.turnOff(everywhere); } - private async isSyncingWithAnotherMachine(): Promise { - const isSyncingWithAnotherMachine = await this.userDataSyncService.isFirstTimeSyncingWithAnotherMachine(); - if (!isSyncingWithAnotherMachine) { - return false; - } + private async turnOnWithProgress(progress: IProgress): Promise { + progress.report({ message: localize('turning on', "Turning on...") }); + const manualSyncTask = await this.userDataSyncService.createManualSyncTask(); + const preview = await manualSyncTask.preview(); + + const hasRemoteData = manualSyncTask.manifest !== null; + const hasLocalData = await this.userDataSyncService.hasLocalData(); + const isLastSyncFromCurrentMachine = preview.every(([, { isLastSyncFromCurrentMachine }]) => isLastSyncFromCurrentMachine); + const hasChanges = preview.some(([, { resourcePreviews }]) => resourcePreviews.some(r => r.localChange !== Change.None || r.remoteChange !== Change.None)); + + const progressDisposable = manualSyncTask.onSynchronizeResources(synchronizingResources => + synchronizingResources.length ? progress.report({ message: localize('syncing resource', "Syncing {0}...", getSyncAreaLabel(synchronizingResources[0][0])) }) : undefined); + + try { + if (!hasLocalData /* no data on local */ + || !hasRemoteData /* no data on remote */ + || !hasChanges /* no changes */ + || isLastSyncFromCurrentMachine /* has changes but last sync is from current machine */ + ) { + await manualSyncTask.merge(); + } else { + const pull = await this.askForPullOrMerge(); + if (pull) { + await manualSyncTask.pull(); + } else { + await manualSyncTask.merge(); + } + } + await this.userDataAutoSyncService.turnOn(); + } catch (error) { + if (isPromiseCanceledError(error)) { + await manualSyncTask.stop(); + } + throw error; + } finally { + manualSyncTask.dispose(); + progressDisposable.dispose(); + } + } + + private async askForPullOrMerge(): Promise { const result = await this.dialogService.show( Severity.Info, localize('Replace or Merge', "Replace or Merge"), diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts index 9c5bad31ae5..5c27df2c985 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts @@ -38,8 +38,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ private _onSyncErrors: Emitter<[SyncResource, UserDataSyncError][]> = this._register(new Emitter<[SyncResource, UserDataSyncError][]>()); readonly onSyncErrors: Event<[SyncResource, UserDataSyncError][]> = this._onSyncErrors.event; - get onSynchronizeResource(): Event { return this.channel.listen('onSynchronizeResource'); } - constructor( @ISharedProcessService private readonly sharedProcessService: ISharedProcessService ) { @@ -76,8 +74,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } async createManualSyncTask(): Promise { - const { initialData, channelName } = await this.channel.call<{ initialData: { manifest: IUserDataManifest | null }, channelName: string }>('createManualSyncTask'); - return new ManualSyncTask(this.sharedProcessService.getChannel(channelName), initialData.manifest); + const { id, manifest } = await this.channel.call<{ id: string, manifest: IUserDataManifest | null }>('createManualSyncTask'); + return new ManualSyncTask(id, manifest, this.sharedProcessService); } replace(uri: URI): Promise { @@ -100,10 +98,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return this.channel.call('hasLocalData'); } - isFirstTimeSyncingWithAnotherMachine(): Promise { - return this.channel.call('isFirstTimeSyncingWithAnotherMachine'); - } - acceptPreviewContent(syncResource: SyncResource, resource: URI, content: string): Promise { return this.channel.call('acceptPreviewContent', [syncResource, resource, content]); } @@ -162,7 +156,17 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ class ManualSyncTask implements IManualSyncTask { - constructor(private readonly channel: IChannel, readonly manifest: IUserDataManifest | null) { } + private readonly channel: IChannel; + + get onSynchronizeResources(): Event<[SyncResource, URI[]][]> { return this.channel.listen<[SyncResource, URI[]][]>('onSynchronizeResources'); } + + constructor( + readonly id: string, + readonly manifest: IUserDataManifest | null, + sharedProcessService: ISharedProcessService, + ) { + this.channel = sharedProcessService.getChannel(`manualSyncTask-${id}`); + } async preview(): Promise<[SyncResource, ISyncResourcePreview][]> { const previews = await this.channel.call<[SyncResource, ISyncResourcePreview][]>('preview'); @@ -185,7 +189,7 @@ class ManualSyncTask implements IManualSyncTask { return this.channel.call('accept', [resource, content]); } - merge(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]> { + merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]> { return this.channel.call('merge', [resource]); } @@ -200,6 +204,10 @@ class ManualSyncTask implements IManualSyncTask { stop(): Promise { return this.channel.call('stop'); } + + dispose(): void { + this.channel.call('dispose'); + } } registerSingleton(IUserDataSyncService, UserDataSyncService);