Skip to content

Windows store locator #14162

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/client/pythonEnvironments/common/externalDependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,11 @@ export function getGlobalPersistentStore<T>(key: string): IPersistentStore<T> {
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(),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -107,5 +114,51 @@ export async function getWindowsStorePythonExes(): Promise<string[]> {
.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<PythonEnvInfo | undefined> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll be removing this right?
If not, this doesn't seem right. resolveEnv should always add to env. So you might wanna clone and replace on existing fields on passed in environments. Right now I see we're returning empty fields there.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. when we get the new interface we should be replacing this. The only thing this will set (after new interface) is the distro.

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<PythonEnvInfo> {
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' },
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Not a real exe.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Not a real exe.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Not a real exe.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Not a real exe.
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, {
versionInfo:(string|number)[],
sysPrefix: string,
sysVersion: string,
is64Bit: boolean
}>();

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<ExecutionResult<string>>({ stdout: '' });
}
if (command.indexOf('python3.7.exe') > 0) {
return Promise.resolve<ExecutionResult<string>>({ stdout: JSON.stringify(python379data) });
}
return Promise.resolve<ExecutionResult<string>>({ 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);
});
});
});