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 as soon as start #1840

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
179 changes: 137 additions & 42 deletions src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { Disposable, EventEmitter, Event, Uri, workspace } from 'vscode';
import { Disposable, EventEmitter, Event, workspace, Uri } from 'vscode';
import * as ch from 'child_process';
import * as path from 'path';
import * as rpc from 'vscode-jsonrpc/node';
import { PassThrough } from 'stream';
import { isWindows } from '../../../../common/platform/platformService';
import { EXTENSION_ROOT_DIR } from '../../../../constants';
import { traceError, traceInfo, traceLog, traceVerbose, traceWarn } from '../../../../logging';
import { createDeferred } from '../../../../common/utils/async';
import { createDeferred, createDeferredFrom } from '../../../../common/utils/async';
import { DisposableBase, DisposableStore } from '../../../../common/utils/resourceLifecycle';
import { getPythonSetting } from '../../../common/externalDependencies';
import { DEFAULT_INTERPRETER_PATH_SETTING_KEY } from '../lowLevel/customWorkspaceLocator';
import { noop } from '../../../../common/utils/misc';
import { getConfiguration } from '../../../../common/vscodeApis/workspaceApis';
import { CONDAPATH_SETTING_KEY } from '../../../common/environmentManagers/conda';
import { VENVFOLDERS_SETTING_KEY, VENVPATH_SETTING_KEY } from '../lowLevel/customVirtualEnvLocator';
import { getUserHomeDir } from '../../../../common/utils/platform';

const untildify = require('untildify');

const NATIVE_LOCATOR = isWindows()
? path.join(EXTENSION_ROOT_DIR, 'native_locator', 'bin', 'pet.exe')
Expand Down Expand Up @@ -43,7 +48,7 @@ export interface NativeEnvManagerInfo {

export interface NativeGlobalPythonFinder extends Disposable {
resolve(executable: string): Promise<NativeEnvInfo>;
refresh(paths: Uri[]): AsyncIterable<NativeEnvInfo>;
refresh(): AsyncIterable<NativeEnvInfo>;
}

interface NativeLog {
Expand All @@ -54,9 +59,12 @@ interface NativeLog {
class NativeGlobalPythonFinderImpl extends DisposableBase implements NativeGlobalPythonFinder {
private readonly connection: rpc.MessageConnection;

private firstRefreshResults: undefined | (() => AsyncGenerator<NativeEnvInfo, void, unknown>);

constructor() {
super();
this.connection = this.start();
this.firstRefreshResults = this.refreshFirstTime();
}

public async resolve(executable: string): Promise<NativeEnvInfo> {
Expand All @@ -71,41 +79,82 @@ class NativeGlobalPythonFinderImpl extends DisposableBase implements NativeGloba
return environment;
}

async *refresh(_paths: Uri[]): AsyncIterable<NativeEnvInfo> {
async *refresh(): AsyncIterable<NativeEnvInfo> {
if (this.firstRefreshResults) {
// If this is the first time we are refreshing,
// Then get the results from the first refresh.
// Those would have started earlier and cached in memory.
const results = this.firstRefreshResults();
this.firstRefreshResults = undefined;
yield* results;
} else {
const result = this.doRefresh();
let completed = false;
void result.completed.finally(() => {
completed = true;
});
const envs: NativeEnvInfo[] = [];
let discovered = createDeferred();
const disposable = result.discovered((data) => {
envs.push(data);
discovered.resolve();
});
do {
if (!envs.length) {
await Promise.race([result.completed, discovered.promise]);
}
if (envs.length) {
const dataToSend = [...envs];
envs.length = 0;
for (const data of dataToSend) {
yield data;
}
}
if (!completed) {
discovered = createDeferred();
}
} while (!completed);
disposable.dispose();
}
}

refreshFirstTime() {
const result = this.doRefresh();
let completed = false;
void result.completed.finally(() => {
completed = true;
});
const completed = createDeferredFrom(result.completed);
const envs: NativeEnvInfo[] = [];
let discovered = createDeferred();
const disposable = result.discovered((data) => {
envs.push(data);
discovered.resolve();
});
do {
if (!envs.length) {
await Promise.race([result.completed, discovered.promise]);
}
if (envs.length) {
const dataToSend = [...envs];
envs.length = 0;
for (const data of dataToSend) {
yield data;

const iterable = async function* () {
do {
if (!envs.length) {
await Promise.race([completed.promise, discovered.promise]);
}
if (envs.length) {
const dataToSend = [...envs];
envs.length = 0;
for (const data of dataToSend) {
yield data;
}
}
}
if (!completed) {
discovered = createDeferred();
}
} while (!completed);
disposable.dispose();
if (!completed.completed) {
discovered = createDeferred();
}
} while (!completed.completed);
disposable.dispose();
};

return iterable.bind(this);
}

// eslint-disable-next-line class-methods-use-this
private start(): rpc.MessageConnection {
const proc = ch.spawn(NATIVE_LOCATOR, ['server'], { env: process.env });
const disposables: Disposable[] = [];
// jsonrpc package cannot handle messages coming through too quicly.
// jsonrpc package cannot handle messages coming through too quickly.
// Lets handle the messages and close the stream only when
// we have got the exit event.
const readable = new PassThrough();
Expand Down Expand Up @@ -213,40 +262,86 @@ class NativeGlobalPythonFinderImpl extends DisposableBase implements NativeGloba
traceInfo(`Resolved Python Environment ${environment.executable} in ${duration}ms`);
discovered.fire(environment);
})
.catch((ex) => traceError(`Error in Resolving Python Environment ${data}`, ex));
.catch((ex) => traceError(`Error in Resolving Python Environment ${JSON.stringify(data)}`, ex));
trackPromiseAndNotifyOnCompletion(promise);
} else {
discovered.fire(data);
}
}),
);

const pythonPathSettings = (workspace.workspaceFolders || []).map((w) =>
getPythonSetting<string>(DEFAULT_INTERPRETER_PATH_SETTING_KEY, w.uri.fsPath),
);
pythonPathSettings.push(getPythonSetting<string>(DEFAULT_INTERPRETER_PATH_SETTING_KEY));
const pythonSettings = Array.from(new Set(pythonPathSettings.filter((item) => !!item)).values()).map((p) =>
// We only want the parent directories.
path.dirname(p!),
);
trackPromiseAndNotifyOnCompletion(
this.connection
.sendRequest<{ duration: number }>('refresh', {
// Send configuration information to the Python finder.
search_paths: (workspace.workspaceFolders || []).map((w) => w.uri.fsPath),
// Also send the python paths that are configured in the settings.
python_path_settings: pythonSettings,
conda_executable: undefined,
})
this.sendRefreshRequest()
.then(({ duration }) => traceInfo(`Native Python Finder completed in ${duration}ms`))
.catch((ex) => traceError('Error in Native Python Finder', ex)),
);

completed.promise.finally(() => disposable.dispose());
return {
completed: completed.promise,
discovered: discovered.event,
};
}

private sendRefreshRequest() {
const pythonPathSettings = (workspace.workspaceFolders || []).map((w) =>
getPythonSettingAndUntildify<string>(DEFAULT_INTERPRETER_PATH_SETTING_KEY, w.uri),
);
pythonPathSettings.push(getPythonSettingAndUntildify<string>(DEFAULT_INTERPRETER_PATH_SETTING_KEY));
// We can have multiple workspaces, each with its own setting.
const pythonSettings = Array.from(
new Set(
pythonPathSettings
.filter((item) => !!item)
// We only want the parent directories.
.map((p) => path.dirname(p!))
/// If setting value is 'python', then `path.dirname('python')` will yield `.`
.filter((item) => item !== '.'),
),
);

return this.connection.sendRequest<{ duration: number }>(
'refresh',
// Send configuration information to the Python finder.
{
// This has a special meaning in locator, its lot a low priority
// as we treat this as workspace folders that can contain a large number of files.
search_paths: (workspace.workspaceFolders || []).map((w) => w.uri.fsPath),
// Also send the python paths that are configured in the settings.
python_interpreter_paths: pythonSettings,
// We do not want to mix this with `search_paths`
virtual_env_paths: getCustomVirtualEnvDirs(),
conda_executable: getPythonSettingAndUntildify<string>(CONDAPATH_SETTING_KEY),
poetry_executable: getPythonSettingAndUntildify<string>('poetryPath'),
pipenv_executable: getPythonSettingAndUntildify<string>('pipenvPath'),
},
);
}
}

/**
* Gets all custom virtual environment locations to look for environments.
*/
async function getCustomVirtualEnvDirs(): Promise<string[]> {
const venvDirs: string[] = [];
const venvPath = getPythonSettingAndUntildify<string>(VENVPATH_SETTING_KEY);
if (venvPath) {
venvDirs.push(untildify(venvPath));
}
const venvFolders = getPythonSettingAndUntildify<string[]>(VENVFOLDERS_SETTING_KEY) ?? [];
const homeDir = getUserHomeDir();
if (homeDir) {
venvFolders.map((item) => path.join(homeDir, item)).forEach((d) => venvDirs.push(d));
}
return Array.from(new Set(venvDirs));
}

function getPythonSettingAndUntildify<T>(name: string, scope?: Uri): T | undefined {
const value = getConfiguration('python', scope).get<T>(name);
if (typeof value === 'string') {
return value ? ((untildify(value as string) as unknown) as T) : undefined;
}
return value;
}

export function createNativeGlobalPythonFinder(): NativeGlobalPythonFinder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export class NativeLocator implements ILocator<BasicEnvInfo>, IDisposable {
const disposables: IDisposable[] = [];
const disposable = new Disposable(() => disposeAll(disposables));
this.disposables.push(disposable);
for await (const data of this.finder.refresh([])) {
for await (const data of this.finder.refresh()) {
if (data.manager) {
switch (toolToKnownEnvironmentTool(data.manager.tool)) {
case 'Conda': {
Expand Down
Loading