Skip to content
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

Refresh list of interpreters if python env is created in workspace #3024

Merged
merged 25 commits into from
Oct 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ npm-debug.log
!yarn.lock
coverage/
.vscode-test/**
.venv*/
**/.venv*/
precommit.hook
pythonFiles/experimental/ptvsd/**
debug_coverage*/**
Expand Down
1 change: 1 addition & 0 deletions news/1 Enhancements/656.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Clear cached list of interpreters when an interpeter is created in the workspace folder (this allows for virtual environments created in one's workspace folder to be detectable immediately).
2 changes: 1 addition & 1 deletion src/client/common/application/debugService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class DebugService implements IDebugService {
public registerDebugConfigurationProvider(debugType: string, provider: any): Disposable {
return debug.registerDebugConfigurationProvider(debugType, provider);
}
public startDebugging(folder: WorkspaceFolder, nameOrConfiguration: string | DebugConfiguration): Thenable<boolean> {
public startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration): Thenable<boolean> {
return debug.startDebugging(folder, nameOrConfiguration);
}
public addBreakpoints(breakpoints: Breakpoint[]): void {
Expand Down
10 changes: 10 additions & 0 deletions src/client/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ export const STANDARD_OUTPUT_CHANNEL = 'STANDARD_OUTPUT_CHANNEL';
export function isTestExecution(): boolean {
return process.env.VSC_PYTHON_CI_TEST === '1';
}

/**
* Whether we're running unit tests (*.unit.test.ts).
* These tests have a speacial meaning, they run fast.
* @export
* @returns {boolean}
*/
export function isUnitTestExecution(): boolean {
return process.env.VSC_PYTHON_UNIT_TEST === '1';
}
export function isLanguageServerTest(): boolean {
return process.env.VSC_PYTHON_LANGUAGE_SERVER === '1';
}
Expand Down
4 changes: 4 additions & 0 deletions src/client/common/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export class Logger implements ILogger {
public static warn(title: string = '', message: any = '') {
new Logger().logWarning(`${title}, ${message}`);
}
// tslint:disable-next-line:no-any
public static verbose(title: string = '') {
new Logger().logInformation(title);
}
@skipIfTest(false)
public logError(message: string, ex?: Error) {
if (ex) {
Expand Down
9 changes: 4 additions & 5 deletions src/client/common/utils/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import * as _ from 'lodash';
import { isTestExecution } from '../constants';

type AsyncVoidAction = (...params: {}[]) => Promise<void>;
type VoidAction = (...params: {}[]) => void;
import { isTestExecution, isUnitTestExecution } from '../constants';

/**
* Debounces a function execution. Function must return either a void or a promise that resolves to a void.
Expand All @@ -12,8 +9,10 @@ type VoidAction = (...params: {}[]) => void;
*/
export function debounce(wait?: number) {
// tslint:disable-next-line:no-any no-function-expression
return function (_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor<VoidAction> | TypedPropertyDescriptor<AsyncVoidAction>) {
return function (_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor<any>) {
const originalMethod = descriptor.value!;
// If running tests, lets not debounce (so tests run fast).
wait = wait && isUnitTestExecution() ? undefined : wait;
// tslint:disable-next-line:no-invalid-this no-any
(descriptor as any).value = _.debounce(function () { return originalMethod.apply(this, arguments); }, wait);
};
Expand Down
5 changes: 1 addition & 4 deletions src/client/debugger/extension/hooks/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,5 @@

export enum PTVSDEvents {
// Event sent by PTVSD when a child process is launched and ready to be attached to for multi-proc debugging.
ChildProcessLaunched = 'ptvsd_subprocess',

// Event sent by PTVSD when a process is started (identital to the `process` event in debugger protocol).
ProcessLaunched = 'ptvsd_process'
ChildProcessLaunched = 'ptvsd_subprocess'
}
484 changes: 245 additions & 239 deletions src/client/extension.ts

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions src/client/interpreter/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,13 @@ export const IInterpreterLocatorHelper = Symbol('IInterpreterLocatorHelper');
export interface IInterpreterLocatorHelper {
mergeInterpreters(interpreters: PythonInterpreter[]): PythonInterpreter[];
}

export const IInterpreterWatcher = Symbol('IInterpreterWatcher');
export interface IInterpreterWatcher {
onDidCreate: Event<void>;
}

export const IInterpreterWatcherBuilder = Symbol('IInterpreterWatcherBuilder');
export interface IInterpreterWatcherBuilder {
getWorkspaceVirtualEnvInterpreterWatcher(resource: Uri | undefined): Promise<IInterpreterWatcher>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@

import { injectable, unmanaged } from 'inversify';
import * as md5 from 'md5';
import { Uri } from 'vscode';
import { Disposable, Uri } from 'vscode';
import { IWorkspaceService } from '../../../common/application/types';
import { IPersistentStateFactory } from '../../../common/types';
import { Logger } from '../../../common/logger';
import { IDisposableRegistry, IPersistentStateFactory } from '../../../common/types';
import { createDeferred, Deferred } from '../../../common/utils/async';
import { IServiceContainer } from '../../../ioc/types';
import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts';
import { IInterpreterLocatorService, IInterpreterWatcher, PythonInterpreter } from '../../contracts';

@injectable()
export abstract class CacheableLocatorService implements IInterpreterLocatorService {
private readonly promisesPerResource = new Map<string, Deferred<PythonInterpreter[]>>();
private readonly handlersAddedToResource = new Set<string>();
private readonly cacheKeyPrefix: string;
constructor(@unmanaged() name: string,
@unmanaged() protected readonly serviceContainer: IServiceContainer,
Expand All @@ -25,9 +27,14 @@ export abstract class CacheableLocatorService implements IInterpreterLocatorServ
public async getInterpreters(resource?: Uri): Promise<PythonInterpreter[]> {
const cacheKey = this.getCacheKey(resource);
let deferred = this.promisesPerResource.get(cacheKey);

if (!deferred) {
deferred = createDeferred<PythonInterpreter[]>();
this.promisesPerResource.set(cacheKey, deferred);

this.addHandlersForInterpreterWatchers(cacheKey, resource)
.ignoreErrors();

this.getInterpretersImplementation(resource)
.then(async items => {
await this.cacheInterpreters(items, resource);
Expand All @@ -42,9 +49,26 @@ export abstract class CacheableLocatorService implements IInterpreterLocatorServ
const cachedInterpreters = this.getCachedInterpreters(resource);
return Array.isArray(cachedInterpreters) ? cachedInterpreters : deferred.promise;
}
protected async addHandlersForInterpreterWatchers(cacheKey: string, resource: Uri | undefined): Promise<void> {
if (this.handlersAddedToResource.has(cacheKey)) {
return;
}
this.handlersAddedToResource.add(cacheKey);
const watchers = await this.getInterpreterWatchers(resource);
const disposableRegisry = this.serviceContainer.get<Disposable[]>(IDisposableRegistry);
watchers.forEach(watcher => {
watcher.onDidCreate(() => {
Logger.verbose(`Interpreter Watcher change handler for ${this.cacheKeyPrefix}`);
this.promisesPerResource.delete(cacheKey);
}, this, disposableRegisry);
});
}
protected async getInterpreterWatchers(_resource: Uri | undefined): Promise<IInterpreterWatcher[]> {
return [];
}

protected abstract getInterpretersImplementation(resource?: Uri): Promise<PythonInterpreter[]>;
private createPersistenceStore(resource?: Uri) {
protected createPersistenceStore(resource?: Uri) {
const cacheKey = this.getCacheKey(resource);
const persistentFactory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory);
if (this.cachePerWorkspace) {
Expand All @@ -54,7 +78,7 @@ export abstract class CacheableLocatorService implements IInterpreterLocatorServ
}

}
private getCachedInterpreters(resource?: Uri) {
protected getCachedInterpreters(resource?: Uri): PythonInterpreter[] | undefined {
const persistence = this.createPersistenceStore(resource);
if (!Array.isArray(persistence.value)) {
return;
Expand All @@ -66,11 +90,11 @@ export abstract class CacheableLocatorService implements IInterpreterLocatorServ
};
});
}
private async cacheInterpreters(interpreters: PythonInterpreter[], resource?: Uri) {
protected async cacheInterpreters(interpreters: PythonInterpreter[], resource?: Uri) {
const persistence = this.createPersistenceStore(resource);
await persistence.updateValue(interpreters);
}
private getCacheKey(resource?: Uri) {
protected getCacheKey(resource?: Uri) {
if (!resource || !this.cachePerWorkspace) {
return this.cacheKeyPrefix;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { inject, injectable } from 'inversify';
import { Uri } from 'vscode';
import { IWorkspaceService } from '../../../common/application/types';
import { traceVerbose } from '../../../common/logger';
import { createDeferred } from '../../../common/utils/async';
import { IServiceContainer } from '../../../ioc/types';
import { IInterpreterWatcher, IInterpreterWatcherBuilder, WORKSPACE_VIRTUAL_ENV_SERVICE } from '../../contracts';
import { WorkspaceVirtualEnvWatcherService } from './workspaceVirtualEnvWatcherService';

@injectable()
export class InterpreterWatcherBuilder implements IInterpreterWatcherBuilder {
private readonly watchersByResource = new Map<string, Promise<IInterpreterWatcher>>();
/**
* Creates an instance of InterpreterWatcherBuilder.
* Inject the DI container, as we need to get a new instance of IInterpreterWatcher to build it.
* @param {IWorkspaceService} workspaceService
* @param {IServiceContainer} serviceContainer
* @memberof InterpreterWatcherBuilder
*/
constructor(@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService,
@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer
) { }

@traceVerbose('Build the workspace interpreter watcher')
public async getWorkspaceVirtualEnvInterpreterWatcher(resource: Uri | undefined): Promise<IInterpreterWatcher> {
const key = this.getResourceKey(resource);
if (!this.watchersByResource.has(key)) {
const deferred = createDeferred<IInterpreterWatcher>();
this.watchersByResource.set(key, deferred.promise);
const watcher = this.serviceContainer.get<WorkspaceVirtualEnvWatcherService>(IInterpreterWatcher, WORKSPACE_VIRTUAL_ENV_SERVICE);
await watcher.register(resource);
deferred.resolve(watcher);
}
return this.watchersByResource.get(key)!;
}
protected getResourceKey(resource: Uri | undefined): string {
const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined;
return workspaceFolder ? workspaceFolder.uri.fsPath : '';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,28 @@

'use strict';

// tslint:disable:no-require-imports

import { inject, injectable, named } from 'inversify';
import * as path from 'path';
// tslint:disable-next-line:no-require-imports
import untildify = require('untildify');
import { Uri } from 'vscode';
import { IWorkspaceService } from '../../../common/application/types';
import { IConfigurationService } from '../../../common/types';
import { IServiceContainer } from '../../../ioc/types';
import { IVirtualEnvironmentsSearchPathProvider } from '../../contracts';
import { IInterpreterWatcher, IInterpreterWatcherBuilder, IVirtualEnvironmentsSearchPathProvider } from '../../contracts';
import { BaseVirtualEnvService } from './baseVirtualEnvService';

@injectable()
export class WorkspaceVirtualEnvService extends BaseVirtualEnvService {
public constructor(
@inject(IVirtualEnvironmentsSearchPathProvider) @named('workspace') globalVirtualEnvPathProvider: IVirtualEnvironmentsSearchPathProvider,
@inject(IServiceContainer) serviceContainer: IServiceContainer) {
super(globalVirtualEnvPathProvider, serviceContainer, 'WorkspaceVirtualEnvService', true);
@inject(IVirtualEnvironmentsSearchPathProvider) @named('workspace') workspaceVirtualEnvPathProvider: IVirtualEnvironmentsSearchPathProvider,
@inject(IServiceContainer) serviceContainer: IServiceContainer,
@inject(IInterpreterWatcherBuilder) private readonly builder: IInterpreterWatcherBuilder) {
super(workspaceVirtualEnvPathProvider, serviceContainer, 'WorkspaceVirtualEnvService', true);
}
protected async getInterpreterWatchers(resource: Uri | undefined): Promise<IInterpreterWatcher[]> {
return [await this.builder.getWorkspaceVirtualEnvInterpreterWatcher(resource)];
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { inject, injectable } from 'inversify';
import * as path from 'path';
import { Disposable, Event, EventEmitter, FileSystemWatcher, RelativePattern, Uri } from 'vscode';
import { IWorkspaceService } from '../../../common/application/types';
import { Logger, traceVerbose } from '../../../common/logger';
import { IPlatformService } from '../../../common/platform/types';
import { IDisposableRegistry } from '../../../common/types';
import { debounce } from '../../../common/utils/decorators';
import { IInterpreterWatcher } from '../../contracts';

@injectable()
export class WorkspaceVirtualEnvWatcherService implements IInterpreterWatcher, Disposable {
private readonly didCreate;
private timer?: NodeJS.Timer;
private fsWatchers: FileSystemWatcher[] = [];
constructor(@inject(IDisposableRegistry) private readonly disposableRegistry: Disposable[],
@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService,
@inject(IPlatformService) private readonly platformService: IPlatformService) {
this.didCreate = new EventEmitter<void>();
disposableRegistry.push(this);
}
public get onDidCreate(): Event<void> {
return this.didCreate.event;
}
public dispose() {
this.clearTimer();
}
@traceVerbose('Register Intepreter Watcher')
public async register(resource: Uri | undefined): Promise<void> {
if (this.fsWatchers.length > 0) {
return;
}

const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined;
const executable = this.platformService.isWindows ? 'python.exe' : 'python';
const patterns = [path.join('*', executable), path.join('*', '*', executable)];

for (const pattern of patterns) {
const globPatern = workspaceFolder ? new RelativePattern(workspaceFolder.uri.fsPath, pattern) : pattern;
Logger.verbose(`Create file systemwatcher with pattern ${pattern}`);

const fsWatcher = this.workspaceService.createFileSystemWatcher(globPatern);
fsWatcher.onDidCreate(e => this.createHandler(e), this, this.disposableRegistry);

this.disposableRegistry.push(fsWatcher);
this.fsWatchers.push(fsWatcher);
}
}
@debounce(2000)
@traceVerbose('Intepreter Watcher change handler')
protected createHandler(e: Uri) {
this.didCreate.fire();
// On Windows, creation of environments are slow, hence lets notify again after 10 seconds.
this.clearTimer();

this.timer = setTimeout(() => {
this.timer = undefined;
this.didCreate.fire();
}, 10000);
}
private clearTimer() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = undefined;
}
}
}
7 changes: 7 additions & 0 deletions src/client/interpreter/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
IInterpreterLocatorService,
IInterpreterService,
IInterpreterVersionService,
IInterpreterWatcher,
IInterpreterWatcherBuilder,
IKnownSearchPathsForInterpreters,
INTERPRETER_LOCATOR_SERVICE,
IPipEnvService,
Expand All @@ -42,10 +44,12 @@ import { CondaEnvService } from './locators/services/condaEnvService';
import { CondaService } from './locators/services/condaService';
import { CurrentPathService } from './locators/services/currentPathService';
import { GlobalVirtualEnvironmentsSearchPathProvider, GlobalVirtualEnvService } from './locators/services/globalVirtualEnvService';
import { InterpreterWatcherBuilder } from './locators/services/interpreterWatcherBuilder';
import { KnownPathsService, KnownSearchPathsForInterpreters } from './locators/services/KnownPathsService';
import { PipEnvService } from './locators/services/pipEnvService';
import { WindowsRegistryService } from './locators/services/windowsRegistryService';
import { WorkspaceVirtualEnvironmentsSearchPathProvider, WorkspaceVirtualEnvService } from './locators/services/workspaceVirtualEnvService';
import { WorkspaceVirtualEnvWatcherService } from './locators/services/workspaceVirtualEnvWatcherService';
import { VirtualEnvironmentManager } from './virtualEnvs/index';
import { IVirtualEnvironmentManager } from './virtualEnvs/types';

Expand All @@ -57,6 +61,9 @@ export function registerTypes(serviceManager: IServiceManager) {
serviceManager.addSingleton<ICondaService>(ICondaService, CondaService);
serviceManager.addSingleton<IVirtualEnvironmentManager>(IVirtualEnvironmentManager, VirtualEnvironmentManager);

serviceManager.add<IInterpreterWatcher>(IInterpreterWatcher, WorkspaceVirtualEnvWatcherService, WORKSPACE_VIRTUAL_ENV_SERVICE);
serviceManager.addSingleton<IInterpreterWatcherBuilder>(IInterpreterWatcherBuilder, InterpreterWatcherBuilder);

serviceManager.addSingleton<IInterpreterVersionService>(IInterpreterVersionService, InterpreterVersionService);
serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, PythonInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE);
serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, CondaEnvFileService, CONDA_ENV_FILE_SERVICE);
Expand Down
Loading