diff --git a/src/common/telemetry/constants.ts b/src/common/telemetry/constants.ts index 9bcd68ea..35cb1c77 100644 --- a/src/common/telemetry/constants.ts +++ b/src/common/telemetry/constants.ts @@ -49,6 +49,7 @@ export enum EventNames { * - errorType: string (error class name, on failure only) */ ENVIRONMENT_DISCOVERY = 'ENVIRONMENT_DISCOVERY', + MANAGER_READY_TIMEOUT = 'MANAGER_READY.TIMEOUT', } // Map all events to their properties @@ -226,4 +227,15 @@ export interface IEventNamePropertyMapping { envCount?: number; errorType?: string; }; + + /* __GDPR__ + "manager_ready.timeout": { + "managerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "managerKind": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ + [EventNames.MANAGER_READY_TIMEOUT]: { + managerId: string; + managerKind: 'environment' | 'package'; + }; } diff --git a/src/features/common/managerReady.ts b/src/features/common/managerReady.ts index a7341b80..c85b3134 100644 --- a/src/features/common/managerReady.ts +++ b/src/features/common/managerReady.ts @@ -1,12 +1,16 @@ import { Disposable, l10n, Uri } from 'vscode'; -import { EnvironmentManagers, PythonProjectManager } from '../../internal.api'; -import { createDeferred, Deferred } from '../../common/utils/deferred'; import { allExtensions, getExtension } from '../../common/extension.apis'; -import { traceError, traceInfo } from '../../common/logging'; -import { showErrorMessage } from '../../common/window.apis'; -import { getDefaultEnvManagerSetting, getDefaultPkgManagerSetting } from '../settings/settingHelpers'; import { WorkbenchStrings } from '../../common/localize'; +import { traceError, traceInfo, traceWarn } from '../../common/logging'; +import { EventNames } from '../../common/telemetry/constants'; +import { sendTelemetryEvent } from '../../common/telemetry/sender'; +import { createDeferred, Deferred } from '../../common/utils/deferred'; +import { showErrorMessage } from '../../common/window.apis'; import { installExtension } from '../../common/workbenchCommands'; +import { EnvironmentManagers, PythonProjectManager } from '../../internal.api'; +import { getDefaultEnvManagerSetting, getDefaultPkgManagerSetting } from '../settings/settingHelpers'; + +const MANAGER_READY_TIMEOUT_MS = 30_000; interface ManagerReady extends Disposable { waitForEnvManager(uris?: Uri[]): Promise; @@ -29,7 +33,10 @@ class ManagerReadyImpl implements ManagerReady { private readonly checked: Set = new Set(); private readonly disposables: Disposable[] = []; - constructor(em: EnvironmentManagers, private readonly pm: PythonProjectManager) { + constructor( + em: EnvironmentManagers, + private readonly pm: PythonProjectManager, + ) { this.disposables.push( em.onDidChangeEnvironmentManager((e) => { if (this.envManagers.has(e.manager.id)) { @@ -104,6 +111,44 @@ class ManagerReadyImpl implements ManagerReady { } } + /** + * Wraps a deferred with a timeout so a missing/dead manager cannot block the API forever. + * On timeout the deferred is resolved (not rejected) so callers proceed with degraded results + * instead of hanging. + */ + private _withTimeout(deferred: Deferred, managerId: string, kind: string): Promise { + if (deferred.completed) { + return deferred.promise; + } + return new Promise((resolve) => { + const timer = setTimeout(() => { + if (!deferred.completed) { + traceWarn( + `Timed out after ${MANAGER_READY_TIMEOUT_MS / 1000}s waiting for ${kind} manager "${managerId}" to register. ` + + `The manager may not be installed or its extension failed to activate. Proceeding without it. ` + + `To prevent this, check your "python-envs.defaultEnvManager" and "python-envs.pythonProjects" settings.`, + ); + sendTelemetryEvent(EventNames.MANAGER_READY_TIMEOUT, undefined, { + managerId, + managerKind: kind as 'environment' | 'package', + }); + deferred.resolve(); + } + }, MANAGER_READY_TIMEOUT_MS); + + deferred.promise.then( + () => { + clearTimeout(timer); + resolve(); + }, + () => { + clearTimeout(timer); + resolve(); + }, + ); + }); + } + public dispose(): void { this.disposables.forEach((d) => d.dispose()); this.envManagers.clear(); @@ -116,7 +161,7 @@ class ManagerReadyImpl implements ManagerReady { } const deferred = createDeferred(); this.envManagers.set(managerId, deferred); - return deferred.promise; + return this._withTimeout(deferred, managerId, 'environment'); } public async waitForEnvManager(uris?: Uri[]): Promise { @@ -165,7 +210,7 @@ class ManagerReadyImpl implements ManagerReady { } const deferred = createDeferred(); this.pkgManagers.set(managerId, deferred); - return deferred.promise; + return this._withTimeout(deferred, managerId, 'package'); } public async waitForPkgManager(uris?: Uri[]): Promise {