Skip to content

Commit ac8abbd

Browse files
authored
Enhance NativePythonFinderImpl with improved logging and configuration comparison (#1149)
## Summary This PR implements code quality improvements for ` ativePythonFinder.ts` as identified in #1142. ## Changes ### 1. Type Guard for `isNativeEnvInfo()` Changed return type from `boolean` to `info is NativeEnvInfo` for proper TypeScript type narrowing. ### 2. Robust Configuration Comparison Replaced fragile `JSON.stringify()` comparison with a dedicated `configurationEquals()` method that: - Compares properties individually - Uses sorted array comparison to handle order differences - Avoids issues with undefined value serialization ### 3. Cache Key Collision Prevention Changed the separator for cache keys from `,` to `\0` (null character) to prevent collisions with paths containing commas. ### 4. Standardized Logging Converted `traceVerbose` calls within `NativePythonFinderImpl` to use `outputChannel.debug()` for consistent instance-level logging. Removed unused import. ### 5. Graceful Shutdown Added a graceful shutdown mechanism that closes stdin before killing the process, giving it 500ms to clean up. ### 6. Documentation Added explanatory comment for the intentional duplicate `venvFolders` fetch explaining why it's needed when `searchPaths` may override `environmentDirectories`. Fixes #1142
1 parent 149dc8e commit ac8abbd

File tree

1 file changed

+61
-11
lines changed

1 file changed

+61
-11
lines changed

src/managers/common/nativePythonFinder.ts

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { PythonProjectApi } from '../../api';
77
import { spawnProcess } from '../../common/childProcess.apis';
88
import { ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID } from '../../common/constants';
99
import { getExtension } from '../../common/extension.apis';
10-
import { traceError, traceLog, traceVerbose, traceWarn } from '../../common/logging';
10+
import { traceError, traceLog, traceWarn } from '../../common/logging';
1111
import { untildify, untildifyArray } from '../../common/utils/pathUtils';
1212
import { isWindows } from '../../common/utils/platformUtils';
1313
import { createRunningWorkerPool, WorkerPool } from '../../common/utils/workerPool';
@@ -52,7 +52,7 @@ export interface NativeEnvManagerInfo {
5252

5353
export type NativeInfo = NativeEnvInfo | NativeEnvManagerInfo;
5454

55-
export function isNativeEnvInfo(info: NativeInfo): boolean {
55+
export function isNativeEnvInfo(info: NativeInfo): info is NativeEnvInfo {
5656
return !(info as NativeEnvManagerInfo).tool;
5757
}
5858

@@ -149,7 +149,8 @@ class NativePythonFinderImpl implements NativePythonFinder {
149149
return options;
150150
}
151151
if (Array.isArray(options)) {
152-
return options.map((item) => item.fsPath).join(',');
152+
// Use null character as separator to avoid collisions with paths containing commas
153+
return options.map((item) => item.fsPath).join('\0');
153154
}
154155
return 'all';
155156
}
@@ -158,9 +159,9 @@ class NativePythonFinderImpl implements NativePythonFinder {
158159
const key = this.getKey(options);
159160
this.cache.delete(key);
160161
if (!options) {
161-
traceVerbose('Finder - refreshing all environments');
162+
this.outputChannel.debug('[Finder] Refreshing all environments');
162163
} else {
163-
traceVerbose('Finder - from cache environments', key);
164+
this.outputChannel.debug(`[Finder] Hard refresh for key: ${key}`);
164165
}
165166
const result = await this.pool.addToQueue(options);
166167
// Validate result from worker pool
@@ -185,9 +186,9 @@ class NativePythonFinderImpl implements NativePythonFinder {
185186
}
186187

187188
if (!options) {
188-
traceVerbose('Finder - from cache refreshing all environments');
189+
this.outputChannel.debug('[Finder] Returning cached environments for all');
189190
} else {
190-
traceVerbose('Finder - from cache environments', key);
191+
this.outputChannel.debug(`[Finder] Returning cached environments for key: ${key}`);
191192
}
192193
return cacheResult;
193194
}
@@ -199,7 +200,10 @@ class NativePythonFinderImpl implements NativePythonFinder {
199200
}
200201

201202
private getRefreshOptions(options?: NativePythonEnvironmentKind | Uri[]): RefreshOptions | undefined {
202-
// settings on where else to search
203+
// Note: venvFolders is also fetched in getAllExtraSearchPaths() for configure().
204+
// This duplication is intentional: when searchPaths is provided to the native finder,
205+
// it may override (not supplement) the configured environmentDirectories.
206+
// We must include venvFolders here to ensure they're always searched during targeted refreshes.
203207
const venvFolders = getPythonSettingAndUntildify<string[]>('venvFolders') ?? [];
204208
if (options) {
205209
if (typeof options === 'string') {
@@ -234,7 +238,16 @@ class NativePythonFinderImpl implements NativePythonFinder {
234238
dispose: () => {
235239
try {
236240
if (proc.exitCode === null) {
237-
proc.kill();
241+
// Attempt graceful shutdown by closing stdin before killing
242+
// This gives the process a chance to clean up
243+
this.outputChannel.debug('[pet] Shutting down Python Locator server');
244+
proc.stdin.end();
245+
// Give process a moment to exit gracefully, then force kill
246+
setTimeout(() => {
247+
if (proc.exitCode === null) {
248+
proc.kill();
249+
}
250+
}, 500);
238251
}
239252
} catch (ex) {
240253
this.outputChannel.error('[pet] Error disposing finder', ex);
@@ -350,8 +363,8 @@ class NativePythonFinderImpl implements NativePythonFinder {
350363
poetryExecutable: getPythonSettingAndUntildify<string>('poetryPath'),
351364
cacheDirectory: this.cacheDirectory?.fsPath,
352365
};
353-
// No need to send a configuration request, is there are no changes.
354-
if (JSON.stringify(options) === JSON.stringify(this.lastConfiguration || {})) {
366+
// No need to send a configuration request if there are no changes.
367+
if (this.lastConfiguration && this.configurationEquals(options, this.lastConfiguration)) {
355368
this.outputChannel.debug('[pet] configure: No changes detected, skipping configuration update.');
356369
return;
357370
}
@@ -363,6 +376,43 @@ class NativePythonFinderImpl implements NativePythonFinder {
363376
this.outputChannel.error('[pet] configure: Configuration error', ex);
364377
}
365378
}
379+
380+
/**
381+
* Compares two ConfigurationOptions objects for equality.
382+
* Uses property-by-property comparison to avoid issues with JSON.stringify
383+
* (property order, undefined values serialization).
384+
*/
385+
private configurationEquals(a: ConfigurationOptions, b: ConfigurationOptions): boolean {
386+
// Compare simple optional string properties
387+
if (a.condaExecutable !== b.condaExecutable) {
388+
return false;
389+
}
390+
if (a.poetryExecutable !== b.poetryExecutable) {
391+
return false;
392+
}
393+
if (a.cacheDirectory !== b.cacheDirectory) {
394+
return false;
395+
}
396+
397+
// Compare array properties using sorted comparison to handle order differences
398+
const arraysEqual = (arr1: string[], arr2: string[]): boolean => {
399+
if (arr1.length !== arr2.length) {
400+
return false;
401+
}
402+
const sorted1 = [...arr1].sort();
403+
const sorted2 = [...arr2].sort();
404+
return sorted1.every((val, idx) => val === sorted2[idx]);
405+
};
406+
407+
if (!arraysEqual(a.workspaceDirectories, b.workspaceDirectories)) {
408+
return false;
409+
}
410+
if (!arraysEqual(a.environmentDirectories, b.environmentDirectories)) {
411+
return false;
412+
}
413+
414+
return true;
415+
}
366416
}
367417

368418
type ConfigurationOptions = {

0 commit comments

Comments
 (0)