From b6f24798ecc9d6f1484a60cfbad4d1f0b66ddaa0 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:54:04 -0800 Subject: [PATCH 1/2] feat: add telemetry for PET process failures and exits --- src/common/telemetry/constants.ts | 32 +++++++++++++++++++ src/extension.ts | 38 +++++++++++++++++++---- src/managers/common/nativePythonFinder.ts | 31 +++++++++++++++--- 3 files changed, 91 insertions(+), 10 deletions(-) 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..ba167709 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -522,12 +522,39 @@ 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; + // Plain Error (no .code) = binary not found by getNativePythonToolsPath. + // Errno error (has .code) = spawn failed (ENOENT, EACCES, EPERM, etc.). + const reason = errnoError.code ? 'spawn_failed' : 'binary_not_found'; + sendTelemetryEvent(EventNames.PET_START_FAILED, undefined, { + errorCode: errnoError.code ?? 'UNKNOWN', + reason, + platform: process.platform, + arch: process.arch, + }); + + window.showErrorMessage( + l10n.t( + 'Python Environments: Failed to start the Python finder. Some features may not work correctly. Check the Output panel for details.', + ), + ); + 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 +594,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 Error('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 Error(`Python finder binary not found at: ${fallbackPath}`); + } + return fallbackPath; } export interface NativeEnvInfo { @@ -224,6 +236,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 +439,7 @@ class NativePythonFinderImpl implements NativePythonFinder { } public dispose() { + this.isDisposed = true; this.pool.stop(); this.startDisposables.forEach((d) => d.dispose()); this.connection.dispose(); @@ -475,11 +489,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; + const wasExpected = this.isRestarting || this.isDisposed; if (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 +919,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)); } From 1bbca8e62dc136e29bc1772a50fc1e67a33ccc26 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:08:33 -0800 Subject: [PATCH 2/2] address comments --- src/extension.ts | 31 +++++++++++++++++------ src/managers/common/nativePythonFinder.ts | 13 +++++++--- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index ba167709..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'; @@ -530,9 +534,13 @@ export async function activate(context: ExtensionContext): Promise { const envsExt = getExtension(ENVS_EXTENSION_ID); if (envsExt) { @@ -91,14 +98,14 @@ export async function getNativePythonToolsPath(): Promise { const python = getExtension(PYTHON_EXTENSION_ID); if (!python) { - throw new Error('Python extension not found and envs extension pet binary is missing'); + throw new PetBinaryNotFoundError('Python extension not found and envs extension pet binary is missing'); } 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 Error(`Python finder binary not found at: ${fallbackPath}`); + throw new PetBinaryNotFoundError(`Python finder binary not found at: ${fallbackPath}`); } return fallbackPath; } @@ -490,7 +497,7 @@ class NativePythonFinderImpl implements NativePythonFinder { this.proc.on('exit', (code, signal) => { this.processExited = true; const wasExpected = this.isRestarting || this.isDisposed; - if (code !== 0) { + if (!wasExpected && code !== 0) { this.outputChannel.error( `[pet] Python Environment Tools exited unexpectedly with code ${code}, signal ${signal}`, );