diff --git a/src/common/telemetry/constants.ts b/src/common/telemetry/constants.ts index 35cb1c77..fd71d555 100644 --- a/src/common/telemetry/constants.ts +++ b/src/common/telemetry/constants.ts @@ -50,6 +50,8 @@ export enum EventNames { */ ENVIRONMENT_DISCOVERY = 'ENVIRONMENT_DISCOVERY', MANAGER_READY_TIMEOUT = 'MANAGER_READY.TIMEOUT', + PET_START_FAILED = 'PET.START_FAILED', + PET_PROCESS_EXIT = 'PET.PROCESS_EXIT', } // Map all events to their properties @@ -238,4 +240,34 @@ export interface IEventNamePropertyMapping { managerId: string; managerKind: 'environment' | 'package'; }; + + /* __GDPR__ + "pet.start_failed": { + "errorCode": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "platform": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "arch": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ + [EventNames.PET_START_FAILED]: { + errorCode: string; + reason: 'binary_not_found' | 'spawn_failed' | 'unknown'; + platform: string; + arch: string; + }; + + /* __GDPR__ + "pet.process_exit": { + "exitCode": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "signal": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "restartAttempt": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "eleanorjboyd" }, + "wasExpected": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ + [EventNames.PET_PROCESS_EXIT]: { + exitCode: number | null; + signal: string | null; + restartAttempt: number; + wasExpected: boolean; + }; } diff --git a/src/extension.ts b/src/extension.ts index 1cd59fb2..34f9c1bf 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -95,7 +95,11 @@ import { collectEnvironmentInfo, getEnvManagerAndPackageManagerConfigLevels, run import { EnvironmentManagers, ProjectCreators, PythonProjectManager } from './internal.api'; import { registerSystemPythonFeatures } from './managers/builtin/main'; import { SysPythonManager } from './managers/builtin/sysPythonManager'; -import { createNativePythonFinder, NativePythonFinder } from './managers/common/nativePythonFinder'; +import { + createNativePythonFinder, + NativePythonFinder, + PetBinaryNotFoundError, +} from './managers/common/nativePythonFinder'; import { IDisposable } from './managers/common/types'; import { registerCondaFeatures } from './managers/conda/main'; import { registerPipenvFeatures } from './managers/pipenv/main'; @@ -522,12 +526,50 @@ export async function activate(context: ExtensionContext): Promise { + // Resolve the pet binary and spawn the finder process. Failures here are pet related. + let nativeFinder: NativePythonFinder; + try { + nativeFinder = await createNativePythonFinder(outputChannel, api, context); + } catch (error) { + traceError('Failed to start Python finder (pet):', error); + + const errnoError = error as NodeJS.ErrnoException; + // PetBinaryNotFoundError = file missing; errno .code = OS spawn failure; anything else = unknown. + const reason = + error instanceof PetBinaryNotFoundError + ? 'binary_not_found' + : errnoError.code + ? 'spawn_failed' + : 'unknown'; + sendTelemetryEvent(EventNames.PET_START_FAILED, undefined, { + errorCode: errnoError.code ?? 'UNKNOWN', + reason, + platform: process.platform, + arch: process.arch, + }); + + const openOutput = l10n.t('Open Output'); + const openSettings = l10n.t('Open Settings'); + const choice = await window.showErrorMessage( + l10n.t('Python Environments: Failed to start the Python finder. Some features may not work correctly.'), + openOutput, + openSettings, + ); + if (choice === openOutput) { + outputChannel.show(); + } else if (choice === openSettings) { + await commands.executeCommand('workbench.action.openSettings', 'python.defaultInterpreterPath'); + } + return; + } + + context.subscriptions.push(nativeFinder); + const sysMgr = new SysPythonManager(nativeFinder, api, outputChannel); + sysPythonManager.resolve(sysMgr); + + // Manager registration and post-registration setup. safeRegister() absorbs + // individual manager failures, so errors here are unexpected and non-pet-related. try { - // This is the finder that is used by all the built in environment managers - const nativeFinder: NativePythonFinder = await createNativePythonFinder(outputChannel, api, context); - context.subscriptions.push(nativeFinder); - const sysMgr = new SysPythonManager(nativeFinder, api, outputChannel); - sysPythonManager.resolve(sysMgr); // Each manager registers independently — one failure must not block the others. await Promise.all([ safeRegister( @@ -567,7 +609,6 @@ export async function activate(context: ExtensionContext): Promise { const envsExt = getExtension(ENVS_EXTENSION_ID); if (envsExt) { const petPath = path.join(envsExt.extensionPath, 'python-env-tools', 'bin', isWindows() ? 'pet.exe' : 'pet'); - if (await fs.pathExists(petPath)) { + const exists = await fs.pathExists(petPath); + traceVerbose(`[pet] Primary path (envs-ext): ${petPath} — exists: ${exists}`); + if (exists) { return petPath; } + } else { + traceVerbose(`[pet] Envs extension (${ENVS_EXTENSION_ID}) not found; trying Python extension fallback`); } const python = getExtension(PYTHON_EXTENSION_ID); if (!python) { - throw new Error('Python extension not found'); + throw new PetBinaryNotFoundError('Python extension not found and envs extension pet binary is missing'); } - return path.join(python.extensionPath, 'python-env-tools', 'bin', isWindows() ? 'pet.exe' : 'pet'); + const fallbackPath = path.join(python.extensionPath, 'python-env-tools', 'bin', isWindows() ? 'pet.exe' : 'pet'); + const fallbackExists = await fs.pathExists(fallbackPath); + traceVerbose(`[pet] Fallback path (python-ext): ${fallbackPath} — exists: ${fallbackExists}`); + if (!fallbackExists) { + throw new PetBinaryNotFoundError(`Python finder binary not found at: ${fallbackPath}`); + } + return fallbackPath; } export interface NativeEnvInfo { @@ -224,6 +243,7 @@ class NativePythonFinderImpl implements NativePythonFinder { private startFailed: boolean = false; private restartAttempts: number = 0; private isRestarting: boolean = false; + private isDisposed: boolean = false; private readonly configureRetry = new ConfigureRetryState(); constructor( @@ -426,6 +446,7 @@ class NativePythonFinderImpl implements NativePythonFinder { } public dispose() { + this.isDisposed = true; this.pool.stop(); this.startDisposables.forEach((d) => d.dispose()); this.connection.dispose(); @@ -475,11 +496,18 @@ class NativePythonFinderImpl implements NativePythonFinder { // Handle process exit - mark as exited so pending requests fail fast this.proc.on('exit', (code, signal) => { this.processExited = true; - if (code !== 0) { + const wasExpected = this.isRestarting || this.isDisposed; + if (!wasExpected && code !== 0) { this.outputChannel.error( `[pet] Python Environment Tools exited unexpectedly with code ${code}, signal ${signal}`, ); } + sendTelemetryEvent(EventNames.PET_PROCESS_EXIT, undefined, { + exitCode: code, + signal: signal ?? null, + restartAttempt: this.restartAttempts, + wasExpected, + }); }); // Handle process errors (e.g., ENOENT if executable not found) @@ -898,5 +926,7 @@ export async function createNativePythonFinder( api: PythonProjectApi, context: ExtensionContext, ): Promise { - return new NativePythonFinderImpl(outputChannel, await getNativePythonToolsPath(), api, getCacheDirectory(context)); + const petPath = await getNativePythonToolsPath(); + traceVerbose(`[pet] Resolved pet binary path: ${petPath}`); + return new NativePythonFinderImpl(outputChannel, petPath, api, getCacheDirectory(context)); }