Skip to content

Commit 2213b58

Browse files
committed
Initial commit for windows store locator
1 parent 57fedbd commit 2213b58

File tree

6 files changed

+400
-102
lines changed

6 files changed

+400
-102
lines changed

src/client/pythonEnvironments/base/info/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ export enum PythonReleaseLevel {
7575
Alpha = 'alpha',
7676
Beta = 'beta',
7777
Candidate = 'candidate',
78-
Final = 'final'
78+
Final = 'final',
79+
Unknown = 'unknown'
7980
}
8081

8182
/**

src/client/pythonEnvironments/common/environmentIdentifier.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { isPyenvEnvironment } from '../discovery/locators/services/pyenvLocator'
77
import { isVenvEnvironment } from '../discovery/locators/services/venvLocator';
88
import { isVirtualenvEnvironment } from '../discovery/locators/services/virtualenvLocator';
99
import { isVirtualenvwrapperEnvironment } from '../discovery/locators/services/virtualenvwrapperLocator';
10-
import { isWindowsStoreEnvironment } from '../discovery/locators/services/windowsStoreLocator';
1110
import { EnvironmentType } from '../info';
11+
import { isWindowsStoreEnvironment } from './windowsUtils';
1212

1313
/**
1414
* Returns environment type.

src/client/pythonEnvironments/common/externalDependencies.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,11 @@ export function arePathsSame(path1: string, path2: string): boolean {
3737
}
3838
return path1 === path2;
3939
}
40+
41+
export async function getFileInfo(filePath: string): Promise<{ctime:number, mtime:number}> {
42+
const data = await fsapi.lstat(filePath);
43+
return {
44+
ctime: data.ctime.getUTCDate(),
45+
mtime: data.mtime.getUTCDate(),
46+
};
47+
}

src/client/pythonEnvironments/common/windowsUtils.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Licensed under the MIT License.
33

44
import * as path from 'path';
5+
import { traceWarning } from '../../common/logger';
6+
import { getEnvironmentVariable } from '../../common/utils/platform';
57

68
/**
79
* Checks if a given path ends with python*.exe
@@ -20,3 +22,77 @@ export function isWindowsPythonExe(interpreterPath:string): boolean {
2022

2123
return windowsPythonExes.test(path.basename(interpreterPath));
2224
}
25+
26+
/**
27+
* Gets path to the Windows Apps directory.
28+
* @returns {string} : Returns path to the Windows Apps directory under
29+
* `%LOCALAPPDATA%/Microsoft/WindowsApps`.
30+
*/
31+
export function getWindowsStoreAppsRoot(): string {
32+
const localAppData = getEnvironmentVariable('LOCALAPPDATA') || '';
33+
return path.join(localAppData, 'Microsoft', 'WindowsApps');
34+
}
35+
36+
/**
37+
* Checks if a given path is under the forbidden windows store directory.
38+
* @param {string} interpreterPath : Absolute path to the python interpreter.
39+
* @returns {boolean} : Returns true if `interpreterPath` is under
40+
* `%ProgramFiles%/WindowsApps`.
41+
*/
42+
export function isForbiddenStorePath(interpreterPath:string):boolean {
43+
const programFilesStorePath = path
44+
.join(getEnvironmentVariable('ProgramFiles') || 'Program Files', 'WindowsApps')
45+
.normalize()
46+
.toUpperCase();
47+
return path.normalize(interpreterPath).toUpperCase().includes(programFilesStorePath);
48+
}
49+
50+
/**
51+
* Checks if the given interpreter belongs to Windows Store Python environment.
52+
* @param interpreterPath: Absolute path to any python interpreter.
53+
*
54+
* Remarks:
55+
* 1. Checking if the path includes `Microsoft\WindowsApps`, `Program Files\WindowsApps`, is
56+
* NOT enough. In WSL, `/mnt/c/users/user/AppData/Local/Microsoft/WindowsApps` is available as a search
57+
* path. It is possible to get a false positive for that path. So the comparison should check if the
58+
* absolute path to 'WindowsApps' directory is present in the given interpreter path. The WSL path to
59+
* 'WindowsApps' is not a valid path to access, Windows Store Python.
60+
*
61+
* 2. 'startsWith' comparison may not be right, user can provide '\\?\C:\users\' style long paths in windows.
62+
*
63+
* 3. A limitation of the checks here is that they don't handle 8.3 style windows paths.
64+
* For example,
65+
* `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE`
66+
* is the shortened form of
67+
* `C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe`
68+
*
69+
* The correct way to compare these would be to always convert given paths to long path (or to short path).
70+
* For either approach to work correctly you need actual file to exist, and accessible from the user's
71+
* account.
72+
*
73+
* To convert to short path without using N-API in node would be to use this command. This is very expensive:
74+
* `> cmd /c for %A in ("C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe") do @echo %~sA`
75+
* The above command will print out this:
76+
* `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE`
77+
*
78+
* If we go down the N-API route, use node-ffi and either call GetShortPathNameW or GetLongPathNameW from,
79+
* Kernel32 to convert between the two path variants.
80+
*
81+
*/
82+
export async function isWindowsStoreEnvironment(interpreterPath: string): Promise<boolean> {
83+
const pythonPathToCompare = path.normalize(interpreterPath).toUpperCase();
84+
const localAppDataStorePath = path
85+
.normalize(getWindowsStoreAppsRoot())
86+
.toUpperCase();
87+
if (pythonPathToCompare.includes(localAppDataStorePath)) {
88+
return true;
89+
}
90+
91+
// Program Files store path is a forbidden path. Only admins and system has access this path.
92+
// We should never have to look at this path or even execute python from this path.
93+
if (isForbiddenStorePath(pythonPathToCompare)) {
94+
traceWarning('isWindowsStoreEnvironment called with Program Files store path.');
95+
return true;
96+
}
97+
return false;
98+
}

src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts

Lines changed: 85 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -3,83 +3,17 @@
33

44
import * as fsapi from 'fs-extra';
55
import * as path from 'path';
6-
import { traceWarning } from '../../../../common/logger';
7-
import { getEnvironmentVariable } from '../../../../common/utils/platform';
8-
import { isWindowsPythonExe } from '../../../common/windowsUtils';
9-
10-
/**
11-
* Gets path to the Windows Apps directory.
12-
* @returns {string} : Returns path to the Windows Apps directory under
13-
* `%LOCALAPPDATA%/Microsoft/WindowsApps`.
14-
*/
15-
export function getWindowsStoreAppsRoot(): string {
16-
const localAppData = getEnvironmentVariable('LOCALAPPDATA') || '';
17-
return path.join(localAppData, 'Microsoft', 'WindowsApps');
18-
}
19-
20-
/**
21-
* Checks if a given path is under the forbidden windows store directory.
22-
* @param {string} interpreterPath : Absolute path to the python interpreter.
23-
* @returns {boolean} : Returns true if `interpreterPath` is under
24-
* `%ProgramFiles%/WindowsApps`.
25-
*/
26-
export function isForbiddenStorePath(interpreterPath:string):boolean {
27-
const programFilesStorePath = path
28-
.join(getEnvironmentVariable('ProgramFiles') || 'Program Files', 'WindowsApps')
29-
.normalize()
30-
.toUpperCase();
31-
return path.normalize(interpreterPath).toUpperCase().includes(programFilesStorePath);
32-
}
33-
34-
/**
35-
* Checks if the given interpreter belongs to Windows Store Python environment.
36-
* @param interpreterPath: Absolute path to any python interpreter.
37-
*
38-
* Remarks:
39-
* 1. Checking if the path includes `Microsoft\WindowsApps`, `Program Files\WindowsApps`, is
40-
* NOT enough. In WSL, `/mnt/c/users/user/AppData/Local/Microsoft/WindowsApps` is available as a search
41-
* path. It is possible to get a false positive for that path. So the comparison should check if the
42-
* absolute path to 'WindowsApps' directory is present in the given interpreter path. The WSL path to
43-
* 'WindowsApps' is not a valid path to access, Windows Store Python.
44-
*
45-
* 2. 'startsWith' comparison may not be right, user can provide '\\?\C:\users\' style long paths in windows.
46-
*
47-
* 3. A limitation of the checks here is that they don't handle 8.3 style windows paths.
48-
* For example,
49-
* `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE`
50-
* is the shortened form of
51-
* `C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe`
52-
*
53-
* The correct way to compare these would be to always convert given paths to long path (or to short path).
54-
* For either approach to work correctly you need actual file to exist, and accessible from the user's
55-
* account.
56-
*
57-
* To convert to short path without using N-API in node would be to use this command. This is very expensive:
58-
* `> cmd /c for %A in ("C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe") do @echo %~sA`
59-
* The above command will print out this:
60-
* `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE`
61-
*
62-
* If we go down the N-API route, use node-ffi and either call GetShortPathNameW or GetLongPathNameW from,
63-
* Kernel32 to convert between the two path variants.
64-
*
65-
*/
66-
export async function isWindowsStoreEnvironment(interpreterPath: string): Promise<boolean> {
67-
const pythonPathToCompare = path.normalize(interpreterPath).toUpperCase();
68-
const localAppDataStorePath = path
69-
.normalize(getWindowsStoreAppsRoot())
70-
.toUpperCase();
71-
if (pythonPathToCompare.includes(localAppDataStorePath)) {
72-
return true;
73-
}
74-
75-
// Program Files store path is a forbidden path. Only admins and system has access this path.
76-
// We should never have to look at this path or even execute python from this path.
77-
if (isForbiddenStorePath(pythonPathToCompare)) {
78-
traceWarning('isWindowsStoreEnvironment called with Program Files store path.');
79-
return true;
80-
}
81-
return false;
82-
}
6+
import { Event, EventEmitter } from 'vscode';
7+
import { Architecture } from '../../../../common/utils/platform';
8+
import {
9+
PythonEnvInfo, PythonEnvKind, PythonReleaseLevel, PythonVersion,
10+
} from '../../../base/info';
11+
import { parseVersion } from '../../../base/info/pythonVersion';
12+
import { ILocator, IPythonEnvsIterator } from '../../../base/locator';
13+
import { PythonEnvsChangedEvent } from '../../../base/watcher';
14+
import { getFileInfo } from '../../../common/externalDependencies';
15+
import { getWindowsStoreAppsRoot, isWindowsPythonExe, isWindowsStoreEnvironment } from '../../../common/windowsUtils';
16+
import { IEnvironmentInfoService } from '../../../info/environmentInfoService';
8317

8418
/**
8519
* Gets paths to the Python executable under Windows Store apps.
@@ -107,5 +41,77 @@ export async function getWindowsStorePythonExes(): Promise<string[]> {
10741
.filter(isWindowsPythonExe);
10842
}
10943

110-
// tslint:disable-next-line: no-suspicious-comment
111-
// TODO: The above APIs will be consumed by the Windows Store locator class when we have it.
44+
export class WindowsStoreLocator implements ILocator {
45+
private readonly kind:PythonEnvKind = PythonEnvKind.WindowsStore;
46+
47+
private readonly eventEmitter = new EventEmitter<PythonEnvsChangedEvent>();
48+
49+
public constructor(private readonly envService:IEnvironmentInfoService) { }
50+
51+
public iterEnvs(): IPythonEnvsIterator {
52+
const buildEnvInfo = (exe:string) => this.buildEnvInfo(exe);
53+
const iterator = async function* () {
54+
const exes = await getWindowsStorePythonExes();
55+
yield* exes.map(buildEnvInfo);
56+
};
57+
return iterator();
58+
}
59+
60+
public async resolveEnv(env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
61+
const executablePath = typeof env === 'string' ? env : env.executable.filename;
62+
if (isWindowsStoreEnvironment(executablePath)) {
63+
const interpreterInfo = await this.envService.getEnvironmentInfo(executablePath);
64+
if (interpreterInfo) {
65+
const data = await getFileInfo(executablePath);
66+
interpreterInfo.executable = {
67+
...interpreterInfo.executable,
68+
...data,
69+
};
70+
return Promise.resolve({
71+
id: '',
72+
name: '',
73+
location: '',
74+
kind: this.kind,
75+
executable: interpreterInfo.executable,
76+
version: interpreterInfo.version,
77+
arch: interpreterInfo.arch,
78+
distro: { org: 'Microsoft' },
79+
});
80+
}
81+
}
82+
return undefined;
83+
}
84+
85+
public get onChanged(): Event<PythonEnvsChangedEvent> {
86+
return this.eventEmitter.event;
87+
}
88+
89+
private async buildEnvInfo(exe:string): Promise<PythonEnvInfo> {
90+
let version:PythonVersion;
91+
try {
92+
version = parseVersion(path.basename(exe));
93+
} catch (e) {
94+
version = {
95+
major: 3,
96+
minor: -1,
97+
micro: -1,
98+
release: { level: PythonReleaseLevel.Unknown, serial: -1 },
99+
sysVersion: undefined,
100+
};
101+
}
102+
return {
103+
id: '',
104+
name: '',
105+
location: '',
106+
kind: this.kind,
107+
executable: {
108+
filename: exe,
109+
sysPrefix: '',
110+
...(await getFileInfo(exe)),
111+
},
112+
version,
113+
arch: Architecture.x64,
114+
distro: { org: 'Microsoft' },
115+
};
116+
}
117+
}

0 commit comments

Comments
 (0)