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: 28 additions & 4 deletions .vscode-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as path from 'path';
// (e.g. /Users/runner/work/<repo>/<repo>/.vscode-test/user-data) overflows.
const userDataDir = path.join(os.tmpdir(), 'vsct-ud');
const inlineScriptsEnabledUserDataDir = path.join(os.tmpdir(), 'vsct-is-ud');
const inlineScriptsExperimentUserDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vsct-is-exp-'));
const diagnosticsDir = process.env.VSCODE_TEST_DIAGNOSTICS_DIR;
const diagnosticArgs = diagnosticsDir
? [`--logsPath=${path.join(diagnosticsDir, 'vscode')}`, '--verbose']
Expand All @@ -19,20 +20,25 @@ const diagnosticMocha = diagnosticsDir
// Seed explicit user values so tests are deterministic even when VS Code applies
// experimentation defaults. The environments extension must activate, while the
// inline-script preview stays off unless a dedicated test host opts in.
function writeUserSettings(targetUserDataDir, inlineScriptsEnabled) {
function writeUserSettings(targetUserDataDir, settings) {
const userDir = path.join(targetUserDataDir, 'User');
fs.mkdirSync(userDir, { recursive: true });
fs.writeFileSync(
path.join(userDir, 'settings.json'),
JSON.stringify({
'python.useEnvironmentsExtension': true,
'python-envs.inlineScripts.enabled': inlineScriptsEnabled,
...settings,
}) + '\n',
);
}

writeUserSettings(userDataDir, false);
writeUserSettings(inlineScriptsEnabledUserDataDir, true);
writeUserSettings(userDataDir, { 'python-envs.inlineScripts.enabled': false });
writeUserSettings(inlineScriptsEnabledUserDataDir, { 'python-envs.inlineScripts.enabled': true });
writeUserSettings(inlineScriptsExperimentUserDataDir, {
'experiments.override./pythonenvs/config.python-envs.inlineScripts.enabled': true,
// Keep the override late enough to reproduce activation before VS Code applies the experimental default.
'experiments.overrideDelay': 30_000,
});

export default defineConfig([
{
Expand Down Expand Up @@ -75,6 +81,24 @@ export default defineConfig([
'--disable-workspace-trust',
],
},
{
label: 'smokeTestsInlineScriptsExperimentEnabled',
files: 'out/test/smoke/inlineScriptsExperimentEnabled.smoke.test.js',
mocha: {
...diagnosticMocha,
ui: 'tdd',
timeout: 120000,
},
env: {
VSC_PYTHON_SMOKE_TEST: '1',
VSC_PYTHON_INLINE_SCRIPTS_EXPERIMENT_SMOKE_TEST: '1',
},
launchArgs: [
...diagnosticArgs,
`--user-data-dir=${inlineScriptsExperimentUserDataDir}`,
'--disable-workspace-trust',
],
},
{
label: 'e2eTests',
files: 'out/test/e2e/**/*.e2e.test.js',
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,7 @@
"pretest": "npm run compile-tests && npm run compile",
"lint": "eslint --config=eslint.config.mjs src",
"unittest": "mocha --config=./build/.mocha.unittests.json",
"smoke-test": "vscode-test --label smokeTests --label smokeTestsInlineScriptsEnabled",
"smoke-test": "vscode-test --label smokeTests --label smokeTestsInlineScriptsEnabled --label smokeTestsInlineScriptsExperimentEnabled",
"e2e-test": "vscode-test --label e2eTests --install-extensions ms-python.python",
"integration-test": "vscode-test --label integrationTests --install-extensions ms-python.python",
"integration-test-multiroot": "vscode-test --label integrationTestsMultiRoot --install-extensions ms-python.python",
Expand Down
138 changes: 102 additions & 36 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
commands,
Disposable,
ExtensionContext,
extensions,
l10n,
Expand All @@ -13,6 +14,7 @@ import { PackageManagementOptions, PythonEnvironment, PythonEnvironmentApi, Pyth
import { ENVS_EXTENSION_ID } from './common/constants';
import { ensureCorrectVersion } from './common/extVersion';
import { InlineScriptPackagesNotManagedError } from './common/inlineScript/errors';
import { InlineScriptRoutingRegistry } from './common/inlineScript/routingRegistry';
import { registerLogger, traceError, traceInfo, traceWarn } from './common/logging';
import { setPersistentState } from './common/persistentState';
import { newProjectSelection } from './common/pickers/managers';
Expand Down Expand Up @@ -67,10 +69,9 @@ import {
setEnvManagerCommand,
setPackageManagerCommand,
} from './features/envCommands';
import type { EnvironmentManagers } from './features/envManagers';
import { PythonEnvironmentManagers } from './features/envManagers';
import { EnvVarManager, PythonEnvVariableManager } from './features/execution/envVariableManager';
import { latchInlineScriptFeatureActivation } from './features/inlineScript/activation';
import { createInlineScriptFeatureActivation } from './features/inlineScript/activation';
import { registerInlineScriptDiagnostics } from './features/inlineScript/diagnostics';
import { InlineScriptLazyDetector } from './features/inlineScript/lazyDetector';
import { registerInlineScriptUx } from './features/inlineScript/setupEnvironment';
Expand Down Expand Up @@ -107,7 +108,10 @@ import {
getEnvManagerAndPackageManagerConfigLevels,
runPetInTerminalImpl,
} from './helpers';
import { registerInlineScriptFeatures } from './managers/builtin/inlineScript/main';
import {
prepareInlineScriptFeatures,
scheduleInlineScriptActivationDiscovery,
} from './managers/builtin/inlineScript/main';
import { registerSystemPythonFeatures } from './managers/builtin/main';
import { SysPythonManager } from './managers/builtin/sysPythonManager';
import {
Expand Down Expand Up @@ -174,18 +178,14 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
const projectManager: PythonProjectManager = new PythonProjectManagerImpl();
context.subscriptions.push(projectManager);

const inlineScriptFeatureActivation = latchInlineScriptFeatureActivation();
const inlineScriptRouting = inlineScriptFeatureActivation.routingRegistry;
if (inlineScriptRouting) {
context.subscriptions.push(inlineScriptRouting);
}

void commands.executeCommand('setContext', 'pythonEnvsInlineScriptsEnabled', inlineScriptFeatureActivation.enabled);
const inlineScriptFeatureActivation = createInlineScriptFeatureActivation();
context.subscriptions.push(inlineScriptFeatureActivation);
await commands.executeCommand('setContext', 'pythonEnvsInlineScriptsEnabled', false);

const envVarManager: EnvVarManager = new PythonEnvVariableManager(projectManager);
context.subscriptions.push(envVarManager);

const envManagers: EnvironmentManagers = new PythonEnvironmentManagers(projectManager, inlineScriptRouting);
const envManagers = new PythonEnvironmentManagers(projectManager);
createManagerReady(envManagers, projectManager, context.subscriptions);
context.subscriptions.push(envManagers);

Expand All @@ -212,12 +212,95 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
// Silent observer for `.py` files that declare PEP 723 inline
// script metadata. Emits anonymized telemetry (inlineScript.detected /
// inlineScript.edited) but does not register projects or surface any UI.
const inlineScriptLazyDetector = new InlineScriptLazyDetector(inlineScriptRouting);
const inlineScriptLazyDetector = new InlineScriptLazyDetector();
inlineScriptLazyDetector.activate();
context.subscriptions.push(inlineScriptLazyDetector);

if (inlineScriptFeatureActivation.enabled) {
context.subscriptions.push(registerInlineScriptDiagnostics());
const inlineScriptManagerDependencies = createDeferred<{
nativeFinder: NativePythonFinder;
baseManager: SysPythonManager;
}>();
void inlineScriptManagerDependencies.promise.catch(() => undefined);
let inlineScriptFeatureRegistration: Promise<void> | undefined;
const activateInlineScriptFeature = async (
nativeFinder: NativePythonFinder,
baseManager: SysPythonManager,
): Promise<void> => {
const featureDisposables: Disposable[] = [];
const routingRegistry = new InlineScriptRoutingRegistry();
featureDisposables.push(routingRegistry);
try {
const prepared = await prepareInlineScriptFeatures(
nativeFinder,
featureDisposables,
outputChannel,
baseManager,
context.globalStorageUri,
routingRegistry,
context.workspaceState,
);
featureDisposables.push(envManagers.attachInlineScriptRouting(routingRegistry));
featureDisposables.push(registerInlineScriptDiagnostics());
featureDisposables.push(
commands.registerCommand('python-envs.clearScriptEnvCache', async () => {
await clearScriptEnvironmentCacheCommand(envManagers);
}),
);
featureDisposables.push(...registerInlineScriptUx(envManagers, routingRegistry));
prepared.manager.start();
featureDisposables.push(prepared.registerManager());
await commands.executeCommand('setContext', 'pythonEnvsInlineScriptsEnabled', true);
inlineScriptLazyDetector.enableRouting(routingRegistry);
featureDisposables.push(scheduleInlineScriptActivationDiscovery(prepared.manager));
context.subscriptions.push(...featureDisposables);
traceInfo('PEP 723 inline-script feature activation completed.');
} catch (error) {
for (const disposable of featureDisposables.reverse()) {
try {
await Promise.resolve(disposable.dispose());
} catch (disposeError) {
traceError('Failed to roll back inline-script feature activation:', disposeError);
}
}
try {
await commands.executeCommand('setContext', 'pythonEnvsInlineScriptsEnabled', false);
} catch (contextError) {
traceError('Failed to reset the inline-script context after activation failure:', contextError);
}
traceError('Failed to activate PEP 723 inline-script environments:', error);
try {
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_FAILED, undefined, {
managerName: 'inlineScript',
errorType: classifyError(error),
failureStage: 'featureActivation',
});
} catch (telemetryError) {
traceError('Failed to report inline-script activation telemetry:', telemetryError);
}
void window
.showErrorMessage(
l10n.t(
'Failed to enable PEP 723 inline-script environments. Check the Python Environments output for details.',
),
)
.then(undefined, (notificationError) =>
traceError('Failed to show the inline-script activation error:', notificationError),
);
throw error;
}
};
const enableInlineScriptFeature = (): Promise<void> => {
inlineScriptFeatureRegistration ??= inlineScriptManagerDependencies.promise.then(
({ nativeFinder, baseManager }) => activateInlineScriptFeature(nativeFinder, baseManager),
);
return inlineScriptFeatureRegistration;
};
const requestInlineScriptFeatureActivation = () => {
void enableInlineScriptFeature().catch(() => undefined);
};
context.subscriptions.push(inlineScriptFeatureActivation.onDidRequestActivation(requestInlineScriptFeatureActivation));
if (inlineScriptFeatureActivation.activationRequested) {
requestInlineScriptFeatureActivation();
}

const environmentManagerRegistration = createDeferred<void>();
Expand Down Expand Up @@ -442,14 +525,6 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
commands.registerCommand('python-envs.clearCache', async () => {
await clearEnvironmentCachesCommand(envManagers, shellStartupProviders, context.workspaceState);
}),
...(inlineScriptFeatureActivation.enabled
? [
commands.registerCommand('python-envs.clearScriptEnvCache', async () => {
await clearScriptEnvironmentCacheCommand(envManagers);
}),
]
: []),
...(inlineScriptRouting ? registerInlineScriptUx(envManagers, inlineScriptRouting) : []),
commands.registerCommand('python-envs.runInTerminal', (item) => {
return runInTerminalCommand(item, api, terminalManager);
}),
Expand Down Expand Up @@ -670,6 +745,7 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
context.subscriptions.push(nativeFinder);
const sysMgr = new SysPythonManager(nativeFinder, api, outputChannel);
sysPythonManager.resolve(sysMgr);
inlineScriptManagerDependencies.resolve({ nativeFinder, baseManager: sysMgr });
// Each manager registers independently — one failure must not block the others.
failureStage = 'managerRegistration';
stageWatch.reset();
Expand All @@ -688,20 +764,9 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
'poetry',
registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager),
),
safeRegister(
'inlineScript',
inlineScriptFeatureActivation.enabled
? registerInlineScriptFeatures(
nativeFinder,
context.subscriptions,
outputChannel,
sysMgr,
context.globalStorageUri,
inlineScriptFeatureActivation,
context.workspaceState,
)
: Promise.resolve(),
),
inlineScriptFeatureActivation.activationRequested
? enableInlineScriptFeature().catch(() => undefined)
: Promise.resolve(),
]);
environmentManagerRegistration.resolve();

Expand Down Expand Up @@ -744,6 +809,7 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
}
} catch (error) {
environmentManagerRegistration.reject(error);
inlineScriptManagerDependencies.reject(error);
clearHangWatchdog();
traceError('Failed to initialize environment managers:', error);
sendTelemetryEvent(
Expand Down
38 changes: 38 additions & 0 deletions src/features/common/managerReady.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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 type { InternalEnvironmentManager, InternalPackageManager } from '../../managers/common/registeredManagers';
import type { EnvironmentManagers } from '../envManagers';
import type { PythonProjectManager } from '../projectManager';
import { getDefaultEnvManagerSetting, getDefaultPkgManagerSetting } from '../settings/settingHelpers';
Expand Down Expand Up @@ -146,15 +147,28 @@ async function promptInstallExtensionIfMissing(managerId: string): Promise<void>
class ManagerReadyImpl implements ManagerReady {
private readonly envManagers: Map<string, Deferred<void>> = new Map();
private readonly pkgManagers: Map<string, Deferred<void>> = new Map();
private readonly envManagerInstances = new Map<string, InternalEnvironmentManager>();
private readonly pkgManagerInstances = new Map<string, InternalPackageManager>();
private readonly checked: Set<string> = new Set();
private readonly disposables: Disposable[] = [];
private disposed = false;

constructor(
em: EnvironmentManagers,
private readonly pm: PythonProjectManager,
) {
this.disposables.push(
em.onDidChangeEnvironmentManager((e) => {
if (e.kind === 'unregistered') {
if (this.envManagerInstances.get(e.manager.id) !== e.manager) {
return;
}
this.envManagerInstances.delete(e.manager.id);
this.envManagers.delete(e.manager.id);
this.checked.delete(e.manager.id);
return;
}
this.envManagerInstances.set(e.manager.id, e.manager);
if (this.envManagers.has(e.manager.id)) {
this.envManagers.get(e.manager.id)?.resolve();
} else {
Expand All @@ -164,6 +178,16 @@ class ManagerReadyImpl implements ManagerReady {
}
}),
em.onDidChangePackageManager((e) => {
if (e.kind === 'unregistered') {
if (this.pkgManagerInstances.get(e.manager.id) !== e.manager) {
return;
}
this.pkgManagerInstances.delete(e.manager.id);
this.pkgManagers.delete(e.manager.id);
this.checked.delete(e.manager.id);
return;
}
this.pkgManagerInstances.set(e.manager.id, e.manager);
if (this.pkgManagers.has(e.manager.id)) {
this.pkgManagers.get(e.manager.id)?.resolve();
} else {
Expand All @@ -188,6 +212,9 @@ class ManagerReadyImpl implements ManagerReady {
const extId = getExtensionId(managerId);
if (extId) {
setImmediate(async () => {
if (this.disposed) {
return;
}
const ext = getExtension(extId);
if (ext && !ext.isActive) {
traceInfo(`Extension for manager ${managerId} is not active: Activating...`);
Expand All @@ -203,12 +230,20 @@ class ManagerReadyImpl implements ManagerReady {
}

public dispose(): void {
this.disposed = true;
this.disposables.forEach((d) => d.dispose());
this.envManagers.forEach((deferred) => deferred.resolve());
this.pkgManagers.forEach((deferred) => deferred.resolve());
this.envManagers.clear();
this.pkgManagers.clear();
this.envManagerInstances.clear();
this.pkgManagerInstances.clear();
}

private _waitForEnvManager(managerId: string): Promise<void> {
if (this.disposed) {
return Promise.resolve();
}
if (this.envManagers.has(managerId)) {
return this.envManagers.get(managerId)!.promise;
}
Expand Down Expand Up @@ -258,6 +293,9 @@ class ManagerReadyImpl implements ManagerReady {
}

private _waitForPkgManager(managerId: string): Promise<void> {
if (this.disposed) {
return Promise.resolve();
}
if (this.pkgManagers.has(managerId)) {
return this.pkgManagers.get(managerId)!.promise;
}
Expand Down
Loading
Loading