/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { equals } from '../../../base/common/arrays.js'; import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from '../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js'; import { toErrorMessage } from '../../../base/common/errorMessage.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; import { isEqual } from '../../../base/common/resources.js'; import { isBoolean, isUndefined } from '../../../base/common/types.js'; import { URI } from '../../../base/common/uri.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { IExtensionGalleryService } from '../../extensionManagement/common/extensionManagement.js'; import { IFileService } from '../../files/common/files.js'; import { IInstantiationService } from '../../instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { IUserDataProfile, IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js'; import { ExtensionsSynchroniser } from './extensionsSync.js'; import { GlobalStateSynchroniser } from './globalStateSync.js'; import { KeybindingsSynchroniser } from './keybindingsSync.js'; import { PromptsSynchronizer } from './promptsSync/promptsSync.js'; import { SettingsSynchroniser } from './settingsSync.js'; import { SnippetsSynchroniser } from './snippetsSync.js'; import { TasksSynchroniser } from './tasksSync.js'; import { McpSynchroniser } from './mcpSync.js'; import { UserDataProfilesManifestSynchroniser } from './userDataProfilesManifestSync.js'; import { ALL_SYNC_RESOURCES, createSyncHeaders, IUserDataManualSyncTask, IUserDataSyncResourceConflicts, IUserDataSyncResourceError, IUserDataSyncResource, ISyncResourceHandle, IUserDataSyncTask, ISyncUserDataProfile, IUserDataManifest, IUserDataSyncConfiguration, IUserDataSyncEnablementService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncService, IUserDataSyncStoreManagementService, IUserDataSyncStoreService, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, UserDataSyncStoreError, USER_DATA_SYNC_CONFIGURATION_SCOPE, IUserDataSyncResourceProviderService, IUserDataSyncActivityData, IUserDataSyncLocalStoreService, IUserDataSyncLatestData, IUserData, isUserDataManifest, } from './userDataSync.js'; type SyncErrorClassification = { owner: 'sandy081'; comment: 'Information about the error that occurred while syncing'; code: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error code' }; service: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync service for which this error has occurred' }; serverCode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync service error code' }; url?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync resource URL for which this error has occurred' }; resource?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync resource for which this error has occurred' }; executionId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Settings Sync execution id for which this error has occurred' }; }; type SyncErrorEvent = { code: string; service: string; serverCode?: string; url?: string; resource?: string; executionId?: string; }; const LAST_SYNC_TIME_KEY = 'sync.lastSyncTime'; export class UserDataSyncService extends Disposable implements IUserDataSyncService { _serviceBrand: undefined; private _status: SyncStatus = SyncStatus.Uninitialized; get status(): SyncStatus { return this._status; } private _onDidChangeStatus: Emitter = this._register(new Emitter()); readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; private _onDidChangeLocal = this._register(new Emitter()); readonly onDidChangeLocal = this._onDidChangeLocal.event; private _conflicts: IUserDataSyncResourceConflicts[] = []; get conflicts(): IUserDataSyncResourceConflicts[] { return this._conflicts; } private _onDidChangeConflicts = this._register(new Emitter()); readonly onDidChangeConflicts = this._onDidChangeConflicts.event; private _syncErrors: IUserDataSyncResourceError[] = []; private _onSyncErrors = this._register(new Emitter()); readonly onSyncErrors = this._onSyncErrors.event; private _lastSyncTime: number | undefined = undefined; get lastSyncTime(): number | undefined { return this._lastSyncTime; } private _onDidChangeLastSyncTime: Emitter = this._register(new Emitter()); readonly onDidChangeLastSyncTime: Event = this._onDidChangeLastSyncTime.event; private _onDidResetLocal = this._register(new Emitter()); readonly onDidResetLocal = this._onDidResetLocal.event; private _onDidResetRemote = this._register(new Emitter()); readonly onDidResetRemote = this._onDidResetRemote.event; private activeProfileSynchronizers = new Map(); constructor( @IFileService private readonly fileService: IFileService, @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IStorageService private readonly storageService: IStorageService, @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IUserDataSyncResourceProviderService private readonly userDataSyncResourceProviderService: IUserDataSyncResourceProviderService, @IUserDataSyncLocalStoreService private readonly userDataSyncLocalStoreService: IUserDataSyncLocalStoreService, ) { super(); this._status = userDataSyncStoreManagementService.userDataSyncStore ? SyncStatus.Idle : SyncStatus.Uninitialized; this._lastSyncTime = this.storageService.getNumber(LAST_SYNC_TIME_KEY, StorageScope.APPLICATION, undefined); this._register(toDisposable(() => this.clearActiveProfileSynchronizers())); this._register(new RunOnceScheduler(() => this.cleanUpStaleStorageData(), 5 * 1000 /* after 5s */)).schedule(); } async createSyncTask(manifest: IUserDataManifest | null, disableCache?: boolean): Promise { this.checkEnablement(); this.logService.info('Sync started.'); const startTime = new Date().getTime(); const executionId = generateUuid(); try { const syncHeaders = createSyncHeaders(executionId); if (disableCache) { syncHeaders['Cache-Control'] = 'no-cache'; } manifest = await this.userDataSyncStoreService.manifest(manifest, syncHeaders); } catch (error) { const userDataSyncError = UserDataSyncError.toUserDataSyncError(error); reportUserDataSyncError(userDataSyncError, executionId, this.userDataSyncStoreManagementService, this.telemetryService); throw userDataSyncError; } const executed = false; const that = this; let cancellablePromise: CancelablePromise | undefined; return { manifest, async run(): Promise { if (executed) { throw new Error('Can run a task only once'); } cancellablePromise = createCancelablePromise(token => that.sync(manifest, false, executionId, token)); await cancellablePromise.finally(() => cancellablePromise = undefined); that.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`); that.updateLastSyncTime(); }, stop(): Promise { cancellablePromise?.cancel(); return that.stop(); } }; } async createManualSyncTask(): Promise { this.checkEnablement(); if (this.userDataSyncEnablementService.isEnabled()) { throw new UserDataSyncError('Cannot start manual sync when sync is enabled', UserDataSyncErrorCode.LocalError); } this.logService.info('Sync started.'); const startTime = new Date().getTime(); const executionId = generateUuid(); const syncHeaders = createSyncHeaders(executionId); let latestUserDataOrManifest: IUserDataSyncLatestData | IUserDataManifest | null; try { latestUserDataOrManifest = await this.userDataSyncStoreService.getLatestData(syncHeaders); } catch (error) { const userDataSyncError = UserDataSyncError.toUserDataSyncError(error); this.telemetryService.publicLog2('sync.download.latest', { code: userDataSyncError.code, serverCode: userDataSyncError instanceof UserDataSyncStoreError ? String(userDataSyncError.serverCode) : undefined, url: userDataSyncError instanceof UserDataSyncStoreError ? userDataSyncError.url : undefined, resource: userDataSyncError.resource, executionId, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() }); // Fallback to manifest in stable try { latestUserDataOrManifest = await this.userDataSyncStoreService.manifest(null, syncHeaders); } catch (error) { const userDataSyncError = UserDataSyncError.toUserDataSyncError(error); reportUserDataSyncError(userDataSyncError, executionId, this.userDataSyncStoreManagementService, this.telemetryService); throw userDataSyncError; } } /* Manual sync shall start on clean local state */ await this.resetLocal(); const that = this; const cancellableToken = new CancellationTokenSource(); return { id: executionId, async merge(): Promise { return that.sync(latestUserDataOrManifest, true, executionId, cancellableToken.token); }, async apply(): Promise { try { try { await that.applyManualSync(latestUserDataOrManifest, executionId, cancellableToken.token); } catch (error) { if (UserDataSyncError.toUserDataSyncError(error).code === UserDataSyncErrorCode.MethodNotFound) { that.logService.info('Client is making invalid requests. Cleaning up data...'); await that.cleanUpRemoteData(); that.logService.info('Applying manual sync again...'); await that.applyManualSync(latestUserDataOrManifest, executionId, cancellableToken.token); } else { throw error; } } } catch (error) { that.logService.error(error); throw error; } that.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`); that.updateLastSyncTime(); }, async stop(): Promise { cancellableToken.cancel(); await that.stop(); await that.resetLocal(); } }; } private async sync(manifestOrLatestData: IUserDataManifest | IUserDataSyncLatestData | null, preview: boolean, executionId: string, token: CancellationToken): Promise { this._syncErrors = []; try { if (this.status !== SyncStatus.HasConflicts) { this.setStatus(SyncStatus.Syncing); } // Sync Default Profile First const defaultProfileSynchronizer = this.getOrCreateActiveProfileSynchronizer(this.userDataProfilesService.defaultProfile, undefined); this._syncErrors.push(...await this.syncProfile(defaultProfileSynchronizer, manifestOrLatestData, preview, executionId, token)); // Sync other profiles const userDataProfileManifestSynchronizer = defaultProfileSynchronizer.enabled.find(s => s.resource === SyncResource.Profiles); if (userDataProfileManifestSynchronizer) { const syncProfiles = (await (userDataProfileManifestSynchronizer as UserDataProfilesManifestSynchroniser).getLastSyncedProfiles()) || []; if (token.isCancellationRequested) { return; } await this.syncRemoteProfiles(syncProfiles, manifestOrLatestData, preview, executionId, token); } } finally { if (this.status !== SyncStatus.HasConflicts) { this.setStatus(SyncStatus.Idle); } this._onSyncErrors.fire(this._syncErrors); } } private async syncRemoteProfiles(remoteProfiles: ISyncUserDataProfile[], manifest: IUserDataManifest | IUserDataSyncLatestData | null, preview: boolean, executionId: string, token: CancellationToken): Promise { for (const syncProfile of remoteProfiles) { if (token.isCancellationRequested) { return; } const profile = this.userDataProfilesService.profiles.find(p => p.id === syncProfile.id); if (!profile) { this.logService.error(`Profile with id:${syncProfile.id} and name: ${syncProfile.name} does not exist locally to sync.`); continue; } this.logService.info('Syncing profile.', syncProfile.name); const profileSynchronizer = this.getOrCreateActiveProfileSynchronizer(profile, syncProfile); this._syncErrors.push(...await this.syncProfile(profileSynchronizer, manifest, preview, executionId, token)); } // Dispose & Delete profile synchronizers which do not exist anymore for (const [key, profileSynchronizerItem] of this.activeProfileSynchronizers.entries()) { if (this.userDataProfilesService.profiles.some(p => p.id === profileSynchronizerItem[0].profile.id)) { continue; } await profileSynchronizerItem[0].resetLocal(); profileSynchronizerItem[1].dispose(); this.activeProfileSynchronizers.delete(key); } } private async applyManualSync(manifestOrLatestData: IUserDataManifest | IUserDataSyncLatestData | null, executionId: string, token: CancellationToken): Promise { try { this.setStatus(SyncStatus.Syncing); const profileSynchronizers = this.getActiveProfileSynchronizers(); for (const profileSynchronizer of profileSynchronizers) { if (token.isCancellationRequested) { return; } await profileSynchronizer.apply(executionId, token); } const defaultProfileSynchronizer = profileSynchronizers.find(s => s.profile.isDefault); if (!defaultProfileSynchronizer) { return; } const userDataProfileManifestSynchronizer = defaultProfileSynchronizer.enabled.find(s => s.resource === SyncResource.Profiles); if (!userDataProfileManifestSynchronizer) { return; } // Sync remote profiles which are not synced locally const remoteProfiles = (await (userDataProfileManifestSynchronizer as UserDataProfilesManifestSynchroniser).getRemoteSyncedProfiles(getRefOrUserData(manifestOrLatestData, undefined, SyncResource.Profiles) ?? null)) || []; const remoteProfilesToSync = remoteProfiles.filter(remoteProfile => profileSynchronizers.every(s => s.profile.id !== remoteProfile.id)); if (remoteProfilesToSync.length) { await this.syncRemoteProfiles(remoteProfilesToSync, manifestOrLatestData, false, executionId, token); } } finally { this.setStatus(SyncStatus.Idle); } } private async syncProfile(profileSynchronizer: ProfileSynchronizer, manifestOrLatestData: IUserDataManifest | IUserDataSyncLatestData | null, preview: boolean, executionId: string, token: CancellationToken): Promise { const errors = await profileSynchronizer.sync(manifestOrLatestData, preview, executionId, token); return errors.map(([syncResource, error]) => ({ profile: profileSynchronizer.profile, syncResource, error })); } private async stop(): Promise { if (this.status !== SyncStatus.Idle) { await Promise.allSettled(this.getActiveProfileSynchronizers().map(profileSynchronizer => profileSynchronizer.stop())); } } async resolveContent(resource: URI): Promise { const content = await this.userDataSyncResourceProviderService.resolveContent(resource); if (content) { return content; } for (const profileSynchronizer of this.getActiveProfileSynchronizers()) { for (const synchronizer of profileSynchronizer.enabled) { const content = await synchronizer.resolveContent(resource); if (content) { return content; } } } return null; } async replace(syncResourceHandle: ISyncResourceHandle): Promise { this.checkEnablement(); const profileSyncResource = this.userDataSyncResourceProviderService.resolveUserDataSyncResource(syncResourceHandle); if (!profileSyncResource) { return; } const content = await this.resolveContent(syncResourceHandle.uri); if (!content) { return; } await this.performAction(profileSyncResource.profile, async synchronizer => { if (profileSyncResource.syncResource === synchronizer.resource) { await synchronizer.replace(content); return true; } return undefined; }); return; } async accept(syncResource: IUserDataSyncResource, resource: URI, content: string | null | undefined, apply: boolean | { force: boolean }): Promise { this.checkEnablement(); await this.performAction(syncResource.profile, async synchronizer => { if (syncResource.syncResource === synchronizer.resource) { await synchronizer.accept(resource, content); if (apply) { await synchronizer.apply(isBoolean(apply) ? false : apply.force, createSyncHeaders(generateUuid())); } return true; } return undefined; }); } async hasLocalData(): Promise { const result = await this.performAction(this.userDataProfilesService.defaultProfile, async synchronizer => { // skip global state synchronizer if (synchronizer.resource !== SyncResource.GlobalState && await synchronizer.hasLocalData()) { return true; } return undefined; }); return !!result; } async hasPreviouslySynced(): Promise { const result = await this.performAction(this.userDataProfilesService.defaultProfile, async synchronizer => { if (await synchronizer.hasPreviouslySynced()) { return true; } return undefined; }); return !!result; } async reset(): Promise { this.checkEnablement(); await this.resetRemote(); await this.resetLocal(); } async resetRemote(): Promise { this.checkEnablement(); try { await this.userDataSyncStoreService.clear(); this.logService.info('Cleared data on server'); } catch (e) { this.logService.error(e); } this._onDidResetRemote.fire(); } async resetLocal(): Promise { this.checkEnablement(); this._lastSyncTime = undefined; this.storageService.remove(LAST_SYNC_TIME_KEY, StorageScope.APPLICATION); for (const [synchronizer] of this.activeProfileSynchronizers.values()) { try { await synchronizer.resetLocal(); } catch (e) { this.logService.error(e); } } this.clearActiveProfileSynchronizers(); this._onDidResetLocal.fire(); this.logService.info('Did reset the local sync state.'); } private async cleanUpStaleStorageData(): Promise { const allKeys = this.storageService.keys(StorageScope.APPLICATION, StorageTarget.MACHINE); const lastSyncProfileKeys: [string, string][] = []; for (const key of allKeys) { if (!key.endsWith('.lastSyncUserData')) { continue; } const segments = key.split('.'); if (segments.length === 3) { lastSyncProfileKeys.push([key, segments[0]]); } } if (!lastSyncProfileKeys.length) { return; } const disposables = new DisposableStore(); try { let defaultProfileSynchronizer = this.activeProfileSynchronizers.get(this.userDataProfilesService.defaultProfile.id)?.[0]; if (!defaultProfileSynchronizer) { defaultProfileSynchronizer = disposables.add(this.instantiationService.createInstance(ProfileSynchronizer, this.userDataProfilesService.defaultProfile, undefined)); } const userDataProfileManifestSynchronizer = defaultProfileSynchronizer.enabled.find(s => s.resource === SyncResource.Profiles) as UserDataProfilesManifestSynchroniser; if (!userDataProfileManifestSynchronizer) { return; } const lastSyncedProfiles = await userDataProfileManifestSynchronizer.getLastSyncedProfiles(); const lastSyncedCollections = lastSyncedProfiles?.map(p => p.collection) ?? []; for (const [key, collection] of lastSyncProfileKeys) { if (!lastSyncedCollections.includes(collection)) { this.logService.info(`Removing last sync state for stale profile: ${collection}`); this.storageService.remove(key, StorageScope.APPLICATION); } } } finally { disposables.dispose(); } } async cleanUpRemoteData(): Promise { const remoteProfiles = await this.userDataSyncResourceProviderService.getRemoteSyncedProfiles(); const remoteProfileCollections = remoteProfiles.map(profile => profile.collection); const allCollections = await this.userDataSyncStoreService.getAllCollections(); const redundantCollections = allCollections.filter(c => !remoteProfileCollections.includes(c)); if (redundantCollections.length) { this.logService.info(`Deleting ${redundantCollections.length} redundant collections on server`); await Promise.allSettled(redundantCollections.map(collectionId => this.userDataSyncStoreService.deleteCollection(collectionId))); this.logService.info(`Deleted redundant collections on server`); } const updatedRemoteProfiles = remoteProfiles.filter(profile => allCollections.includes(profile.collection)); if (updatedRemoteProfiles.length !== remoteProfiles.length) { const profileManifestSynchronizer = this.instantiationService.createInstance(UserDataProfilesManifestSynchroniser, this.userDataProfilesService.defaultProfile, undefined); try { this.logService.info('Resetting the last synced state of profiles'); await profileManifestSynchronizer.resetLocal(); this.logService.info('Did reset the last synced state of profiles'); this.logService.info(`Updating remote profiles with invalid collections on server`); await profileManifestSynchronizer.updateRemoteProfiles(updatedRemoteProfiles, null); this.logService.info(`Updated remote profiles on server`); } finally { profileManifestSynchronizer.dispose(); } } } async saveRemoteActivityData(location: URI): Promise { this.checkEnablement(); const data = await this.userDataSyncStoreService.getActivityData(); await this.fileService.writeFile(location, data); } async extractActivityData(activityDataResource: URI, location: URI): Promise { const content = (await this.fileService.readFile(activityDataResource)).value.toString(); const activityData: IUserDataSyncActivityData = JSON.parse(content); if (activityData.resources) { for (const resource in activityData.resources) { for (const version of activityData.resources[resource]) { await this.userDataSyncLocalStoreService.writeResource(resource as SyncResource, version.content, new Date(version.created * 1000), undefined, location); } } } if (activityData.collections) { for (const collection in activityData.collections) { for (const resource in activityData.collections[collection].resources) { for (const version of activityData.collections[collection].resources?.[resource] ?? []) { await this.userDataSyncLocalStoreService.writeResource(resource as SyncResource, version.content, new Date(version.created * 1000), collection, location); } } } } } private async performAction(profile: IUserDataProfile, action: (synchroniser: IUserDataSynchroniser) => Promise): Promise { const disposables = new DisposableStore(); try { const activeProfileSyncronizer = this.activeProfileSynchronizers.get(profile.id); if (activeProfileSyncronizer) { const result = await this.performActionWithProfileSynchronizer(activeProfileSyncronizer[0], action, disposables); return isUndefined(result) ? null : result; } if (profile.isDefault) { const defaultProfileSynchronizer = disposables.add(this.instantiationService.createInstance(ProfileSynchronizer, profile, undefined)); const result = await this.performActionWithProfileSynchronizer(defaultProfileSynchronizer, action, disposables); return isUndefined(result) ? null : result; } const userDataProfileManifestSynchronizer = disposables.add(this.instantiationService.createInstance(UserDataProfilesManifestSynchroniser, profile, undefined)); const manifest = await this.userDataSyncStoreService.manifest(null); const syncProfiles = (await userDataProfileManifestSynchronizer.getRemoteSyncedProfiles(manifest?.latest?.profiles ?? null)) || []; const syncProfile = syncProfiles.find(syncProfile => syncProfile.id === profile.id); if (syncProfile) { const profileSynchronizer = disposables.add(this.instantiationService.createInstance(ProfileSynchronizer, profile, syncProfile.collection)); const result = await this.performActionWithProfileSynchronizer(profileSynchronizer, action, disposables); return isUndefined(result) ? null : result; } return null; } finally { disposables.dispose(); } } private async performActionWithProfileSynchronizer(profileSynchronizer: ProfileSynchronizer, action: (synchroniser: IUserDataSynchroniser) => Promise, disposables: DisposableStore): Promise { const allSynchronizers = [...profileSynchronizer.enabled, ...profileSynchronizer.disabled.reduce<(IUserDataSynchroniser & IDisposable)[]>((synchronizers, syncResource) => { if (syncResource !== SyncResource.WorkspaceState) { synchronizers.push(disposables.add(profileSynchronizer.createSynchronizer(syncResource))); } return synchronizers; }, [])]; for (const synchronizer of allSynchronizers) { const result = await action(synchronizer); if (!isUndefined(result)) { return result; } } return undefined; } private setStatus(status: SyncStatus): void { const oldStatus = this._status; if (this._status !== status) { this._status = status; this._onDidChangeStatus.fire(status); if (oldStatus === SyncStatus.HasConflicts) { this.updateLastSyncTime(); } } } private updateConflicts(): void { const conflicts = this.getActiveProfileSynchronizers().map(synchronizer => synchronizer.conflicts).flat(); if (!equals(this._conflicts, conflicts, (a, b) => a.profile.id === b.profile.id && a.syncResource === b.syncResource && equals(a.conflicts, b.conflicts, (a, b) => isEqual(a.previewResource, b.previewResource)))) { this._conflicts = conflicts; this._onDidChangeConflicts.fire(conflicts); } } private updateLastSyncTime(): void { if (this.status === SyncStatus.Idle) { this._lastSyncTime = new Date().getTime(); this.storageService.store(LAST_SYNC_TIME_KEY, this._lastSyncTime, StorageScope.APPLICATION, StorageTarget.MACHINE); this._onDidChangeLastSyncTime.fire(this._lastSyncTime); } } getOrCreateActiveProfileSynchronizer(profile: IUserDataProfile, syncProfile: ISyncUserDataProfile | undefined): ProfileSynchronizer { let activeProfileSynchronizer = this.activeProfileSynchronizers.get(profile.id); if (activeProfileSynchronizer && activeProfileSynchronizer[0].collection !== syncProfile?.collection) { this.logService.error('Profile synchronizer collection does not match with the remote sync profile collection'); activeProfileSynchronizer[1].dispose(); activeProfileSynchronizer = undefined; this.activeProfileSynchronizers.delete(profile.id); } if (!activeProfileSynchronizer) { const disposables = new DisposableStore(); const profileSynchronizer = disposables.add(this.instantiationService.createInstance(ProfileSynchronizer, profile, syncProfile?.collection)); disposables.add(profileSynchronizer.onDidChangeStatus(e => this.setStatus(e))); disposables.add(profileSynchronizer.onDidChangeConflicts(conflicts => this.updateConflicts())); disposables.add(profileSynchronizer.onDidChangeLocal(e => this._onDidChangeLocal.fire(e))); this.activeProfileSynchronizers.set(profile.id, activeProfileSynchronizer = [profileSynchronizer, disposables]); } return activeProfileSynchronizer[0]; } private getActiveProfileSynchronizers(): ProfileSynchronizer[] { const profileSynchronizers: ProfileSynchronizer[] = []; for (const [profileSynchronizer] of this.activeProfileSynchronizers.values()) { profileSynchronizers.push(profileSynchronizer); } return profileSynchronizers; } private clearActiveProfileSynchronizers(): void { this.activeProfileSynchronizers.forEach(([, disposable]) => disposable.dispose()); this.activeProfileSynchronizers.clear(); } private checkEnablement(): void { if (!this.userDataSyncStoreManagementService.userDataSyncStore) { throw new Error('Not enabled'); } } } class ProfileSynchronizer extends Disposable { private _enabled: [IUserDataSynchroniser, number, IDisposable][] = []; get enabled(): IUserDataSynchroniser[] { return this._enabled.sort((a, b) => a[1] - b[1]).map(([synchronizer]) => synchronizer); } get disabled(): SyncResource[] { return ALL_SYNC_RESOURCES.filter(syncResource => !this.userDataSyncEnablementService.isResourceEnabled(syncResource)); } private _status: SyncStatus = SyncStatus.Idle; get status(): SyncStatus { return this._status; } private _onDidChangeStatus: Emitter = this._register(new Emitter()); readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; private _onDidChangeLocal = this._register(new Emitter()); readonly onDidChangeLocal = this._onDidChangeLocal.event; private _conflicts: IUserDataSyncResourceConflicts[] = []; get conflicts(): IUserDataSyncResourceConflicts[] { return this._conflicts; } private _onDidChangeConflicts = this._register(new Emitter()); readonly onDidChangeConflicts = this._onDidChangeConflicts.event; constructor( readonly profile: IUserDataProfile, readonly collection: string | undefined, @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(); this._register(userDataSyncEnablementService.onDidChangeResourceEnablement(([syncResource, enablement]) => this.onDidChangeResourceEnablement(syncResource, enablement))); this._register(toDisposable(() => this._enabled.splice(0, this._enabled.length).forEach(([, , disposable]) => disposable.dispose()))); for (const syncResource of ALL_SYNC_RESOURCES) { if (userDataSyncEnablementService.isResourceEnabled(syncResource)) { this.registerSynchronizer(syncResource); } } } private onDidChangeResourceEnablement(syncResource: SyncResource, enabled: boolean): void { if (enabled) { this.registerSynchronizer(syncResource); } else { this.deRegisterSynchronizer(syncResource); } } protected registerSynchronizer(syncResource: SyncResource): void { if (this._enabled.some(([synchronizer]) => synchronizer.resource === syncResource)) { return; } if (syncResource === SyncResource.Extensions && !this.extensionGalleryService.isEnabled()) { this.logService.info('Skipping extensions sync because gallery is not configured'); return; } if (syncResource === SyncResource.Profiles) { if (!this.profile.isDefault) { return; } } if (syncResource === SyncResource.WorkspaceState) { return; } if (syncResource !== SyncResource.Profiles && this.profile.useDefaultFlags?.[syncResource]) { this.logService.debug(`Skipping syncing ${syncResource} in ${this.profile.name} because it is already synced by default profile`); return; } const disposables = new DisposableStore(); const synchronizer = disposables.add(this.createSynchronizer(syncResource)); disposables.add(synchronizer.onDidChangeStatus(() => this.updateStatus())); disposables.add(synchronizer.onDidChangeConflicts(() => this.updateConflicts())); disposables.add(synchronizer.onDidChangeLocal(() => this._onDidChangeLocal.fire(syncResource))); const order = this.getOrder(syncResource); this._enabled.push([synchronizer, order, disposables]); } private deRegisterSynchronizer(syncResource: SyncResource): void { const index = this._enabled.findIndex(([synchronizer]) => synchronizer.resource === syncResource); if (index !== -1) { const [[synchronizer, , disposable]] = this._enabled.splice(index, 1); disposable.dispose(); this.updateStatus(); synchronizer.stop().then(null, error => this.logService.error(error)); } } createSynchronizer(syncResource: Exclude): IUserDataSynchroniser & IDisposable { switch (syncResource) { case SyncResource.Settings: return this.instantiationService.createInstance(SettingsSynchroniser, this.profile, this.collection); case SyncResource.Keybindings: return this.instantiationService.createInstance(KeybindingsSynchroniser, this.profile, this.collection); case SyncResource.Snippets: return this.instantiationService.createInstance(SnippetsSynchroniser, this.profile, this.collection); case SyncResource.Prompts: return this.instantiationService.createInstance(PromptsSynchronizer, this.profile, this.collection); case SyncResource.Tasks: return this.instantiationService.createInstance(TasksSynchroniser, this.profile, this.collection); case SyncResource.Mcp: return this.instantiationService.createInstance(McpSynchroniser, this.profile, this.collection); case SyncResource.GlobalState: return this.instantiationService.createInstance(GlobalStateSynchroniser, this.profile, this.collection); case SyncResource.Extensions: return this.instantiationService.createInstance(ExtensionsSynchroniser, this.profile, this.collection); case SyncResource.Profiles: return this.instantiationService.createInstance(UserDataProfilesManifestSynchroniser, this.profile, this.collection); } } async sync(manifestOrLatestData: IUserDataManifest | IUserDataSyncLatestData | null, preview: boolean, executionId: string, token: CancellationToken): Promise<[SyncResource, UserDataSyncError][]> { // Return if cancellation is requested if (token.isCancellationRequested) { return []; } const synchronizers = this.enabled; if (!synchronizers.length) { return []; } try { const syncErrors: [SyncResource, UserDataSyncError][] = []; const syncHeaders = createSyncHeaders(executionId); const userDataSyncConfiguration = preview ? await this.getUserDataSyncConfiguration(manifestOrLatestData) : this.getLocalUserDataSyncConfiguration(); for (const synchroniser of synchronizers) { // Return if cancellation is requested if (token.isCancellationRequested) { return []; } // Return if resource is not enabled if (!this.userDataSyncEnablementService.isResourceEnabled(synchroniser.resource)) { return []; } try { const refOrUserData = getRefOrUserData(manifestOrLatestData, this.collection, synchroniser.resource) ?? null; await synchroniser.sync(refOrUserData, preview, userDataSyncConfiguration, syncHeaders); } catch (e) { const userDataSyncError = UserDataSyncError.toUserDataSyncError(e); reportUserDataSyncError(userDataSyncError, executionId, this.userDataSyncStoreManagementService, this.telemetryService); if (canBailout(e)) { throw userDataSyncError; } // Log and and continue this.logService.error(e); this.logService.error(`${synchroniser.resource}: ${toErrorMessage(e)}`); syncErrors.push([synchroniser.resource, userDataSyncError]); } } return syncErrors; } finally { this.updateStatus(); } } async apply(executionId: string, token: CancellationToken): Promise { const syncHeaders = createSyncHeaders(executionId); for (const synchroniser of this.enabled) { if (token.isCancellationRequested) { return; } try { await synchroniser.apply(false, syncHeaders); } catch (e) { const userDataSyncError = UserDataSyncError.toUserDataSyncError(e); reportUserDataSyncError(userDataSyncError, executionId, this.userDataSyncStoreManagementService, this.telemetryService); if (canBailout(e)) { throw userDataSyncError; } // Log and and continue this.logService.error(e); this.logService.error(`${synchroniser.resource}: ${toErrorMessage(e)}`); } } } async stop(): Promise { for (const synchroniser of this.enabled) { try { if (synchroniser.status !== SyncStatus.Idle) { await synchroniser.stop(); } } catch (e) { this.logService.error(e); } } } async resetLocal(): Promise { for (const synchroniser of this.enabled) { try { await synchroniser.resetLocal(); } catch (e) { this.logService.error(`${synchroniser.resource}: ${toErrorMessage(e)}`); this.logService.error(e); } } } private async getUserDataSyncConfiguration(manifestOrLatestData: IUserDataManifest | IUserDataSyncLatestData | null): Promise { if (!this.profile.isDefault) { return {}; } const local = this.getLocalUserDataSyncConfiguration(); const settingsSynchronizer = this.enabled.find(synchronizer => synchronizer instanceof SettingsSynchroniser); if (settingsSynchronizer) { const remote = await settingsSynchronizer.getRemoteUserDataSyncConfiguration(getRefOrUserData(manifestOrLatestData, this.collection, SyncResource.Settings) ?? null); return { ...local, ...remote }; } return local; } private getLocalUserDataSyncConfiguration(): IUserDataSyncConfiguration { return this.configurationService.getValue(USER_DATA_SYNC_CONFIGURATION_SCOPE); } private setStatus(status: SyncStatus): void { if (this._status !== status) { this._status = status; this._onDidChangeStatus.fire(status); } } private updateStatus(): void { this.updateConflicts(); if (this.enabled.some(s => s.status === SyncStatus.HasConflicts)) { return this.setStatus(SyncStatus.HasConflicts); } if (this.enabled.some(s => s.status === SyncStatus.Syncing)) { return this.setStatus(SyncStatus.Syncing); } return this.setStatus(SyncStatus.Idle); } private updateConflicts(): void { const conflicts = this.enabled.filter(s => s.status === SyncStatus.HasConflicts) .filter(s => s.conflicts.conflicts.length > 0) .map(s => s.conflicts); if (!equals(this._conflicts, conflicts, (a, b) => a.syncResource === b.syncResource && equals(a.conflicts, b.conflicts, (a, b) => isEqual(a.previewResource, b.previewResource)))) { this._conflicts = conflicts; this._onDidChangeConflicts.fire(conflicts); } } private getOrder(syncResource: SyncResource): number { switch (syncResource) { case SyncResource.Settings: return 0; case SyncResource.Keybindings: return 1; case SyncResource.Snippets: return 2; case SyncResource.Tasks: return 3; case SyncResource.Mcp: return 4; case SyncResource.GlobalState: return 5; case SyncResource.Extensions: return 6; case SyncResource.Prompts: return 7; case SyncResource.Profiles: return 8; case SyncResource.WorkspaceState: return 9; } } } function canBailout(e: unknown): boolean { if (e instanceof UserDataSyncError) { switch (e.code) { case UserDataSyncErrorCode.MethodNotFound: case UserDataSyncErrorCode.TooLarge: case UserDataSyncErrorCode.TooManyRequests: case UserDataSyncErrorCode.TooManyRequestsAndRetryAfter: case UserDataSyncErrorCode.LocalTooManyRequests: case UserDataSyncErrorCode.LocalTooManyProfiles: case UserDataSyncErrorCode.Gone: case UserDataSyncErrorCode.UpgradeRequired: case UserDataSyncErrorCode.IncompatibleRemoteContent: case UserDataSyncErrorCode.IncompatibleLocalContent: return true; } } return false; } function reportUserDataSyncError(userDataSyncError: UserDataSyncError, executionId: string, userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, telemetryService: ITelemetryService): void { telemetryService.publicLog2('sync/error', { code: userDataSyncError.code, serverCode: userDataSyncError instanceof UserDataSyncStoreError ? String(userDataSyncError.serverCode) : undefined, url: userDataSyncError instanceof UserDataSyncStoreError ? userDataSyncError.url : undefined, resource: userDataSyncError.resource, executionId, service: userDataSyncStoreManagementService.userDataSyncStore!.url.toString() }); } function getRefOrUserData(manifestOrLatestData: IUserDataManifest | IUserDataSyncLatestData | null, collection: string | undefined, resource: SyncResource): string | IUserData | undefined { if (isUserDataManifest(manifestOrLatestData)) { if (collection) { return manifestOrLatestData?.collections?.[collection]?.latest?.[resource]; } return manifestOrLatestData?.latest?.[resource]; } if (collection) { return manifestOrLatestData?.collections?.[collection]?.resources?.[resource]; } return manifestOrLatestData?.resources?.[resource]; }