fix: resolve Python environment race condition (#142)

Implemented promise queue pattern in PythonEnvManager to handle
concurrent initialization requests. Previously, multiple simultaneous
requests (e.g., startup + merge) would fail with "Already
initializing" error.

Also fixed parsePythonCommand() to handle file paths with spaces by
checking file existence before splitting on whitespace.

Changes:
- Added initializationPromise field to queue concurrent requests
- Split initialize() into public and private _doInitialize()
- Enhanced parsePythonCommand() with existsSync() check

Co-authored-by: Joris Slagter <mail@jorisslagter.nl>
This commit is contained in:
Joris Slagter
2025-12-22 22:50:56 +01:00
committed by GitHub
parent df779530e7
commit ebd8340d82
2 changed files with 40 additions and 8 deletions
+9 -2
View File
@@ -1,4 +1,5 @@
import { execSync } from 'child_process';
import { existsSync } from 'fs';
/**
* Detect and return the best available Python command.
@@ -48,12 +49,18 @@ export function getDefaultPythonCommand(): string {
/**
* Parse a Python command string into command and base arguments.
* Handles space-separated commands like "py -3".
* Handles space-separated commands like "py -3" and file paths with spaces.
*
* @param pythonPath - The Python command string (e.g., "python3", "py -3")
* @param pythonPath - The Python command string (e.g., "python3", "py -3", "/path/with spaces/python")
* @returns Tuple of [command, baseArgs] ready for use with spawn()
*/
export function parsePythonCommand(pythonPath: string): [string, string[]] {
// If the path points to an actual file, use it directly (handles paths with spaces)
if (existsSync(pythonPath)) {
return [pythonPath, []];
}
// Otherwise, split on spaces for commands like "py -3"
const parts = pythonPath.split(' ');
const command = parts[0];
const baseArgs = parts.slice(1);
+31 -6
View File
@@ -24,6 +24,7 @@ export class PythonEnvManager extends EventEmitter {
private pythonPath: string | null = null;
private isInitializing = false;
private isReady = false;
private initializationPromise: Promise<PythonEnvStatus> | null = null;
/**
* Get the path where the venv should be created.
@@ -309,18 +310,42 @@ export class PythonEnvManager extends EventEmitter {
/**
* Initialize the Python environment.
* Creates venv and installs deps if needed.
*
* If initialization is already in progress, this will wait for and return
* the existing initialization promise instead of starting a new one.
*/
async initialize(autoBuildSourcePath: string): Promise<PythonEnvStatus> {
if (this.isInitializing) {
// If there's already an initialization in progress, wait for it
if (this.initializationPromise) {
console.warn('[PythonEnvManager] Initialization already in progress, waiting...');
return this.initializationPromise;
}
// If already ready and pointing to the same source, return cached status
if (this.isReady && this.autoBuildSourcePath === autoBuildSourcePath) {
return {
ready: false,
pythonPath: null,
venvExists: false,
depsInstalled: false,
error: 'Already initializing'
ready: true,
pythonPath: this.pythonPath,
venvExists: true,
depsInstalled: true
};
}
// Start new initialization and store the promise
this.initializationPromise = this._doInitialize(autoBuildSourcePath);
try {
return await this.initializationPromise;
} finally {
this.initializationPromise = null;
}
}
/**
* Internal initialization method that performs the actual setup.
* This is separated from initialize() to support the promise queue pattern.
*/
private async _doInitialize(autoBuildSourcePath: string): Promise<PythonEnvStatus> {
this.isInitializing = true;
this.autoBuildSourcePath = autoBuildSourcePath;