Skip to content
Open
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
32 changes: 32 additions & 0 deletions src/common/telemetry/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
};
}
55 changes: 48 additions & 7 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -522,12 +526,50 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
* Below are all the contributed features using the APIs.
*/
setImmediate(async () => {
// 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,
});
Comment on lines +536 to +549
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PET_START_FAILED reason classification assumes that any error without an errnoError.code must be "binary_not_found", but createNativePythonFinder() can also throw plain Errors after the binary is found/spawned (e.g., missing stdio streams, JSON-RPC setup failures). This will misattribute failures in telemetry. Consider setting reason to 'unknown' by default and only using 'binary_not_found' for the specific errors thrown by getNativePythonToolsPath() (e.g., by throwing a dedicated error type or attaching a discriminator).

Copilot uses AI. Check for mistakes.

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(
Expand Down Expand Up @@ -567,7 +609,6 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
await logDiscoverySummary(envManagers);
} catch (error) {
traceError('Failed to initialize environment managers:', error);
// Show a user-friendly error message
window.showErrorMessage(
l10n.t(
'Python Environments: Failed to initialize environment managers. Some features may not work correctly. Check the Output panel for details.',
Expand Down
40 changes: 35 additions & 5 deletions src/managers/common/nativePythonFinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { spawnProcess } from '../../common/childProcess.apis';
import { ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID } from '../../common/constants';
import { getExtension } from '../../common/extension.apis';
import { traceError, traceVerbose, traceWarn } from '../../common/logging';
import { EventNames } from '../../common/telemetry/constants';
import { sendTelemetryEvent } from '../../common/telemetry/sender';
import { untildify, untildifyArray } from '../../common/utils/pathUtils';
import { isWindows } from '../../common/utils/platformUtils';
import { createRunningWorkerPool, WorkerPool } from '../../common/utils/workerPool';
Expand Down Expand Up @@ -74,21 +76,38 @@ export class ConfigureRetryState {
}
}

export class PetBinaryNotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = 'PetBinaryNotFoundError';
}
}

export async function getNativePythonToolsPath(): Promise<string> {
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 {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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}`,
);
}
Comment on lines +499 to 504
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exit handler logs an "unexpected" error whenever code !== 0, but code can be null (terminated by signal) and exits during restart/dispose are expected (wasExpected === true). This can generate noisy error logs on normal shutdown/restart. Consider gating the error log on !wasExpected && code !== 0 (and treating null as non-error unless !wasExpected).

Copilot uses AI. Check for mistakes.
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)
Expand Down Expand Up @@ -898,5 +926,7 @@ export async function createNativePythonFinder(
api: PythonProjectApi,
context: ExtensionContext,
): Promise<NativePythonFinder> {
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));
}