diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 5a8ac34d5d6..a4147b57382 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -69,7 +69,7 @@ import { CustomEndpointTelemetryService } from 'vs/platform/telemetry/node/custo import { LocalReconnectConstants, TerminalIpcChannels, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { ILocalPtyService } from 'vs/platform/terminal/electron-sandbox/terminal'; import { PtyHostService } from 'vs/platform/terminal/node/ptyHostService'; -import { ExtensionsStorageSyncService, IExtensionsStorageSyncService } from 'vs/platform/userDataSync/common/extensionsStorageSync'; +import { ExtensionStorageService, IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; import { IgnoredExtensionsManagementService, IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; import { IUserDataSyncBackupStoreService, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncService, IUserDataSyncStoreManagementService, IUserDataSyncStoreService, IUserDataSyncUtilService, registerConfiguration as registerUserDataSyncConfiguration } from 'vs/platform/userDataSync/common/userDataSync'; import { IUserDataSyncAccountService, UserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; @@ -327,7 +327,7 @@ class SharedProcessMain extends Disposable { services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(this.server.getChannel('userDataSyncUtil', client => client.ctx !== 'main'))); services.set(IGlobalExtensionEnablementService, new SyncDescriptor(GlobalExtensionEnablementService)); services.set(IIgnoredExtensionsManagementService, new SyncDescriptor(IgnoredExtensionsManagementService)); - services.set(IExtensionsStorageSyncService, new SyncDescriptor(ExtensionsStorageSyncService)); + services.set(IExtensionStorageService, new SyncDescriptor(ExtensionStorageService)); services.set(IUserDataSyncStoreManagementService, new SyncDescriptor(UserDataSyncStoreManagementService)); services.set(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService)); services.set(IUserDataSyncMachinesService, new SyncDescriptor(UserDataSyncMachinesService)); diff --git a/src/vs/platform/extensionManagement/common/extensionStorage.ts b/src/vs/platform/extensionManagement/common/extensionStorage.ts new file mode 100644 index 00000000000..c2ad01f1390 --- /dev/null +++ b/src/vs/platform/extensionManagement/common/extensionStorage.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { adoptToGalleryExtensionId, getExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { distinct } from 'vs/base/common/arrays'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IExtension } from 'vs/platform/extensions/common/extensions'; +import { isString } from 'vs/base/common/types'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; + +export interface IExtensionIdWithVersion { + id: string; + version: string; +} + +export const IExtensionStorageService = createDecorator('IExtensionStorageService'); + +export interface IExtensionStorageService { + readonly _serviceBrand: undefined; + + getExtensionState(extension: IExtension | IGalleryExtension | string, global: boolean): IStringDictionary | undefined; + setExtensionState(extension: IExtension | IGalleryExtension | string, state: IStringDictionary | undefined, global: boolean): void; + + readonly onDidChangeExtensionStorageToSync: Event; + setKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion, keys: string[]): void; + getKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion): string[] | undefined; +} + +const EXTENSION_KEYS_ID_VERSION_REGEX = /^extensionKeys\/([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/; + +export class ExtensionStorageService extends Disposable implements IExtensionStorageService { + + readonly _serviceBrand: undefined; + + private static toKey(extension: IExtensionIdWithVersion): string { + return `extensionKeys/${adoptToGalleryExtensionId(extension.id)}@${extension.version}`; + } + + private static fromKey(key: string): IExtensionIdWithVersion | undefined { + const matches = EXTENSION_KEYS_ID_VERSION_REGEX.exec(key); + if (matches && matches[1]) { + return { id: matches[1], version: matches[2] }; + } + return undefined; + } + + private readonly _onDidChangeExtensionStorageToSync = this._register(new Emitter()); + readonly onDidChangeExtensionStorageToSync = this._onDidChangeExtensionStorageToSync.event; + + private readonly extensionsWithKeysForSync = new Set(); + + constructor( + @IStorageService private readonly storageService: IStorageService, + @IProductService private readonly productService: IProductService, + @ILogService private readonly logService: ILogService, + ) { + super(); + this.initialize(); + this._register(this.storageService.onDidChangeValue(e => this.onDidChangeStorageValue(e))); + } + + private initialize(): void { + const keys = this.storageService.keys(StorageScope.GLOBAL, StorageTarget.MACHINE); + for (const key of keys) { + const extensionIdWithVersion = ExtensionStorageService.fromKey(key); + if (extensionIdWithVersion) { + this.extensionsWithKeysForSync.add(extensionIdWithVersion.id.toLowerCase()); + } + } + } + + private onDidChangeStorageValue(e: IStorageValueChangeEvent): void { + if (e.scope !== StorageScope.GLOBAL) { + return; + } + + // State of extension with keys for sync has changed + if (this.extensionsWithKeysForSync.has(e.key.toLowerCase())) { + this._onDidChangeExtensionStorageToSync.fire(); + return; + } + + // Keys for sync of an extension has changed + const extensionIdWithVersion = ExtensionStorageService.fromKey(e.key); + if (extensionIdWithVersion) { + this.extensionsWithKeysForSync.add(extensionIdWithVersion.id.toLowerCase()); + this._onDidChangeExtensionStorageToSync.fire(); + return; + } + } + + private getExtensionId(extension: IExtension | IGalleryExtension | string): string { + if (isString(extension)) { + return extension; + } + const publisher = (extension as IExtension).manifest ? (extension as IExtension).manifest.publisher : (extension as IGalleryExtension).publisher; + const name = (extension as IExtension).manifest ? (extension as IExtension).manifest.name : (extension as IGalleryExtension).name; + return getExtensionId(publisher, name); + } + + getExtensionState(extension: IExtension | IGalleryExtension | string, global: boolean): IStringDictionary | undefined { + const extensionId = this.getExtensionId(extension); + const jsonValue = this.storageService.get(extensionId, global ? StorageScope.GLOBAL : StorageScope.WORKSPACE); + if (jsonValue) { + try { + return JSON.parse(jsonValue); + } catch (error) { + // Do not fail this call but log it for diagnostics + // https://github.com/microsoft/vscode/issues/132777 + this.logService.error(`[mainThreadStorage] unexpected error parsing storage contents (extensionId: ${extensionId}, global: ${global}): ${error}`); + } + } + + return undefined; + } + + setExtensionState(extension: IExtension | IGalleryExtension | string, state: IStringDictionary | undefined, global: boolean): void { + const extensionId = this.getExtensionId(extension); + if (state === undefined) { + this.storageService.remove(extensionId, global ? StorageScope.GLOBAL : StorageScope.WORKSPACE); + } else { + this.storageService.store(extensionId, JSON.stringify(state), global ? StorageScope.GLOBAL : StorageScope.WORKSPACE, StorageTarget.MACHINE /* Extension state is synced separately through extensions */); + } + } + + setKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion, keys: string[]): void { + this.storageService.store(ExtensionStorageService.toKey(extensionIdWithVersion), JSON.stringify(keys), StorageScope.GLOBAL, StorageTarget.MACHINE); + } + + getKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion): string[] | undefined { + const extensionKeysForSyncFromProduct = this.productService.extensionSyncedKeys?.[extensionIdWithVersion.id.toLowerCase()]; + const extensionKeysForSyncFromStorageValue = this.storageService.get(ExtensionStorageService.toKey(extensionIdWithVersion), StorageScope.GLOBAL); + const extensionKeysForSyncFromStorage = extensionKeysForSyncFromStorageValue ? JSON.parse(extensionKeysForSyncFromStorageValue) : undefined; + + return extensionKeysForSyncFromStorage && extensionKeysForSyncFromProduct + ? distinct([...extensionKeysForSyncFromStorage, ...extensionKeysForSyncFromProduct]) + : (extensionKeysForSyncFromStorage || extensionKeysForSyncFromProduct); + } + +} diff --git a/src/vs/platform/userDataSync/common/extensionsStorageSync.ts b/src/vs/platform/userDataSync/common/extensionsStorageSync.ts deleted file mode 100644 index caa44b09237..00000000000 --- a/src/vs/platform/userDataSync/common/extensionsStorageSync.ts +++ /dev/null @@ -1,106 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { distinct } from 'vs/base/common/arrays'; -import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { adoptToGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; - -export interface IExtensionIdWithVersion { - id: string; - version: string; -} - -export const IExtensionsStorageSyncService = createDecorator('IExtensionsStorageSyncService'); - -export interface IExtensionsStorageSyncService { - - _serviceBrand: any; - - readonly onDidChangeExtensionsStorage: Event; - setKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion, keys: string[]): void; - getKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion): string[] | undefined; - -} - -const EXTENSION_KEYS_ID_VERSION_REGEX = /^extensionKeys\/([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/; - -export class ExtensionsStorageSyncService extends Disposable implements IExtensionsStorageSyncService { - - declare readonly _serviceBrand: undefined; - - private static toKey(extension: IExtensionIdWithVersion): string { - return `extensionKeys/${adoptToGalleryExtensionId(extension.id)}@${extension.version}`; - } - - private static fromKey(key: string): IExtensionIdWithVersion | undefined { - const matches = EXTENSION_KEYS_ID_VERSION_REGEX.exec(key); - if (matches && matches[1]) { - return { id: matches[1], version: matches[2] }; - } - return undefined; - } - - private readonly _onDidChangeExtensionsStorage = this._register(new Emitter()); - readonly onDidChangeExtensionsStorage = this._onDidChangeExtensionsStorage.event; - - private readonly extensionsWithKeysForSync = new Set(); - - constructor( - @IStorageService private readonly storageService: IStorageService, - @IProductService private readonly productService: IProductService, - ) { - super(); - this.initialize(); - this._register(this.storageService.onDidChangeValue(e => this.onDidChangeStorageValue(e))); - } - - private initialize(): void { - const keys = this.storageService.keys(StorageScope.GLOBAL, StorageTarget.MACHINE); - for (const key of keys) { - const extensionIdWithVersion = ExtensionsStorageSyncService.fromKey(key); - if (extensionIdWithVersion) { - this.extensionsWithKeysForSync.add(extensionIdWithVersion.id.toLowerCase()); - } - } - } - - private onDidChangeStorageValue(e: IStorageValueChangeEvent): void { - if (e.scope !== StorageScope.GLOBAL) { - return; - } - - // State of extension with keys for sync has changed - if (this.extensionsWithKeysForSync.has(e.key.toLowerCase())) { - this._onDidChangeExtensionsStorage.fire(); - return; - } - - // Keys for sync of an extension has changed - const extensionIdWithVersion = ExtensionsStorageSyncService.fromKey(e.key); - if (extensionIdWithVersion) { - this.extensionsWithKeysForSync.add(extensionIdWithVersion.id.toLowerCase()); - this._onDidChangeExtensionsStorage.fire(); - return; - } - } - - setKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion, keys: string[]): void { - this.storageService.store(ExtensionsStorageSyncService.toKey(extensionIdWithVersion), JSON.stringify(keys), StorageScope.GLOBAL, StorageTarget.MACHINE); - } - - getKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion): string[] | undefined { - const extensionKeysForSyncFromProduct = this.productService.extensionSyncedKeys?.[extensionIdWithVersion.id.toLowerCase()]; - const extensionKeysForSyncFromStorageValue = this.storageService.get(ExtensionsStorageSyncService.toKey(extensionIdWithVersion), StorageScope.GLOBAL); - const extensionKeysForSyncFromStorage = extensionKeysForSyncFromStorageValue ? JSON.parse(extensionKeysForSyncFromStorageValue) : undefined; - - return extensionKeysForSyncFromStorage && extensionKeysForSyncFromProduct - ? distinct([...extensionKeysForSyncFromStorage, ...extensionKeysForSyncFromProduct]) - : (extensionKeysForSyncFromStorage || extensionKeysForSyncFromProduct); - } -} diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index c0afb917a1b..59a5944dbad 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -13,17 +13,17 @@ import { compare } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension, ExtensionManagementError, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { areSameExtensions, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension, ExtensionManagementError, ExtensionManagementErrorCode, IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { AbstractInitializer, AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { IMergeResult as IExtensionMergeResult, merge } from 'vs/platform/userDataSync/common/extensionsMerge'; -import { IExtensionsStorageSyncService } from 'vs/platform/userDataSync/common/extensionsStorageSync'; import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; import { Change, IRemoteUserData, ISyncData, ISyncExtension, ISyncExtensionWithVersion, ISyncResourceHandle, IUserDataSyncBackupStoreService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; @@ -67,15 +67,6 @@ async function parseAndMigrateExtensions(syncData: ISyncData, extensionManagemen return extensions; } -export function getExtensionStorageState(publisher: string, name: string, storageService: IStorageService): IStringDictionary { - const extensionStorageValue = storageService.get(getExtensionId(publisher, name) /* use the same id used in extension host */, StorageScope.GLOBAL) || '{}'; - return JSON.parse(extensionStorageValue); -} - -export function storeExtensionStorageState(publisher: string, name: string, extensionState: IStringDictionary, storageService: IStorageService): void { - storageService.store(getExtensionId(publisher, name) /* use the same id used in extension host */, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE); -} - export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/extensions.json` }); @@ -96,7 +87,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse constructor( @IEnvironmentService environmentService: IEnvironmentService, @IFileService fileService: IFileService, - @IStorageService private readonly storageService: IStorageService, + @IStorageService storageService: IStorageService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @@ -107,7 +98,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse @IConfigurationService configurationService: IConfigurationService, @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @ITelemetryService telemetryService: ITelemetryService, - @IExtensionsStorageSyncService private readonly extensionsStorageSyncService: IExtensionsStorageSyncService, + @IExtensionStorageService private readonly extensionStorageService: IExtensionStorageService, @IUriIdentityService uriIdentityService: IUriIdentityService, ) { super(SyncResource.Extensions, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService, uriIdentityService); @@ -117,7 +108,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse Event.filter(this.extensionManagementService.onDidInstallExtensions, (e => e.some(({ local }) => !!local))), Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error)), this.extensionEnablementService.onDidChangeEnablement, - this.extensionsStorageSyncService.onDidChangeExtensionsStorage), + this.extensionStorageService.onDidChangeExtensionStorageToSync), () => undefined, 500)(() => this.triggerLocalChange())); } @@ -368,7 +359,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse // Builtin Extension Sync: Enablement & State if (installedExtension && installedExtension.isBuiltin) { if (e.state && installedExtension.manifest.version === e.version) { - this.updateExtensionState(e.state, installedExtension.manifest.publisher, installedExtension.manifest.name, installedExtension.manifest.version); + this.updateExtensionState(e.state, installedExtension, installedExtension.manifest.version); } const isDisabled = this.extensionEnablementService.getDisabledExtensions().some(disabledExtension => areSameExtensions(disabledExtension, e.identifier)); if (isDisabled !== !!e.disabled) { @@ -397,9 +388,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse (installedExtension ? installedExtension.manifest.version === e.version /* Installed and has same version */ : !!extension /* Installable */) ) { - const publisher = installedExtension ? installedExtension.manifest.publisher : extension!.publisher; - const name = installedExtension ? installedExtension.manifest.name : extension!.name; - this.updateExtensionState(e.state, publisher, name, installedExtension?.manifest.version); + this.updateExtensionState(e.state, installedExtension || extension, installedExtension?.manifest.version); } if (extension) { @@ -460,15 +449,15 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse return newSkippedExtensions; } - private updateExtensionState(state: IStringDictionary, publisher: string, name: string, version: string | undefined): void { - const extensionState = getExtensionStorageState(publisher, name, this.storageService); - const keys = version ? this.extensionsStorageSyncService.getKeysForSync({ id: getGalleryExtensionId(publisher, name), version }) : undefined; + private updateExtensionState(state: IStringDictionary, extension: ILocalExtension | IGalleryExtension, version: string | undefined): void { + const extensionState = this.extensionStorageService.getExtensionState(extension, true) || {}; + const keys = version ? this.extensionStorageService.getKeysForSync({ id: extension.identifier.id, version }) : undefined; if (keys) { keys.forEach(key => { extensionState[key] = state[key]; }); } else { Object.keys(state).forEach(key => extensionState[key] = state[key]); } - storeExtensionStorageState(publisher, name, extensionState, this.storageService); + this.extensionStorageService.setExtensionState(extension, extensionState, true); } private parseExtensions(syncData: ISyncData): ISyncExtension[] { @@ -478,7 +467,8 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse private getLocalExtensions(installedExtensions: ILocalExtension[]): ISyncExtensionWithVersion[] { const disabledExtensions = this.extensionEnablementService.getDisabledExtensions(); return installedExtensions - .map(({ identifier, isBuiltin, manifest, preRelease }) => { + .map(extension => { + const { identifier, isBuiltin, manifest, preRelease } = extension; const syncExntesion: ISyncExtensionWithVersion = { identifier, preRelease, version: manifest.version }; if (disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier))) { syncExntesion.disabled = true; @@ -487,9 +477,9 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse syncExntesion.installed = true; } try { - const keys = this.extensionsStorageSyncService.getKeysForSync({ id: identifier.id, version: manifest.version }); + const keys = this.extensionStorageService.getKeysForSync({ id: identifier.id, version: manifest.version }); if (keys) { - const extensionStorageState = getExtensionStorageState(manifest.publisher, manifest.name, this.storageService); + const extensionStorageState = this.extensionStorageService.getExtensionState(extension, true) || {}; syncExntesion.state = Object.keys(extensionStorageState).reduce((state: IStringDictionary, key) => { if (keys.includes(key)) { state[key] = extensionStorageState[key]; diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index df284282c0e..02729fc3378 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -32,7 +32,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; -import { ExtensionsStorageSyncService, IExtensionsStorageSyncService } from 'vs/platform/userDataSync/common/extensionsStorageSync'; +import { ExtensionStorageService, IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; import { IgnoredExtensionsManagementService, IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; import { ALL_SYNC_RESOURCES, getDefaultIgnoredSettings, IUserData, IUserDataManifest, IUserDataSyncBackupStoreService, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncService, IUserDataSyncStoreManagementService, IUserDataSyncStoreService, IUserDataSyncUtilService, registerConfiguration, ServerResource, SyncResource, IUserDataSynchroniser } from 'vs/platform/userDataSync/common/userDataSync'; import { IUserDataSyncAccountService, UserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; @@ -113,7 +113,7 @@ export class UserDataSyncClient extends Disposable { onDidUninstallExtension: new Emitter().event, }); this.instantiationService.stub(IGlobalExtensionEnablementService, this._register(this.instantiationService.createInstance(GlobalExtensionEnablementService))); - this.instantiationService.stub(IExtensionsStorageSyncService, this._register(this.instantiationService.createInstance(ExtensionsStorageSyncService))); + this.instantiationService.stub(IExtensionStorageService, this._register(this.instantiationService.createInstance(ExtensionStorageService))); this.instantiationService.stub(IIgnoredExtensionsManagementService, this.instantiationService.createInstance(IgnoredExtensionsManagementService)); this.instantiationService.stub(IExtensionGalleryService, >{ isEnabled() { return true; }, diff --git a/src/vs/workbench/api/browser/mainThreadStorage.ts b/src/vs/workbench/api/browser/mainThreadStorage.ts index 480a6826418..bd6ff064ac8 100644 --- a/src/vs/workbench/api/browser/mainThreadStorage.ts +++ b/src/vs/workbench/api/browser/mainThreadStorage.ts @@ -3,18 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { MainThreadStorageShape, MainContext, IExtHostContext, ExtHostStorageShape, ExtHostContext } from '../common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { IExtensionIdWithVersion, IExtensionsStorageSyncService } from 'vs/platform/userDataSync/common/extensionsStorageSync'; -import { ILogService } from 'vs/platform/log/common/log'; -import { FileSystemProviderError, FileSystemProviderErrorCode, IFileService } from 'vs/platform/files/common/files'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { isWeb } from 'vs/base/common/platform'; -import { getErrorMessage } from 'vs/base/common/errors'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IExtensionIdWithVersion, IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; +import { migrateExtensionStorage } from 'vs/workbench/services/extensions/common/extensionStorageMigration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @extHostNamedCustomer(MainContext.MainThreadStorage) export class MainThreadStorage implements MainThreadStorageShape { @@ -25,20 +21,16 @@ export class MainThreadStorage implements MainThreadStorageShape { constructor( extHostContext: IExtHostContext, + @IExtensionStorageService private readonly _extensionStorageService: IExtensionStorageService, @IStorageService private readonly _storageService: IStorageService, - @IExtensionsStorageSyncService private readonly _extensionsStorageSyncService: IExtensionsStorageSyncService, - @IFileService private readonly _fileService: IFileService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @ILogService private readonly _logService: ILogService + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostStorage); this._storageListener = this._storageService.onDidChangeValue(e => { const shared = e.scope === StorageScope.GLOBAL; if (shared && this._sharedStorageKeysToWatch.has(e.key)) { - this._proxy.$acceptValue(shared, e.key, this._getValue(shared, e.key)); + this._proxy.$acceptValue(shared, e.key, this._extensionStorageService.getExtensionState(e.key, shared)); } }); } @@ -47,91 +39,21 @@ export class MainThreadStorage implements MainThreadStorageShape { this._storageListener.dispose(); } - async $getValue(shared: boolean, key: string): Promise { - if (isWeb && key !== key.toLowerCase()) { - await this._migrateExtensionStorage(key.toLowerCase(), key, `extension.storage.migrateFromLowerCaseKey.${key.toLowerCase()}`); + async $initializeExtensionStorage(shared: boolean, extensionId: string): Promise { + if (isWeb && extensionId !== extensionId.toLowerCase()) { + await migrateExtensionStorage(extensionId.toLowerCase(), extensionId, `extension.storage.migrateFromLowerCaseKey.${extensionId.toLowerCase()}`, this._instantiationService); } if (shared) { - this._sharedStorageKeysToWatch.set(key, true); + this._sharedStorageKeysToWatch.set(extensionId, true); } - return this._getValue(shared, key); - } - - private _getValue(shared: boolean, key: string): T | undefined { - const jsonValue = this._storageService.get(key, shared ? StorageScope.GLOBAL : StorageScope.WORKSPACE); - if (jsonValue) { - try { - return JSON.parse(jsonValue); - } catch (error) { - // Do not fail this call but log it for diagnostics - // https://github.com/microsoft/vscode/issues/132777 - this._logService.error(`[mainThreadStorage] unexpected error parsing storage contents (key: ${key}, shared: ${shared}): ${error}`); - } - } - - return undefined; + return this._extensionStorageService.getExtensionState(extensionId, shared); } async $setValue(shared: boolean, key: string, value: object): Promise { - this._storageService.store(key, JSON.stringify(value), shared ? StorageScope.GLOBAL : StorageScope.WORKSPACE, StorageTarget.MACHINE /* Extension state is synced separately through extensions */); - } - - private _remove(shared: boolean, key: string): void { - this._storageService.remove(key, shared ? StorageScope.GLOBAL : StorageScope.WORKSPACE); + this._extensionStorageService.setExtensionState(key, value, shared); } $registerExtensionStorageKeysToSync(extension: IExtensionIdWithVersion, keys: string[]): void { - this._extensionsStorageSyncService.setKeysForSync(extension, keys); - } - - private async _migrateExtensionStorage(from: string, to: string, storageMigratedKey: string): Promise { - if (from === to) { - return; - } - - const extUri = this.uriIdentityService.extUri; - // Migrate Global Storage - if (!this._storageService.getBoolean(storageMigratedKey, StorageScope.GLOBAL, false)) { - const value = this._getValue(true, from); - if (value) { - this.$setValue(true, to, value); - this._remove(true, from); - } - - const fromPath = extUri.joinPath(this.environmentService.globalStorageHome, from); - const toPath = extUri.joinPath(this.environmentService.globalStorageHome, to.toLowerCase() /* Extension id is lower cased for global storage */); - if (!extUri.isEqual(fromPath, toPath)) { - try { - await this._fileService.move(fromPath, toPath, true); - } catch (error) { - if ((error).code !== FileSystemProviderErrorCode.FileNotFound) { - this._logService.info(`Error while migrating global storage from '${from}' to '${to}'`, getErrorMessage(error)); - } - } - } - - this._storageService.store(storageMigratedKey, true, StorageScope.GLOBAL, StorageTarget.MACHINE); - } - - // Migrate Workspace Storage - if (!this._storageService.getBoolean(storageMigratedKey, StorageScope.WORKSPACE, false)) { - const value = this._getValue(false, from); - if (value) { - this.$setValue(false, to, value); - this._remove(false, from); - } - - const fromPath = extUri.joinPath(this.environmentService.workspaceStorageHome, this.workspaceContextService.getWorkspace().id, from); - const toPath = extUri.joinPath(this.environmentService.workspaceStorageHome, this.workspaceContextService.getWorkspace().id, to); - try { - await this._fileService.move(fromPath, toPath, true); - } catch (error) { - if ((error).code !== FileSystemProviderErrorCode.FileNotFound) { - this._logService.info(`Error while migrating workspace storage from '${from}' to '${to}'`, getErrorMessage(error)); - } - } - - this._storageService.store(storageMigratedKey, true, StorageScope.WORKSPACE, StorageTarget.MACHINE); - } + this._extensionStorageService.setKeysForSync(extension, keys); } } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index db0307e22df..f0f13d0c7d5 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -41,7 +41,7 @@ import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/pla import { ITelemetryInfo } from 'vs/platform/telemetry/common/telemetry'; import { ICreateContributedTerminalProfileOptions, IProcessProperty, IShellLaunchConfigDto, ITerminalEnvironment, ITerminalLaunchError, ITerminalProfile, TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { ThemeColor, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync'; +import { IExtensionIdWithVersion } from 'vs/platform/extensionManagement/common/extensionStorage'; import { WorkspaceTrustRequestOptions } from 'vs/platform/workspace/common/workspaceTrust'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; import { ExtHostInteractive } from 'vs/workbench/api/common/extHostInteractive'; @@ -610,8 +610,8 @@ export interface MainThreadStatusBarShape extends IDisposable { } export interface MainThreadStorageShape extends IDisposable { - $getValue(shared: boolean, key: string): Promise; - $setValue(shared: boolean, key: string, value: object): Promise; + $initializeExtensionStorage(shared: boolean, extensionId: string): Promise; + $setValue(shared: boolean, extensionId: string, value: object): Promise; $registerExtensionStorageKeysToSync(extension: IExtensionIdWithVersion, keys: string[]): void; } @@ -2101,7 +2101,7 @@ export interface ExtHostInteractiveShape { } export interface ExtHostStorageShape { - $acceptValue(shared: boolean, key: string, value: object | undefined): void; + $acceptValue(shared: boolean, extensionId: string, value: object | undefined): void; } export interface ExtHostThemingShape { diff --git a/src/vs/workbench/api/common/extHostMemento.ts b/src/vs/workbench/api/common/extHostMemento.ts index b49a1b434ca..303e1ca90bc 100644 --- a/src/vs/workbench/api/common/extHostMemento.ts +++ b/src/vs/workbench/api/common/extHostMemento.ts @@ -27,7 +27,7 @@ export class ExtensionMemento implements vscode.Memento { this._shared = global; this._storage = storage; - this._init = this._storage.getValue(this._shared, this._id, Object.create(null)).then(value => { + this._init = this._storage.initializeExtensionStorage(this._shared, this._id, Object.create(null)).then(value => { this._value = value; return this; }); diff --git a/src/vs/workbench/api/common/extHostStorage.ts b/src/vs/workbench/api/common/extHostStorage.ts index 01712256fed..1bd1365132b 100644 --- a/src/vs/workbench/api/common/extHostStorage.ts +++ b/src/vs/workbench/api/common/extHostStorage.ts @@ -7,7 +7,7 @@ import { MainContext, MainThreadStorageShape, ExtHostStorageShape } from './extH import { Emitter } from 'vs/base/common/event'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync'; +import { IExtensionIdWithVersion } from 'vs/platform/extensionManagement/common/extensionStorage'; export interface IStorageChangeEvent { shared: boolean; @@ -32,8 +32,8 @@ export class ExtHostStorage implements ExtHostStorageShape { this._proxy.$registerExtensionStorageKeysToSync(extension, keys); } - getValue(shared: boolean, key: string, defaultValue?: T): Promise { - return this._proxy.$getValue(shared, key).then(value => value || defaultValue); + initializeExtensionStorage(shared: boolean, key: string, defaultValue?: object): Promise { + return this._proxy.$initializeExtensionStorage(shared, key).then(value => value || defaultValue); } setValue(shared: boolean, key: string, value: object): Promise { diff --git a/src/vs/workbench/services/extensions/common/extensionStorageMigration.ts b/src/vs/workbench/services/extensions/common/extensionStorageMigration.ts new file mode 100644 index 00000000000..7f5a4abb2ef --- /dev/null +++ b/src/vs/workbench/services/extensions/common/extensionStorageMigration.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getErrorMessage } from 'vs/base/common/errors'; +import { URI } from 'vs/base/common/uri'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; +import { FileSystemProviderError, FileSystemProviderErrorCode, IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; + +/** + * An extension storage has following + * - State: Stored using storage service with extension id as key and state as value. + * - Resources: Stored under a location scoped to the extension. + */ +export async function migrateExtensionStorage(fromExtensionId: string, toExtensionId: string, storageMigratedKey: string, instantionService: IInstantiationService): Promise { + return instantionService.invokeFunction(async serviceAccessor => { + const environmentService = serviceAccessor.get(IEnvironmentService); + const extensionStorageService = serviceAccessor.get(IExtensionStorageService); + const storageService = serviceAccessor.get(IStorageService); + const uriIdentityService = serviceAccessor.get(IUriIdentityService); + const fileService = serviceAccessor.get(IFileService); + const workspaceContextService = serviceAccessor.get(IWorkspaceContextService); + const logService = serviceAccessor.get(ILogService); + + if (fromExtensionId === toExtensionId) { + return; + } + + const getExtensionStorageLocation = (extensionId: string, global: boolean): URI => { + if (global) { + return uriIdentityService.extUri.joinPath(environmentService.globalStorageHome, extensionId.toLowerCase() /* Extension id is lower cased for global storage */); + } + return uriIdentityService.extUri.joinPath(environmentService.workspaceStorageHome, workspaceContextService.getWorkspace().id, extensionId); + }; + + const migrateStorage = async (global: boolean) => { + // Migrate state + const value = extensionStorageService.getExtensionState(fromExtensionId, global); + if (value) { + extensionStorageService.setExtensionState(toExtensionId, value, global); + extensionStorageService.setExtensionState(fromExtensionId, undefined, global); + } + + // Migrate stored files + const fromPath = getExtensionStorageLocation(fromExtensionId, global); + const toPath = getExtensionStorageLocation(toExtensionId, global); + if (!uriIdentityService.extUri.isEqual(fromPath, toPath)) { + try { + await fileService.move(fromPath, toPath, true); + } catch (error) { + if ((error).code !== FileSystemProviderErrorCode.FileNotFound) { + logService.info(`Error while migrating ${global ? 'global' : 'workspace'} storage from '${fromExtensionId}' to '${toExtensionId}'`, getErrorMessage(error)); + } + } + } + }; + + // Migrate Global Storage + if (!storageService.getBoolean(storageMigratedKey, StorageScope.GLOBAL, false)) { + migrateStorage(true); + storageService.store(storageMigratedKey, true, StorageScope.GLOBAL, StorageTarget.MACHINE); + } + + // Migrate Workspace Storage + if (!storageService.getBoolean(storageMigratedKey, StorageScope.WORKSPACE, false)) { + migrateStorage(false); + storageService.store(storageMigratedKey, true, StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + }); +} diff --git a/src/vs/workbench/services/userData/browser/userDataInit.ts b/src/vs/workbench/services/userData/browser/userDataInit.ts index 1fb4714891a..3db531110e5 100644 --- a/src/vs/workbench/services/userData/browser/userDataInit.ts +++ b/src/vs/workbench/services/userData/browser/userDataInit.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { AbstractExtensionsInitializer, getExtensionStorageState, IExtensionsInitializerPreviewResult, storeExtensionStorageState } from 'vs/platform/userDataSync/common/extensionsSync'; +import { AbstractExtensionsInitializer, IExtensionsInitializerPreviewResult } from 'vs/platform/userDataSync/common/extensionsSync'; import { GlobalStateInitializer, UserDataSyncStoreTypeSynchronizer } from 'vs/platform/userDataSync/common/globalStateSync'; import { KeybindingsInitializer } from 'vs/platform/userDataSync/common/keybindingsSync'; import { SettingsInitializer } from 'vs/platform/userDataSync/common/settingsSync'; @@ -34,6 +34,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { isEqual } from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; export const IUserDataInitializationService = createDecorator('IUserDataInitializationService'); export interface IUserDataInitializationService { @@ -325,7 +326,7 @@ class InstalledExtensionsInitializer implements IUserDataInitializer { constructor( private readonly extensionsPreviewInitializer: ExtensionsPreviewInitializer, @IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService, - @IStorageService private readonly storageService: IStorageService, + @IExtensionStorageService private readonly extensionStorageService: IExtensionStorageService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, ) { } @@ -340,9 +341,9 @@ class InstalledExtensionsInitializer implements IUserDataInitializer { for (const installedExtension of preview.installedExtensions) { const syncExtension = preview.remoteExtensions.find(({ identifier }) => areSameExtensions(identifier, installedExtension.identifier)); if (syncExtension?.state) { - const extensionState = getExtensionStorageState(installedExtension.manifest.publisher, installedExtension.manifest.name, this.storageService); + const extensionState = this.extensionStorageService.getExtensionState(installedExtension, true) || {}; Object.keys(syncExtension.state).forEach(key => extensionState[key] = syncExtension.state![key]); - storeExtensionStorageState(installedExtension.manifest.publisher, installedExtension.manifest.name, extensionState, this.storageService); + this.extensionStorageService.setExtensionState(installedExtension, extensionState, true); } } @@ -362,7 +363,7 @@ class NewExtensionsInitializer implements IUserDataInitializer { constructor( private readonly extensionsPreviewInitializer: ExtensionsPreviewInitializer, @IExtensionService private readonly extensionService: IExtensionService, - @IStorageService private readonly storageService: IStorageService, + @IExtensionStorageService private readonly extensionStorageService: IExtensionStorageService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @@ -392,7 +393,7 @@ class NewExtensionsInitializer implements IUserDataInitializer { continue; } if (extensionToSync.state) { - storeExtensionStorageState(galleryExtension.publisher, galleryExtension.name, extensionToSync.state, this.storageService); + this.extensionStorageService.setExtensionState(galleryExtension, extensionToSync.state, true); } this.logService.trace(`Installing extension...`, galleryExtension.identifier.id); const local = await this.extensionManagementService.installFromGallery(galleryExtension, { isMachineScoped: false, donotIncludePackAndDependencies: true, installPreReleaseVersion: extensionToSync.preRelease } /* set isMachineScoped to prevent install and sync dialog in web */); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index ee1856d0b20..dc35c48aad5 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -121,14 +121,14 @@ import { DownloadService } from 'vs/platform/download/common/downloadService'; import { OpenerService } from 'vs/editor/browser/services/openerService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IgnoredExtensionsManagementService, IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; -import { ExtensionsStorageSyncService, IExtensionsStorageSyncService } from 'vs/platform/userDataSync/common/extensionsStorageSync'; +import { ExtensionStorageService, IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; import { IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; registerSingleton(IUserDataSyncLogService, UserDataSyncLogService); registerSingleton(IIgnoredExtensionsManagementService, IgnoredExtensionsManagementService); registerSingleton(IGlobalExtensionEnablementService, GlobalExtensionEnablementService); -registerSingleton(IExtensionsStorageSyncService, ExtensionsStorageSyncService); +registerSingleton(IExtensionStorageService, ExtensionStorageService); registerSingleton(IExtensionGalleryService, ExtensionGalleryService, true); registerSingleton(IContextViewService, ContextViewService, true); registerSingleton(IListService, ListService, true);