/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncSource, ISettingsSyncService, IUserDataSyncLogService, IUserDataAuthTokenService, IUserDataSynchroniser, UserDataSyncStoreError, UserDataSyncErrorCode, UserDataSyncError } from 'vs/platform/userDataSync/common/userDataSync'; import { Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Emitter, Event } from 'vs/base/common/event'; import { ExtensionsSynchroniser } from 'vs/platform/userDataSync/common/extensionsSync'; import { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync'; import { GlobalStateSynchroniser } from 'vs/platform/userDataSync/common/globalStateSync'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { equals } from 'vs/base/common/arrays'; import { localize } from 'vs/nls'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; type SyncErrorClassification = { source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; }; const SESSION_ID_KEY = 'sync.sessionId'; export class UserDataSyncService extends Disposable implements IUserDataSyncService { _serviceBrand: any; private readonly synchronisers: IUserDataSynchroniser[]; private _status: SyncStatus = SyncStatus.Uninitialized; get status(): SyncStatus { return this._status; } private _onDidChangeStatus: Emitter = this._register(new Emitter()); readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; readonly onDidChangeLocal: Event; private _conflictsSources: SyncSource[] = []; get conflictsSources(): SyncSource[] { return this._conflictsSources; } private _onDidChangeConflicts: Emitter = this._register(new Emitter()); readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; private readonly keybindingsSynchroniser: KeybindingsSynchroniser; private readonly extensionsSynchroniser: ExtensionsSynchroniser; private readonly globalStateSynchroniser: GlobalStateSynchroniser; constructor( @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ISettingsSyncService private readonly settingsSynchroniser: ISettingsSyncService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @IUserDataAuthTokenService private readonly userDataAuthTokenService: IUserDataAuthTokenService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IStorageService private readonly storageService: IStorageService, ) { super(); this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser)); this.globalStateSynchroniser = this._register(this.instantiationService.createInstance(GlobalStateSynchroniser)); this.extensionsSynchroniser = this._register(this.instantiationService.createInstance(ExtensionsSynchroniser)); this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.globalStateSynchroniser, this.extensionsSynchroniser]; this.updateStatus(); if (this.userDataSyncStoreService.userDataSyncStore) { this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus())); this._register(this.userDataAuthTokenService.onDidChangeToken(e => this.onDidChangeAuthTokenStatus(e))); } this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => s.onDidChangeLocal)); } async pull(): Promise { await this.checkEnablement(); for (const synchroniser of this.synchronisers) { try { await synchroniser.pull(); } catch (e) { this.handleSyncError(e, synchroniser.source); } } } async push(): Promise { await this.checkEnablement(); for (const synchroniser of this.synchronisers) { try { await synchroniser.push(); } catch (e) { this.handleSyncError(e, synchroniser.source); } } } async sync(): Promise { await this.checkEnablement(); const startTime = new Date().getTime(); try { this.logService.trace('Sync started.'); if (this.status !== SyncStatus.HasConflicts) { this.setStatus(SyncStatus.Syncing); } let manifest = await this.userDataSyncStoreService.manifest(); // Server has no data but this machine was synced before if (manifest === null && await this.hasPreviouslySynced()) { // Sync was turned off from other machine 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); } for (const synchroniser of this.synchronisers) { try { await synchroniser.sync(manifest && manifest.latest ? manifest.latest[synchroniser.resourceKey] : undefined); } catch (e) { this.handleSyncError(e, synchroniser.source); } } // After syncing, get the manifest if it was not available before if (manifest === null) { manifest = await this.userDataSyncStoreService.manifest(); } // Update local session id if (manifest && manifest.session !== sessionId) { this.storageService.store(SESSION_ID_KEY, manifest.session, StorageScope.GLOBAL); } this.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`); } finally { this.updateStatus(); } } async stop(): Promise { if (this.status === SyncStatus.Idle) { return; } for (const synchroniser of this.synchronisers) { try { if (synchroniser.status !== SyncStatus.Idle) { await synchroniser.stop(); } } catch (e) { this.logService.error(e); } } } async accept(source: SyncSource, content: string): Promise { await this.checkEnablement(); const synchroniser = this.getSynchroniser(source); return synchroniser.accept(content); } async getRemoteContent(source: SyncSource, preview: boolean): Promise { await this.checkEnablement(); for (const synchroniser of this.synchronisers) { if (synchroniser.source === source) { return synchroniser.getRemoteContent(preview); } } return null; } async isFirstTimeSyncWithMerge(): Promise { await this.checkEnablement(); if (!await this.userDataSyncStoreService.manifest()) { return false; } if (await this.hasPreviouslySynced()) { return false; } return await this.hasLocalData(); } async reset(): Promise { await this.checkEnablement(); await this.resetRemote(); await this.resetLocal(); } async resetLocal(): Promise { await this.checkEnablement(); this.storageService.remove(SESSION_ID_KEY, StorageScope.GLOBAL); for (const synchroniser of this.synchronisers) { try { synchroniser.resetLocal(); } catch (e) { this.logService.error(`${synchroniser.source}: ${toErrorMessage(e)}`); this.logService.error(e); } } } private async hasPreviouslySynced(): Promise { await this.checkEnablement(); for (const synchroniser of this.synchronisers) { if (await synchroniser.hasPreviouslySynced()) { return true; } } return false; } private async hasLocalData(): Promise { await this.checkEnablement(); for (const synchroniser of this.synchronisers) { if (await synchroniser.hasLocalData()) { return true; } } return false; } private async resetRemote(): Promise { await this.checkEnablement(); try { await this.userDataSyncStoreService.clear(); } catch (e) { this.logService.error(e); } } private setStatus(status: SyncStatus): void { if (this._status !== status) { this._status = status; this._onDidChangeStatus.fire(status); } } private updateStatus(): void { const conflictsSources = this.computeConflictsSources(); if (!equals(this._conflictsSources, conflictsSources)) { this._conflictsSources = this.computeConflictsSources(); this._onDidChangeConflicts.fire(conflictsSources); } const status = this.computeStatus(); this.setStatus(status); } private computeStatus(): SyncStatus { if (!this.userDataSyncStoreService.userDataSyncStore) { return SyncStatus.Uninitialized; } if (this.synchronisers.some(s => s.status === SyncStatus.HasConflicts)) { return SyncStatus.HasConflicts; } if (this.synchronisers.some(s => s.status === SyncStatus.Syncing)) { return SyncStatus.Syncing; } return SyncStatus.Idle; } private handleSyncError(e: Error, source: SyncSource): void { if (e instanceof UserDataSyncStoreError) { switch (e.code) { case UserDataSyncErrorCode.TooLarge: this.telemetryService.publicLog2<{ source: string }, SyncErrorClassification>('sync/errorTooLarge', { source }); } throw e; } this.logService.error(e); this.logService.error(`${source}: ${toErrorMessage(e)}`); } private computeConflictsSources(): SyncSource[] { return this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts).map(s => s.source); } private getSynchroniser(source: SyncSource): IUserDataSynchroniser { switch (source) { case SyncSource.Settings: return this.settingsSynchroniser; case SyncSource.Keybindings: return this.keybindingsSynchroniser; case SyncSource.Extensions: return this.extensionsSynchroniser; case SyncSource.GlobalState: return this.globalStateSynchroniser; } } private async checkEnablement(): Promise { if (!this.userDataSyncStoreService.userDataSyncStore) { throw new Error('Not enabled'); } if (!(await this.userDataAuthTokenService.getToken())) { throw new UserDataSyncError('Not Authenticated. Please sign in to start sync.', UserDataSyncErrorCode.Unauthorized); } } private onDidChangeAuthTokenStatus(token: string | undefined): void { if (!token) { this.stop(); } } }