From 05c3bd29b13505ddf24b8566f670ebec0d448d65 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 29 Sep 2020 12:04:42 -0700 Subject: [PATCH 1/6] Initial commit for windows store locator --- .../pythonEnvironments/base/info/index.ts | 3 +- .../common/environmentIdentifier.ts | 2 +- .../common/externalDependencies.ts | 8 + .../pythonEnvironments/common/windowsUtils.ts | 76 ++++++ .../locators/services/windowsStoreLocator.ts | 164 ++++++------ .../locators/windowsStoreLocator.unit.test.ts | 249 ++++++++++++++++-- 6 files changed, 400 insertions(+), 102 deletions(-) diff --git a/src/client/pythonEnvironments/base/info/index.ts b/src/client/pythonEnvironments/base/info/index.ts index 4418860a12e5..1534cdaabd2b 100644 --- a/src/client/pythonEnvironments/base/info/index.ts +++ b/src/client/pythonEnvironments/base/info/index.ts @@ -81,7 +81,8 @@ export enum PythonReleaseLevel { Alpha = 'alpha', Beta = 'beta', Candidate = 'candidate', - Final = 'final' + Final = 'final', + Unknown = 'unknown' } /** diff --git a/src/client/pythonEnvironments/common/environmentIdentifier.ts b/src/client/pythonEnvironments/common/environmentIdentifier.ts index 3340cb01bdaf..33af08a24b20 100644 --- a/src/client/pythonEnvironments/common/environmentIdentifier.ts +++ b/src/client/pythonEnvironments/common/environmentIdentifier.ts @@ -7,8 +7,8 @@ import { isPyenvEnvironment } from '../discovery/locators/services/pyenvLocator' import { isVenvEnvironment } from '../discovery/locators/services/venvLocator'; import { isVirtualenvEnvironment } from '../discovery/locators/services/virtualenvLocator'; import { isVirtualenvwrapperEnvironment } from '../discovery/locators/services/virtualenvwrapperLocator'; -import { isWindowsStoreEnvironment } from '../discovery/locators/services/windowsStoreLocator'; import { EnvironmentType } from '../info'; +import { isWindowsStoreEnvironment } from './windowsUtils'; /** * Gets a prioritized list of environment types for identification. diff --git a/src/client/pythonEnvironments/common/externalDependencies.ts b/src/client/pythonEnvironments/common/externalDependencies.ts index 0ff392ed51d4..dd44f69c224f 100644 --- a/src/client/pythonEnvironments/common/externalDependencies.ts +++ b/src/client/pythonEnvironments/common/externalDependencies.ts @@ -57,3 +57,11 @@ export function getGlobalPersistentStore(key: string): IPersistentStore { set(value: T) { return state.updateValue(value); }, }; } + +export async function getFileInfo(filePath: string): Promise<{ctime:number, mtime:number}> { + const data = await fsapi.lstat(filePath); + return { + ctime: data.ctime.getUTCDate(), + mtime: data.mtime.getUTCDate(), + }; +} diff --git a/src/client/pythonEnvironments/common/windowsUtils.ts b/src/client/pythonEnvironments/common/windowsUtils.ts index d9be7d78fdf9..48eb418db68e 100644 --- a/src/client/pythonEnvironments/common/windowsUtils.ts +++ b/src/client/pythonEnvironments/common/windowsUtils.ts @@ -2,6 +2,8 @@ // Licensed under the MIT License. import * as path from 'path'; +import { traceWarning } from '../../common/logger'; +import { getEnvironmentVariable } from '../../common/utils/platform'; /** * Checks if a given path ends with python*.exe @@ -20,3 +22,77 @@ export function isWindowsPythonExe(interpreterPath:string): boolean { return windowsPythonExes.test(path.basename(interpreterPath)); } + +/** + * Gets path to the Windows Apps directory. + * @returns {string} : Returns path to the Windows Apps directory under + * `%LOCALAPPDATA%/Microsoft/WindowsApps`. + */ +export function getWindowsStoreAppsRoot(): string { + const localAppData = getEnvironmentVariable('LOCALAPPDATA') || ''; + return path.join(localAppData, 'Microsoft', 'WindowsApps'); +} + +/** + * Checks if a given path is under the forbidden windows store directory. + * @param {string} interpreterPath : Absolute path to the python interpreter. + * @returns {boolean} : Returns true if `interpreterPath` is under + * `%ProgramFiles%/WindowsApps`. + */ +export function isForbiddenStorePath(interpreterPath:string):boolean { + const programFilesStorePath = path + .join(getEnvironmentVariable('ProgramFiles') || 'Program Files', 'WindowsApps') + .normalize() + .toUpperCase(); + return path.normalize(interpreterPath).toUpperCase().includes(programFilesStorePath); +} + +/** + * Checks if the given interpreter belongs to Windows Store Python environment. + * @param interpreterPath: Absolute path to any python interpreter. + * + * Remarks: + * 1. Checking if the path includes `Microsoft\WindowsApps`, `Program Files\WindowsApps`, is + * NOT enough. In WSL, `/mnt/c/users/user/AppData/Local/Microsoft/WindowsApps` is available as a search + * path. It is possible to get a false positive for that path. So the comparison should check if the + * absolute path to 'WindowsApps' directory is present in the given interpreter path. The WSL path to + * 'WindowsApps' is not a valid path to access, Windows Store Python. + * + * 2. 'startsWith' comparison may not be right, user can provide '\\?\C:\users\' style long paths in windows. + * + * 3. A limitation of the checks here is that they don't handle 8.3 style windows paths. + * For example, + * `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE` + * is the shortened form of + * `C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe` + * + * The correct way to compare these would be to always convert given paths to long path (or to short path). + * For either approach to work correctly you need actual file to exist, and accessible from the user's + * account. + * + * To convert to short path without using N-API in node would be to use this command. This is very expensive: + * `> cmd /c for %A in ("C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe") do @echo %~sA` + * The above command will print out this: + * `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE` + * + * If we go down the N-API route, use node-ffi and either call GetShortPathNameW or GetLongPathNameW from, + * Kernel32 to convert between the two path variants. + * + */ +export async function isWindowsStoreEnvironment(interpreterPath: string): Promise { + const pythonPathToCompare = path.normalize(interpreterPath).toUpperCase(); + const localAppDataStorePath = path + .normalize(getWindowsStoreAppsRoot()) + .toUpperCase(); + if (pythonPathToCompare.includes(localAppDataStorePath)) { + return true; + } + + // Program Files store path is a forbidden path. Only admins and system has access this path. + // We should never have to look at this path or even execute python from this path. + if (isForbiddenStorePath(pythonPathToCompare)) { + traceWarning('isWindowsStoreEnvironment called with Program Files store path.'); + return true; + } + return false; +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts b/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts index 6eae9dac5126..27b01747ad51 100644 --- a/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts +++ b/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts @@ -3,83 +3,17 @@ import * as fsapi from 'fs-extra'; import * as path from 'path'; -import { traceWarning } from '../../../../common/logger'; -import { getEnvironmentVariable } from '../../../../common/utils/platform'; -import { isWindowsPythonExe } from '../../../common/windowsUtils'; - -/** - * Gets path to the Windows Apps directory. - * @returns {string} : Returns path to the Windows Apps directory under - * `%LOCALAPPDATA%/Microsoft/WindowsApps`. - */ -export function getWindowsStoreAppsRoot(): string { - const localAppData = getEnvironmentVariable('LOCALAPPDATA') || ''; - return path.join(localAppData, 'Microsoft', 'WindowsApps'); -} - -/** - * Checks if a given path is under the forbidden windows store directory. - * @param {string} interpreterPath : Absolute path to the python interpreter. - * @returns {boolean} : Returns true if `interpreterPath` is under - * `%ProgramFiles%/WindowsApps`. - */ -export function isForbiddenStorePath(interpreterPath:string):boolean { - const programFilesStorePath = path - .join(getEnvironmentVariable('ProgramFiles') || 'Program Files', 'WindowsApps') - .normalize() - .toUpperCase(); - return path.normalize(interpreterPath).toUpperCase().includes(programFilesStorePath); -} - -/** - * Checks if the given interpreter belongs to Windows Store Python environment. - * @param interpreterPath: Absolute path to any python interpreter. - * - * Remarks: - * 1. Checking if the path includes `Microsoft\WindowsApps`, `Program Files\WindowsApps`, is - * NOT enough. In WSL, `/mnt/c/users/user/AppData/Local/Microsoft/WindowsApps` is available as a search - * path. It is possible to get a false positive for that path. So the comparison should check if the - * absolute path to 'WindowsApps' directory is present in the given interpreter path. The WSL path to - * 'WindowsApps' is not a valid path to access, Windows Store Python. - * - * 2. 'startsWith' comparison may not be right, user can provide '\\?\C:\users\' style long paths in windows. - * - * 3. A limitation of the checks here is that they don't handle 8.3 style windows paths. - * For example, - * `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE` - * is the shortened form of - * `C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe` - * - * The correct way to compare these would be to always convert given paths to long path (or to short path). - * For either approach to work correctly you need actual file to exist, and accessible from the user's - * account. - * - * To convert to short path without using N-API in node would be to use this command. This is very expensive: - * `> cmd /c for %A in ("C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe") do @echo %~sA` - * The above command will print out this: - * `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE` - * - * If we go down the N-API route, use node-ffi and either call GetShortPathNameW or GetLongPathNameW from, - * Kernel32 to convert between the two path variants. - * - */ -export async function isWindowsStoreEnvironment(interpreterPath: string): Promise { - const pythonPathToCompare = path.normalize(interpreterPath).toUpperCase(); - const localAppDataStorePath = path - .normalize(getWindowsStoreAppsRoot()) - .toUpperCase(); - if (pythonPathToCompare.includes(localAppDataStorePath)) { - return true; - } - - // Program Files store path is a forbidden path. Only admins and system has access this path. - // We should never have to look at this path or even execute python from this path. - if (isForbiddenStorePath(pythonPathToCompare)) { - traceWarning('isWindowsStoreEnvironment called with Program Files store path.'); - return true; - } - return false; -} +import { Event, EventEmitter } from 'vscode'; +import { Architecture } from '../../../../common/utils/platform'; +import { + PythonEnvInfo, PythonEnvKind, PythonReleaseLevel, PythonVersion, +} from '../../../base/info'; +import { parseVersion } from '../../../base/info/pythonVersion'; +import { ILocator, IPythonEnvsIterator } from '../../../base/locator'; +import { PythonEnvsChangedEvent } from '../../../base/watcher'; +import { getFileInfo } from '../../../common/externalDependencies'; +import { getWindowsStoreAppsRoot, isWindowsPythonExe, isWindowsStoreEnvironment } from '../../../common/windowsUtils'; +import { IEnvironmentInfoService } from '../../../info/environmentInfoService'; /** * Gets paths to the Python executable under Windows Store apps. @@ -107,5 +41,77 @@ export async function getWindowsStorePythonExes(): Promise { .filter(isWindowsPythonExe); } -// tslint:disable-next-line: no-suspicious-comment -// TODO: The above APIs will be consumed by the Windows Store locator class when we have it. +export class WindowsStoreLocator implements ILocator { + private readonly kind:PythonEnvKind = PythonEnvKind.WindowsStore; + + private readonly eventEmitter = new EventEmitter(); + + public constructor(private readonly envService:IEnvironmentInfoService) { } + + public iterEnvs(): IPythonEnvsIterator { + const buildEnvInfo = (exe:string) => this.buildEnvInfo(exe); + const iterator = async function* () { + const exes = await getWindowsStorePythonExes(); + yield* exes.map(buildEnvInfo); + }; + return iterator(); + } + + public async resolveEnv(env: string | PythonEnvInfo): Promise { + const executablePath = typeof env === 'string' ? env : env.executable.filename; + if (isWindowsStoreEnvironment(executablePath)) { + const interpreterInfo = await this.envService.getEnvironmentInfo(executablePath); + if (interpreterInfo) { + const data = await getFileInfo(executablePath); + interpreterInfo.executable = { + ...interpreterInfo.executable, + ...data, + }; + return Promise.resolve({ + id: '', + name: '', + location: '', + kind: this.kind, + executable: interpreterInfo.executable, + version: interpreterInfo.version, + arch: interpreterInfo.arch, + distro: { org: 'Microsoft' }, + }); + } + } + return undefined; + } + + public get onChanged(): Event { + return this.eventEmitter.event; + } + + private async buildEnvInfo(exe:string): Promise { + let version:PythonVersion; + try { + version = parseVersion(path.basename(exe)); + } catch (e) { + version = { + major: 3, + minor: -1, + micro: -1, + release: { level: PythonReleaseLevel.Unknown, serial: -1 }, + sysVersion: undefined, + }; + } + return { + id: '', + name: '', + location: '', + kind: this.kind, + executable: { + filename: exe, + sysPrefix: '', + ...(await getFileInfo(exe)), + }, + version, + arch: Architecture.x64, + distro: { org: 'Microsoft' }, + }; + } +} diff --git a/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts index 6bbba7c7bef2..a54531238d38 100644 --- a/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts +++ b/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts @@ -2,32 +2,239 @@ // Licensed under the MIT License. import * as assert from 'assert'; +import { zip } from 'lodash'; import * as path from 'path'; import * as sinon from 'sinon'; +import { ExecutionResult } from '../../../../client/common/process/types'; import * as platformApis from '../../../../client/common/utils/platform'; -import * as storeApis from '../../../../client/pythonEnvironments/discovery/locators/services/windowsStoreLocator'; +import { + PythonEnvInfo, PythonEnvKind, PythonReleaseLevel, PythonVersion, +} from '../../../../client/pythonEnvironments/base/info'; +import { InterpreterInformation } from '../../../../client/pythonEnvironments/base/info/interpreter'; +import { parseVersion } from '../../../../client/pythonEnvironments/base/info/pythonVersion'; +import * as externalDep from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { getWindowsStorePythonExes, WindowsStoreLocator } from '../../../../client/pythonEnvironments/discovery/locators/services/windowsStoreLocator'; +import { EnvironmentInfoService } from '../../../../client/pythonEnvironments/info/environmentInfoService'; +import { getEnvs } from '../../base/common'; import { TEST_LAYOUT_ROOT } from '../../common/commonTestConstants'; -suite('Windows Store Utils', () => { - let getEnvVar: sinon.SinonStub; - const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps'); - const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps'); - setup(() => { - getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); - getEnvVar.withArgs('LOCALAPPDATA').returns(testLocalAppData); - }); - teardown(() => { - getEnvVar.restore(); +suite('Windows Store', () => { + suite('Utils', () => { + let getEnvVar: sinon.SinonStub; + const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps'); + const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps'); + + setup(() => { + getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); + getEnvVar.withArgs('LOCALAPPDATA').returns(testLocalAppData); + }); + + teardown(() => { + getEnvVar.restore(); + }); + + test('Store Python Interpreters', async () => { + const expected = [ + path.join(testStoreAppRoot, 'python.exe'), + path.join(testStoreAppRoot, 'python3.7.exe'), + path.join(testStoreAppRoot, 'python3.8.exe'), + path.join(testStoreAppRoot, 'python3.exe'), + ]; + + const actual = await getWindowsStorePythonExes(); + assert.deepEqual(actual, expected); + }); }); - test('Store Python Interpreters', async () => { - const expected = [ - path.join(testStoreAppRoot, 'python.exe'), - path.join(testStoreAppRoot, 'python3.7.exe'), - path.join(testStoreAppRoot, 'python3.8.exe'), - path.join(testStoreAppRoot, 'python3.exe'), - ]; - - const actual = await storeApis.getWindowsStorePythonExes(); - assert.deepEqual(actual, expected); + + suite('Locator', () => { + let stubShellExec: sinon.SinonStub; + let getEnvVar: sinon.SinonStub; + + const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps'); + const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps'); + const pathToData = new Map(); + + const python383data = { + versionInfo: [3, 8, 3, 'final', 0], + sysPrefix: 'path', + sysVersion: '3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]', + is64Bit: true, + }; + + const python379data = { + versionInfo: [3, 7, 9, 'final', 0], + sysPrefix: 'path', + sysVersion: '3.7.9 (tags/v3.7.9:13c94747c7, Aug 17 2020, 16:30:00) [MSC v.1900 64 bit (AMD64)]', + is64Bit: true, + }; + + pathToData.set(path.join(testStoreAppRoot, 'python.exe'), python383data); + pathToData.set(path.join(testStoreAppRoot, 'python3.exe'), python383data); + pathToData.set(path.join(testStoreAppRoot, 'python3.8.exe'), python383data); + pathToData.set(path.join(testStoreAppRoot, 'python3.7.exe'), python379data); + + function createExpectedInterpreterInfo( + executable: string, + sysVersion?: string, + sysPrefix?: string, + versionStr?:string, + ): InterpreterInformation { + let version:PythonVersion; + try { + version = parseVersion(versionStr ?? path.basename(executable)); + if (sysVersion) { + version.sysVersion = sysVersion; + } + } catch (e) { + version = { + major: 3, + minor: -1, + micro: -1, + release: { level: PythonReleaseLevel.Unknown, serial: -1 }, + sysVersion, + }; + } + return { + version, + arch: platformApis.Architecture.x64, + executable: { + filename: executable, + sysPrefix: sysPrefix ?? '', + ctime: -1, + mtime: -1, + }, + }; + } + + setup(() => { + stubShellExec = sinon.stub(externalDep, 'shellExecute'); + stubShellExec.callsFake((command:string) => { + if (command.indexOf('notpython.exe') > 0) { + return Promise.resolve>({ stdout: '' }); + } + if (command.indexOf('python3.7.exe') > 0) { + return Promise.resolve>({ stdout: JSON.stringify(python379data) }); + } + return Promise.resolve>({ stdout: JSON.stringify(python383data) }); + }); + + getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); + getEnvVar.withArgs('LOCALAPPDATA').returns(testLocalAppData); + }); + + teardown(() => { + stubShellExec.restore(); + getEnvVar.restore(); + }); + + function assertEnvEqual(actual:PythonEnvInfo | undefined, expected: PythonEnvInfo | undefined):void { + assert.notStrictEqual(actual, undefined); + assert.notStrictEqual(expected, undefined); + + if (actual) { + // ensure ctime and mtime are greater than -1 + assert.ok(actual?.executable.ctime > -1); + assert.ok(actual?.executable.mtime > -1); + + // No need to match these, so reset them + actual.executable.ctime = -1; + actual.executable.mtime = -1; + + assert.deepStrictEqual(actual, expected); + } + } + + test('iterEnvs()', async () => { + const expectedEnvs = [...pathToData.keys()] + .sort((a: string, b: string) => a.localeCompare(b)) + .map((k): PythonEnvInfo|undefined => { + const data = pathToData.get(k); + if (data) { + return { + id: '', + name: '', + location: '', + kind: PythonEnvKind.WindowsStore, + distro: { org: 'Microsoft' }, + ...createExpectedInterpreterInfo(k), + }; + } + return undefined; + }); + + const envService = new EnvironmentInfoService(); + const locator = new WindowsStoreLocator(envService); + const iterator = locator.iterEnvs(); + const actualEnvs = (await getEnvs(iterator)) + .sort((a, b) => a.executable.filename.localeCompare(b.executable.filename)); + + zip(actualEnvs, expectedEnvs).forEach((value) => { + const [actual, expected] = value; + assertEnvEqual(actual, expected); + }); + }); + + test('resolveEnv(string)', async () => { + const python38path = path.join(testStoreAppRoot, 'python3.8.exe'); + const expected = { + id: '', + name: '', + location: '', + kind: PythonEnvKind.WindowsStore, + distro: { org: 'Microsoft' }, + ...createExpectedInterpreterInfo(python38path, python383data.sysVersion, python383data.sysPrefix, '3.8.3'), + }; + + const envService = new EnvironmentInfoService(); + const locator = new WindowsStoreLocator(envService); + const actual = await locator.resolveEnv(python38path); + + assertEnvEqual(actual, expected); + }); + + test('resolveEnv(PythonEnvInfo)', async () => { + const python38path = path.join(testStoreAppRoot, 'python3.8.exe'); + const expected = { + id: '', + name: '', + location: '', + kind: PythonEnvKind.WindowsStore, + distro: { org: 'Microsoft' }, + ...createExpectedInterpreterInfo(python38path, python383data.sysVersion, python383data.sysPrefix, '3.8.3'), + }; + + // Partially filled in env info object + const input:PythonEnvInfo = { + id: '', + name: '', + location: '', + kind: PythonEnvKind.WindowsStore, + distro: { org: 'Microsoft' }, + arch: platformApis.Architecture.x64, + executable: { + filename: python38path, + sysPrefix: '', + ctime: -1, + mtime: -1, + }, + version: { + major: 3, + minor: -1, + micro: -1, + release: { level: PythonReleaseLevel.Unknown, serial: -1 }, + }, + }; + + const envService = new EnvironmentInfoService(); + const locator = new WindowsStoreLocator(envService); + const actual = await locator.resolveEnv(input); + + assertEnvEqual(actual, expected); + }); }); }); From 0112df5efc0075796db0841b211bfd13a2374a64 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 29 Sep 2020 12:26:36 -0700 Subject: [PATCH 2/6] More tests --- .../common/environmentIdentifier.ts | 2 +- .../pythonEnvironments/common/windowsUtils.ts | 76 ----------------- .../locators/services/windowsStoreLocator.ts | 83 +++++++++++++++++-- .../locators/windowsStoreLocator.unit.test.ts | 17 +++- 4 files changed, 92 insertions(+), 86 deletions(-) diff --git a/src/client/pythonEnvironments/common/environmentIdentifier.ts b/src/client/pythonEnvironments/common/environmentIdentifier.ts index 33af08a24b20..3340cb01bdaf 100644 --- a/src/client/pythonEnvironments/common/environmentIdentifier.ts +++ b/src/client/pythonEnvironments/common/environmentIdentifier.ts @@ -7,8 +7,8 @@ import { isPyenvEnvironment } from '../discovery/locators/services/pyenvLocator' import { isVenvEnvironment } from '../discovery/locators/services/venvLocator'; import { isVirtualenvEnvironment } from '../discovery/locators/services/virtualenvLocator'; import { isVirtualenvwrapperEnvironment } from '../discovery/locators/services/virtualenvwrapperLocator'; +import { isWindowsStoreEnvironment } from '../discovery/locators/services/windowsStoreLocator'; import { EnvironmentType } from '../info'; -import { isWindowsStoreEnvironment } from './windowsUtils'; /** * Gets a prioritized list of environment types for identification. diff --git a/src/client/pythonEnvironments/common/windowsUtils.ts b/src/client/pythonEnvironments/common/windowsUtils.ts index 48eb418db68e..d9be7d78fdf9 100644 --- a/src/client/pythonEnvironments/common/windowsUtils.ts +++ b/src/client/pythonEnvironments/common/windowsUtils.ts @@ -2,8 +2,6 @@ // Licensed under the MIT License. import * as path from 'path'; -import { traceWarning } from '../../common/logger'; -import { getEnvironmentVariable } from '../../common/utils/platform'; /** * Checks if a given path ends with python*.exe @@ -22,77 +20,3 @@ export function isWindowsPythonExe(interpreterPath:string): boolean { return windowsPythonExes.test(path.basename(interpreterPath)); } - -/** - * Gets path to the Windows Apps directory. - * @returns {string} : Returns path to the Windows Apps directory under - * `%LOCALAPPDATA%/Microsoft/WindowsApps`. - */ -export function getWindowsStoreAppsRoot(): string { - const localAppData = getEnvironmentVariable('LOCALAPPDATA') || ''; - return path.join(localAppData, 'Microsoft', 'WindowsApps'); -} - -/** - * Checks if a given path is under the forbidden windows store directory. - * @param {string} interpreterPath : Absolute path to the python interpreter. - * @returns {boolean} : Returns true if `interpreterPath` is under - * `%ProgramFiles%/WindowsApps`. - */ -export function isForbiddenStorePath(interpreterPath:string):boolean { - const programFilesStorePath = path - .join(getEnvironmentVariable('ProgramFiles') || 'Program Files', 'WindowsApps') - .normalize() - .toUpperCase(); - return path.normalize(interpreterPath).toUpperCase().includes(programFilesStorePath); -} - -/** - * Checks if the given interpreter belongs to Windows Store Python environment. - * @param interpreterPath: Absolute path to any python interpreter. - * - * Remarks: - * 1. Checking if the path includes `Microsoft\WindowsApps`, `Program Files\WindowsApps`, is - * NOT enough. In WSL, `/mnt/c/users/user/AppData/Local/Microsoft/WindowsApps` is available as a search - * path. It is possible to get a false positive for that path. So the comparison should check if the - * absolute path to 'WindowsApps' directory is present in the given interpreter path. The WSL path to - * 'WindowsApps' is not a valid path to access, Windows Store Python. - * - * 2. 'startsWith' comparison may not be right, user can provide '\\?\C:\users\' style long paths in windows. - * - * 3. A limitation of the checks here is that they don't handle 8.3 style windows paths. - * For example, - * `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE` - * is the shortened form of - * `C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe` - * - * The correct way to compare these would be to always convert given paths to long path (or to short path). - * For either approach to work correctly you need actual file to exist, and accessible from the user's - * account. - * - * To convert to short path without using N-API in node would be to use this command. This is very expensive: - * `> cmd /c for %A in ("C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe") do @echo %~sA` - * The above command will print out this: - * `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE` - * - * If we go down the N-API route, use node-ffi and either call GetShortPathNameW or GetLongPathNameW from, - * Kernel32 to convert between the two path variants. - * - */ -export async function isWindowsStoreEnvironment(interpreterPath: string): Promise { - const pythonPathToCompare = path.normalize(interpreterPath).toUpperCase(); - const localAppDataStorePath = path - .normalize(getWindowsStoreAppsRoot()) - .toUpperCase(); - if (pythonPathToCompare.includes(localAppDataStorePath)) { - return true; - } - - // Program Files store path is a forbidden path. Only admins and system has access this path. - // We should never have to look at this path or even execute python from this path. - if (isForbiddenStorePath(pythonPathToCompare)) { - traceWarning('isWindowsStoreEnvironment called with Program Files store path.'); - return true; - } - return false; -} diff --git a/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts b/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts index 27b01747ad51..fcf77b6d371a 100644 --- a/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts +++ b/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts @@ -4,7 +4,8 @@ import * as fsapi from 'fs-extra'; import * as path from 'path'; import { Event, EventEmitter } from 'vscode'; -import { Architecture } from '../../../../common/utils/platform'; +import { traceWarning } from '../../../../common/logger'; +import { Architecture, getEnvironmentVariable } from '../../../../common/utils/platform'; import { PythonEnvInfo, PythonEnvKind, PythonReleaseLevel, PythonVersion, } from '../../../base/info'; @@ -12,9 +13,83 @@ import { parseVersion } from '../../../base/info/pythonVersion'; import { ILocator, IPythonEnvsIterator } from '../../../base/locator'; import { PythonEnvsChangedEvent } from '../../../base/watcher'; import { getFileInfo } from '../../../common/externalDependencies'; -import { getWindowsStoreAppsRoot, isWindowsPythonExe, isWindowsStoreEnvironment } from '../../../common/windowsUtils'; +import { isWindowsPythonExe } from '../../../common/windowsUtils'; import { IEnvironmentInfoService } from '../../../info/environmentInfoService'; +/** + * Gets path to the Windows Apps directory. + * @returns {string} : Returns path to the Windows Apps directory under + * `%LOCALAPPDATA%/Microsoft/WindowsApps`. + */ +export function getWindowsStoreAppsRoot(): string { + const localAppData = getEnvironmentVariable('LOCALAPPDATA') || ''; + return path.join(localAppData, 'Microsoft', 'WindowsApps'); +} + +/** + * Checks if a given path is under the forbidden windows store directory. + * @param {string} interpreterPath : Absolute path to the python interpreter. + * @returns {boolean} : Returns true if `interpreterPath` is under + * `%ProgramFiles%/WindowsApps`. + */ +export function isForbiddenStorePath(interpreterPath:string):boolean { + const programFilesStorePath = path + .join(getEnvironmentVariable('ProgramFiles') || 'Program Files', 'WindowsApps') + .normalize() + .toUpperCase(); + return path.normalize(interpreterPath).toUpperCase().includes(programFilesStorePath); +} + +/** + * Checks if the given interpreter belongs to Windows Store Python environment. + * @param interpreterPath: Absolute path to any python interpreter. + * + * Remarks: + * 1. Checking if the path includes `Microsoft\WindowsApps`, `Program Files\WindowsApps`, is + * NOT enough. In WSL, `/mnt/c/users/user/AppData/Local/Microsoft/WindowsApps` is available as a search + * path. It is possible to get a false positive for that path. So the comparison should check if the + * absolute path to 'WindowsApps' directory is present in the given interpreter path. The WSL path to + * 'WindowsApps' is not a valid path to access, Windows Store Python. + * + * 2. 'startsWith' comparison may not be right, user can provide '\\?\C:\users\' style long paths in windows. + * + * 3. A limitation of the checks here is that they don't handle 8.3 style windows paths. + * For example, + * `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE` + * is the shortened form of + * `C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe` + * + * The correct way to compare these would be to always convert given paths to long path (or to short path). + * For either approach to work correctly you need actual file to exist, and accessible from the user's + * account. + * + * To convert to short path without using N-API in node would be to use this command. This is very expensive: + * `> cmd /c for %A in ("C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe") do @echo %~sA` + * The above command will print out this: + * `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE` + * + * If we go down the N-API route, use node-ffi and either call GetShortPathNameW or GetLongPathNameW from, + * Kernel32 to convert between the two path variants. + * + */ +export async function isWindowsStoreEnvironment(interpreterPath: string): Promise { + const pythonPathToCompare = path.normalize(interpreterPath).toUpperCase(); + const localAppDataStorePath = path + .normalize(getWindowsStoreAppsRoot()) + .toUpperCase(); + if (pythonPathToCompare.includes(localAppDataStorePath)) { + return true; + } + + // Program Files store path is a forbidden path. Only admins and system has access this path. + // We should never have to look at this path or even execute python from this path. + if (isForbiddenStorePath(pythonPathToCompare)) { + traceWarning('isWindowsStoreEnvironment called with Program Files store path.'); + return true; + } + return false; +} + /** * Gets paths to the Python executable under Windows Store apps. * @returns: Returns python*.exe for the windows store app root directory. @@ -59,7 +134,7 @@ export class WindowsStoreLocator implements ILocator { public async resolveEnv(env: string | PythonEnvInfo): Promise { const executablePath = typeof env === 'string' ? env : env.executable.filename; - if (isWindowsStoreEnvironment(executablePath)) { + if (await isWindowsStoreEnvironment(executablePath)) { const interpreterInfo = await this.envService.getEnvironmentInfo(executablePath); if (interpreterInfo) { const data = await getFileInfo(executablePath); @@ -68,7 +143,6 @@ export class WindowsStoreLocator implements ILocator { ...data, }; return Promise.resolve({ - id: '', name: '', location: '', kind: this.kind, @@ -100,7 +174,6 @@ export class WindowsStoreLocator implements ILocator { }; } return { - id: '', name: '', location: '', kind: this.kind, diff --git a/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts index a54531238d38..e6fecc4243e4 100644 --- a/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts +++ b/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts @@ -156,7 +156,7 @@ suite('Windows Store', () => { const data = pathToData.get(k); if (data) { return { - id: '', + name: '', location: '', kind: PythonEnvKind.WindowsStore, @@ -182,7 +182,7 @@ suite('Windows Store', () => { test('resolveEnv(string)', async () => { const python38path = path.join(testStoreAppRoot, 'python3.8.exe'); const expected = { - id: '', + name: '', location: '', kind: PythonEnvKind.WindowsStore, @@ -200,7 +200,7 @@ suite('Windows Store', () => { test('resolveEnv(PythonEnvInfo)', async () => { const python38path = path.join(testStoreAppRoot, 'python3.8.exe'); const expected = { - id: '', + name: '', location: '', kind: PythonEnvKind.WindowsStore, @@ -210,7 +210,6 @@ suite('Windows Store', () => { // Partially filled in env info object const input:PythonEnvInfo = { - id: '', name: '', location: '', kind: PythonEnvKind.WindowsStore, @@ -236,5 +235,15 @@ suite('Windows Store', () => { assertEnvEqual(actual, expected); }); + test('resolveEnv(string): Non store python', async () => { + // Use a non store root path + const python38path = path.join(testLocalAppData, 'python3.8.exe'); + + const envService = new EnvironmentInfoService(); + const locator = new WindowsStoreLocator(envService); + const actual = await locator.resolveEnv(python38path); + + assert.deepStrictEqual(actual, undefined); + }); }); }); From 24445acf71ead4d9ad31fa2166576594cd8c6536 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 29 Sep 2020 19:56:59 -0700 Subject: [PATCH 3/6] Simplify locator --- .../locators/services/windowsStoreLocator.ts | 34 +++---------------- .../locators/windowsStoreLocator.unit.test.ts | 30 +++++++++------- 2 files changed, 21 insertions(+), 43 deletions(-) diff --git a/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts b/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts index fcf77b6d371a..14f8cb1fc482 100644 --- a/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts +++ b/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts @@ -3,7 +3,6 @@ import * as fsapi from 'fs-extra'; import * as path from 'path'; -import { Event, EventEmitter } from 'vscode'; import { traceWarning } from '../../../../common/logger'; import { Architecture, getEnvironmentVariable } from '../../../../common/utils/platform'; import { @@ -11,10 +10,9 @@ import { } from '../../../base/info'; import { parseVersion } from '../../../base/info/pythonVersion'; import { ILocator, IPythonEnvsIterator } from '../../../base/locator'; -import { PythonEnvsChangedEvent } from '../../../base/watcher'; +import { PythonEnvsWatcher } from '../../../base/watcher'; import { getFileInfo } from '../../../common/externalDependencies'; import { isWindowsPythonExe } from '../../../common/windowsUtils'; -import { IEnvironmentInfoService } from '../../../info/environmentInfoService'; /** * Gets path to the Windows Apps directory. @@ -116,13 +114,9 @@ export async function getWindowsStorePythonExes(): Promise { .filter(isWindowsPythonExe); } -export class WindowsStoreLocator implements ILocator { +export class WindowsStoreLocator extends PythonEnvsWatcher implements ILocator { private readonly kind:PythonEnvKind = PythonEnvKind.WindowsStore; - private readonly eventEmitter = new EventEmitter(); - - public constructor(private readonly envService:IEnvironmentInfoService) { } - public iterEnvs(): IPythonEnvsIterator { const buildEnvInfo = (exe:string) => this.buildEnvInfo(exe); const iterator = async function* () { @@ -135,31 +129,11 @@ export class WindowsStoreLocator implements ILocator { public async resolveEnv(env: string | PythonEnvInfo): Promise { const executablePath = typeof env === 'string' ? env : env.executable.filename; if (await isWindowsStoreEnvironment(executablePath)) { - const interpreterInfo = await this.envService.getEnvironmentInfo(executablePath); - if (interpreterInfo) { - const data = await getFileInfo(executablePath); - interpreterInfo.executable = { - ...interpreterInfo.executable, - ...data, - }; - return Promise.resolve({ - name: '', - location: '', - kind: this.kind, - executable: interpreterInfo.executable, - version: interpreterInfo.version, - arch: interpreterInfo.arch, - distro: { org: 'Microsoft' }, - }); - } + return this.buildEnvInfo(executablePath); } return undefined; } - public get onChanged(): Event { - return this.eventEmitter.event; - } - private async buildEnvInfo(exe:string): Promise { let version:PythonVersion; try { @@ -169,7 +143,7 @@ export class WindowsStoreLocator implements ILocator { major: 3, minor: -1, micro: -1, - release: { level: PythonReleaseLevel.Unknown, serial: -1 }, + release: { level: PythonReleaseLevel.Final, serial: -1 }, sysVersion: undefined, }; } diff --git a/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts index e6fecc4243e4..93c116a7270a 100644 --- a/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts +++ b/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts @@ -14,7 +14,6 @@ import { InterpreterInformation } from '../../../../client/pythonEnvironments/ba import { parseVersion } from '../../../../client/pythonEnvironments/base/info/pythonVersion'; import * as externalDep from '../../../../client/pythonEnvironments/common/externalDependencies'; import { getWindowsStorePythonExes, WindowsStoreLocator } from '../../../../client/pythonEnvironments/discovery/locators/services/windowsStoreLocator'; -import { EnvironmentInfoService } from '../../../../client/pythonEnvironments/info/environmentInfoService'; import { getEnvs } from '../../base/common'; import { TEST_LAYOUT_ROOT } from '../../common/commonTestConstants'; @@ -95,7 +94,7 @@ suite('Windows Store', () => { major: 3, minor: -1, micro: -1, - release: { level: PythonReleaseLevel.Unknown, serial: -1 }, + release: { level: PythonReleaseLevel.Final, serial: -1 }, sysVersion, }; } @@ -167,8 +166,7 @@ suite('Windows Store', () => { return undefined; }); - const envService = new EnvironmentInfoService(); - const locator = new WindowsStoreLocator(envService); + const locator = new WindowsStoreLocator(); const iterator = locator.iterEnvs(); const actualEnvs = (await getEnvs(iterator)) .sort((a, b) => a.executable.filename.localeCompare(b.executable.filename)); @@ -187,11 +185,10 @@ suite('Windows Store', () => { location: '', kind: PythonEnvKind.WindowsStore, distro: { org: 'Microsoft' }, - ...createExpectedInterpreterInfo(python38path, python383data.sysVersion, python383data.sysPrefix, '3.8.3'), + ...createExpectedInterpreterInfo(python38path), }; - const envService = new EnvironmentInfoService(); - const locator = new WindowsStoreLocator(envService); + const locator = new WindowsStoreLocator(); const actual = await locator.resolveEnv(python38path); assertEnvEqual(actual, expected); @@ -205,7 +202,7 @@ suite('Windows Store', () => { location: '', kind: PythonEnvKind.WindowsStore, distro: { org: 'Microsoft' }, - ...createExpectedInterpreterInfo(python38path, python383data.sysVersion, python383data.sysPrefix, '3.8.3'), + ...createExpectedInterpreterInfo(python38path), }; // Partially filled in env info object @@ -225,12 +222,11 @@ suite('Windows Store', () => { major: 3, minor: -1, micro: -1, - release: { level: PythonReleaseLevel.Unknown, serial: -1 }, + release: { level: PythonReleaseLevel.Final, serial: -1 }, }, }; - const envService = new EnvironmentInfoService(); - const locator = new WindowsStoreLocator(envService); + const locator = new WindowsStoreLocator(); const actual = await locator.resolveEnv(input); assertEnvEqual(actual, expected); @@ -239,8 +235,16 @@ suite('Windows Store', () => { // Use a non store root path const python38path = path.join(testLocalAppData, 'python3.8.exe'); - const envService = new EnvironmentInfoService(); - const locator = new WindowsStoreLocator(envService); + const locator = new WindowsStoreLocator(); + const actual = await locator.resolveEnv(python38path); + + assert.deepStrictEqual(actual, undefined); + }); + test('resolveEnv(string): forbidden path', async () => { + // Use a non store root path + const python38path = path.join(testLocalAppData, 'Program Files', 'WindowsApps', 'python3.8.exe'); + + const locator = new WindowsStoreLocator(); const actual = await locator.resolveEnv(python38path); assert.deepStrictEqual(actual, undefined); From e0fe21cf3fb853aacb9420b86eed75bd545546dc Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 30 Sep 2020 13:46:19 -0700 Subject: [PATCH 4/6] Tweaks --- src/client/pythonEnvironments/base/info/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client/pythonEnvironments/base/info/index.ts b/src/client/pythonEnvironments/base/info/index.ts index 1534cdaabd2b..4418860a12e5 100644 --- a/src/client/pythonEnvironments/base/info/index.ts +++ b/src/client/pythonEnvironments/base/info/index.ts @@ -81,8 +81,7 @@ export enum PythonReleaseLevel { Alpha = 'alpha', Beta = 'beta', Candidate = 'candidate', - Final = 'final', - Unknown = 'unknown' + Final = 'final' } /** From 1cf45da105f8fc5dd7ab5b1adb1f93a5172879aa Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 30 Sep 2020 13:51:06 -0700 Subject: [PATCH 5/6] Test fixes --- .../envlayouts/storeApps/Program Files/WindowsApps/python.exe | 1 + .../envlayouts/storeApps/Program Files/WindowsApps/python3.7.exe | 1 + .../envlayouts/storeApps/Program Files/WindowsApps/python3.8.exe | 1 + .../envlayouts/storeApps/Program Files/WindowsApps/python3.exe | 1 + 4 files changed, 4 insertions(+) create mode 100644 src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python.exe create mode 100644 src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.7.exe create mode 100644 src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.8.exe create mode 100644 src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.exe diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.7.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.7.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.7.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.8.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.8.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.8.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.exe @@ -0,0 +1 @@ +Not a real exe. From 4aec02caf11d68842e7c1918a93f580076188851 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 30 Sep 2020 20:54:25 -0700 Subject: [PATCH 6/6] Fix tests --- .../locators/windowsStoreLocator.unit.test.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts index 93c116a7270a..f6855a9e4617 100644 --- a/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts +++ b/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts @@ -231,18 +231,25 @@ suite('Windows Store', () => { assertEnvEqual(actual, expected); }); - test('resolveEnv(string): Non store python', async () => { - // Use a non store root path - const python38path = path.join(testLocalAppData, 'python3.8.exe'); + test('resolveEnv(string): forbidden path', async () => { + const python38path = path.join(testLocalAppData, 'Program Files', 'WindowsApps', 'python3.8.exe'); + const expected = { + + name: '', + location: '', + kind: PythonEnvKind.WindowsStore, + distro: { org: 'Microsoft' }, + ...createExpectedInterpreterInfo(python38path), + }; const locator = new WindowsStoreLocator(); const actual = await locator.resolveEnv(python38path); - assert.deepStrictEqual(actual, undefined); + assertEnvEqual(actual, expected); }); - test('resolveEnv(string): forbidden path', async () => { + test('resolveEnv(string): Non store python', async () => { // Use a non store root path - const python38path = path.join(testLocalAppData, 'Program Files', 'WindowsApps', 'python3.8.exe'); + const python38path = path.join(testLocalAppData, 'python3.8.exe'); const locator = new WindowsStoreLocator(); const actual = await locator.resolveEnv(python38path);