Skip to content

Commit c7e6d57

Browse files
authored
Merge branch 'main' into copilot/fix-multi-root-workspace-settings
2 parents a237618 + 1fe3322 commit c7e6d57

8 files changed

Lines changed: 71 additions & 29 deletions

File tree

examples/sample1/src/api.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -818,10 +818,14 @@ export interface PythonEnvironmentManagerRegistrationApi {
818818
* Register an environment manager implementation.
819819
*
820820
* @param manager Environment Manager implementation to register.
821+
* @param options Optional registration options.
822+
* @param options.extensionId The extension ID of the calling extension. This is used as a fallback when
823+
* automatic extension detection fails, such as during F5 debugging where the extension's file path
824+
* does not contain its marketplace ID. If automatic detection succeeds, this value is ignored.
821825
* @returns A disposable that can be used to unregister the environment manager.
822826
* @see {@link EnvironmentManager}
823827
*/
824-
registerEnvironmentManager(manager: EnvironmentManager): Disposable;
828+
registerEnvironmentManager(manager: EnvironmentManager, options?: { extensionId?: string }): Disposable;
825829
}
826830

827831
export interface PythonEnvironmentItemApi {
@@ -922,10 +926,14 @@ export interface PythonPackageManagerRegistrationApi {
922926
* Register a package manager implementation.
923927
*
924928
* @param manager Package Manager implementation to register.
929+
* @param options Optional registration options.
930+
* @param options.extensionId The extension ID of the calling extension. This is used as a fallback when
931+
* automatic extension detection fails, such as during F5 debugging where the extension's file path
932+
* does not contain its marketplace ID. If automatic detection succeeds, this value is ignored.
925933
* @returns A disposable that can be used to unregister the package manager.
926934
* @see {@link PackageManager}
927935
*/
928-
registerPackageManager(manager: PackageManager): Disposable;
936+
registerPackageManager(manager: PackageManager, options?: { extensionId?: string }): Disposable;
929937
}
930938

931939
export interface PythonPackageGetterApi {

pythonEnvironmentsApi/src/main.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -842,10 +842,14 @@ export interface PythonEnvironmentManagerRegistrationApi {
842842
* Register an environment manager implementation.
843843
*
844844
* @param manager Environment Manager implementation to register.
845+
* @param options Optional registration options.
846+
* @param options.extensionId The extension ID of the calling extension. This is used as a fallback when
847+
* automatic extension detection fails, such as during F5 debugging where the extension's file path
848+
* does not contain its marketplace ID. If automatic detection succeeds, this value is ignored.
845849
* @returns A disposable that can be used to unregister the environment manager.
846850
* @see {@link EnvironmentManager}
847851
*/
848-
registerEnvironmentManager(manager: EnvironmentManager): Disposable;
852+
registerEnvironmentManager(manager: EnvironmentManager, options?: { extensionId?: string }): Disposable;
849853
}
850854

851855
export interface PythonEnvironmentItemApi {
@@ -947,10 +951,14 @@ export interface PythonPackageManagerRegistrationApi {
947951
* Register a package manager implementation.
948952
*
949953
* @param manager Package Manager implementation to register.
954+
* @param options Optional registration options.
955+
* @param options.extensionId The extension ID of the calling extension. This is used as a fallback when
956+
* automatic extension detection fails, such as during F5 debugging where the extension's file path
957+
* does not contain its marketplace ID. If automatic detection succeeds, this value is ignored.
950958
* @returns A disposable that can be used to unregister the package manager.
951959
* @see {@link PackageManager}
952960
*/
953-
registerPackageManager(manager: PackageManager): Disposable;
961+
registerPackageManager(manager: PackageManager, options?: { extensionId?: string }): Disposable;
954962
}
955963

956964
export interface PythonPackageGetterApi {

src/api.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -836,10 +836,14 @@ export interface PythonEnvironmentManagerRegistrationApi {
836836
* Register an environment manager implementation.
837837
*
838838
* @param manager Environment Manager implementation to register.
839+
* @param options Optional registration options.
840+
* @param options.extensionId The extension ID of the calling extension. This is used as a fallback when
841+
* automatic extension detection fails, such as during F5 debugging where the extension's file path
842+
* does not contain its marketplace ID. If automatic detection succeeds, this value is ignored.
839843
* @returns A disposable that can be used to unregister the environment manager.
840844
* @see {@link EnvironmentManager}
841845
*/
842-
registerEnvironmentManager(manager: EnvironmentManager): Disposable;
846+
registerEnvironmentManager(manager: EnvironmentManager, options?: { extensionId?: string }): Disposable;
843847
}
844848

845849
export interface PythonEnvironmentItemApi {
@@ -941,10 +945,14 @@ export interface PythonPackageManagerRegistrationApi {
941945
* Register a package manager implementation.
942946
*
943947
* @param manager Package Manager implementation to register.
948+
* @param options Optional registration options.
949+
* @param options.extensionId The extension ID of the calling extension. This is used as a fallback when
950+
* automatic extension detection fails, such as during F5 debugging where the extension's file path
951+
* does not contain its marketplace ID. If automatic detection succeeds, this value is ignored.
944952
* @returns A disposable that can be used to unregister the package manager.
945953
* @see {@link PackageManager}
946954
*/
947-
registerPackageManager(manager: PackageManager): Disposable;
955+
registerPackageManager(manager: PackageManager, options?: { extensionId?: string }): Disposable;
948956
}
949957

950958
export interface PythonPackageGetterApi {

src/common/utils/frameUtils.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function getPathFromFrame(frame: FrameData): string {
2828
return frame.filePath;
2929
}
3030

31-
export function getCallingExtension(): string {
31+
export function getCallingExtension(extensionIdHint?: string): string {
3232
const pythonExts = [ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID];
3333
const extensions = allExtensions();
3434
const otherExts = extensions.filter((ext) => !pythonExts.includes(ext.id));
@@ -94,6 +94,19 @@ export function getCallingExtension(): string {
9494
}
9595
}
9696

97+
// Use the provided extensionId hint as a fallback (e.g., during F5 debugging where
98+
// stack-based detection fails because the file path doesn't contain the extension ID).
99+
// Only accept the hint if it matches an actually loaded extension for safety.
100+
if (extensionIdHint) {
101+
const hintExt = extensions.find((ext) => ext.id === extensionIdHint);
102+
if (hintExt) {
103+
traceVerbose(`Using provided extensionId hint: ${extensionIdHint}`);
104+
extensionIdCache.set(cacheKey, extensionIdHint);
105+
return extensionIdHint;
106+
}
107+
traceWarn(`Provided extensionId hint '${extensionIdHint}' not found in loaded extensions, ignoring`);
108+
}
109+
97110
// Fallback - we're likely being called from Python extension or built-in managers
98111
traceWarn(
99112
`Could not determine calling extension from stack frames. ` +

src/features/envManagers.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@ import {
4040
setAllManagerSettings,
4141
} from './settings/settingHelpers';
4242

43-
function generateId(name: string): string {
43+
function generateId(name: string, extensionId?: string): string {
4444
const newName = name.toLowerCase().replace(/[^a-zA-Z0-9-_]/g, '_');
4545
if (name !== newName) {
4646
traceVerbose(`Environment manager name "${name}" was normalized to "${newName}"`);
4747
}
48-
return `${getCallingExtension()}:${newName}`;
48+
return `${getCallingExtension(extensionId)}:${newName}`;
4949
}
5050

5151
export class PythonEnvironmentManagers implements EnvironmentManagers {
@@ -71,9 +71,9 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
7171

7272
constructor(private readonly pm: PythonProjectManager) {}
7373

74-
public registerEnvironmentManager(manager: EnvironmentManager): Disposable {
74+
public registerEnvironmentManager(manager: EnvironmentManager, options?: { extensionId?: string }): Disposable {
7575
const registrationStopWatch = new StopWatch();
76-
const managerId = generateId(manager.name);
76+
const managerId = generateId(manager.name, options?.extensionId);
7777
if (this._environmentManagers.has(managerId)) {
7878
const ex = new EnvironmentManagerAlreadyRegisteredError(
7979
`Environment manager with id ${managerId} already registered`,
@@ -119,8 +119,8 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
119119
});
120120
}
121121

122-
public registerPackageManager(manager: PackageManager): Disposable {
123-
const managerId = generateId(manager.name);
122+
public registerPackageManager(manager: PackageManager, options?: { extensionId?: string }): Disposable {
123+
const managerId = generateId(manager.name, options?.extensionId);
124124
if (this._packageManagers.has(managerId)) {
125125
const ex = new PackageManagerAlreadyRegisteredError(
126126
`Package manager with id ${managerId} already registered`,

src/features/pythonApi.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
8282
);
8383
}
8484

85-
registerEnvironmentManager(manager: EnvironmentManager): Disposable {
85+
registerEnvironmentManager(manager: EnvironmentManager, options?: { extensionId?: string }): Disposable {
8686
const disposables: Disposable[] = [];
87-
disposables.push(this.envManagers.registerEnvironmentManager(manager));
87+
disposables.push(this.envManagers.registerEnvironmentManager(manager, options));
8888
if (manager.onDidChangeEnvironments) {
8989
disposables.push(manager.onDidChangeEnvironments((e) => this._onDidChangeEnvironments.fire(e)));
9090
}
@@ -233,9 +233,9 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
233233
return await handlePythonPath(context, this.envManagers.managers, projectEnvManagers);
234234
}
235235

236-
registerPackageManager(manager: PackageManager): Disposable {
236+
registerPackageManager(manager: PackageManager, options?: { extensionId?: string }): Disposable {
237237
const disposables: Disposable[] = [];
238-
disposables.push(this.envManagers.registerPackageManager(manager));
238+
disposables.push(this.envManagers.registerPackageManager(manager, options));
239239
if (manager.onDidChangePackages) {
240240
disposables.push(manager.onDidChangePackages((e) => this._onDidChangePackages.fire(e)));
241241
}

src/internal.api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ export interface InternalDidChangeEnvironmentsEventArgs {
7676
}
7777

7878
export interface EnvironmentManagers extends Disposable {
79-
registerEnvironmentManager(manager: EnvironmentManager): Disposable;
80-
registerPackageManager(manager: PackageManager): Disposable;
79+
registerEnvironmentManager(manager: EnvironmentManager, options?: { extensionId?: string }): Disposable;
80+
registerPackageManager(manager: PackageManager, options?: { extensionId?: string }): Disposable;
8181

8282
/**
8383
* This event is fired when any environment manager changes its collection of environments.

src/managers/common/nativePythonFinder.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -260,10 +260,11 @@ class NativePythonFinderImpl implements NativePythonFinder {
260260
this.restartAttempts = 0;
261261
return environment;
262262
} catch (ex) {
263-
// On resolve timeout (not configure — configure handles its own timeout),
263+
// On resolve timeout or connection error (not configure — configure handles its own timeout),
264264
// kill the hung process so next request triggers restart
265-
if (ex instanceof RpcTimeoutError && ex.method !== 'configure') {
266-
this.outputChannel.warn('[pet] Resolve request timed out, killing hung process for restart');
265+
if ((ex instanceof RpcTimeoutError && ex.method !== 'configure') || ex instanceof rpc.ConnectionError) {
266+
const reason = ex instanceof rpc.ConnectionError ? 'crashed' : 'timed out';
267+
this.outputChannel.warn(`[pet] Resolve request ${reason}, killing process for restart`);
267268
this.killProcess();
268269
this.processExited = true;
269270
}
@@ -574,11 +575,14 @@ class NativePythonFinderImpl implements NativePythonFinder {
574575
} catch (ex) {
575576
lastError = ex;
576577

577-
// Only retry on timeout errors
578-
if (ex instanceof RpcTimeoutError && ex.method !== 'configure') {
578+
// Retry on timeout or connection errors (PET hung or crashed mid-request)
579+
const isRetryable =
580+
(ex instanceof RpcTimeoutError && ex.method !== 'configure') || ex instanceof rpc.ConnectionError;
581+
if (isRetryable) {
579582
if (attempt < MAX_REFRESH_RETRIES) {
583+
const reason = ex instanceof rpc.ConnectionError ? 'crashed' : 'timed out';
580584
this.outputChannel.warn(
581-
`[pet] Refresh timed out (attempt ${attempt + 1}/${MAX_REFRESH_RETRIES + 1}), restarting and retrying...`,
585+
`[pet] Refresh ${reason} (attempt ${attempt + 1}/${MAX_REFRESH_RETRIES + 1}), restarting and retrying...`,
582586
);
583587
// Kill and restart for retry
584588
this.killProcess();
@@ -588,7 +592,7 @@ class NativePythonFinderImpl implements NativePythonFinder {
588592
// Final attempt failed
589593
this.outputChannel.error(`[pet] Refresh failed after ${MAX_REFRESH_RETRIES + 1} attempts`);
590594
}
591-
// Non-timeout errors or final timeout - rethrow
595+
// Non-retryable errors or final attempt - rethrow
592596
throw ex;
593597
}
594598
}
@@ -652,10 +656,11 @@ class NativePythonFinderImpl implements NativePythonFinder {
652656
this.outputChannel.info(`[pet] Refresh succeeded on retry attempt ${attempt + 1}`);
653657
}
654658
} catch (ex) {
655-
// On refresh timeout (not configure — configure handles its own timeout),
659+
// On refresh timeout or connection error (not configure — configure handles its own timeout),
656660
// kill the hung process so next request triggers restart
657-
if (ex instanceof RpcTimeoutError && ex.method !== 'configure') {
658-
this.outputChannel.warn('[pet] Request timed out, killing hung process for restart');
661+
if ((ex instanceof RpcTimeoutError && ex.method !== 'configure') || ex instanceof rpc.ConnectionError) {
662+
const reason = ex instanceof rpc.ConnectionError ? 'crashed' : 'timed out';
663+
this.outputChannel.warn(`[pet] PET process ${reason}, killing for restart`);
659664
this.killProcess();
660665
this.processExited = true;
661666
}

0 commit comments

Comments
 (0)