Path: blob/main/src/vs/platform/extensionManagement/common/extensionStorage.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { createDecorator } from '../../instantiation/common/instantiation.js';6import { Emitter, Event } from '../../../base/common/event.js';7import { Disposable } from '../../../base/common/lifecycle.js';8import { IProfileStorageValueChangeEvent, IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';9import { adoptToGalleryExtensionId, areSameExtensions, getExtensionId } from './extensionManagementUtil.js';10import { IProductService } from '../../product/common/productService.js';11import { distinct } from '../../../base/common/arrays.js';12import { ILogService } from '../../log/common/log.js';13import { IExtension } from '../../extensions/common/extensions.js';14import { isString } from '../../../base/common/types.js';15import { IStringDictionary } from '../../../base/common/collections.js';16import { IExtensionManagementService, IGalleryExtension } from './extensionManagement.js';1718export interface IExtensionIdWithVersion {19id: string;20version: string;21}2223export const IExtensionStorageService = createDecorator<IExtensionStorageService>('IExtensionStorageService');2425export interface IExtensionStorageService {26readonly _serviceBrand: undefined;2728getExtensionState(extension: IExtension | IGalleryExtension | string, global: boolean): IStringDictionary<any> | undefined;29getExtensionStateRaw(extension: IExtension | IGalleryExtension | string, global: boolean): string | undefined;30setExtensionState(extension: IExtension | IGalleryExtension | string, state: IStringDictionary<any> | undefined, global: boolean): void;3132readonly onDidChangeExtensionStorageToSync: Event<void>;33setKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion, keys: string[]): void;34getKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion): string[] | undefined;3536addToMigrationList(from: string, to: string): void;37getSourceExtensionToMigrate(target: string): string | undefined;38}3940const EXTENSION_KEYS_ID_VERSION_REGEX = /^extensionKeys\/([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/;4142export class ExtensionStorageService extends Disposable implements IExtensionStorageService {4344readonly _serviceBrand: undefined;4546private static LARGE_STATE_WARNING_THRESHOLD = 512 * 1024;4748private static toKey(extension: IExtensionIdWithVersion): string {49return `extensionKeys/${adoptToGalleryExtensionId(extension.id)}@${extension.version}`;50}5152private static fromKey(key: string): IExtensionIdWithVersion | undefined {53const matches = EXTENSION_KEYS_ID_VERSION_REGEX.exec(key);54if (matches && matches[1]) {55return { id: matches[1], version: matches[2] };56}57return undefined;58}5960/* TODO @sandy081: This has to be done across all profiles */61static async removeOutdatedExtensionVersions(extensionManagementService: IExtensionManagementService, storageService: IStorageService): Promise<void> {62const extensions = await extensionManagementService.getInstalled();63const extensionVersionsToRemove: string[] = [];64for (const [id, versions] of ExtensionStorageService.readAllExtensionsWithKeysForSync(storageService)) {65const extensionVersion = extensions.find(e => areSameExtensions(e.identifier, { id }))?.manifest.version;66for (const version of versions) {67if (extensionVersion !== version) {68extensionVersionsToRemove.push(ExtensionStorageService.toKey({ id, version }));69}70}71}72for (const key of extensionVersionsToRemove) {73storageService.remove(key, StorageScope.PROFILE);74}75}7677private static readAllExtensionsWithKeysForSync(storageService: IStorageService): Map<string, string[]> {78const extensionsWithKeysForSync = new Map<string, string[]>();79const keys = storageService.keys(StorageScope.PROFILE, StorageTarget.MACHINE);80for (const key of keys) {81const extensionIdWithVersion = ExtensionStorageService.fromKey(key);82if (extensionIdWithVersion) {83let versions = extensionsWithKeysForSync.get(extensionIdWithVersion.id.toLowerCase());84if (!versions) {85extensionsWithKeysForSync.set(extensionIdWithVersion.id.toLowerCase(), versions = []);86}87versions.push(extensionIdWithVersion.version);88}89}90return extensionsWithKeysForSync;91}9293private readonly _onDidChangeExtensionStorageToSync = this._register(new Emitter<void>());94readonly onDidChangeExtensionStorageToSync = this._onDidChangeExtensionStorageToSync.event;9596private readonly extensionsWithKeysForSync: Map<string, string[]>;9798constructor(99@IStorageService private readonly storageService: IStorageService,100@IProductService private readonly productService: IProductService,101@ILogService private readonly logService: ILogService,102) {103super();104this.extensionsWithKeysForSync = ExtensionStorageService.readAllExtensionsWithKeysForSync(storageService);105this._register(this.storageService.onDidChangeValue(StorageScope.PROFILE, undefined, this._store)(e => this.onDidChangeStorageValue(e)));106}107108private onDidChangeStorageValue(e: IProfileStorageValueChangeEvent): void {109110// State of extension with keys for sync has changed111if (this.extensionsWithKeysForSync.has(e.key.toLowerCase())) {112this._onDidChangeExtensionStorageToSync.fire();113return;114}115116// Keys for sync of an extension has changed117const extensionIdWithVersion = ExtensionStorageService.fromKey(e.key);118if (extensionIdWithVersion) {119if (this.storageService.get(e.key, StorageScope.PROFILE) === undefined) {120this.extensionsWithKeysForSync.delete(extensionIdWithVersion.id.toLowerCase());121} else {122let versions = this.extensionsWithKeysForSync.get(extensionIdWithVersion.id.toLowerCase());123if (!versions) {124this.extensionsWithKeysForSync.set(extensionIdWithVersion.id.toLowerCase(), versions = []);125}126versions.push(extensionIdWithVersion.version);127this._onDidChangeExtensionStorageToSync.fire();128}129return;130}131}132133private getExtensionId(extension: IExtension | IGalleryExtension | string): string {134if (isString(extension)) {135return extension;136}137const publisher = (extension as IExtension).manifest ? (extension as IExtension).manifest.publisher : (extension as IGalleryExtension).publisher;138const name = (extension as IExtension).manifest ? (extension as IExtension).manifest.name : (extension as IGalleryExtension).name;139return getExtensionId(publisher, name);140}141142getExtensionState(extension: IExtension | IGalleryExtension | string, global: boolean): IStringDictionary<any> | undefined {143const extensionId = this.getExtensionId(extension);144const jsonValue = this.getExtensionStateRaw(extension, global);145if (jsonValue) {146try {147return JSON.parse(jsonValue);148} catch (error) {149// Do not fail this call but log it for diagnostics150// https://github.com/microsoft/vscode/issues/132777151this.logService.error(`[mainThreadStorage] unexpected error parsing storage contents (extensionId: ${extensionId}, global: ${global}): ${error}`);152}153}154155return undefined;156}157158getExtensionStateRaw(extension: IExtension | IGalleryExtension | string, global: boolean): string | undefined {159const extensionId = this.getExtensionId(extension);160const rawState = this.storageService.get(extensionId, global ? StorageScope.PROFILE : StorageScope.WORKSPACE);161162if (rawState && rawState?.length > ExtensionStorageService.LARGE_STATE_WARNING_THRESHOLD) {163this.logService.warn(`[mainThreadStorage] large extension state detected (extensionId: ${extensionId}, global: ${global}): ${rawState.length / 1024}kb. Consider to use 'storageUri' or 'globalStorageUri' to store this data on disk instead.`);164}165166return rawState;167}168169setExtensionState(extension: IExtension | IGalleryExtension | string, state: IStringDictionary<any> | undefined, global: boolean): void {170const extensionId = this.getExtensionId(extension);171if (state === undefined) {172this.storageService.remove(extensionId, global ? StorageScope.PROFILE : StorageScope.WORKSPACE);173} else {174this.storageService.store(extensionId, JSON.stringify(state), global ? StorageScope.PROFILE : StorageScope.WORKSPACE, StorageTarget.MACHINE /* Extension state is synced separately through extensions */);175}176}177178setKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion, keys: string[]): void {179this.storageService.store(ExtensionStorageService.toKey(extensionIdWithVersion), JSON.stringify(keys), StorageScope.PROFILE, StorageTarget.MACHINE);180}181182getKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion): string[] | undefined {183const extensionKeysForSyncFromProduct = this.productService.extensionSyncedKeys?.[extensionIdWithVersion.id.toLowerCase()];184const extensionKeysForSyncFromStorageValue = this.storageService.get(ExtensionStorageService.toKey(extensionIdWithVersion), StorageScope.PROFILE);185const extensionKeysForSyncFromStorage = extensionKeysForSyncFromStorageValue ? JSON.parse(extensionKeysForSyncFromStorageValue) : undefined;186187return extensionKeysForSyncFromStorage && extensionKeysForSyncFromProduct188? distinct([...extensionKeysForSyncFromStorage, ...extensionKeysForSyncFromProduct])189: (extensionKeysForSyncFromStorage || extensionKeysForSyncFromProduct);190}191192addToMigrationList(from: string, to: string): void {193if (from !== to) {194// remove the duplicates195const migrationList: [string, string][] = this.migrationList.filter(entry => !entry.includes(from) && !entry.includes(to));196migrationList.push([from, to]);197this.migrationList = migrationList;198}199}200201getSourceExtensionToMigrate(toExtensionId: string): string | undefined {202const entry = this.migrationList.find(([, to]) => toExtensionId === to);203return entry ? entry[0] : undefined;204}205206private get migrationList(): [string, string][] {207const value = this.storageService.get('extensionStorage.migrationList', StorageScope.APPLICATION, '[]');208try {209const migrationList = JSON.parse(value);210if (Array.isArray(migrationList)) {211return migrationList;212}213} catch (error) { /* ignore */ }214return [];215}216217private set migrationList(migrationList: [string, string][]) {218if (migrationList.length) {219this.storageService.store('extensionStorage.migrationList', JSON.stringify(migrationList), StorageScope.APPLICATION, StorageTarget.MACHINE);220} else {221this.storageService.remove('extensionStorage.migrationList', StorageScope.APPLICATION);222}223}224225}226227228