-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Added Windows store file system watcher #14577
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
Changes from all commits
ec02410
1461297
73fc23e
d4818ea
feb7f18
595cc32
a7b1753
ad31943
79b25d1
f3262ac
4cde6df
a1dcd0a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,19 +3,23 @@ | |
|
||
'use strict'; | ||
|
||
import * as minimatch from 'minimatch'; | ||
import * as path from 'path'; | ||
import { FileChangeType, watchLocationForPattern } from '../../common/platform/fileSystemWatcher'; | ||
import { getOSType, OSType } from '../../common/utils/platform'; | ||
|
||
const [executable, binName] = getOSType() === OSType.Windows ? ['python.exe', 'Scripts'] : ['python', 'bin']; | ||
const patterns = [executable, `*/${executable}`, `*/${binName}/${executable}`]; | ||
|
||
export function watchLocationForPythonBinaries( | ||
baseDir: string, | ||
callback: (type: FileChangeType, absPath: string) => void, | ||
executableGlob: string = executable, | ||
): void { | ||
const patterns = [executableGlob, `*/${executableGlob}`, `*/${binName}/${executableGlob}`]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be helpful to callers to throw an error here if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I went with your initial suggestion to expand the glob pattern to support more than base patterns. #14577 (comment) So we don't have to put a restriction on the wildcards or path separators. I can rename it to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't see that. What I see is:
A more-than-base pattern would be:
or:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm talking about the API, not this particular usecase. The API supports more than base patterns, i.e user can pass in a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you're assuming Hence why I suggested to rename it to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The patterns on this line suggest otherwise:
What you seen to be saying is that a caller may want to search at some arbitrary depth relative to a given root:
However, those don't seem right to me. Perhaps you meant a user might want to do something like the following?
The problem is that kind of defeats the structure-oriented design of this function. So I don't see that to be right either. All this is why I would expect There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I realize it doesn't seem right. But if you look closely, the first pattern is enough to do what the user asks (watch for suffixes ending with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is only true if the glob is for the basename. If they put |
||
for (const pattern of patterns) { | ||
watchLocationForPattern(baseDir, pattern, (type: FileChangeType, e: string) => { | ||
if (!e.endsWith(executable)) { | ||
const isMatch = minimatch(e, path.join('**', executableGlob), { nocase: getOSType() === OSType.Windows }); | ||
if (!isMatch) { | ||
// When deleting the file for some reason path to all directories leading up to python are reported | ||
// Skip those events | ||
return; | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -2,16 +2,17 @@ | |||||||||||||||||||
// Licensed under the MIT License. | ||||||||||||||||||||
|
||||||||||||||||||||
import * as fsapi from 'fs-extra'; | ||||||||||||||||||||
import * as minimatch from 'minimatch'; | ||||||||||||||||||||
import * as path from 'path'; | ||||||||||||||||||||
import { traceWarning } from '../../../../common/logger'; | ||||||||||||||||||||
import { FileChangeType } from '../../../../common/platform/fileSystemWatcher'; | ||||||||||||||||||||
import { Architecture, getEnvironmentVariable } from '../../../../common/utils/platform'; | ||||||||||||||||||||
import { | ||||||||||||||||||||
PythonEnvInfo, PythonEnvKind, | ||||||||||||||||||||
} from '../../../base/info'; | ||||||||||||||||||||
import { PythonEnvInfo, PythonEnvKind } from '../../../base/info'; | ||||||||||||||||||||
import { buildEnvInfo } from '../../../base/info/env'; | ||||||||||||||||||||
import { getPythonVersionFromPath } from '../../../base/info/pythonVersion'; | ||||||||||||||||||||
import { IPythonEnvsIterator, Locator } from '../../../base/locator'; | ||||||||||||||||||||
import { getFileInfo } from '../../../common/externalDependencies'; | ||||||||||||||||||||
import { watchLocationForPythonBinaries } from '../../../common/pythonBinariesWatcher'; | ||||||||||||||||||||
|
||||||||||||||||||||
/** | ||||||||||||||||||||
* Gets path to the Windows Apps directory. | ||||||||||||||||||||
|
@@ -29,7 +30,7 @@ export function getWindowsStoreAppsRoot(): string { | |||||||||||||||||||
* @returns {boolean} : Returns true if `interpreterPath` is under | ||||||||||||||||||||
* `%ProgramFiles%/WindowsApps`. | ||||||||||||||||||||
*/ | ||||||||||||||||||||
export function isForbiddenStorePath(interpreterPath:string):boolean { | ||||||||||||||||||||
export function isForbiddenStorePath(interpreterPath: string): boolean { | ||||||||||||||||||||
const programFilesStorePath = path | ||||||||||||||||||||
.join(getEnvironmentVariable('ProgramFiles') || 'Program Files', 'WindowsApps') | ||||||||||||||||||||
.normalize() | ||||||||||||||||||||
|
@@ -87,26 +88,27 @@ export async function isWindowsStoreEnvironment(interpreterPath: string): Promis | |||||||||||||||||||
return false; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
/** | ||||||||||||||||||||
* This is a glob pattern which matches following file names: | ||||||||||||||||||||
karrtikr marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||
* python3.8.exe | ||||||||||||||||||||
* python3.9.exe | ||||||||||||||||||||
* This pattern does not match: | ||||||||||||||||||||
* python.exe | ||||||||||||||||||||
* python2.7.exe | ||||||||||||||||||||
* python3.exe | ||||||||||||||||||||
* python38.exe | ||||||||||||||||||||
* 'python.exe', 'python3.exe', and 'python3.8.exe' can point to the same executable, hence only capture python3.*.exes. | ||||||||||||||||||||
*/ | ||||||||||||||||||||
const pythonExeGlob = 'python3\.[0-9]*\.exe'; | ||||||||||||||||||||
karrtikr marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||
|
||||||||||||||||||||
/** | ||||||||||||||||||||
* Checks if a given path ends with python3.*.exe. Not all python executables are matched as | ||||||||||||||||||||
* we do not want to return duplicate executables. | ||||||||||||||||||||
* @param {string} interpreterPath : Path to python interpreter. | ||||||||||||||||||||
* @returns {boolean} : Returns true if the path matches pattern for windows python executable. | ||||||||||||||||||||
*/ | ||||||||||||||||||||
export function isWindowsStorePythonExe(interpreterPath:string): boolean { | ||||||||||||||||||||
/** | ||||||||||||||||||||
* This Reg-ex matches following file names: | ||||||||||||||||||||
* python3.8.exe | ||||||||||||||||||||
* python3.9.exe | ||||||||||||||||||||
* This Reg-ex does not match: | ||||||||||||||||||||
* python.exe | ||||||||||||||||||||
* python2.7.exe | ||||||||||||||||||||
* python3.exe | ||||||||||||||||||||
* python38.exe | ||||||||||||||||||||
*/ | ||||||||||||||||||||
const windowsPythonExes = /^python(3(.\d+))\.exe$/; | ||||||||||||||||||||
|
||||||||||||||||||||
return windowsPythonExes.test(path.basename(interpreterPath)); | ||||||||||||||||||||
export function isWindowsStorePythonExe(interpreterPath: string): boolean { | ||||||||||||||||||||
return minimatch(path.basename(interpreterPath), pythonExeGlob, { nocase: true }); | ||||||||||||||||||||
} | ||||||||||||||||||||
Comment on lines
+110
to
112
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why aren't we just using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because of the reason mentioned in the sprint demo today: We're matching only There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The doc comment explains this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then I'd expect the following to take care of it:
Suggested change
That way we don't accept executables we shouldn't, like "python3-whoa!!!.exe". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, there's also There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, so we're okay with "extended glob". What you have looks fine, but I'd change the |
||||||||||||||||||||
|
||||||||||||||||||||
/** | ||||||||||||||||||||
|
@@ -131,17 +133,21 @@ export async function getWindowsStorePythonExes(): Promise<string[]> { | |||||||||||||||||||
// Collect python*.exe directly under %LOCALAPPDATA%/Microsoft/WindowsApps | ||||||||||||||||||||
const files = await fsapi.readdir(windowsAppsRoot); | ||||||||||||||||||||
return files | ||||||||||||||||||||
.map((filename:string) => path.join(windowsAppsRoot, filename)) | ||||||||||||||||||||
.map((filename: string) => path.join(windowsAppsRoot, filename)) | ||||||||||||||||||||
.filter(isWindowsStorePythonExe); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
export class WindowsStoreLocator extends Locator { | ||||||||||||||||||||
private readonly kind:PythonEnvKind = PythonEnvKind.WindowsStore; | ||||||||||||||||||||
private readonly kind: PythonEnvKind = PythonEnvKind.WindowsStore; | ||||||||||||||||||||
|
||||||||||||||||||||
public initialize(): void { | ||||||||||||||||||||
this.startWatcher(); | ||||||||||||||||||||
ericsnowcurrently marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
public iterEnvs(): IPythonEnvsIterator { | ||||||||||||||||||||
const iterator = async function* (kind: PythonEnvKind) { | ||||||||||||||||||||
const exes = await getWindowsStorePythonExes(); | ||||||||||||||||||||
yield* exes.map(async (executable:string) => buildEnvInfo({ | ||||||||||||||||||||
yield* exes.map(async (executable: string) => buildEnvInfo({ | ||||||||||||||||||||
kind, | ||||||||||||||||||||
executable, | ||||||||||||||||||||
version: getPythonVersionFromPath(executable), | ||||||||||||||||||||
|
@@ -167,4 +173,15 @@ export class WindowsStoreLocator extends Locator { | |||||||||||||||||||
} | ||||||||||||||||||||
return undefined; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
private startWatcher(): void { | ||||||||||||||||||||
const windowsAppsRoot = getWindowsStoreAppsRoot(); | ||||||||||||||||||||
watchLocationForPythonBinaries( | ||||||||||||||||||||
windowsAppsRoot, | ||||||||||||||||||||
(type: FileChangeType) => { | ||||||||||||||||||||
this.emitter.fire({ type, kind: this.kind }); | ||||||||||||||||||||
}, | ||||||||||||||||||||
pythonExeGlob, | ||||||||||||||||||||
); | ||||||||||||||||||||
} | ||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
import { assert } from 'chai'; | ||
import * as fs from 'fs-extra'; | ||
import * as path from 'path'; | ||
import { traceWarning } from '../../../../client/common/logger'; | ||
import { FileChangeType } from '../../../../client/common/platform/fileSystemWatcher'; | ||
import { createDeferred, Deferred, sleep } from '../../../../client/common/utils/async'; | ||
import { PythonEnvKind } from '../../../../client/pythonEnvironments/base/info'; | ||
import { getEnvs } from '../../../../client/pythonEnvironments/base/locatorUtils'; | ||
import { PythonEnvsChangedEvent } from '../../../../client/pythonEnvironments/base/watcher'; | ||
import { arePathsSame } from '../../../../client/pythonEnvironments/common/externalDependencies'; | ||
import { WindowsStoreLocator } from '../../../../client/pythonEnvironments/discovery/locators/services/windowsStoreLocator'; | ||
import { TEST_TIMEOUT } from '../../../constants'; | ||
import { TEST_LAYOUT_ROOT } from '../../common/commonTestConstants'; | ||
|
||
class WindowsStoreEnvs { | ||
karrtikr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
private executables: string[] = []; | ||
|
||
constructor(private readonly storeAppRoot: string) {} | ||
|
||
public async create(basename: string): Promise<string> { | ||
const filename = path.join(this.storeAppRoot, basename); | ||
try { | ||
await fs.createFile(filename); | ||
} catch (err) { | ||
throw new Error(`Failed to create Windows Apps executable ${filename}, Error: ${err}`); | ||
} | ||
this.executables.push(filename); | ||
return filename; | ||
} | ||
|
||
public async update(basename: string): Promise<void> { | ||
const filename = path.join(this.storeAppRoot, basename); | ||
try { | ||
await fs.writeFile(filename, 'Environment has been updated'); | ||
} catch (err) { | ||
throw new Error(`Failed to update Windows Apps executable ${filename}, Error: ${err}`); | ||
} | ||
} | ||
|
||
public async cleanUp() { | ||
await Promise.all( | ||
this.executables.map(async (filename: string) => { | ||
try { | ||
await fs.remove(filename); | ||
} catch (err) { | ||
traceWarning(`Failed to clean up ${filename}`); | ||
} | ||
}), | ||
); | ||
} | ||
} | ||
|
||
suite('Windows Store Locator', async () => { | ||
const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps'); | ||
const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps'); | ||
const windowsStoreEnvs = new WindowsStoreEnvs(testStoreAppRoot); | ||
let locator: WindowsStoreLocator; | ||
const localAppDataOldValue = process.env.LOCALAPPDATA; | ||
|
||
async function waitForChangeToBeDetected(deferred: Deferred<void>) { | ||
const timeout = setTimeout( | ||
() => { | ||
clearTimeout(timeout); | ||
deferred.reject(new Error('Environment not detected')); | ||
}, | ||
TEST_TIMEOUT, | ||
); | ||
await deferred.promise; | ||
} | ||
|
||
async function isLocated(executable: string): Promise<boolean> { | ||
const items = await getEnvs(locator.iterEnvs()); | ||
return items.some((item) => arePathsSame(item.executable.filename, executable)); | ||
} | ||
|
||
suiteSetup(async () => { | ||
process.env.LOCALAPPDATA = testLocalAppData; | ||
await windowsStoreEnvs.cleanUp(); | ||
karrtikr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
|
||
async function setupLocator(onChanged: (e: PythonEnvsChangedEvent) => Promise<void>) { | ||
locator = new WindowsStoreLocator(); | ||
locator.initialize(); | ||
karrtikr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Wait for watchers to get ready | ||
await sleep(1000); | ||
karrtikr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
locator.onChanged(onChanged); | ||
} | ||
|
||
teardown(() => windowsStoreEnvs.cleanUp()); | ||
suiteTeardown(async () => { | ||
process.env.LOCALAPPDATA = localAppDataOldValue; | ||
}); | ||
|
||
test('Detect a new environment', async () => { | ||
let actualEvent: PythonEnvsChangedEvent; | ||
const deferred = createDeferred<void>(); | ||
const expectedEvent = { | ||
kind: PythonEnvKind.WindowsStore, | ||
type: FileChangeType.Created, | ||
}; | ||
await setupLocator(async (e) => { | ||
actualEvent = e; | ||
deferred.resolve(); | ||
}); | ||
|
||
const executable = await windowsStoreEnvs.create('python3.4.exe'); | ||
await waitForChangeToBeDetected(deferred); | ||
const isFound = await isLocated(executable); | ||
|
||
assert.ok(isFound); | ||
assert.deepEqual(actualEvent!, expectedEvent, 'Wrong event emitted'); | ||
}); | ||
|
||
test('Detect when an environment has been deleted', async () => { | ||
let actualEvent: PythonEnvsChangedEvent; | ||
const deferred = createDeferred<void>(); | ||
const expectedEvent = { | ||
kind: PythonEnvKind.WindowsStore, | ||
type: FileChangeType.Deleted, | ||
}; | ||
const executable = await windowsStoreEnvs.create('python3.4.exe'); | ||
// Wait before the change event has been sent. If both operations occur almost simultaneously no event is sent. | ||
await sleep(100); | ||
ericsnowcurrently marked this conversation as resolved.
Show resolved
Hide resolved
|
||
await setupLocator(async (e) => { | ||
actualEvent = e; | ||
deferred.resolve(); | ||
}); | ||
|
||
await windowsStoreEnvs.cleanUp(); | ||
await waitForChangeToBeDetected(deferred); | ||
const isFound = await isLocated(executable); | ||
|
||
assert.notOk(isFound); | ||
assert.deepEqual(actualEvent!, expectedEvent, 'Wrong event emitted'); | ||
}); | ||
|
||
test('Detect when an environment has been updated', async () => { | ||
let actualEvent: PythonEnvsChangedEvent; | ||
const deferred = createDeferred<void>(); | ||
const expectedEvent = { | ||
kind: PythonEnvKind.WindowsStore, | ||
type: FileChangeType.Changed, | ||
}; | ||
const executable = await windowsStoreEnvs.create('python3.4.exe'); | ||
// Wait before the change event has been sent. If both operations occur almost simultaneously no event is sent. | ||
await sleep(100); | ||
await setupLocator(async (e) => { | ||
actualEvent = e; | ||
deferred.resolve(); | ||
}); | ||
|
||
await windowsStoreEnvs.update('python3.4.exe'); | ||
await waitForChangeToBeDetected(deferred); | ||
const isFound = await isLocated(executable); | ||
|
||
assert.ok(isFound); | ||
assert.deepEqual(actualEvent!, expectedEvent, 'Wrong event emitted'); | ||
}); | ||
}); |
Uh oh!
There was an error while loading. Please reload this page.