From b68ddee506cb8330e12c27e9c6421e1e0430763a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 9 Nov 2023 16:44:14 -0800 Subject: [PATCH] Support deactivating virtual environments without user intervention (#22405) Closes https://github.com/microsoft/vscode-python/issues/22448 Adds deactivate script to `PATH` --- pythonFiles/deactivate.csh | 6 - pythonFiles/deactivate.fish | 36 --- pythonFiles/deactivate.ps1 | 31 -- pythonFiles/deactivate/bash/deactivate | 44 +++ pythonFiles/deactivate/fish/deactivate | 44 +++ .../deactivate/powershell/deactivate.ps1 | 11 + pythonFiles/{ => deactivate/zsh}/deactivate | 17 +- pythonFiles/printEnvVariablesToFile.py | 11 +- src/client/common/utils/async.ts | 34 +++ src/client/common/utils/localize.ts | 2 +- src/client/interpreter/activation/service.ts | 8 +- .../deactivatePrompt.ts | 201 ------------- .../deactivateScripts.ts | 108 ------- .../deactivateService.ts | 97 +++++++ .../envCollectionActivation/service.ts | 14 +- .../shellIntegrationService.ts | 8 +- src/client/terminals/serviceRegistry.ts | 8 +- src/client/terminals/types.ts | 6 + ...rminalEnvVarCollectionService.unit.test.ts | 77 ++++- .../deactivatePrompt.unit.test.ts | 271 ------------------ .../terminals/serviceRegistry.unit.test.ts | 5 +- 21 files changed, 359 insertions(+), 680 deletions(-) delete mode 100644 pythonFiles/deactivate.csh delete mode 100644 pythonFiles/deactivate.fish delete mode 100644 pythonFiles/deactivate.ps1 create mode 100755 pythonFiles/deactivate/bash/deactivate create mode 100755 pythonFiles/deactivate/fish/deactivate create mode 100644 pythonFiles/deactivate/powershell/deactivate.ps1 rename pythonFiles/{ => deactivate/zsh}/deactivate (61%) mode change 100644 => 100755 delete mode 100644 src/client/terminals/envCollectionActivation/deactivatePrompt.ts delete mode 100644 src/client/terminals/envCollectionActivation/deactivateScripts.ts create mode 100644 src/client/terminals/envCollectionActivation/deactivateService.ts delete mode 100644 src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts diff --git a/pythonFiles/deactivate.csh b/pythonFiles/deactivate.csh deleted file mode 100644 index ef4d0d393897..000000000000 --- a/pythonFiles/deactivate.csh +++ /dev/null @@ -1,6 +0,0 @@ -# Same as deactivate in "/bin/activate.csh" -alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' - -# Initialize the variables required by deactivate function -set _OLD_VIRTUAL_PROMPT="$prompt" -set _OLD_VIRTUAL_PATH="$PATH" diff --git a/pythonFiles/deactivate.fish b/pythonFiles/deactivate.fish deleted file mode 100644 index c652a8c1e3d7..000000000000 --- a/pythonFiles/deactivate.fish +++ /dev/null @@ -1,36 +0,0 @@ -# Same as deactivate in "/bin/activate.fish" -function deactivate -d "Exit virtual environment and return to normal shell environment" - # reset old environment variables - if test -n "$_OLD_VIRTUAL_PATH" - set -gx PATH $_OLD_VIRTUAL_PATH - set -e _OLD_VIRTUAL_PATH - end - if test -n "$_OLD_VIRTUAL_PYTHONHOME" - set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME - set -e _OLD_VIRTUAL_PYTHONHOME - end - - if test -n "$vscode_python_old_fish_prompt_OVERRIDE" - set -e vscode_python_old_fish_prompt_OVERRIDE - if functions -q vscode_python_old_fish_prompt - functions -e fish_prompt - functions -c vscode_python_old_fish_prompt fish_prompt - functions -e vscode_python_old_fish_prompt - end - end - - set -e VIRTUAL_ENV - set -e VIRTUAL_ENV_PROMPT - if test "$argv[1]" != "nondestructive" - functions -e deactivate - end -end - -# Initialize the variables required by deactivate function -set -gx _OLD_VIRTUAL_PATH $PATH -if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" - functions -c fish_prompt vscode_python_old_fish_prompt -end -if set -q PYTHONHOME - set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME -end diff --git a/pythonFiles/deactivate.ps1 b/pythonFiles/deactivate.ps1 deleted file mode 100644 index 65dd80907d90..000000000000 --- a/pythonFiles/deactivate.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -# Same as deactivate in "Activate.ps1" -function global:deactivate ([switch]$NonDestructive) { - if (Test-Path function:_OLD_VIRTUAL_PROMPT) { - copy-item function:_OLD_VIRTUAL_PROMPT function:prompt - remove-item function:_OLD_VIRTUAL_PROMPT - } - if (Test-Path env:_OLD_VIRTUAL_PYTHONHOME) { - copy-item env:_OLD_VIRTUAL_PYTHONHOME env:PYTHONHOME - remove-item env:_OLD_VIRTUAL_PYTHONHOME - } - if (Test-Path env:_OLD_VIRTUAL_PATH) { - copy-item env:_OLD_VIRTUAL_PATH env:PATH - remove-item env:_OLD_VIRTUAL_PATH - } - if (Test-Path env:VIRTUAL_ENV) { - remove-item env:VIRTUAL_ENV - } - if (!$NonDestructive) { - remove-item function:deactivate - } -} - -# Initialize the variables required by deactivate function -if (! $env:VIRTUAL_ENV_DISABLE_PROMPT) { - function global:_OLD_VIRTUAL_PROMPT {""} - copy-item function:prompt function:_OLD_VIRTUAL_PROMPT -} -if (Test-Path env:PYTHONHOME) { - copy-item env:PYTHONHOME env:_OLD_VIRTUAL_PYTHONHOME -} -copy-item env:PATH env:_OLD_VIRTUAL_PATH diff --git a/pythonFiles/deactivate/bash/deactivate b/pythonFiles/deactivate/bash/deactivate new file mode 100755 index 000000000000..f6dd33425d1a --- /dev/null +++ b/pythonFiles/deactivate/bash/deactivate @@ -0,0 +1,44 @@ +# Same as deactivate in "/bin/activate" +deactivate () { + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + unset -f deactivate + fi +} + +# Get the directory of the current script +SCRIPT_DIR=$(dirname "$0") +# Construct the path to envVars.txt relative to the script directory +ENV_FILE="$SCRIPT_DIR/envVars.txt" + +# Read the JSON file and set the variables +TEMP_PS1=$(grep '^PS1=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PATH=$(grep '^PATH=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PYTHONHOME=$(grep '^PYTHONHOME=' $ENV_FILE | cut -d '=' -f 2) +# Initialize the variables required by deactivate function +_OLD_VIRTUAL_PS1="${TEMP_PS1:-}" +_OLD_VIRTUAL_PATH="$TEMP_PATH" +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${TEMP_PYTHONHOME:-}" +fi +deactivate +bash diff --git a/pythonFiles/deactivate/fish/deactivate b/pythonFiles/deactivate/fish/deactivate new file mode 100755 index 000000000000..3a9d50ccde2b --- /dev/null +++ b/pythonFiles/deactivate/fish/deactivate @@ -0,0 +1,44 @@ +# Same as deactivate in "/bin/activate" +deactivate () { + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + unset -f deactivate + fi +} + +# Get the directory of the current script +SCRIPT_DIR=$(dirname "$0") +# Construct the path to envVars.txt relative to the script directory +ENV_FILE="$SCRIPT_DIR/envVars.txt" + +# Read the JSON file and set the variables +TEMP_PS1=$(grep '^PS1=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PATH=$(grep '^PATH=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PYTHONHOME=$(grep '^PYTHONHOME=' $ENV_FILE | cut -d '=' -f 2) +# Initialize the variables required by deactivate function +_OLD_VIRTUAL_PS1="${TEMP_PS1:-}" +_OLD_VIRTUAL_PATH="$TEMP_PATH" +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${TEMP_PYTHONHOME:-}" +fi +deactivate +fish diff --git a/pythonFiles/deactivate/powershell/deactivate.ps1 b/pythonFiles/deactivate/powershell/deactivate.ps1 new file mode 100644 index 000000000000..49365e0fbeff --- /dev/null +++ b/pythonFiles/deactivate/powershell/deactivate.ps1 @@ -0,0 +1,11 @@ +# Load dotenv-style file and restore environment variables +Get-Content -Path "$PSScriptRoot\envVars.txt" | ForEach-Object { + # Split each line into key and value at the first '=' + $parts = $_ -split '=', 2 + if ($parts.Count -eq 2) { + $key = $parts[0].Trim() + $value = $parts[1].Trim() + # Set the environment variable + Set-Item -Path "env:$key" -Value $value + } +} diff --git a/pythonFiles/deactivate b/pythonFiles/deactivate/zsh/deactivate old mode 100644 new mode 100755 similarity index 61% rename from pythonFiles/deactivate rename to pythonFiles/deactivate/zsh/deactivate index 6ede3da311a9..8b059318f988 --- a/pythonFiles/deactivate +++ b/pythonFiles/deactivate/zsh/deactivate @@ -25,9 +25,20 @@ deactivate () { fi } +# Get the directory of the current script +SCRIPT_DIR=$(dirname "$0") +# Construct the path to envVars.txt relative to the script directory +ENV_FILE="$SCRIPT_DIR/envVars.txt" + +# Read the JSON file and set the variables +TEMP_PS1=$(grep '^PS1=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PATH=$(grep '^PATH=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PYTHONHOME=$(grep '^PYTHONHOME=' $ENV_FILE | cut -d '=' -f 2) # Initialize the variables required by deactivate function -_OLD_VIRTUAL_PS1="${PS1:-}" -_OLD_VIRTUAL_PATH="$PATH" +_OLD_VIRTUAL_PS1="${TEMP_PS1:-}" +_OLD_VIRTUAL_PATH="$TEMP_PATH" if [ -n "${PYTHONHOME:-}" ] ; then - _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + _OLD_VIRTUAL_PYTHONHOME="${TEMP_PYTHONHOME:-}" fi +deactivate +zsh diff --git a/pythonFiles/printEnvVariablesToFile.py b/pythonFiles/printEnvVariablesToFile.py index be966bcac28c..a4e0d24abbe0 100644 --- a/pythonFiles/printEnvVariablesToFile.py +++ b/pythonFiles/printEnvVariablesToFile.py @@ -2,12 +2,11 @@ # Licensed under the MIT License. import os -import json import sys +# Last argument is the target file into which we'll write the env variables line by line. +output_file = sys.argv[-1] -# Last argument is the target file into which we'll write the env variables as json. -json_file = sys.argv[-1] - -with open(json_file, "w") as outfile: - json.dump(dict(os.environ), outfile) +with open(output_file, "w") as outfile: + for key, val in os.environ.items(): + outfile.write(f"{key}={val}\n") diff --git a/src/client/common/utils/async.ts b/src/client/common/utils/async.ts index 5905399cd4a1..c119d8f19b06 100644 --- a/src/client/common/utils/async.ts +++ b/src/client/common/utils/async.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ +/* eslint-disable no-async-promise-executor */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -228,3 +230,35 @@ export async function flattenIterator(iterator: IAsyncIterator): Promise Promise} condition + * @param {number} timeoutMs + * @param {string} errorMessage + * @returns {Promise} + */ +export async function waitForCondition( + condition: () => Promise, + timeoutMs: number, + errorMessage: string, +): Promise { + return new Promise(async (resolve, reject) => { + const timeout = setTimeout(() => { + clearTimeout(timeout); + + clearTimeout(timer); + reject(new Error(errorMessage)); + }, timeoutMs); + const timer = setInterval(async () => { + if (!(await condition().catch(() => false))) { + return; + } + clearTimeout(timeout); + clearTimeout(timer); + resolve(); + }, 10); + }); +} diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 95252b361c76..fc9e7fff6b2f 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -197,7 +197,7 @@ export namespace Interpreters { export const activatingTerminals = l10n.t('Reactivating terminals...'); export const activateTerminalDescription = l10n.t('Activated environment for'); export const terminalEnvVarCollectionPrompt = l10n.t( - '{0} environment was successfully activated, even though {1} may not be present in the terminal prompt. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', + '{0} environment was successfully activated, even though {1} indicator may not be present in the terminal prompt. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', ); export const terminalDeactivateProgress = l10n.t('Editing {0}...'); export const restartingTerminal = l10n.t('Restarting terminal and deactivating...'); diff --git a/src/client/interpreter/activation/service.ts b/src/client/interpreter/activation/service.ts index f97545a5823a..d6a3d608007e 100644 --- a/src/client/interpreter/activation/service.ts +++ b/src/client/interpreter/activation/service.ts @@ -273,15 +273,15 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi } return undefined; } - // Run the activate command collect the environment from it. - const activationCommand = fixActivationCommands(activationCommands).join(' && '); - // In order to make sure we know where the environment output is, - // put in a dummy echo we can look for const commandSeparator = [TerminalShellType.powershell, TerminalShellType.powershellCore].includes( shellInfo.shellType, ) ? ';' : '&&'; + // Run the activate command collect the environment from it. + const activationCommand = fixActivationCommands(activationCommands).join(` ${commandSeparator} `); + // In order to make sure we know where the environment output is, + // put in a dummy echo we can look for command = `${activationCommand} ${commandSeparator} echo '${ENVIRONMENT_PREFIX}' ${commandSeparator} python ${args.join( ' ', )}`; diff --git a/src/client/terminals/envCollectionActivation/deactivatePrompt.ts b/src/client/terminals/envCollectionActivation/deactivatePrompt.ts deleted file mode 100644 index a9fd804291a5..000000000000 --- a/src/client/terminals/envCollectionActivation/deactivatePrompt.ts +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { - Position, - Uri, - WorkspaceEdit, - Range, - TextEditorRevealType, - ProgressLocation, - Terminal, - Selection, -} from 'vscode'; -import { - IApplicationEnvironment, - IApplicationShell, - IDocumentManager, - ITerminalManager, -} from '../../common/application/types'; -import { IDisposableRegistry, IExperimentService, IPersistentStateFactory } from '../../common/types'; -import { Common, Interpreters } from '../../common/utils/localize'; -import { IExtensionSingleActivationService } from '../../activation/types'; -import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; -import { IInterpreterService } from '../../interpreter/contracts'; -import { PythonEnvType } from '../../pythonEnvironments/base/info'; -import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; -import { TerminalShellType } from '../../common/terminal/types'; -import { traceError } from '../../logging'; -import { shellExec } from '../../common/process/rawProcessApis'; -import { sleep } from '../../common/utils/async'; -import { getDeactivateShellInfo } from './deactivateScripts'; -import { isTestExecution } from '../../common/constants'; -import { ProgressService } from '../../common/application/progressService'; -import { copyFile, createFile, pathExists } from '../../common/platform/fs-paths'; -import { getOSType, OSType } from '../../common/utils/platform'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; - -export const terminalDeactivationPromptKey = 'TERMINAL_DEACTIVATION_PROMPT_KEY'; -@injectable() -export class TerminalDeactivateLimitationPrompt implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - private terminalProcessId: number | undefined; - - private readonly progressService: ProgressService; - - constructor( - @inject(IApplicationShell) private readonly appShell: IApplicationShell, - @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, - @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, - @inject(IDocumentManager) private readonly documentManager: IDocumentManager, - @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, - @inject(IExperimentService) private readonly experimentService: IExperimentService, - ) { - this.progressService = new ProgressService(this.appShell); - } - - public async activate(): Promise { - if (!inTerminalEnvVarExperiment(this.experimentService)) { - return; - } - if (!isTestExecution()) { - // Avoid showing prompt until startup completes. - await sleep(6000); - } - this.disposableRegistry.push( - this.appShell.onDidWriteTerminalData(async (e) => { - if (!e.data.includes('deactivate')) { - return; - } - let shellType = identifyShellFromShellPath(this.appEnvironment.shell); - if (shellType === TerminalShellType.commandPrompt) { - return; - } - if (getOSType() === OSType.OSX && shellType === TerminalShellType.bash) { - // On macOS, sometimes bash is overriden by OS to actually launch zsh, so we need to execute inside - // the shell to get the correct shell type. - const shell = await shellExec('echo $SHELL', { shell: this.appEnvironment.shell }).then((output) => - output.stdout.trim(), - ); - shellType = identifyShellFromShellPath(shell); - } - const { terminal } = e; - const cwd = - 'cwd' in terminal.creationOptions && terminal.creationOptions.cwd - ? terminal.creationOptions.cwd - : undefined; - const resource = typeof cwd === 'string' ? Uri.file(cwd) : cwd; - const interpreter = await this.interpreterService.getActiveInterpreter(resource); - if (interpreter?.type !== PythonEnvType.Virtual) { - return; - } - await this._notifyUsers(shellType, terminal).catch((ex) => traceError('Deactivate prompt failed', ex)); - }), - ); - } - - public async _notifyUsers(shellType: TerminalShellType, terminal: Terminal): Promise { - const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( - `${terminalDeactivationPromptKey}-${shellType}`, - true, - ); - if (!notificationPromptEnabled.value) { - const processId = await terminal.processId; - if (processId && this.terminalProcessId === processId) { - // Existing terminal needs to be restarted for changes to take effect. - await this.forceRestartShell(terminal); - } - return; - } - const scriptInfo = getDeactivateShellInfo(shellType); - if (!scriptInfo) { - // Shell integration is not supported for these shells, in which case this workaround won't work. - return; - } - const telemetrySelections: ['Edit script', "Don't show again"] = ['Edit script', "Don't show again"]; - const { initScript, source, destination } = scriptInfo; - const prompts = [Common.editSomething.format(initScript.displayName), Common.doNotShowAgain]; - const selection = await this.appShell.showWarningMessage( - Interpreters.terminalDeactivatePrompt.format(initScript.displayName), - ...prompts, - ); - let index = selection ? prompts.indexOf(selection) : 0; - if (selection === prompts[0]) { - index = 0; - } - sendTelemetryEvent(EventName.TERMINAL_DEACTIVATE_PROMPT, undefined, { - selection: selection ? telemetrySelections[index] : undefined, - }); - if (!selection) { - return; - } - if (selection === prompts[0]) { - this.progressService.showProgress({ - location: ProgressLocation.Window, - title: Interpreters.terminalDeactivateProgress.format(initScript.displayName), - }); - await copyFile(source, destination); - await this.openScriptWithEdits(initScript.command, initScript.contents); - await notificationPromptEnabled.updateValue(false); - this.progressService.hideProgress(); - this.terminalProcessId = await terminal.processId; - } - if (selection === prompts[1]) { - await notificationPromptEnabled.updateValue(false); - } - } - - private async openScriptWithEdits(command: string, content: string) { - const document = await this.openScript(command); - const hookMarker = 'VSCode venv deactivate hook'; - content = ` -# >>> ${hookMarker} >>> -${content} -# <<< ${hookMarker} <<<`; - // If script already has the hook, don't add it again. - const editor = await this.documentManager.showTextDocument(document); - if (document.getText().includes(hookMarker)) { - editor.revealRange( - new Range(new Position(document.lineCount - 3, 0), new Position(document.lineCount, 0)), - TextEditorRevealType.AtTop, - ); - return; - } - const editorEdit = new WorkspaceEdit(); - editorEdit.insert(document.uri, new Position(document.lineCount, 0), content); - await this.documentManager.applyEdit(editorEdit); - // Reveal the edits. - editor.selection = new Selection(new Position(document.lineCount - 3, 0), new Position(document.lineCount, 0)); - editor.revealRange( - new Range(new Position(document.lineCount - 3, 0), new Position(document.lineCount, 0)), - TextEditorRevealType.AtTop, - ); - } - - private async openScript(command: string) { - const initScriptPath = await this.getPathToScript(command); - if (!(await pathExists(initScriptPath))) { - await createFile(initScriptPath); - } - const document = await this.documentManager.openTextDocument(initScriptPath); - return document; - } - - private async getPathToScript(command: string) { - return shellExec(command, { shell: this.appEnvironment.shell }).then((output) => output.stdout.trim()); - } - - public async forceRestartShell(terminal: Terminal): Promise { - terminal.dispose(); - terminal = this.terminalManager.createTerminal({ - message: Interpreters.restartingTerminal, - }); - terminal.show(true); - terminal.sendText('deactivate'); - } -} diff --git a/src/client/terminals/envCollectionActivation/deactivateScripts.ts b/src/client/terminals/envCollectionActivation/deactivateScripts.ts deleted file mode 100644 index 34917e44bbdf..000000000000 --- a/src/client/terminals/envCollectionActivation/deactivateScripts.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* eslint-disable no-case-declarations */ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as path from 'path'; -import { _SCRIPTS_DIR } from '../../common/process/internal/scripts/constants'; -import { TerminalShellType } from '../../common/terminal/types'; - -type DeactivateShellInfo = { - /** - * Full path to source deactivate script to copy. - */ - source: string; - /** - * Full path to destination to copy deactivate script to. - */ - destination: string; - initScript: { - /** - * Display name of init script for the shell. - */ - displayName: string; - /** - * Command to run in shell to output the full path to init script. - */ - command: string; - /** - * Contents to add to init script. - */ - contents: string; - }; -}; - -// eslint-disable-next-line global-require -const untildify: (value: string) => string = require('untildify'); - -export function getDeactivateShellInfo(shellType: TerminalShellType): DeactivateShellInfo | undefined { - switch (shellType) { - case TerminalShellType.bash: - return buildInfo( - 'deactivate', - { - displayName: '~/.bashrc', - path: '~/.bashrc', - }, - `source {0}`, - ); - case TerminalShellType.powershellCore: - case TerminalShellType.powershell: - return buildInfo( - 'deactivate.ps1', - { - displayName: 'Powershell Profile', - path: '$Profile', - }, - `& "{0}"`, - ); - case TerminalShellType.zsh: - return buildInfo( - 'deactivate', - { - displayName: '~/.zshrc', - path: '~/.zshrc', - }, - `source {0}`, - ); - case TerminalShellType.fish: - return buildInfo( - 'deactivate.fish', - { - displayName: 'config.fish', - path: '$__fish_config_dir/config.fish', - }, - `source {0}`, - ); - case TerminalShellType.cshell: - return buildInfo( - 'deactivate.csh', - { - displayName: '~/.cshrc', - path: '~/.cshrc', - }, - `source {0}`, - ); - default: - return undefined; - } -} - -function buildInfo( - deactivate: string, - initScript: { - path: string; - displayName: string; - }, - scriptCommandFormat: string, -) { - const scriptPath = path.join('~', '.vscode-python', deactivate); - return { - source: path.join(_SCRIPTS_DIR, deactivate), - destination: untildify(scriptPath), - initScript: { - displayName: initScript.displayName, - command: `echo ${initScript.path}`, - contents: scriptCommandFormat.format(scriptPath), - }, - }; -} diff --git a/src/client/terminals/envCollectionActivation/deactivateService.ts b/src/client/terminals/envCollectionActivation/deactivateService.ts new file mode 100644 index 000000000000..b763bd95e88c --- /dev/null +++ b/src/client/terminals/envCollectionActivation/deactivateService.ts @@ -0,0 +1,97 @@ +/* eslint-disable class-methods-use-this */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { ITerminalManager } from '../../common/application/types'; +import { pathExists } from '../../common/platform/fs-paths'; +import { _SCRIPTS_DIR } from '../../common/process/internal/scripts/constants'; +import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; +import { TerminalShellType } from '../../common/terminal/types'; +import { Resource } from '../../common/types'; +import { waitForCondition } from '../../common/utils/async'; +import { cache } from '../../common/utils/decorators'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { traceVerbose } from '../../logging'; +import { PythonEnvType } from '../../pythonEnvironments/base/info'; +import { ITerminalDeactivateService } from '../types'; + +/** + * This is a list of shells which support shell integration: + * https://code.visualstudio.com/docs/terminal/shell-integration + */ +const ShellIntegrationShells = [ + TerminalShellType.powershell, + TerminalShellType.powershellCore, + TerminalShellType.bash, + TerminalShellType.zsh, + TerminalShellType.fish, +]; + +@injectable() +export class TerminalDeactivateService implements ITerminalDeactivateService { + private readonly envVarScript = path.join(_SCRIPTS_DIR, 'printEnvVariablesToFile.py'); + + constructor( + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + ) {} + + @cache(-1, true) + public async initializeScriptParams(shell: string): Promise { + const location = this.getLocation(shell); + if (!location) { + return; + } + const shellType = identifyShellFromShellPath(shell); + const terminal = this.terminalManager.createTerminal({ + name: `Python ${shellType} Deactivate`, + shellPath: shell, + hideFromUser: true, + cwd: location, + }); + const globalInterpreters = this.interpreterService.getInterpreters().filter((i) => !i.type); + const outputFile = path.join(location, `envVars.txt`); + const interpreterPath = + globalInterpreters.length > 0 && globalInterpreters[0] ? globalInterpreters[0].path : 'python'; + const checkIfFileHasBeenCreated = () => pathExists(outputFile); + const stopWatch = new StopWatch(); + terminal.sendText(`${interpreterPath} "${this.envVarScript}" "${outputFile}"`); + await waitForCondition(checkIfFileHasBeenCreated, 30_000, `"${outputFile}" file not created`); + traceVerbose(`Time taken to get env vars using terminal is ${stopWatch.elapsedTime}ms`); + } + + public async getScriptLocation(shell: string, resource: Resource): Promise { + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (interpreter?.type !== PythonEnvType.Virtual) { + return undefined; + } + return this.getLocation(shell); + } + + private getLocation(shell: string) { + const shellType = identifyShellFromShellPath(shell); + if (!ShellIntegrationShells.includes(shellType)) { + return undefined; + } + return path.join(_SCRIPTS_DIR, 'deactivate', this.getShellFolderName(shellType)); + } + + private getShellFolderName(shellType: TerminalShellType): string { + switch (shellType) { + case TerminalShellType.powershell: + case TerminalShellType.powershellCore: + return 'powershell'; + case TerminalShellType.fish: + return 'fish'; + case TerminalShellType.zsh: + return 'zsh'; + case TerminalShellType.bash: + return 'bash'; + default: + throw new Error(`Unsupported shell type ${shellType}`); + } + } +} diff --git a/src/client/terminals/envCollectionActivation/service.ts b/src/client/terminals/envCollectionActivation/service.ts index f645b19967f4..c9fa125324a0 100644 --- a/src/client/terminals/envCollectionActivation/service.ts +++ b/src/client/terminals/envCollectionActivation/service.ts @@ -37,7 +37,7 @@ import { TerminalShellType } from '../../common/terminal/types'; import { OSType } from '../../common/utils/platform'; import { normCase } from '../../common/platform/fs-paths'; import { PythonEnvType } from '../../pythonEnvironments/base/info'; -import { IShellIntegrationService, ITerminalEnvVarCollectionService } from '../types'; +import { IShellIntegrationService, ITerminalDeactivateService, ITerminalEnvVarCollectionService } from '../types'; import { ProgressService } from '../../common/application/progressService'; @injectable() @@ -78,6 +78,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ @inject(IEnvironmentActivationService) private environmentActivationService: IEnvironmentActivationService, @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(ITerminalDeactivateService) private readonly terminalDeactivateService: ITerminalDeactivateService, @inject(IPathUtils) private readonly pathUtils: IPathUtils, @inject(IShellIntegrationService) private readonly shellIntegrationService: IShellIntegrationService, ) { @@ -191,6 +192,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ // Clear any previously set env vars from collection envVarCollection.clear(); + const deactivate = await this.terminalDeactivateService.getScriptLocation(shell, resource); Object.keys(env).forEach((key) => { if (shouldSkip(key)) { return; @@ -209,14 +211,19 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (processEnv.PATH && env.PATH?.endsWith(processEnv.PATH)) { // Prefer prepending to PATH instead of replacing it, as we do not want to replace any // changes to PATH users might have made it in their init scripts (~/.bashrc etc.) - const prependedPart = env.PATH.slice(0, -processEnv.PATH.length); - value = prependedPart; + value = env.PATH.slice(0, -processEnv.PATH.length); + if (deactivate) { + value = `${deactivate}${this.separator}${value}`; + } traceVerbose(`Prepending environment variable ${key} in collection with ${value}`); envVarCollection.prepend(key, value, prependOptions); } else { if (!value.endsWith(this.separator)) { value = value.concat(this.separator); } + if (deactivate) { + value = `${deactivate}${this.separator}${value}`; + } traceVerbose(`Prepending environment variable ${key} in collection to ${value}`); envVarCollection.prepend(key, value, prependOptions); } @@ -236,6 +243,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ envVarCollection.description = description; await this.trackTerminalPrompt(shell, resource, env); + await this.terminalDeactivateService.initializeScriptParams(shell); } private isPromptSet = new Map(); diff --git a/src/client/terminals/envCollectionActivation/shellIntegrationService.ts b/src/client/terminals/envCollectionActivation/shellIntegrationService.ts index 06487e0ff982..485af6f3cc99 100644 --- a/src/client/terminals/envCollectionActivation/shellIntegrationService.ts +++ b/src/client/terminals/envCollectionActivation/shellIntegrationService.ts @@ -7,7 +7,7 @@ import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors import { TerminalShellType } from '../../common/terminal/types'; import { createDeferred, sleep } from '../../common/utils/async'; import { cache } from '../../common/utils/decorators'; -import { traceError, traceVerbose } from '../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../logging'; import { IShellIntegrationService } from '../types'; /** @@ -45,7 +45,8 @@ export class ShellIntegrationService implements IShellIntegrationService { if (!isEnabled) { traceVerbose('Shell integrated is disabled in user settings.'); } - const isSupposedToWork = isEnabled && ShellIntegrationShells.includes(identifyShellFromShellPath(shell)); + const shellType = identifyShellFromShellPath(shell); + const isSupposedToWork = isEnabled && ShellIntegrationShells.includes(shellType); if (!isSupposedToWork) { return false; } @@ -71,6 +72,9 @@ export class ShellIntegrationService implements IShellIntegrationService { terminal.sendText(`echo ${shell}`); const success = await Promise.race([sleep(3000).then(() => false), deferred.promise.then(() => true)]); disposable.dispose(); + if (!success) { + traceInfo(`Shell integration is not working for ${shellType}`); + } return success; } catch (ex) { traceVerbose(`Proposed API is not available, failed to subscribe to onDidExecuteTerminalCommand`, ex); diff --git a/src/client/terminals/serviceRegistry.ts b/src/client/terminals/serviceRegistry.ts index 86e2efb376e8..3474edadd744 100644 --- a/src/client/terminals/serviceRegistry.ts +++ b/src/client/terminals/serviceRegistry.ts @@ -14,13 +14,14 @@ import { ICodeExecutionService, IShellIntegrationService, ITerminalAutoActivation, + ITerminalDeactivateService, ITerminalEnvVarCollectionService, } from './types'; import { TerminalEnvVarCollectionService } from './envCollectionActivation/service'; import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; -import { TerminalDeactivateLimitationPrompt } from './envCollectionActivation/deactivatePrompt'; import { TerminalIndicatorPrompt } from './envCollectionActivation/indicatorPrompt'; import { ShellIntegrationService } from './envCollectionActivation/shellIntegrationService'; +import { TerminalDeactivateService } from './envCollectionActivation/deactivateService'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(ICodeExecutionHelper, CodeExecutionHelper); @@ -44,14 +45,11 @@ export function registerTypes(serviceManager: IServiceManager): void { ITerminalEnvVarCollectionService, TerminalEnvVarCollectionService, ); + serviceManager.addSingleton(ITerminalDeactivateService, TerminalDeactivateService); serviceManager.addSingleton( IExtensionSingleActivationService, TerminalIndicatorPrompt, ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - TerminalDeactivateLimitationPrompt, - ); serviceManager.addSingleton(IShellIntegrationService, ShellIntegrationService); serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); } diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index 02b038bd239d..0fb268fe192c 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -46,3 +46,9 @@ export const IShellIntegrationService = Symbol('IShellIntegrationService'); export interface IShellIntegrationService { isWorking(shell: string): Promise; } + +export const ITerminalDeactivateService = Symbol('ITerminalDeactivateService'); +export interface ITerminalDeactivateService { + initializeScriptParams(shell: string): Promise; + getScriptLocation(shell: string, resource: Resource): Promise; +} diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 52efc3a45faa..55e0b80d3e1e 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -37,7 +37,7 @@ import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PathUtils } from '../../../client/common/platform/pathUtils'; import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import { IShellIntegrationService } from '../../../client/terminals/types'; +import { IShellIntegrationService, ITerminalDeactivateService } from '../../../client/terminals/types'; suite('Terminal Environment Variable Collection Service', () => { let platform: IPlatformService; @@ -51,6 +51,7 @@ suite('Terminal Environment Variable Collection Service', () => { let environmentActivationService: IEnvironmentActivationService; let workspaceService: IWorkspaceService; let terminalEnvVarCollectionService: TerminalEnvVarCollectionService; + let terminalDeactivateService: ITerminalDeactivateService; const progressOptions = { location: ProgressLocation.Window, title: Interpreters.activatingTerminals, @@ -63,6 +64,9 @@ suite('Terminal Environment Variable Collection Service', () => { setup(() => { workspaceService = mock(); + terminalDeactivateService = mock(); + when(terminalDeactivateService.getScriptLocation(anything(), anything())).thenResolve(undefined); + when(terminalDeactivateService.initializeScriptParams(anything())).thenResolve(); when(workspaceService.getWorkspaceFolder(anything())).thenReturn(undefined); when(workspaceService.workspaceFolders).thenReturn(undefined); platform = mock(); @@ -106,6 +110,7 @@ suite('Terminal Environment Variable Collection Service', () => { instance(environmentActivationService), instance(workspaceService), instance(configService), + instance(terminalDeactivateService), new PathUtils(getOSType() === OSType.Windows), instance(shellIntegrationService), ); @@ -334,6 +339,41 @@ suite('Terminal Environment Variable Collection Service', () => { assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); }); + test('Also prepend deactivate script location if available', async () => { + reset(terminalDeactivateService); + when(terminalDeactivateService.getScriptLocation(anything(), anything())).thenResolve('scriptLocation'); + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const prependedPart = 'path/to/activate/dir:'; + const envVars: NodeJS.ProcessEnv = { PATH: `${prependedPart}${processEnv.PATH}` }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + const separator = getOSType() === OSType.Windows ? ';' : ':'; + verify(collection.prepend('PATH', `scriptLocation${separator}${prependedPart}`, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); + }); + test('Prepend full PATH with separator otherwise', async () => { const processEnv = { PATH: 'hello/1/2/3' }; reset(environmentActivationService); @@ -367,6 +407,41 @@ suite('Terminal Environment Variable Collection Service', () => { assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); }); + test('Prepend full PATH with separator otherwise', async () => { + reset(terminalDeactivateService); + when(terminalDeactivateService.getScriptLocation(anything(), anything())).thenResolve('scriptLocation'); + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const separator = getOSType() === OSType.Windows ? ';' : ':'; + const finalPath = 'hello/3/2/1'; + const envVars: NodeJS.ProcessEnv = { PATH: finalPath }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.prepend('PATH', `scriptLocation${separator}${finalPath}${separator}`, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); + }); + test('Verify envs are not applied if env activation is disabled', async () => { const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; when( diff --git a/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts b/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts deleted file mode 100644 index f775241abb32..000000000000 --- a/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts +++ /dev/null @@ -1,271 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { mock, when, anything, instance, verify, reset } from 'ts-mockito'; -import { EventEmitter, Terminal, TerminalDataWriteEvent, TextDocument, TextEditor, Uri } from 'vscode'; -import * as sinon from 'sinon'; -import { expect } from 'chai'; -import { - IApplicationEnvironment, - IApplicationShell, - IDocumentManager, - ITerminalManager, -} from '../../../client/common/application/types'; -import { IExperimentService, IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; -import { Common, Interpreters } from '../../../client/common/utils/localize'; -import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; -import { sleep } from '../../core'; -import { IInterpreterService } from '../../../client/interpreter/contracts'; -import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import { TerminalDeactivateLimitationPrompt } from '../../../client/terminals/envCollectionActivation/deactivatePrompt'; -import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; -import { TerminalShellType } from '../../../client/common/terminal/types'; -import * as processApi from '../../../client/common/process/rawProcessApis'; -import * as fsapi from '../../../client/common/platform/fs-paths'; -import { noop } from '../../../client/common/utils/misc'; - -suite('Terminal Deactivation Limitation Prompt', () => { - let shell: IApplicationShell; - let experimentService: IExperimentService; - let persistentStateFactory: IPersistentStateFactory; - let appEnvironment: IApplicationEnvironment; - let deactivatePrompt: TerminalDeactivateLimitationPrompt; - let terminalWriteEvent: EventEmitter; - let notificationEnabled: IPersistentState; - let interpreterService: IInterpreterService; - let terminalManager: ITerminalManager; - let documentManager: IDocumentManager; - const prompts = [Common.editSomething.format('~/.bashrc'), Common.doNotShowAgain]; - const expectedMessage = Interpreters.terminalDeactivatePrompt.format('~/.bashrc'); - const initScriptPath = 'home/node/.bashrc'; - const resource = Uri.file('a'); - let terminal: Terminal; - - setup(async () => { - const activeEditorEvent = new EventEmitter(); - const document = ({ - uri: Uri.file(''), - getText: () => '', - } as unknown) as TextDocument; - sinon.stub(processApi, 'shellExec').callsFake(async (command: string) => { - if (command !== 'echo ~/.bashrc') { - throw new Error(`Unexpected command: ${command}`); - } - await sleep(1500); - return { stdout: initScriptPath }; - }); - documentManager = mock(); - terminalManager = mock(); - terminal = ({ - creationOptions: { cwd: resource }, - processId: Promise.resolve(1), - dispose: noop, - show: noop, - sendText: noop, - } as unknown) as Terminal; - when(terminalManager.createTerminal(anything())).thenReturn(terminal); - when(documentManager.openTextDocument(initScriptPath)).thenReturn(Promise.resolve(document)); - when(documentManager.onDidChangeActiveTextEditor).thenReturn(activeEditorEvent.event); - shell = mock(); - interpreterService = mock(); - experimentService = mock(); - persistentStateFactory = mock(); - appEnvironment = mock(); - when(appEnvironment.shell).thenReturn('bash'); - notificationEnabled = mock>(); - terminalWriteEvent = new EventEmitter(); - when(persistentStateFactory.createGlobalPersistentState(anything(), true)).thenReturn( - instance(notificationEnabled), - ); - when(shell.onDidWriteTerminalData).thenReturn(terminalWriteEvent.event); - when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); - deactivatePrompt = new TerminalDeactivateLimitationPrompt( - instance(shell), - instance(persistentStateFactory), - [], - instance(interpreterService), - instance(appEnvironment), - instance(documentManager), - instance(terminalManager), - instance(experimentService), - ); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Show notification when "deactivate" command is run when a virtual env is selected', async () => { - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); - }); - - test('When using cmd, do not show notification for the same', async () => { - reset(appEnvironment); - when(appEnvironment.shell).thenReturn(TerminalShellType.commandPrompt); - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); - }); - - test('When not in experiment, do not show notification for the same', async () => { - reset(experimentService); - when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); - - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); - }); - - test('Do not show notification if notification is disabled', async () => { - when(notificationEnabled.value).thenReturn(false); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); - }); - - test('Do not show notification when virtual env is not activated for terminal', async () => { - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Conda, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); - }); - - test("Disable notification if `Don't show again` is clicked", async () => { - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenReturn(Promise.resolve(Common.doNotShowAgain)); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(notificationEnabled.updateValue(false)).once(); - }); - - test('Edit script correctly if `Edit