diff --git a/news/1 Enhancements/18031.md b/news/1 Enhancements/18031.md new file mode 100644 index 000000000000..23bd3e202378 --- /dev/null +++ b/news/1 Enhancements/18031.md @@ -0,0 +1 @@ +Declare limited support for untrusted workspaces by only supporting Pylance. diff --git a/package.json b/package.json index 2698e216d7e2..a12c3cf6de1c 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ }, "capabilities": { "untrustedWorkspaces": { - "supported": false + "supported": "limited", + "description": "Only Partial IntelliSense with Pylance is supported. Cannot execute Python with untrusted files." }, "virtualWorkspaces": { "supported": "limited", diff --git a/package.nls.json b/package.nls.json index 1e980bdaaa9c..50d448a4fe07 100644 --- a/package.nls.json +++ b/package.nls.json @@ -171,6 +171,7 @@ "LanguageService.lsFailedToStart": "We encountered an issue starting the language server. Reverting to Jedi language engine. Check the Python output panel for details.", "LanguageService.lsFailedToDownload": "We encountered an issue downloading the language server. Reverting to Jedi language engine. Check the Python output panel for details.", "LanguageService.lsFailedToExtract": "We encountered an issue extracting the language server. Reverting to Jedi language engine. Check the Python output panel for details.", + "LanguageService.untrustedWorkspaceMessage": "Only Pylance is supported in untrusted workspaces, setting language server to None.", "LanguageService.downloadFailedOutputMessage": "Language server download failed", "LanguageService.extractionFailedOutputMessage": "Language server extraction failed", "LanguageService.extractionCompletedOutputMessage": "Language server download complete", diff --git a/src/client/activation/activationManager.ts b/src/client/activation/activationManager.ts index 20f51d33d57f..bfd9352f8183 100644 --- a/src/client/activation/activationManager.ts +++ b/src/client/activation/activationManager.ts @@ -40,6 +40,14 @@ export class ExtensionActivationManager implements IExtensionActivationManager { @inject(IExperimentService) private readonly experiments: IExperimentService, @inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService, ) { + if (!this.workspaceService.isTrusted) { + this.activationServices = this.activationServices.filter( + (service) => service.supportedWorkspaceTypes.untrustedWorkspace, + ); + this.singleActivationServices = this.singleActivationServices.filter( + (service) => service.supportedWorkspaceTypes.untrustedWorkspace, + ); + } if (this.workspaceService.isVirtualWorkspace) { this.activationServices = this.activationServices.filter( (service) => service.supportedWorkspaceTypes.virtualWorkspace, @@ -80,13 +88,14 @@ export class ExtensionActivationManager implements IExtensionActivationManager { } this.activatedWorkspaces.add(key); - if (this.experiments.inExperimentSync(DeprecatePythonPath.experiment)) { - await this.interpreterPathService.copyOldInterpreterStorageValuesToNew(resource); + if (this.workspaceService.isTrusted) { + // Do not interact with interpreters in a untrusted workspace. + if (this.experiments.inExperimentSync(DeprecatePythonPath.experiment)) { + await this.interpreterPathService.copyOldInterpreterStorageValuesToNew(resource); + } + await this.autoSelection.autoSelectInterpreter(resource); } - await sendActivationTelemetry(this.fileSystem, this.workspaceService, resource); - - await this.autoSelection.autoSelectInterpreter(resource); await Promise.all(this.activationServices.map((item) => item.activate(resource))); await this.appDiagnostics.performPreStartupHealthCheck(resource); } diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts index d8189412c90b..c04b257091bd 100644 --- a/src/client/activation/activationService.ts +++ b/src/client/activation/activationService.ts @@ -57,7 +57,7 @@ export class LanguageServerExtensionActivationService private readonly output: OutputChannel; - private readonly interpreterService: IInterpreterService; + private readonly interpreterService?: IInterpreterService; private readonly languageServerChangeHandler: LanguageServerChangeHandler; @@ -69,14 +69,16 @@ export class LanguageServerExtensionActivationService ) { this.workspaceService = this.serviceContainer.get(IWorkspaceService); this.configurationService = this.serviceContainer.get(IConfigurationService); - this.interpreterService = this.serviceContainer.get(IInterpreterService); this.output = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); const disposables = serviceContainer.get(IDisposableRegistry); disposables.push(this); disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); disposables.push(this.workspaceService.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this)); - disposables.push(this.interpreterService.onDidChangeInterpreter(this.onDidChangeInterpreter.bind(this))); + if (this.workspaceService.isTrusted) { + this.interpreterService = this.serviceContainer.get(IInterpreterService); + disposables.push(this.interpreterService.onDidChangeInterpreter(this.onDidChangeInterpreter.bind(this))); + } this.languageServerChangeHandler = new LanguageServerChangeHandler( this.getCurrentLanguageServerType(), @@ -93,7 +95,7 @@ export class LanguageServerExtensionActivationService const stopWatch = new StopWatch(); // Get a new server and dispose of the old one (might be the same one) this.resource = resource; - const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const interpreter = await this.interpreterService?.getActiveInterpreter(resource); const key = await this.getKey(resource, interpreter); // If we have an old server with a different key, then deactivate it as the @@ -235,6 +237,14 @@ export class LanguageServerExtensionActivationService } } + if ( + !this.workspaceService.isTrusted && + serverType !== LanguageServerType.Node && + serverType !== LanguageServerType.None + ) { + this.output.appendLine(LanguageService.untrustedWorkspaceMessage()); + serverType = LanguageServerType.None; + } this.sendTelemetryForChosenLanguageServer(serverType).ignoreErrors(); await this.logStartup(serverType); let server = this.serviceContainer.get(ILanguageServerActivator, serverType); @@ -305,7 +315,7 @@ export class LanguageServerExtensionActivationService resource, workspacePathNameForGlobalWorkspaces, ); - interpreter = interpreter || (await this.interpreterService.getActiveInterpreter(resource)); + interpreter = interpreter || (await this.interpreterService?.getActiveInterpreter(resource)); const interperterPortion = interpreter ? `${interpreter.path}-${interpreter.envName}` : ''; return `${resourcePortion}-${interperterPortion}`; } diff --git a/src/client/activation/node/analysisOptions.ts b/src/client/activation/node/analysisOptions.ts index 2bde9f9cafbe..077665b7b2c0 100644 --- a/src/client/activation/node/analysisOptions.ts +++ b/src/client/activation/node/analysisOptions.ts @@ -18,6 +18,7 @@ export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOpt protected async getInitializationOptions() { return { experimentationSupport: true, + trustedWorkspaceSupport: true, }; } } diff --git a/src/client/activation/node/languageServerProxy.ts b/src/client/activation/node/languageServerProxy.ts index 37f5a681cd04..005c2719a1ec 100644 --- a/src/client/activation/node/languageServerProxy.ts +++ b/src/client/activation/node/languageServerProxy.ts @@ -23,6 +23,7 @@ import { FileBasedCancellationStrategy } from '../common/cancellationUtils'; import { ProgressReporting } from '../progress'; import { ILanguageClientFactory, ILanguageServerFolderService, ILanguageServerProxy } from '../types'; import { traceDecoratorError, traceDecoratorVerbose, traceError } from '../../logging'; +import { IWorkspaceService } from '../../common/application/types'; namespace InExperiment { export const Method = 'python/inExperiment'; @@ -64,6 +65,7 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { @inject(IExperimentService) private readonly experimentService: IExperimentService, @inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService, @inject(IEnvironmentVariablesProvider) private readonly environmentService: IEnvironmentVariablesProvider, + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, ) { this.startupCompleted = createDeferred(); } @@ -127,6 +129,14 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { } }); + this.disposables.push( + this.workspace.onDidGrantWorkspaceTrust(() => { + this.languageClient!.onReady().then(() => { + this.languageClient!.sendNotification('python/workspaceTrusted', { isTrusted: true }); + }); + }), + ); + this.disposables.push(this.languageClient.start()); await this.serverReady(); @@ -224,5 +234,13 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy { return { value }; }, ); + + this.disposables.push( + this.languageClient!.onRequest('python/isTrustedWorkspace', async () => { + return { + isTrusted: this.workspace.isTrusted, + }; + }), + ); } } diff --git a/src/client/application/diagnostics/applicationDiagnostics.ts b/src/client/application/diagnostics/applicationDiagnostics.ts index 2081f55c2f19..c4a0a9a36c75 100644 --- a/src/client/application/diagnostics/applicationDiagnostics.ts +++ b/src/client/application/diagnostics/applicationDiagnostics.ts @@ -5,6 +5,7 @@ import { inject, injectable, named } from 'inversify'; import { DiagnosticSeverity } from 'vscode'; +import { IWorkspaceService } from '../../common/application/types'; import { isTestExecution, STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; import { IOutputChannel, Resource } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; @@ -26,7 +27,11 @@ export class ApplicationDiagnostics implements IApplicationDiagnostics { if (isTestExecution()) { return; } - const services = this.serviceContainer.getAll(IDiagnosticsService); + let services = this.serviceContainer.getAll(IDiagnosticsService); + const workspaceService = this.serviceContainer.get(IWorkspaceService); + if (!workspaceService.isTrusted) { + services = services.filter((item) => item.runInUntrustedWorkspace); + } // Perform these validation checks in the foreground. await this.runDiagnostics( services.filter((item) => !item.runInBackground), diff --git a/src/client/application/diagnostics/base.ts b/src/client/application/diagnostics/base.ts index 34bcca90a779..abc70e210e1f 100644 --- a/src/client/application/diagnostics/base.ts +++ b/src/client/application/diagnostics/base.ts @@ -35,6 +35,7 @@ export abstract class BaseDiagnosticsService implements IDiagnosticsService, IDi @unmanaged() protected serviceContainer: IServiceContainer, @unmanaged() disposableRegistry: IDisposableRegistry, @unmanaged() public readonly runInBackground: boolean = false, + @unmanaged() public readonly runInUntrustedWorkspace: boolean = false, ) { this.filterService = serviceContainer.get(IDiagnosticFilterService); disposableRegistry.push(this); diff --git a/src/client/application/diagnostics/checks/envPathVariable.ts b/src/client/application/diagnostics/checks/envPathVariable.ts index 840061a54083..59a9854a92ca 100644 --- a/src/client/application/diagnostics/checks/envPathVariable.ts +++ b/src/client/application/diagnostics/checks/envPathVariable.ts @@ -43,7 +43,13 @@ export class EnvironmentPathVariableDiagnosticsService extends BaseDiagnosticsSe @inject(IServiceContainer) serviceContainer: IServiceContainer, @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, ) { - super([DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic], serviceContainer, disposableRegistry, true); + super( + [DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic], + serviceContainer, + disposableRegistry, + true, + true, + ); this.platform = this.serviceContainer.get(IPlatformService); this.messageService = serviceContainer.get>( IDiagnosticHandlerService, diff --git a/src/client/application/diagnostics/checks/pylanceDefault.ts b/src/client/application/diagnostics/checks/pylanceDefault.ts index 3ffb11830adc..eec3082f8165 100644 --- a/src/client/application/diagnostics/checks/pylanceDefault.ts +++ b/src/client/application/diagnostics/checks/pylanceDefault.ts @@ -40,7 +40,7 @@ export class PylanceDefaultDiagnosticService extends BaseDiagnosticsService { protected readonly messageService: IDiagnosticHandlerService, @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, ) { - super([DiagnosticCodes.PylanceDefaultDiagnostic], serviceContainer, disposableRegistry, true); + super([DiagnosticCodes.PylanceDefaultDiagnostic], serviceContainer, disposableRegistry, true, true); this.initialMementoValue = this.context.globalState.get(EXTENSION_VERSION_MEMENTO); } diff --git a/src/client/application/diagnostics/checks/switchToDefaultLS.ts b/src/client/application/diagnostics/checks/switchToDefaultLS.ts index e17cc75e3a08..76830ea6fdf7 100644 --- a/src/client/application/diagnostics/checks/switchToDefaultLS.ts +++ b/src/client/application/diagnostics/checks/switchToDefaultLS.ts @@ -38,7 +38,7 @@ export class SwitchToDefaultLanguageServerDiagnosticService extends BaseDiagnost protected readonly messageService: IDiagnosticHandlerService, @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, ) { - super([DiagnosticCodes.JediPython27NotSupportedDiagnostic], serviceContainer, disposableRegistry, true); + super([DiagnosticCodes.JediPython27NotSupportedDiagnostic], serviceContainer, disposableRegistry, true, true); } public diagnose(resource: Resource): Promise { diff --git a/src/client/application/diagnostics/checks/upgradeCodeRunner.ts b/src/client/application/diagnostics/checks/upgradeCodeRunner.ts index 8a01dfedb4f3..89a57f189746 100644 --- a/src/client/application/diagnostics/checks/upgradeCodeRunner.ts +++ b/src/client/application/diagnostics/checks/upgradeCodeRunner.ts @@ -43,7 +43,7 @@ export class UpgradeCodeRunnerDiagnosticService extends BaseDiagnosticsService { @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, @inject(IExtensions) private readonly extensions: IExtensions, ) { - super([DiagnosticCodes.UpgradeCodeRunnerDiagnostic], serviceContainer, disposableRegistry, true); + super([DiagnosticCodes.UpgradeCodeRunnerDiagnostic], serviceContainer, disposableRegistry, true, true); this.workspaceService = this.serviceContainer.get(IWorkspaceService); } diff --git a/src/client/application/diagnostics/types.ts b/src/client/application/diagnostics/types.ts index 91195ae780b4..343ba0f02cd3 100644 --- a/src/client/application/diagnostics/types.ts +++ b/src/client/application/diagnostics/types.ts @@ -27,6 +27,7 @@ export const IDiagnosticsService = Symbol('IDiagnosticsService'); export interface IDiagnosticsService { readonly runInBackground: boolean; + readonly runInUntrustedWorkspace: boolean; diagnose(resource: Resource): Promise; canHandle(diagnostic: IDiagnostic): Promise; handle(diagnostics: IDiagnostic[]): Promise; diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index ec5c7dcf5cab..ae62d880e18d 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -675,6 +675,16 @@ export interface IWorkspaceService { */ readonly rootPath: string | undefined; + /** + * When true, the user has explicitly trusted the contents of the workspace. + */ + readonly isTrusted: boolean; + + /** + * Event that fires when the current workspace has been trusted. + */ + readonly onDidGrantWorkspaceTrust: Event; + /** * List of workspace folders or `undefined` when no folder is open. * *Note* that the first entry corresponds to the value of `rootPath`. diff --git a/src/client/common/application/workspace.ts b/src/client/common/application/workspace.ts index 5ef4870a8808..c830401decfb 100644 --- a/src/client/common/application/workspace.ts +++ b/src/client/common/application/workspace.ts @@ -89,6 +89,14 @@ export class WorkspaceService implements IWorkspaceService { return !!isVirtualWorkspace; } + public get isTrusted(): boolean { + return workspace.isTrusted; + } + + public get onDidGrantWorkspaceTrust(): Event { + return workspace.onDidGrantWorkspaceTrust; + } + private get searchExcludes() { const searchExcludes = this.getConfiguration('search.exclude'); const enabledSearchExcludes = Object.keys(searchExcludes).filter((key) => searchExcludes.get(key) === true); diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 1b245a896f89..2885432a26d7 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -210,6 +210,10 @@ export namespace LanguageService { 'LanguageService.startingNone', 'Editor support is inactive since language server is set to None.', ); + export const untrustedWorkspaceMessage = localize( + 'LanguageService.untrustedWorkspaceMessage', + 'Only Pylance is supported in untrusted workspaces, setting language server to None.', + ); export const reloadAfterLanguageServerChange = localize( 'LanguageService.reloadAfterLanguageServerChange', diff --git a/src/client/extension.ts b/src/client/extension.ts index c04dfb3c3c98..c2c756d678d3 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -32,7 +32,7 @@ import { ProgressLocation, ProgressOptions, window } from 'vscode'; import { buildApi, IExtensionApi } from './api'; import { IApplicationShell, IWorkspaceService } from './common/application/types'; -import { IAsyncDisposableRegistry, IExperimentService, IExtensionContext } from './common/types'; +import { IAsyncDisposableRegistry, IDisposableRegistry, IExperimentService, IExtensionContext } from './common/types'; import { createDeferred } from './common/utils/async'; import { Common } from './common/utils/localize'; import { activateComponents } from './extensionActivation'; @@ -42,6 +42,7 @@ import { sendErrorTelemetry, sendStartupTelemetry } from './startupTelemetry'; import { IStartupDurations } from './types'; import { runAfterActivation } from './common/utils/runAfterActivation'; import { IInterpreterService } from './interpreter/contracts'; +import { WorkspaceService } from './common/application/workspace'; durations.codeLoadingTime = stopWatch.elapsedTime; @@ -59,6 +60,13 @@ export async function activate(context: IExtensionContext): Promise; let serviceContainer: IServiceContainer; try { + const workspaceService = new WorkspaceService(); + context.subscriptions.push( + workspaceService.onDidGrantWorkspaceTrust(async () => { + await deactivate(); + await activate(context); + }), + ); [api, ready, serviceContainer] = await activateUnsafe(context, stopWatch, durations); } catch (ex) { // We want to completely handle the error @@ -79,9 +87,13 @@ export function deactivate(): Thenable { // Make sure to shutdown anybody who needs it. if (activatedServiceContainer) { const registry = activatedServiceContainer.get(IAsyncDisposableRegistry); - if (registry) { - return registry.dispose(); - } + const disposables = activatedServiceContainer.get(IDisposableRegistry); + const promises = Promise.all(disposables.map((d) => d.dispose())); + return promises.then(() => { + if (registry) { + return registry.dispose(); + } + }); } return Promise.resolve(); @@ -133,11 +145,13 @@ async function activateUnsafe( setTimeout(async () => { if (activatedServiceContainer) { const workspaceService = activatedServiceContainer.get(IWorkspaceService); - const interpreterManager = activatedServiceContainer.get(IInterpreterService); - const workspaces = workspaceService.workspaceFolders ?? []; - await interpreterManager - .refresh(workspaces.length > 0 ? workspaces[0].uri : undefined) - .catch((ex) => traceError('Python Extension: interpreterManager.refresh', ex)); + if (workspaceService.isTrusted) { + const interpreterManager = activatedServiceContainer.get(IInterpreterService); + const workspaces = workspaceService.workspaceFolders ?? []; + await interpreterManager + .refresh(workspaces.length > 0 ? workspaces[0].uri : undefined) + .catch((ex) => traceError('Python Extension: interpreterManager.refresh', ex)); + } } runAfterActivation(); diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index fae6c717e5ff..74b6cc066c7b 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -47,6 +47,7 @@ import { getLoggingLevel } from './logging/settings'; import { DebugService } from './common/application/debugService'; import { DebugSessionEventDispatcher } from './debugger/extension/hooks/eventHandlerDispatcher'; import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; +import { WorkspaceService } from './common/application/workspace'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -70,6 +71,10 @@ export async function activateComponents( // https://github.com/microsoft/vscode-python/issues/15380 // These will go away eventually once everything is refactored into components. const legacyActivationResult = await activateLegacy(ext); + const workspaceService = new WorkspaceService(); + if (!workspaceService.isTrusted) { + return [legacyActivationResult]; + } const promises: Promise[] = [ // More component activations will go here pythonEnvironments.activate(components.pythonEnvs, ext), @@ -133,56 +138,64 @@ async function activateLegacy(ext: ExtensionState): Promise { const workspaceService = serviceContainer.get(IWorkspaceService); const cmdManager = serviceContainer.get(ICommandManager); languages.setLanguageConfiguration(PYTHON_LANGUAGE, getLanguageConfiguration()); - const interpreterManager = serviceContainer.get(IInterpreterService); - interpreterManager.initialize(); - if (!workspaceService.isVirtualWorkspace) { - const handlers = serviceManager.getAll(IDebugSessionEventHandlers); - const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); - dispatcher.registerEventHandlers(); + if (workspaceService.isTrusted) { + const interpreterManager = serviceContainer.get(IInterpreterService); + interpreterManager.initialize(); + if (!workspaceService.isVirtualWorkspace) { + const handlers = serviceManager.getAll(IDebugSessionEventHandlers); + const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); + dispatcher.registerEventHandlers(); - const outputChannel = serviceManager.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - disposables.push(cmdManager.registerCommand(Commands.ViewOutput, () => outputChannel.show())); - cmdManager.executeCommand('setContext', 'python.vscode.channel', applicationEnv.channel).then(noop, noop); + const outputChannel = serviceManager.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + disposables.push(cmdManager.registerCommand(Commands.ViewOutput, () => outputChannel.show())); + cmdManager.executeCommand('setContext', 'python.vscode.channel', applicationEnv.channel).then(noop, noop); - serviceContainer.get(IApplicationDiagnostics).register(); + serviceContainer.get(IApplicationDiagnostics).register(); - serviceManager.get(ITerminalAutoActivation).register(); - const pythonSettings = configuration.getSettings(); + serviceManager.get(ITerminalAutoActivation).register(); + const pythonSettings = configuration.getSettings(); - const sortImports = serviceContainer.get(ISortImportsEditingProvider); - sortImports.registerCommands(); + const sortImports = serviceContainer.get(ISortImportsEditingProvider); + sortImports.registerCommands(); - serviceManager.get(ICodeExecutionManager).registerCommands(); + serviceManager.get(ICodeExecutionManager).registerCommands(); - context.subscriptions.push(new LinterCommands(serviceManager)); + context.subscriptions.push(new LinterCommands(serviceManager)); - if (pythonSettings && pythonSettings.formatting && pythonSettings.formatting.provider !== 'internalConsole') { - const formatProvider = new PythonFormattingEditProvider(context, serviceContainer); - context.subscriptions.push(languages.registerDocumentFormattingEditProvider(PYTHON, formatProvider)); - context.subscriptions.push(languages.registerDocumentRangeFormattingEditProvider(PYTHON, formatProvider)); - } + if ( + pythonSettings && + pythonSettings.formatting && + pythonSettings.formatting.provider !== 'internalConsole' + ) { + const formatProvider = new PythonFormattingEditProvider(context, serviceContainer); + context.subscriptions.push(languages.registerDocumentFormattingEditProvider(PYTHON, formatProvider)); + context.subscriptions.push( + languages.registerDocumentRangeFormattingEditProvider(PYTHON, formatProvider), + ); + } - context.subscriptions.push(new ReplProvider(serviceContainer)); + context.subscriptions.push(new ReplProvider(serviceContainer)); - const terminalProvider = new TerminalProvider(serviceContainer); - terminalProvider.initialize(window.activeTerminal).ignoreErrors(); - context.subscriptions.push(terminalProvider); + const terminalProvider = new TerminalProvider(serviceContainer); + terminalProvider.initialize(window.activeTerminal).ignoreErrors(); + context.subscriptions.push(terminalProvider); - context.subscriptions.push( - languages.registerCodeActionsProvider(PYTHON, new PythonCodeActionProvider(), { - providedCodeActionKinds: [CodeActionKind.SourceOrganizeImports], - }), - ); + context.subscriptions.push( + languages.registerCodeActionsProvider(PYTHON, new PythonCodeActionProvider(), { + providedCodeActionKinds: [CodeActionKind.SourceOrganizeImports], + }), + ); - serviceContainer - .getAll(IDebugConfigurationService) - .forEach((debugConfigProvider) => { - context.subscriptions.push( - debug.registerDebugConfigurationProvider(DebuggerTypeName, debugConfigProvider), - ); - }); + serviceContainer + .getAll(IDebugConfigurationService) + .forEach((debugConfigProvider) => { + context.subscriptions.push( + debug.registerDebugConfigurationProvider(DebuggerTypeName, debugConfigProvider), + ); + }); - serviceContainer.get(IDebuggerBanner).initialize(); + serviceContainer.get(IDebuggerBanner).initialize(); + } } // "activate" everything else diff --git a/src/client/extensionInit.ts b/src/client/extensionInit.ts index b481325c69d0..1644fbdf1e99 100644 --- a/src/client/extensionInit.ts +++ b/src/client/extensionInit.ts @@ -57,10 +57,11 @@ export function initializeGlobals( context.subscriptions.push(registerLogger(new OutputChannelLogger(standardOutputChannel))); const workspaceService = new WorkspaceService(); - const unitTestOutChannel = workspaceService.isVirtualWorkspace - ? // Do not create any test related output UI when using virtual workspaces. - instance(mock()) - : window.createOutputChannel(OutputChannelNames.pythonTest()); + const unitTestOutChannel = + workspaceService.isVirtualWorkspace || !workspaceService.isTrusted + ? // Do not create any test related output UI when using virtual workspaces. + instance(mock()) + : window.createOutputChannel(OutputChannelNames.pythonTest()); serviceManager.addSingletonInstance(IOutputChannel, standardOutputChannel, STANDARD_OUTPUT_CHANNEL); serviceManager.addSingletonInstance(IOutputChannel, unitTestOutChannel, TEST_OUTPUT_CHANNEL); diff --git a/src/client/jupyter/jupyterIntegration.ts b/src/client/jupyter/jupyterIntegration.ts index 0851eefefdad..876fe5980cf8 100644 --- a/src/client/jupyter/jupyterIntegration.ts +++ b/src/client/jupyter/jupyterIntegration.ts @@ -9,6 +9,7 @@ import { dirname } from 'path'; import { CancellationToken, Disposable, Event, Extension, Memento, Uri } from 'vscode'; import * as lsp from 'vscode-languageserver-protocol'; import { ILanguageServerCache, ILanguageServerConnection } from '../activation/types'; +import { IWorkspaceService } from '../common/application/types'; import { JUPYTER_EXTENSION_ID } from '../common/constants'; import { InterpreterUri, ModuleInstallFlags } from '../common/installer/types'; import { @@ -180,9 +181,13 @@ export class JupyterExtensionIntegration { @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, @inject(IInterpreterDisplay) private interpreterDisplay: IInterpreterDisplay, @inject(IComponentAdapter) private pyenvs: IComponentAdapter, + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, ) {} public registerApi(jupyterExtensionApi: JupyterExtensionApi): JupyterExtensionApi | undefined { + if (!this.workspaceService.isTrusted) { + return undefined; + } // Forward python parts jupyterExtensionApi.registerPythonApi({ onDidChangeInterpreter: this.interpreterService.onDidChangeInterpreter, diff --git a/src/client/startupTelemetry.ts b/src/client/startupTelemetry.ts index 5390016253cc..b9dff5974492 100644 --- a/src/client/startupTelemetry.ts +++ b/src/client/startupTelemetry.ts @@ -95,11 +95,15 @@ async function getActivationTelemetryProps(serviceContainer: IServiceContainer): // TODO: If any one of these parts fails we send no info. We should // be able to partially populate as much as possible instead // (through granular try-catch statements). + const workspaceService = serviceContainer.get(IWorkspaceService); + const workspaceFolderCount = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders!.length : 0; const terminalHelper = serviceContainer.get(ITerminalHelper); const terminalShellType = terminalHelper.identifyTerminalShell(); + if (!workspaceService.isTrusted) { + return { workspaceFolderCount, terminal: terminalShellType }; + } const condaLocator = serviceContainer.get(ICondaService); const interpreterService = serviceContainer.get(IInterpreterService); - const workspaceService = serviceContainer.get(IWorkspaceService); const configurationService = serviceContainer.get(IConfigurationService); const mainWorkspaceUri = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders![0].uri @@ -112,7 +116,6 @@ async function getActivationTelemetryProps(serviceContainer: IServiceContainer): .catch(() => ''), interpreterService.hasInterpreters(async (item) => item.version?.major === 3), ]); - const workspaceFolderCount = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders!.length : 0; // If an unknown type environment can be found from windows registry or path env var, // consider them as global type instead of unknown. Such types can only be known after // windows registry is queried. So wait for the refresh of windows registry locator to diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index cabd2f4a1829..a8d40b98e902 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -639,15 +639,15 @@ export interface IEventNamePropertyMapping { /** * The conda version if selected */ - condaVersion: string | undefined; + condaVersion?: string | undefined; /** * The python interpreter version if selected */ - pythonVersion: string | undefined; + pythonVersion?: string | undefined; /** * The type of interpreter (conda, virtualenv, pipenv etc.) */ - interpreterType: EnvironmentType | undefined; + interpreterType?: EnvironmentType | undefined; /** * The type of terminal shell created: powershell, cmd, zsh, bash etc. * @@ -661,15 +661,15 @@ export interface IEventNamePropertyMapping { /** * If interpreters found for the main workspace contains a python3 interpreter */ - hasPython3: boolean; + hasPython3?: boolean; /** * If user has defined an interpreter in settings.json */ - usingUserDefinedInterpreter: boolean; + usingUserDefinedInterpreter?: boolean; /** * If global interpreter is being used */ - usingGlobalInterpreter: boolean; + usingGlobalInterpreter?: boolean; }; /** * Telemetry event sent when substituting Environment variables to calculate value of variables diff --git a/src/test/activation/activationManager.unit.test.ts b/src/test/activation/activationManager.unit.test.ts index 0f34208aed46..8b73247bfae4 100644 --- a/src/test/activation/activationManager.unit.test.ts +++ b/src/test/activation/activationManager.unit.test.ts @@ -60,11 +60,21 @@ suite('Activation Manager', () => { autoSelection = typemoq.Mock.ofType(); documentManager = typemoq.Mock.ofType(); activationService1 = mock(LanguageServerExtensionActivationService); + when(activationService1.supportedWorkspaceTypes).thenReturn({ + virtualWorkspace: true, + untrustedWorkspace: true, + }); activationService2 = mock(LanguageServerExtensionActivationService); + when(activationService2.supportedWorkspaceTypes).thenReturn({ + virtualWorkspace: true, + untrustedWorkspace: true, + }); fileSystem = mock(FileSystem); interpreterPathService .setup((i) => i.onDidChange(typemoq.It.isAny())) .returns(() => typemoq.Mock.ofType().object); + when(workspaceService.isTrusted).thenReturn(true); + when(workspaceService.isVirtualWorkspace).thenReturn(false); managerTest = new ExtensionActivationManagerTest( [instance(activationService1), instance(activationService2)], [], @@ -85,6 +95,114 @@ suite('Activation Manager', () => { sinon.restore(); }); + test('If running in a virtual workspace, do not activate services that do not support it', async () => { + when(workspaceService.isVirtualWorkspace).thenReturn(true); + when(activationService1.supportedWorkspaceTypes).thenReturn({ + virtualWorkspace: false, + untrustedWorkspace: true, + }); + when(activationService2.supportedWorkspaceTypes).thenReturn({ + virtualWorkspace: true, + untrustedWorkspace: true, + }); + const resource = Uri.parse('two'); + when(activationService1.activate(resource)).thenResolve(); + when(activationService2.activate(resource)).thenResolve(); + + autoSelection + .setup((a) => a.autoSelectInterpreter(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + appDiagnostics + .setup((a) => a.performPreStartupHealthCheck(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + managerTest = new ExtensionActivationManagerTest( + [instance(activationService1), instance(activationService2)], + [], + documentManager.object, + autoSelection.object, + appDiagnostics.object, + instance(workspaceService), + instance(fileSystem), + instance(activeResourceService), + instance(experiments), + interpreterPathService.object, + ); + await managerTest.activateWorkspace(resource); + + verify(activationService1.activate(resource)).never(); + verify(activationService2.activate(resource)).once(); + autoSelection.verifyAll(); + appDiagnostics.verifyAll(); + }); + + test('If running in a untrusted workspace, do not activate services that do not support it', async () => { + when(workspaceService.isTrusted).thenReturn(false); + when(activationService1.supportedWorkspaceTypes).thenReturn({ + virtualWorkspace: true, + untrustedWorkspace: false, + }); + when(activationService2.supportedWorkspaceTypes).thenReturn({ + virtualWorkspace: true, + untrustedWorkspace: true, + }); + const resource = Uri.parse('two'); + when(activationService1.activate(resource)).thenResolve(); + when(activationService2.activate(resource)).thenResolve(); + + autoSelection + .setup((a) => a.autoSelectInterpreter(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.never()); + appDiagnostics + .setup((a) => a.performPreStartupHealthCheck(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + managerTest = new ExtensionActivationManagerTest( + [instance(activationService1), instance(activationService2)], + [], + documentManager.object, + autoSelection.object, + appDiagnostics.object, + instance(workspaceService), + instance(fileSystem), + instance(activeResourceService), + instance(experiments), + interpreterPathService.object, + ); + await managerTest.activateWorkspace(resource); + + verify(activationService1.activate(resource)).never(); + verify(activationService2.activate(resource)).once(); + autoSelection.verifyAll(); + appDiagnostics.verifyAll(); + }); + + test('Otherwise activate all services filtering to the current resource', async () => { + const resource = Uri.parse('two'); + when(activationService1.activate(resource)).thenResolve(); + when(activationService2.activate(resource)).thenResolve(); + + autoSelection + .setup((a) => a.autoSelectInterpreter(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + appDiagnostics + .setup((a) => a.performPreStartupHealthCheck(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await managerTest.activateWorkspace(resource); + + verify(activationService1.activate(resource)).once(); + verify(activationService2.activate(resource)).once(); + autoSelection.verifyAll(); + appDiagnostics.verifyAll(); + }); + test('Initialize will add event handlers and will dispose them when running dispose', async () => { const disposable = typemoq.Mock.ofType(); const disposable2 = typemoq.Mock.ofType(); @@ -219,26 +337,6 @@ suite('Activation Manager', () => { verify(activationService2.activate(resource)).once(); }); - test('Function activateWorkspace() will be filtered to current resource', async () => { - const resource = Uri.parse('two'); - when(activationService1.activate(resource)).thenResolve(); - when(activationService2.activate(resource)).thenResolve(); - - autoSelection - .setup((a) => a.autoSelectInterpreter(resource)) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - appDiagnostics - .setup((a) => a.performPreStartupHealthCheck(resource)) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - - await managerTest.activateWorkspace(resource); - - verify(activationService1.activate(resource)).once(); - verify(activationService2.activate(resource)).once(); - }); - test('If in Deprecate PythonPath experiment, method activateWorkspace() will copy old interpreter storage values to new', async () => { const resource = Uri.parse('two'); when(activationService1.activate(resource)).thenResolve(); @@ -414,7 +512,17 @@ suite('Activation Manager', () => { interpreterPathService = typemoq.Mock.ofType(); documentManager = typemoq.Mock.ofType(); activationService1 = mock(LanguageServerExtensionActivationService); + when(activationService1.supportedWorkspaceTypes).thenReturn({ + virtualWorkspace: true, + untrustedWorkspace: true, + }); activationService2 = mock(LanguageServerExtensionActivationService); + when(activationService2.supportedWorkspaceTypes).thenReturn({ + virtualWorkspace: true, + untrustedWorkspace: true, + }); + when(workspaceService.isTrusted).thenReturn(true); + when(workspaceService.isVirtualWorkspace).thenReturn(false); fileSystem = mock(FileSystem); singleActivationService = typemoq.Mock.ofType(); initialize = sinon.stub(ExtensionActivationManager.prototype, 'initialize'); diff --git a/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts b/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts index e56cc0d1e072..a63abc2ff2ff 100644 --- a/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts +++ b/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts @@ -17,6 +17,7 @@ import { ISourceMapSupportService, } from '../../../client/application/diagnostics/types'; import { IApplicationDiagnostics } from '../../../client/application/types'; +import { IWorkspaceService } from '../../../client/common/application/types'; import { STANDARD_OUTPUT_CHANNEL } from '../../../client/common/constants'; import { IOutputChannel } from '../../../client/common/types'; import { createDeferred, createDeferredFromPromise } from '../../../client/common/utils/async'; @@ -30,6 +31,7 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { let lsNotSupportedCheck: typemoq.IMock; let pythonInterpreterCheck: typemoq.IMock; let outputChannel: typemoq.IMock; + let workspaceService: typemoq.IMock; let appDiagnostics: IApplicationDiagnostics; const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; @@ -44,7 +46,10 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { lsNotSupportedCheck.setup((service) => service.runInBackground).returns(() => false); pythonInterpreterCheck = typemoq.Mock.ofType(); pythonInterpreterCheck.setup((service) => service.runInBackground).returns(() => false); + pythonInterpreterCheck.setup((service) => service.runInUntrustedWorkspace).returns(() => false); outputChannel = typemoq.Mock.ofType(); + workspaceService = typemoq.Mock.ofType(); + workspaceService.setup((w) => w.isTrusted).returns(() => true); serviceContainer .setup((d) => d.getAll(typemoq.It.isValue(IDiagnosticsService))) @@ -52,6 +57,9 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { serviceContainer .setup((d) => d.get(typemoq.It.isValue(IOutputChannel), typemoq.It.isValue(STANDARD_OUTPUT_CHANNEL))) .returns(() => outputChannel.object); + serviceContainer + .setup((d) => d.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); appDiagnostics = new ApplicationDiagnostics(serviceContainer.object, outputChannel.object); }); @@ -95,6 +103,29 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { pythonInterpreterCheck.verifyAll(); }); + test('When running in a untrusted workspace skip diagnosing validation checks which do not support it', async () => { + workspaceService.reset(); + workspaceService.setup((w) => w.isTrusted).returns(() => false); + envHealthCheck + .setup((e) => e.diagnose(typemoq.It.isAny())) + .returns(() => Promise.resolve([])) + .verifiable(typemoq.Times.once()); + lsNotSupportedCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) + .returns(() => Promise.resolve([])) + .verifiable(typemoq.Times.once()); + pythonInterpreterCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) + .returns(() => Promise.resolve([])) + .verifiable(typemoq.Times.never()); + + await appDiagnostics.performPreStartupHealthCheck(undefined); + + envHealthCheck.verifyAll(); + lsNotSupportedCheck.verifyAll(); + pythonInterpreterCheck.verifyAll(); + }); + test('Performing Pre Startup Health Check must handles all validation checks only once either in background or foreground', async () => { const diagnostic: IDiagnostic = { code: 'Error' as any, @@ -215,9 +246,12 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { const foreGroundService = mock(InvalidPythonInterpreterService); const backGroundService = mock(EnvironmentPathVariableDiagnosticsService); const svcContainer = mock(ServiceContainer); + const workspaceService = mock(); const foreGroundDeferred = createDeferred(); const backgroundGroundDeferred = createDeferred(); + when(svcContainer.get(IWorkspaceService)).thenReturn(workspaceService); + when(workspaceService.isTrusted).thenReturn(true); when(svcContainer.getAll(IDiagnosticsService)).thenReturn([ instance(foreGroundService), instance(backGroundService), diff --git a/types/vscode.proposed.d.ts b/types/vscode.proposed.d.ts index 3a0ae806c476..6586e9375456 100644 --- a/types/vscode.proposed.d.ts +++ b/types/vscode.proposed.d.ts @@ -982,6 +982,18 @@ declare module 'vscode' { export const onDidChangeTestResults: Event; } + export namespace workspace { + /** + * When true, the user has explicitly trusted the contents of the workspace. + */ + export const isTrusted: boolean; + + /** + * Event that fires when the current workspace has been trusted. + */ + export const onDidGrantWorkspaceTrust: Event; + } + export interface TestObserver { /** * List of tests returned by test provider for files in the workspace.