From cfe12a7855d1e05d2b46365a40910337f97ce434 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 1 Oct 2020 01:11:31 -0700 Subject: [PATCH] Windows store locator (#14162) * Initial commit for windows store locator * More tests * Simplify locator * Tweaks * Test fixes * Fix tests --- .../common/externalDependencies.ts | 8 + .../locators/services/windowsStoreLocator.ts | 59 +++- .../Program Files/WindowsApps/python.exe | 1 + .../Program Files/WindowsApps/python3.7.exe | 1 + .../Program Files/WindowsApps/python3.8.exe | 1 + .../Program Files/WindowsApps/python3.exe | 1 + .../locators/windowsStoreLocator.unit.test.ts | 269 ++++++++++++++++-- 7 files changed, 316 insertions(+), 24 deletions(-) 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/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/discovery/locators/services/windowsStoreLocator.ts b/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts index 6eae9dac5126..14f8cb1fc482 100644 --- a/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts +++ b/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts @@ -4,7 +4,14 @@ import * as fsapi from 'fs-extra'; import * as path from 'path'; import { traceWarning } from '../../../../common/logger'; -import { getEnvironmentVariable } from '../../../../common/utils/platform'; +import { Architecture, getEnvironmentVariable } 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 { PythonEnvsWatcher } from '../../../base/watcher'; +import { getFileInfo } from '../../../common/externalDependencies'; import { isWindowsPythonExe } from '../../../common/windowsUtils'; /** @@ -107,5 +114,51 @@ 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 extends PythonEnvsWatcher implements ILocator { + private readonly kind:PythonEnvKind = PythonEnvKind.WindowsStore; + + 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 (await isWindowsStoreEnvironment(executablePath)) { + return this.buildEnvInfo(executablePath); + } + return undefined; + } + + 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.Final, serial: -1 }, + sysVersion: undefined, + }; + } + return { + 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/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. diff --git a/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts index 6bbba7c7bef2..f6855a9e4617 100644 --- a/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts +++ b/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts @@ -2,32 +2,259 @@ // 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 { 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.Final, 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 { + + name: '', + location: '', + kind: PythonEnvKind.WindowsStore, + distro: { org: 'Microsoft' }, + ...createExpectedInterpreterInfo(k), + }; + } + return undefined; + }); + + const locator = new WindowsStoreLocator(); + 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 = { + + name: '', + location: '', + kind: PythonEnvKind.WindowsStore, + distro: { org: 'Microsoft' }, + ...createExpectedInterpreterInfo(python38path), + }; + + const locator = new WindowsStoreLocator(); + const actual = await locator.resolveEnv(python38path); + + assertEnvEqual(actual, expected); + }); + + test('resolveEnv(PythonEnvInfo)', async () => { + const python38path = path.join(testStoreAppRoot, 'python3.8.exe'); + const expected = { + + name: '', + location: '', + kind: PythonEnvKind.WindowsStore, + distro: { org: 'Microsoft' }, + ...createExpectedInterpreterInfo(python38path), + }; + + // Partially filled in env info object + const input:PythonEnvInfo = { + 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.Final, serial: -1 }, + }, + }; + + const locator = new WindowsStoreLocator(); + const actual = await locator.resolveEnv(input); + + assertEnvEqual(actual, expected); + }); + 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); + + 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 locator = new WindowsStoreLocator(); + const actual = await locator.resolveEnv(python38path); + + assert.deepStrictEqual(actual, undefined); + }); }); });