From 8c38db8bd534538c560663d6a66dc43fe18c97b0 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 15 Jul 2022 15:05:21 -0700 Subject: [PATCH 1/2] Ensure `Install Python` button on the walkthrough opens and fills in the suggested command --- package-lock.json | 231 ++++++------------ package.json | 8 +- src/client/common/application/commands.ts | 2 + src/client/common/constants.ts | 2 + .../index.ts} | 14 +- .../installPython/installPythonViaTerminal.ts | 81 ++++++ src/client/interpreter/serviceRegistry.ts | 5 + .../installPythonViaTerminal.unit.test.ts | 85 +++++++ .../interpreters/serviceRegistry.unit.test.ts | 2 + 9 files changed, 261 insertions(+), 169 deletions(-) rename src/client/interpreter/configuration/interpreterSelector/commands/{installPython.ts => installPython/index.ts} (84%) create mode 100644 src/client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal.ts create mode 100644 src/test/configuration/interpreterSelector/commands/installPythonViaTerminal.unit.test.ts diff --git a/package-lock.json b/package-lock.json index d9ed493405cc..f0b41c1c5e3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "vscode-languageserver-protocol": "3.17.2-next.6", "vscode-nls": "^5.0.1", "vscode-tas-client": "^0.1.22", + "which": "^2.0.2", "winreg": "^1.2.4", "xml2js": "^0.4.19" }, @@ -67,6 +68,7 @@ "@types/tmp": "^0.0.33", "@types/uuid": "^8.3.4", "@types/vscode": "~1.68.0", + "@types/which": "^2.0.1", "@types/winreg": "^1.2.30", "@types/xml2js": "^0.4.2", "@typescript-eslint/eslint-plugin": "^3.7.0", @@ -840,6 +842,12 @@ "integrity": "sha512-fXlaq13NT5yHh6yZ3c+UxXloTSk34mIvsNFYyQCeO5Po2BLFAwz7EZT4kQ43B64/aPcnAenyWy3QasrTofBOnQ==", "dev": true }, + "node_modules/@types/which": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.1.tgz", + "integrity": "sha512-Jjakcv8Roqtio6w1gr0D7y6twbhx6gGgFGF5BLwajPpnOIOxFkakFhCq+LmyyeAz7BX6ULrjBOxdKaCDy+4+dQ==", + "dev": true + }, "node_modules/@types/winreg": { "version": "1.2.31", "resolved": "https://registry.npmjs.org/@types/winreg/-/winreg-1.2.31.tgz", @@ -3543,6 +3551,18 @@ "node": ">=4.8" } }, + "node_modules/cross-spawn/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/crypt": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", @@ -5165,21 +5185,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/espree": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", @@ -5391,21 +5396,6 @@ "node": ">=8" } }, - "node_modules/execa/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -6080,21 +6070,6 @@ "node": ">=8" } }, - "node_modules/foreground-child/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -6607,6 +6582,18 @@ "node": ">=0.10.0" } }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -7933,8 +7920,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "node_modules/isobject": { "version": "3.0.1", @@ -8251,21 +8237,6 @@ "uuid": "bin/uuid" } }, - "node_modules/istanbul-lib-processinfo/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/istanbul-lib-report": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", @@ -9617,21 +9588,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/mocha/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/mocha/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -12517,21 +12473,6 @@ "node": ">=8" } }, - "node_modules/spawn-wrap/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/spdx-correct": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", @@ -15037,15 +14978,17 @@ } }, "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dependencies": { "isexe": "^2.0.0" }, "bin": { - "which": "bin/which" + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, "node_modules/which-boxed-primitive": { @@ -16141,6 +16084,12 @@ "integrity": "sha512-fXlaq13NT5yHh6yZ3c+UxXloTSk34mIvsNFYyQCeO5Po2BLFAwz7EZT4kQ43B64/aPcnAenyWy3QasrTofBOnQ==", "dev": true }, + "@types/which": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.1.tgz", + "integrity": "sha512-Jjakcv8Roqtio6w1gr0D7y6twbhx6gGgFGF5BLwajPpnOIOxFkakFhCq+LmyyeAz7BX6ULrjBOxdKaCDy+4+dQ==", + "dev": true + }, "@types/winreg": { "version": "1.2.31", "resolved": "https://registry.npmjs.org/@types/winreg/-/winreg-1.2.31.tgz", @@ -18276,6 +18225,17 @@ "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" + }, + "dependencies": { + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } } }, "crypt": { @@ -19291,15 +19251,6 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } } } }, @@ -19739,15 +19690,6 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } } } }, @@ -20280,15 +20222,6 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } } } }, @@ -20704,6 +20637,17 @@ "ini": "^1.3.4", "is-windows": "^1.0.1", "which": "^1.2.14" + }, + "dependencies": { + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } } }, "globals": { @@ -21691,8 +21635,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isobject": { "version": "3.0.1", @@ -21955,15 +21898,6 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } } } }, @@ -22995,15 +22929,6 @@ "has-flag": "^4.0.0" } }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -25314,17 +25239,6 @@ "rimraf": "^3.0.0", "signal-exit": "^3.0.2", "which": "^2.0.1" - }, - "dependencies": { - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } } }, "spdx-correct": { @@ -27290,10 +27204,9 @@ "dev": true }, "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "requires": { "isexe": "^2.0.0" } diff --git a/package.json b/package.json index 5afa6ff42ae3..4093222911e3 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ { "id": "python.installPythonMac", "title": "Install Python", - "description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Open Terminal](command:workbench.action.terminal.new)\n", + "description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via Brew](command:python.installPythonOnMac)\n", "media": { "markdown": "resources/walkthrough/install-python-macos.md" }, @@ -136,7 +136,7 @@ { "id": "python.installPythonLinux", "title": "Install Python", - "description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Open Terminal](command:workbench.action.terminal.new)\n", + "description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via terminal](command:python.installPythonOnLinux)\n", "media": { "markdown": "resources/walkthrough/install-python-linux.md" }, @@ -1804,7 +1804,8 @@ "vscode-nls": "^5.0.1", "vscode-tas-client": "^0.1.22", "winreg": "^1.2.4", - "xml2js": "^0.4.19" + "xml2js": "^0.4.19", + "which":"^2.0.2" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", @@ -1828,6 +1829,7 @@ "@types/uuid": "^8.3.4", "@types/vscode": "~1.68.0", "@types/winreg": "^1.2.30", + "@types/which":"^2.0.1", "@types/xml2js": "^0.4.2", "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index e7f7fb14b3f0..d26e2a33047e 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -16,6 +16,8 @@ export type CommandsWithoutArgs = keyof ICommandNameWithoutArgumentTypeMapping; * @interface ICommandNameWithoutArgumentTypeMapping */ interface ICommandNameWithoutArgumentTypeMapping { + [Commands.InstallPythonOnMac]: []; + [Commands.InstallPythonOnLinux]: []; [Commands.InstallPython]: []; [Commands.ClearWorkspaceInterpreter]: []; [Commands.Set_Interpreter]: []; diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index 8a4c70cdc5d4..8dac6253e17c 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -57,6 +57,8 @@ export namespace Commands { export const RefreshTensorBoard = 'python.refreshTensorBoard'; export const ReportIssue = 'python.reportIssue'; export const InstallPython = 'python.installPython'; + export const InstallPythonOnMac = 'python.installPythonOnMac'; + export const InstallPythonOnLinux = 'python.installPythonOnLinux'; export const TriggerEnvironmentSelection = 'python.triggerEnvSelection'; } diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/installPython.ts b/src/client/interpreter/configuration/interpreterSelector/commands/installPython/index.ts similarity index 84% rename from src/client/interpreter/configuration/interpreterSelector/commands/installPython.ts rename to src/client/interpreter/configuration/interpreterSelector/commands/installPython/index.ts index ba837afeb4bc..fa3ecdc4b4d6 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/installPython.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/installPython/index.ts @@ -4,13 +4,13 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { IExtensionSingleActivationService } from '../../../../activation/types'; -import { ExtensionContextKey } from '../../../../common/application/contextKeys'; -import { ICommandManager, IContextKeyManager } from '../../../../common/application/types'; -import { PythonWelcome } from '../../../../common/application/walkThroughs'; -import { Commands, PVSC_EXTENSION_ID } from '../../../../common/constants'; -import { IBrowserService, IDisposableRegistry } from '../../../../common/types'; -import { IPlatformService } from '../../../../common/platform/types'; +import { IExtensionSingleActivationService } from '../../../../../activation/types'; +import { ExtensionContextKey } from '../../../../../common/application/contextKeys'; +import { ICommandManager, IContextKeyManager } from '../../../../../common/application/types'; +import { PythonWelcome } from '../../../../../common/application/walkThroughs'; +import { Commands, PVSC_EXTENSION_ID } from '../../../../../common/constants'; +import { IBrowserService, IDisposableRegistry } from '../../../../../common/types'; +import { IPlatformService } from '../../../../../common/platform/types'; @injectable() export class InstallPythonCommand implements IExtensionSingleActivationService { diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal.ts b/src/client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal.ts new file mode 100644 index 000000000000..f8f1c9a02e9b --- /dev/null +++ b/src/client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal.ts @@ -0,0 +1,81 @@ +/* eslint-disable global-require */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import type * as whichTypes from 'which'; +import { inject, injectable } from 'inversify'; +import { IExtensionSingleActivationService } from '../../../../../activation/types'; +import { Commands } from '../../../../../common/constants'; +import { IDisposableRegistry } from '../../../../../common/types'; +import { ITerminalServiceFactory } from '../../../../../common/terminal/types'; +import { ICommandManager } from '../../../../../common/application/types'; +import { sleep } from '../../../../../common/utils/async'; +import { OSType } from '../../../../../common/utils/platform'; +import { traceVerbose } from '../../../../../logging'; + +/** + * Runs commands listed in walkthrough to install Python. + */ +@injectable() +export class InstallPythonViaTerminal implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(ITerminalServiceFactory) private readonly terminalServiceFactory: ITerminalServiceFactory, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + ) {} + + public async activate(): Promise { + this.disposables.push( + this.commandManager.registerCommand(Commands.InstallPythonOnMac, () => + this._installPythonOnUnix(OSType.OSX), + ), + ); + this.disposables.push( + this.commandManager.registerCommand(Commands.InstallPythonOnLinux, () => + this._installPythonOnUnix(OSType.Linux), + ), + ); + } + + public async _installPythonOnUnix(os: OSType.Linux | OSType.OSX): Promise { + const terminalService = this.terminalServiceFactory.getTerminalService({}); + const commands = await getCommands(os); + for (const command of commands) { + await terminalService.sendText(command); + await waitForCommandToProcess(); + } + } +} + +async function getCommands(os: OSType.Linux | OSType.OSX) { + if (os === OSType.OSX) { + return ['brew install python3']; + } + return getCommandsForLinux(); +} + +async function getCommandsForLinux() { + let isDnfAvailable = false; + try { + const which = require('which') as typeof whichTypes; + const resolvedPath = await which('dnf'); + traceVerbose('Resolved path to dnf module:', resolvedPath); + isDnfAvailable = resolvedPath.trim().length > 0; + } catch (ex) { + traceVerbose('Dnf not found', ex); + isDnfAvailable = false; + } + return isDnfAvailable + ? ['sudo dnf install python3'] + : ['sudo apt-get update', 'sudo apt-get install python3 python3-venv python3-pip']; +} + +async function waitForCommandToProcess() { + // Give the command some time to complete. + // Its been observed that sending commands too early will strip some text off in VS Code Terminal. + await sleep(500); +} diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index af39479601de..6d36a4b9277d 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -12,6 +12,7 @@ import { InterpreterAutoSelectionProxyService } from './autoSelection/proxy'; import { IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService } from './autoSelection/types'; import { EnvironmentTypeComparer } from './configuration/environmentTypeComparer'; import { InstallPythonCommand } from './configuration/interpreterSelector/commands/installPython'; +import { InstallPythonViaTerminal } from './configuration/interpreterSelector/commands/installPython/installPythonViaTerminal'; import { ResetInterpreterCommand } from './configuration/interpreterSelector/commands/resetInterpreter'; import { SetInterpreterCommand } from './configuration/interpreterSelector/commands/setInterpreter'; import { SetShebangInterpreterCommand } from './configuration/interpreterSelector/commands/setShebangInterpreter'; @@ -45,6 +46,10 @@ export function registerInterpreterTypes(serviceManager: IServiceManager): void IExtensionSingleActivationService, InstallPythonCommand, ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + InstallPythonViaTerminal, + ); serviceManager.addSingleton( IExtensionSingleActivationService, SetInterpreterCommand, diff --git a/src/test/configuration/interpreterSelector/commands/installPythonViaTerminal.unit.test.ts b/src/test/configuration/interpreterSelector/commands/installPythonViaTerminal.unit.test.ts new file mode 100644 index 000000000000..8ad7138649c2 --- /dev/null +++ b/src/test/configuration/interpreterSelector/commands/installPythonViaTerminal.unit.test.ts @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import rewiremock from 'rewiremock'; +import * as sinon from 'sinon'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { ICommandManager } from '../../../../client/common/application/types'; +import { Commands } from '../../../../client/common/constants'; +import { ITerminalService, ITerminalServiceFactory } from '../../../../client/common/terminal/types'; +import { IDisposable } from '../../../../client/common/types'; +import { InstallPythonViaTerminal } from '../../../../client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal'; + +suite('Install Python via Terminal', () => { + let cmdManager: ICommandManager; + let terminalServiceFactory: ITerminalServiceFactory; + let installPythonCommand: InstallPythonViaTerminal; + let terminalService: ITerminalService; + setup(() => { + rewiremock.enable(); + cmdManager = mock(); + terminalServiceFactory = mock(); + terminalService = mock(); + when(terminalServiceFactory.getTerminalService(anything())).thenReturn(instance(terminalService)); + installPythonCommand = new InstallPythonViaTerminal(instance(cmdManager), instance(terminalServiceFactory), []); + }); + + teardown(() => { + rewiremock.disable(); + sinon.restore(); + }); + + test('Sends expected commands when InstallPythonOnLinux command is executed if no dnf is available', async () => { + let installCommandHandler: () => Promise; + when(cmdManager.registerCommand(Commands.InstallPythonOnLinux, anything())).thenCall((_, cb) => { + installCommandHandler = cb; + return TypeMoq.Mock.ofType().object; + }); + await installPythonCommand.activate(); + when(terminalService.sendText('sudo apt-get update')).thenResolve(); + when(terminalService.sendText('sudo apt-get install python3 python3-venv python3-pip')).thenResolve(); + + await installCommandHandler!(); + + verify(terminalService.sendText('sudo apt-get update')).once(); + verify(terminalService.sendText('sudo apt-get install python3 python3-venv python3-pip')).once(); + }); + + test('Sends expected commands when InstallPythonOnLinux command is executed if dnf is available', async () => { + let installCommandHandler: () => Promise; + when(cmdManager.registerCommand(Commands.InstallPythonOnLinux, anything())).thenCall((_, cb) => { + installCommandHandler = cb; + return TypeMoq.Mock.ofType().object; + }); + rewiremock('which').with((cmd: string) => { + if (cmd === 'dnf') { + return 'path/to/dnf'; + } + throw new Error('Command not found'); + }); + + await installPythonCommand.activate(); + when(terminalService.sendText('sudo dnf install python3')).thenResolve(); + + await installCommandHandler!(); + + verify(terminalService.sendText('sudo dnf install python3')).once(); + }); + + test('Sends expected commands on Mac when InstallPythonOnMac command is executed if no dnf is available', async () => { + let installCommandHandler: () => Promise; + when(cmdManager.registerCommand(Commands.InstallPythonOnMac, anything())).thenCall((_, cb) => { + installCommandHandler = cb; + return TypeMoq.Mock.ofType().object; + }); + await installPythonCommand.activate(); + when(terminalService.sendText('brew install python3')).thenResolve(); + + await installCommandHandler!(); + + verify(terminalService.sendText('brew install python3')).once(); + }); +}); diff --git a/src/test/interpreters/serviceRegistry.unit.test.ts b/src/test/interpreters/serviceRegistry.unit.test.ts index 44e14c6c09e3..442ad7cdf3d6 100644 --- a/src/test/interpreters/serviceRegistry.unit.test.ts +++ b/src/test/interpreters/serviceRegistry.unit.test.ts @@ -15,6 +15,7 @@ import { } from '../../client/interpreter/autoSelection/types'; import { EnvironmentTypeComparer } from '../../client/interpreter/configuration/environmentTypeComparer'; import { InstallPythonCommand } from '../../client/interpreter/configuration/interpreterSelector/commands/installPython'; +import { InstallPythonViaTerminal } from '../../client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal'; import { ResetInterpreterCommand } from '../../client/interpreter/configuration/interpreterSelector/commands/resetInterpreter'; import { SetInterpreterCommand } from '../../client/interpreter/configuration/interpreterSelector/commands/setInterpreter'; import { SetShebangInterpreterCommand } from '../../client/interpreter/configuration/interpreterSelector/commands/setShebangInterpreter'; @@ -50,6 +51,7 @@ suite('Interpreters - Service Registry', () => { [ [IExtensionSingleActivationService, InstallPythonCommand], + [IExtensionSingleActivationService, InstallPythonViaTerminal], [IExtensionSingleActivationService, SetInterpreterCommand], [IExtensionSingleActivationService, ResetInterpreterCommand], [IExtensionSingleActivationService, SetShebangInterpreterCommand], From f731c149f9e9e7ac1cfdeb6c4a23ab17b6f7ece1 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 15 Jul 2022 15:10:58 -0700 Subject: [PATCH 2/2] Register commands regardless of whether workspace is trusted or not --- .../interpreterSelector/commands/installPython/index.ts | 2 +- .../commands/installPython/installPythonViaTerminal.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/installPython/index.ts b/src/client/interpreter/configuration/interpreterSelector/commands/installPython/index.ts index fa3ecdc4b4d6..fef63a49e6d2 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/installPython/index.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/installPython/index.ts @@ -14,7 +14,7 @@ import { IPlatformService } from '../../../../../common/platform/types'; @injectable() export class InstallPythonCommand implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: false }; constructor( @inject(ICommandManager) private readonly commandManager: ICommandManager, diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal.ts b/src/client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal.ts index f8f1c9a02e9b..45de9ac5d527 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal.ts @@ -20,7 +20,7 @@ import { traceVerbose } from '../../../../../logging'; */ @injectable() export class InstallPythonViaTerminal implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: false }; constructor( @inject(ICommandManager) private readonly commandManager: ICommandManager,