From e010704c68f5768a95384aadea057fc86160de3e Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 7 Apr 2021 14:09:40 -0700 Subject: [PATCH 1/7] Add command to clear extension related storage --- package.json | 5 +++ package.nls.json | 1 + src/client/common/constants.ts | 1 + src/client/common/persistentState.ts | 53 +++++++++++++++++++++++++++- src/client/common/serviceRegistry.ts | 1 + src/client/common/types.ts | 1 + 6 files changed, 61 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index e1ba8c732f09..2df5848d2e99 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,11 @@ } ], "commands": [ + { + "command": "python.clearPersistentStorage", + "title": "%python.command.python.clearPersistentStorage.title%", + "category": "Python" + }, { "command": "python.enableSourceMapSupport", "title": "%python.command.python.enableSourceMapSupport.title%", diff --git a/package.nls.json b/package.nls.json index 48c92f0673e4..64246336187b 100644 --- a/package.nls.json +++ b/package.nls.json @@ -34,6 +34,7 @@ "python.command.python.enableLinting.title": "Enable/Disable Linting", "python.command.python.runLinting.title": "Run Linting", "python.command.python.enableSourceMapSupport.title": "Enable Source Map Support For Extension Debugging", + "python.command.python.clearPersistentStorage.title": "Clear internal extension cache", "python.command.python.startPage.open.title": "Open Start Page", "python.command.python.analysis.clearCache.title": "Clear Module Analysis Cache", "python.command.python.analysis.restartLanguageServer.title": "Restart Language Server", diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index 581decb351aa..f08b5b97f9dc 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -63,6 +63,7 @@ export namespace Commands { export const SwitchToInsidersWeekly = 'python.switchToWeeklyChannel'; export const PickLocalProcess = 'python.pickLocalProcess'; export const GetSelectedInterpreterPath = 'python.interpreterPath'; + export const ClearStorage = 'python.clearPersistentStorage'; export const ClearWorkspaceInterpreter = 'python.clearWorkspaceInterpreter'; export const ResetInterpreterSecurityStorage = 'python.resetInterpreterSecurityStorage'; export const OpenStartPage = 'python.startPage.open'; diff --git a/src/client/common/persistentState.ts b/src/client/common/persistentState.ts index 49bf31d6beb8..7452ad5f8fc0 100644 --- a/src/client/common/persistentState.ts +++ b/src/client/common/persistentState.ts @@ -5,6 +5,9 @@ import { inject, injectable, named } from 'inversify'; import { Memento } from 'vscode'; +import { IExtensionSingleActivationService } from '../activation/types'; +import { ICommandManager } from './application/types'; +import { Commands } from './constants'; import { GLOBAL_MEMENTO, IExtensionContext, @@ -44,26 +47,72 @@ export class PersistentState implements IPersistentState { } } +const GLOBAL_PERSISTENT_KEYS = 'zzzzPYTHON_EXTENSION_GLOBAL_STORAGE_KEYS'; +const WORKSPACE_PERSISTENT_KEYS = 'zzzzPYTHON_EXTENSION_WORKSPACE_STORAGE_KEYS'; +type keysStorage = { key: string; defaultValue: unknown }; + @injectable() -export class PersistentStateFactory implements IPersistentStateFactory { +export class PersistentStateFactory implements IPersistentStateFactory, IExtensionSingleActivationService { + private readonly globalKeysStorage = new PersistentState( + this.globalState, + GLOBAL_PERSISTENT_KEYS, + [], + ); + private readonly workspaceKeysStorage = new PersistentState( + this.workspaceState, + WORKSPACE_PERSISTENT_KEYS, + [], + ); constructor( @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, @inject(IMemento) @named(WORKSPACE_MEMENTO) private workspaceState: Memento, + @inject(ICommandManager) private cmdManager: ICommandManager, ) {} + + public async activate(): Promise { + this.cmdManager.registerCommand(Commands.ClearStorage, this.cleanAllPersistentStates.bind(this)); + } + public createGlobalPersistentState( key: string, defaultValue?: T, expiryDurationMs?: number, ): IPersistentState { + if (!this.globalKeysStorage.value.includes({ key, defaultValue })) { + this.globalKeysStorage.updateValue([{ key, defaultValue }, ...this.globalKeysStorage.value]).ignoreErrors(); + } return new PersistentState(this.globalState, key, defaultValue, expiryDurationMs); } + public createWorkspacePersistentState( key: string, defaultValue?: T, expiryDurationMs?: number, ): IPersistentState { + if (!this.workspaceKeysStorage.value.includes({ key, defaultValue })) { + this.workspaceKeysStorage + .updateValue([{ key, defaultValue }, ...this.workspaceKeysStorage.value]) + .ignoreErrors(); + } return new PersistentState(this.workspaceState, key, defaultValue, expiryDurationMs); } + + public async cleanAllPersistentStates(): Promise { + await Promise.all( + this.globalKeysStorage.value.map(async (keyContent) => { + const storage = this.createGlobalPersistentState(keyContent.key); + await storage.updateValue(keyContent.defaultValue); + }), + ); + await Promise.all( + this.workspaceKeysStorage.value.map(async (keyContent) => { + const storage = this.createWorkspacePersistentState(keyContent.key); + await storage.updateValue(keyContent.defaultValue); + }), + ); + await this.globalKeysStorage.updateValue([]); + await this.workspaceKeysStorage.updateValue([]); + } } ///////////////////////////// @@ -79,6 +128,8 @@ interface IPersistentStorage { * Build a global storage object for the given key. */ export function getGlobalStorage(context: IExtensionContext, key: string): IPersistentStorage { + const globalKeysStorage = new PersistentState(context.globalState, GLOBAL_PERSISTENT_KEYS, []); + globalKeysStorage.updateValue([{ key, defaultValue: undefined }, ...globalKeysStorage.value]).ignoreErrors(); const raw = new PersistentState(context.globalState, key); return { // We adapt between PersistentState and IPersistentStorage. diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index ac023f46da33..3a8406e25d7b 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -131,6 +131,7 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IExtensions, Extensions); serviceManager.addSingleton(IRandom, Random); serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); + serviceManager.addBinding(IPersistentStateFactory, IExtensionSingleActivationService); serviceManager.addSingleton(ITerminalServiceFactory, TerminalServiceFactory); serviceManager.addSingleton(IPathUtils, PathUtils); serviceManager.addSingleton(IApplicationShell, ApplicationShell); diff --git a/src/client/common/types.ts b/src/client/common/types.ts index f9cf4e09db3d..ad5d69b738cf 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -51,6 +51,7 @@ export const IPersistentStateFactory = Symbol('IPersistentStateFactory'); export interface IPersistentStateFactory { createGlobalPersistentState(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState; createWorkspacePersistentState(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState; + cleanAllPersistentStates(): Promise; } export type ExecutionInfo = { From 4a0adf670d0752068a2a46debcab82d72b9c98af Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 7 Apr 2021 15:05:35 -0700 Subject: [PATCH 2/7] News entry --- news/1 Enhancements/15883.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/1 Enhancements/15883.md diff --git a/news/1 Enhancements/15883.md b/news/1 Enhancements/15883.md new file mode 100644 index 000000000000..5186003c9487 --- /dev/null +++ b/news/1 Enhancements/15883.md @@ -0,0 +1 @@ +Added command `Python: Clear internal extension cache` to clear extension related cache. From bf6025b873305c62f08f0d8b025e6cbc2f4ae57b Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 7 Apr 2021 15:17:53 -0700 Subject: [PATCH 3/7] Clean up --- src/client/common/persistentState.ts | 10 ++++++---- src/client/common/types.ts | 1 - 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/client/common/persistentState.ts b/src/client/common/persistentState.ts index 7452ad5f8fc0..f34e4ce873fa 100644 --- a/src/client/common/persistentState.ts +++ b/src/client/common/persistentState.ts @@ -47,8 +47,8 @@ export class PersistentState implements IPersistentState { } } -const GLOBAL_PERSISTENT_KEYS = 'zzzzPYTHON_EXTENSION_GLOBAL_STORAGE_KEYS'; -const WORKSPACE_PERSISTENT_KEYS = 'zzzzPYTHON_EXTENSION_WORKSPACE_STORAGE_KEYS'; +const GLOBAL_PERSISTENT_KEYS = 'PYTHON_EXTENSION_GLOBAL_STORAGE_KEYS'; +const WORKSPACE_PERSISTENT_KEYS = 'PYTHON_EXTENSION_WORKSPACE_STORAGE_KEYS'; type keysStorage = { key: string; defaultValue: unknown }; @injectable() @@ -97,7 +97,7 @@ export class PersistentStateFactory implements IPersistentStateFactory, IExtensi return new PersistentState(this.workspaceState, key, defaultValue, expiryDurationMs); } - public async cleanAllPersistentStates(): Promise { + private async cleanAllPersistentStates(): Promise { await Promise.all( this.globalKeysStorage.value.map(async (keyContent) => { const storage = this.createGlobalPersistentState(keyContent.key); @@ -129,7 +129,9 @@ interface IPersistentStorage { */ export function getGlobalStorage(context: IExtensionContext, key: string): IPersistentStorage { const globalKeysStorage = new PersistentState(context.globalState, GLOBAL_PERSISTENT_KEYS, []); - globalKeysStorage.updateValue([{ key, defaultValue: undefined }, ...globalKeysStorage.value]).ignoreErrors(); + if (!globalKeysStorage.value.includes({ key, defaultValue: undefined })) { + globalKeysStorage.updateValue([{ key, defaultValue: undefined }, ...globalKeysStorage.value]).ignoreErrors(); + } const raw = new PersistentState(context.globalState, key); return { // We adapt between PersistentState and IPersistentStorage. diff --git a/src/client/common/types.ts b/src/client/common/types.ts index ad5d69b738cf..f9cf4e09db3d 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -51,7 +51,6 @@ export const IPersistentStateFactory = Symbol('IPersistentStateFactory'); export interface IPersistentStateFactory { createGlobalPersistentState(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState; createWorkspacePersistentState(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState; - cleanAllPersistentStates(): Promise; } export type ExecutionInfo = { From c9611be03266b4d05578fbbb8363af133a942022 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 7 Apr 2021 15:20:39 -0700 Subject: [PATCH 4/7] Fix linting tests --- src/test/linters/lint.provider.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/test/linters/lint.provider.test.ts b/src/test/linters/lint.provider.test.ts index 432b9290edf5..62782c492f49 100644 --- a/src/test/linters/lint.provider.test.ts +++ b/src/test/linters/lint.provider.test.ts @@ -7,7 +7,12 @@ import { Container } from 'inversify'; import * as TypeMoq from 'typemoq'; import * as vscode from 'vscode'; import { LanguageServerType } from '../../client/activation/types'; -import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import { + IApplicationShell, + ICommandManager, + IDocumentManager, + IWorkspaceService, +} from '../../client/common/application/types'; import { PersistentStateFactory } from '../../client/common/persistentState'; import { IFileSystem } from '../../client/common/platform/types'; import { @@ -109,6 +114,7 @@ suite('Linting - Provider', () => { serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); serviceManager.addSingleton(IMemento, MockMemento, GLOBAL_MEMENTO); serviceManager.addSingleton(IMemento, MockMemento, WORKSPACE_MEMENTO); + serviceManager.addSingletonInstance(ICommandManager, TypeMoq.Mock.ofType().object); lm = new LinterManager(serviceContainer, workspaceService.object); serviceManager.addSingletonInstance(ILinterManager, lm); emitter = new vscode.EventEmitter(); From 46ca571e08e8d9ed830b39ab43c22499769dd927 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 7 Apr 2021 16:28:46 -0700 Subject: [PATCH 5/7] Fix linting --- src/test/linters/lint.provider.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/linters/lint.provider.test.ts b/src/test/linters/lint.provider.test.ts index 62782c492f49..7600f59d6c9e 100644 --- a/src/test/linters/lint.provider.test.ts +++ b/src/test/linters/lint.provider.test.ts @@ -114,7 +114,10 @@ suite('Linting - Provider', () => { serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); serviceManager.addSingleton(IMemento, MockMemento, GLOBAL_MEMENTO); serviceManager.addSingleton(IMemento, MockMemento, WORKSPACE_MEMENTO); - serviceManager.addSingletonInstance(ICommandManager, TypeMoq.Mock.ofType().object); + serviceManager.addSingletonInstance( + ICommandManager, + TypeMoq.Mock.ofType().object, + ); lm = new LinterManager(serviceContainer, workspaceService.object); serviceManager.addSingletonInstance(ILinterManager, lm); emitter = new vscode.EventEmitter(); From 518a517410be89007fd9bbeae181ecd99368ef88 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 7 Apr 2021 17:32:30 -0700 Subject: [PATCH 6/7] Add tests --- src/test/common/persistentState.unit.test.ts | 90 ++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/test/common/persistentState.unit.test.ts diff --git a/src/test/common/persistentState.unit.test.ts b/src/test/common/persistentState.unit.test.ts new file mode 100644 index 000000000000..a4cb10d19a4d --- /dev/null +++ b/src/test/common/persistentState.unit.test.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Memento } from 'vscode'; +import { IExtensionSingleActivationService } from '../../client/activation/types'; +import { ICommandManager } from '../../client/common/application/types'; +import { Commands } from '../../client/common/constants'; +import { PersistentStateFactory } from '../../client/common/persistentState'; +import { IDisposable, IPersistentStateFactory } from '../../client/common/types'; +import { MockMemento } from '../mocks/mementos'; + +suite('Persistent State', () => { + let cmdManager: TypeMoq.IMock; + let persistentStateFactory: IPersistentStateFactory & IExtensionSingleActivationService; + let workspaceMemento: Memento; + let globalMemento: Memento; + setup(() => { + cmdManager = TypeMoq.Mock.ofType(); + workspaceMemento = new MockMemento(); + globalMemento = new MockMemento(); + persistentStateFactory = new PersistentStateFactory(globalMemento, workspaceMemento, cmdManager.object); + }); + + test('Global states created are restored on invoking clean storage command', async () => { + let clearStorageCommand: (() => Promise) | undefined; + cmdManager + .setup((c) => c.registerCommand(Commands.ClearStorage, TypeMoq.It.isAny())) + .callback((_, c) => { + clearStorageCommand = c; + }) + .returns(() => TypeMoq.Mock.ofType().object); + + // Register command to clean storage + await persistentStateFactory.activate(); + + expect(clearStorageCommand).to.not.equal(undefined, 'Callback not registered'); + + const globalKey1State = persistentStateFactory.createGlobalPersistentState('key1', 'defaultKey1Value'); + await globalKey1State.updateValue('key1Value'); + const globalKey2State = persistentStateFactory.createGlobalPersistentState( + 'key2', + undefined, + ); + await globalKey2State.updateValue('key2Value'); + + // Verify states are updated correctly + expect(globalKey1State.value).to.equal('key1Value'); + expect(globalKey2State.value).to.equal('key2Value'); + + await clearStorageCommand!(); // Invoke command + + // Verify states are now reset to their default value. + expect(globalKey1State.value).to.equal('defaultKey1Value'); + expect(globalKey2State.value).to.equal(undefined); + }); + + test('Workspace states created are restored on invoking clean storage command', async () => { + let clearStorageCommand: (() => Promise) | undefined; + cmdManager + .setup((c) => c.registerCommand(Commands.ClearStorage, TypeMoq.It.isAny())) + .callback((_, c) => { + clearStorageCommand = c; + }) + .returns(() => TypeMoq.Mock.ofType().object); + + // Register command to clean storage + await persistentStateFactory.activate(); + + expect(clearStorageCommand).to.not.equal(undefined, 'Callback not registered'); + + const workspaceKey1State = persistentStateFactory.createWorkspacePersistentState('key1'); + await workspaceKey1State.updateValue('key1Value'); + const workspaceKey2State = persistentStateFactory.createWorkspacePersistentState('key2', 'defaultKey2Value'); + await workspaceKey2State.updateValue('key2Value'); + + // Verify states are updated correctly + expect(workspaceKey1State.value).to.equal('key1Value'); + expect(workspaceKey2State.value).to.equal('key2Value'); + + await clearStorageCommand!(); // Invoke command + + // Verify states are now reset to their default value. + expect(workspaceKey1State.value).to.equal(undefined); + expect(workspaceKey2State.value).to.equal('defaultKey2Value'); + }); +}); From 73b64e983e17847bf949de4d483c1631eb38812a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 7 Apr 2021 23:37:29 -0700 Subject: [PATCH 7/7] Capitalize --- package.nls.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.nls.json b/package.nls.json index 64246336187b..ba01e6ad5e80 100644 --- a/package.nls.json +++ b/package.nls.json @@ -34,7 +34,7 @@ "python.command.python.enableLinting.title": "Enable/Disable Linting", "python.command.python.runLinting.title": "Run Linting", "python.command.python.enableSourceMapSupport.title": "Enable Source Map Support For Extension Debugging", - "python.command.python.clearPersistentStorage.title": "Clear internal extension cache", + "python.command.python.clearPersistentStorage.title": "Clear Internal Extension Cache", "python.command.python.startPage.open.title": "Open Start Page", "python.command.python.analysis.clearCache.title": "Clear Module Analysis Cache", "python.command.python.analysis.restartLanguageServer.title": "Restart Language Server",