Fix asynchronous environment discovery and selection (#7489)

This commit is contained in:
Adam Yoblick 2023-04-25 14:40:51 -05:00 коммит произвёл GitHub
Родитель 9f5c165ddc
Коммит 0ff2570567
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 157 добавлений и 75 удалений

Просмотреть файл

@ -22,17 +22,14 @@ using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using System.Xml; using System.Xml;
using System.Xml.XPath; using System.Xml.XPath;
using Microsoft.Build.Execution; using Microsoft.Build.Execution;
using Microsoft.PythonTools.Commands; using Microsoft.PythonTools.Commands;
using Microsoft.PythonTools.Common; using Microsoft.PythonTools.Common;
using Microsoft.PythonTools.Editor;
using Microsoft.PythonTools.Environments; using Microsoft.PythonTools.Environments;
using Microsoft.PythonTools.Infrastructure; using Microsoft.PythonTools.Infrastructure;
using Microsoft.PythonTools.Intellisense;
using Microsoft.PythonTools.Interpreter; using Microsoft.PythonTools.Interpreter;
using Microsoft.PythonTools.Logging; using Microsoft.PythonTools.Logging;
using Microsoft.PythonTools.Projects; using Microsoft.PythonTools.Projects;
@ -43,8 +40,6 @@ using Microsoft.VisualStudio.Imaging.Interop;
using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TextManager.Interop; using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Utilities;
using Microsoft.VisualStudio.Workspace.VSIntegration.Contracts;
using Microsoft.VisualStudioTools; using Microsoft.VisualStudioTools;
using Microsoft.VisualStudioTools.Project; using Microsoft.VisualStudioTools.Project;
using IServiceProvider = System.IServiceProvider; using IServiceProvider = System.IServiceProvider;
@ -88,6 +83,7 @@ namespace Microsoft.PythonTools.Project {
private readonly PythonProject _pythonProject; private readonly PythonProject _pythonProject;
private bool _infoBarCheckTriggered = false; private bool _infoBarCheckTriggered = false;
private bool _asyncInfoBarCheckTriggered = false;
private readonly CondaEnvCreateInfoBar _condaEnvCreateInfoBar; private readonly CondaEnvCreateInfoBar _condaEnvCreateInfoBar;
private readonly VirtualEnvCreateInfoBar _virtualEnvCreateInfoBar; private readonly VirtualEnvCreateInfoBar _virtualEnvCreateInfoBar;
private readonly PackageInstallInfoBar _packageInstallInfoBar; private readonly PackageInstallInfoBar _packageInstallInfoBar;
@ -95,6 +91,7 @@ namespace Microsoft.PythonTools.Project {
private readonly PythonNotSupportedInfoBar _pythonVersionNotSupportedInfoBar; private readonly PythonNotSupportedInfoBar _pythonVersionNotSupportedInfoBar;
private readonly SemaphoreSlim _recreatingAnalyzer = new SemaphoreSlim(1); private readonly SemaphoreSlim _recreatingAnalyzer = new SemaphoreSlim(1);
private bool _isRefreshingInterpreters = false;
public event EventHandler LanguageServerInterpreterChanged; public event EventHandler LanguageServerInterpreterChanged;
@ -118,6 +115,7 @@ namespace Microsoft.PythonTools.Project {
// hooked up. // hooked up.
InterpreterOptions.DefaultInterpreterChanged += GlobalDefaultInterpreterChanged; InterpreterOptions.DefaultInterpreterChanged += GlobalDefaultInterpreterChanged;
InterpreterRegistry.InterpretersChanged += OnInterpreterRegistryChanged; InterpreterRegistry.InterpretersChanged += OnInterpreterRegistryChanged;
InterpreterRegistry.AsyncInterpreterDiscoveryCompleted += OnInterpreterDiscoveryCompleted;
_pythonProject = new VsPythonProject(this); _pythonProject = new VsPythonProject(this);
_condaEnvCreateInfoBar = new CondaEnvCreateProjectInfoBar(Site, this); _condaEnvCreateInfoBar = new CondaEnvCreateProjectInfoBar(Site, this);
@ -204,6 +202,16 @@ namespace Microsoft.PythonTools.Project {
Site.GetUIThread().Invoke(() => RefreshInterpreters()); Site.GetUIThread().Invoke(() => RefreshInterpreters());
} }
// Called once all async interpreter factories have finished discovering interpreters
private void OnInterpreterDiscoveryCompleted(object sender, EventArgs e) {
if (!_asyncInfoBarCheckTriggered) {
_asyncInfoBarCheckTriggered = true;
// Check for any missing environments and show info bars for them
_condaEnvCreateInfoBar.CheckAsync().HandleAllExceptions(Site, typeof(PythonProjectNode)).DoNotWait();
}
}
private void OnInterpreterRegistryChanged(object sender, EventArgs e) { private void OnInterpreterRegistryChanged(object sender, EventArgs e) {
Site.GetUIThread().Invoke(() => { Site.GetUIThread().Invoke(() => {
// Check whether the active interpreter factory has changed. // Check whether the active interpreter factory has changed.
@ -229,36 +237,32 @@ namespace Microsoft.PythonTools.Project {
Debug.Assert(this.FileName != null); Debug.Assert(this.FileName != null);
var oldActive = _active; var oldActive = _active;
// stop listening for installed files changed
var oldPms = _activePackageManagers; var oldPms = _activePackageManagers;
_activePackageManagers = null; _activePackageManagers = null;
foreach (var pm in oldPms.MaybeEnumerate()) { foreach (var pm in oldPms.MaybeEnumerate()) {
pm.InstalledFilesChanged -= PackageManager_InstalledFilesChanged; pm.InstalledFilesChanged -= PackageManager_InstalledFilesChanged;
} }
lock (_validFactories) { lock (_validFactories) {
if (_validFactories.Count == 0) { // if there are no valid factories,
// No factories, so we must use the global default. // or the specified factory isn't in the valid list,
// use the global default
if (_validFactories.Count == 0 || value == null || !_validFactories.Contains(value.Configuration.Id)) {
_active = null; _active = null;
} else if (value == null || !_validFactories.Contains(value.Configuration.Id)) {
// Choose a factory and make it our default.
// TODO: We should have better ordering than this...
var compModel = Site.GetComponentModel();
_active = InterpreterRegistry.FindInterpreter(
_validFactories.ToList().OrderBy(f => f).LastOrDefault()
);
} else { } else {
_active = value; _active = value;
} }
} }
// start listening for package changes on the active interpreter again
_activePackageManagers = InterpreterOptions.GetPackageManagers(_active).ToArray(); _activePackageManagers = InterpreterOptions.GetPackageManagers(_active).ToArray();
foreach (var pm in _activePackageManagers) { foreach (var pm in _activePackageManagers) {
pm.InstalledFilesChanged += PackageManager_InstalledFilesChanged; pm.InstalledFilesChanged += PackageManager_InstalledFilesChanged;
pm.EnableNotifications(); pm.EnableNotifications();
} }
// update the InterpreterId element in the pyproj with the new active interpreter
if (_active != oldActive) { if (_active != oldActive) {
if (_active != null) { if (_active != null) {
BuildProject.SetProperty( BuildProject.SetProperty(
@ -694,7 +698,6 @@ namespace Microsoft.PythonTools.Project {
private async Task TriggerInfoBarsAsync() { private async Task TriggerInfoBarsAsync() {
await Task.WhenAll( await Task.WhenAll(
_condaEnvCreateInfoBar.CheckAsync(),
_virtualEnvCreateInfoBar.CheckAsync(), _virtualEnvCreateInfoBar.CheckAsync(),
_packageInstallInfoBar.CheckAsync(), _packageInstallInfoBar.CheckAsync(),
_testFrameworkInfoBar.CheckAsync(), _testFrameworkInfoBar.CheckAsync(),
@ -777,76 +780,94 @@ namespace Microsoft.PythonTools.Project {
} }
} }
private static bool RemoveFirst<T>(List<T> list, Func<T, bool> condition) { // Refresh the interpreters under the "Python Environments" node.
for (int i = 0; i < list.Count; ++i) { // This gets called from two places - once when the project loads,
if (condition(list[i])) { // and any time interpreter factories are changed after that.
list.RemoveAt(i); // For example, conda environments are discovered asynchronously and are
return true; // not available when the project it loaded. So once they are enumerated,
} // OnInterpreterFactoriesChanged is triggered, which calls this method.
}
return false;
}
private void RefreshInterpreters(bool alwaysCollapse = false) { private void RefreshInterpreters(bool alwaysCollapse = false) {
if (IsClosed) {
// This method is re-entrant the first time it's called because GetInterpreterConfigurations() calls EnsureInitialized(),
// which triggers the OnInterpreterFactoriesChanged event. So only allow this method to run if it's not already running
if (_isRefreshingInterpreters) {
return; return;
} }
_isRefreshingInterpreters = true;
var node = _interpretersContainer; try {
if (node == null) {
return;
}
var remaining = node.AllChildren.OfType<InterpretersNode>().ToList(); // if the project is closed, we're done
if (IsClosed) {
if (!IsActiveInterpreterGlobalDefault) { return;
foreach (var fact in InterpreterFactories) {
if (!RemoveFirst(remaining, n => !n._isGlobalDefault && n._factory == fact)) {
bool isProjectSpecific = _vsProjectContext.IsProjectSpecific(fact.Configuration);
bool canRemove = !this.IsAppxPackageableProject(); // Do not allow change python enivronment for UWP
node.AddChild(new InterpretersNode(
this,
fact,
isInterpreterReference: !isProjectSpecific,
canDelete:
isProjectSpecific &&
Directory.Exists(fact.Configuration.GetPrefixPath()),
isGlobalDefault: false,
canRemove: canRemove
));
}
} }
} else {
var fact = ActiveInterpreter;
if (fact.IsRunnable() && !RemoveFirst(remaining, n => n._isGlobalDefault && n._factory == fact)) {
node.AddChild(new InterpretersNode(this, fact, true, false, true));
}
}
foreach (var id in InvalidInterpreterIds) { // if the "Python Environments" node doesn't exist, we're done
if (!RemoveFirst(remaining, n => n._absentId == id)) { var pythonEnvironmentsNode = _interpretersContainer;
node.AddChild(InterpretersNode.CreateAbsentInterpreterNode(this, id)); if (pythonEnvironmentsNode == null) {
return;
} }
}
foreach (var child in remaining) { // clear out all interpreter nodes since we're going to re-add them
node.RemoveChild(child); var interpreterNodes = pythonEnvironmentsNode.AllChildren.OfType<InterpretersNode>().ToList();
} interpreterNodes.ForEach(pythonEnvironmentsNode.RemoveChild);
if (alwaysCollapse || ParentHierarchy == null) { // if we have no interpreter factories, and the active interpreter is the global default,
OnInvalidateItems(node); // add a node for it
} else { if (!InterpreterFactories.Any() && IsActiveInterpreterGlobalDefault && ActiveInterpreter.IsRunnable()) {
bool wasExpanded = node.GetIsExpanded(); var newNode = new InterpretersNode(
var expandAfter = node.AllChildren.Where(n => n.GetIsExpanded()).ToArray(); this,
OnInvalidateItems(node); ActiveInterpreter,
if (wasExpanded) { isInterpreterReference: true,
node.ExpandItem(EXPANDFLAGS.EXPF_ExpandFolder); canDelete: false,
isGlobalDefault: true
);
pythonEnvironmentsNode.AddChild(newNode);
} }
foreach (var child in expandAfter) {
child.ExpandItem(EXPANDFLAGS.EXPF_ExpandFolder); // add all the factories we have
foreach (var interpreterFactory in InterpreterFactories) {
var isProjectSpecific = _vsProjectContext.IsProjectSpecific(interpreterFactory.Configuration);
var canRemove = !this.IsAppxPackageableProject(); // Do not allow change python environment for UWP
var canDelete = isProjectSpecific && Directory.Exists(interpreterFactory.Configuration.GetPrefixPath());
var newNode = new InterpretersNode(
this,
interpreterFactory,
isInterpreterReference: !isProjectSpecific,
canDelete,
isGlobalDefault: false,
canRemove
);
pythonEnvironmentsNode.AddChild(newNode);
} }
// If the project is referencing interpreters that we can't find, add dummy nodes for them.
// This can include virtual environments that have been deleted, interpreters that have been uninstalled,
// or conda environments that are still being discovered asynchronously.
foreach (var id in InvalidInterpreterIds) {
pythonEnvironmentsNode.AddChild(InterpretersNode.CreateAbsentInterpreterNode(this, id));
}
// Expand the Python Environments node, if appropriate
OnInvalidateItems(pythonEnvironmentsNode);
if (!alwaysCollapse && ParentHierarchy != null) {
pythonEnvironmentsNode.ExpandItem(EXPANDFLAGS.EXPF_ExpandFolder);
}
// update the active interpreter based on the "InterpreterId" element in the pyproj
UpdateActiveInterpreter();
// finally, bold the active environment
BoldActiveEnvironment();
} finally {
// allow the method to run again
_isRefreshingInterpreters = false;
} }
BoldActiveEnvironment();
} }
private void BoldActiveEnvironment() { private void BoldActiveEnvironment() {

Просмотреть файл

@ -38,7 +38,7 @@ namespace Microsoft.PythonTools.Interpreter {
[Export(typeof(IPythonInterpreterFactoryProvider))] [Export(typeof(IPythonInterpreterFactoryProvider))]
[Export(typeof(CondaEnvironmentFactoryProvider))] [Export(typeof(CondaEnvironmentFactoryProvider))]
[PartCreationPolicy(CreationPolicy.Shared)] [PartCreationPolicy(CreationPolicy.Shared)]
class CondaEnvironmentFactoryProvider : IPythonInterpreterFactoryProvider, IDisposable { class CondaEnvironmentFactoryProvider : IPythonInterpreterFactoryProvider, IPythonInterpreterFactoryProviderAsync, IDisposable {
private readonly Dictionary<string, PythonInterpreterInformation> _factories = new Dictionary<string, PythonInterpreterInformation>(); private readonly Dictionary<string, PythonInterpreterInformation> _factories = new Dictionary<string, PythonInterpreterInformation>();
internal const string FactoryProviderName = "CondaEnv"; internal const string FactoryProviderName = "CondaEnv";
internal const string EnvironmentCompanyName = "CondaEnv"; internal const string EnvironmentCompanyName = "CondaEnv";
@ -62,6 +62,7 @@ namespace Microsoft.PythonTools.Interpreter {
}; };
internal event EventHandler DiscoveryStarted; internal event EventHandler DiscoveryStarted;
public event EventHandler InterpreterDiscoveryCompleted;
[ImportingConstructor] [ImportingConstructor]
public CondaEnvironmentFactoryProvider( public CondaEnvironmentFactoryProvider(
@ -267,6 +268,8 @@ namespace Microsoft.PythonTools.Interpreter {
if (anyChanged) { if (anyChanged) {
OnInterpreterFactoriesChanged(); OnInterpreterFactoriesChanged();
} }
InterpreterDiscoveryCompleted?.Invoke(this, EventArgs.Empty);
} }
internal async static Task<CondaInfoResult> ExecuteCondaInfoAsync(string condaPath) { internal async static Task<CondaInfoResult> ExecuteCondaInfoAsync(string condaPath) {

Просмотреть файл

@ -61,6 +61,11 @@ namespace Microsoft.PythonTools.Interpreter {
/// </summary> /// </summary>
event EventHandler InterpretersChanged; event EventHandler InterpretersChanged;
/// <summary>
/// Raised when all async interpreter factory providers have completed discovering interpreters
/// </summary>
event EventHandler AsyncInterpreterDiscoveryCompleted;
/// <summary> /// <summary>
/// Called to suppress the <see cref="InterpretersChanged"/> event while /// Called to suppress the <see cref="InterpretersChanged"/> event while
/// making changes to the registry. If the event is triggered while /// making changes to the registry. If the event is triggered while

Просмотреть файл

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PythonTools.Interpreter {
public interface IPythonInterpreterFactoryProviderAsync {
/// <summary>
/// Raised when interpreter discovery is completed for this provider
/// </summary>
event EventHandler InterpreterDiscoveryCompleted;
}
}

Просмотреть файл

@ -36,6 +36,10 @@ namespace Microsoft.PythonTools.Interpreter {
private readonly Lazy<IInterpreterLog>[] _loggers; private readonly Lazy<IInterpreterLog>[] _loggers;
private const string InterpreterFactoryIdMetadata = "InterpreterFactoryId"; private const string InterpreterFactoryIdMetadata = "InterpreterFactoryId";
private int _asyncInterpreterDiscoveryCompletedCount;
private readonly object _asyncInterpreterDiscoveryCompletedCountLock = new object();
private int _asyncProviderCount;
[ImportingConstructor] [ImportingConstructor]
public InterpreterRegistryService([ImportMany]Lazy<IPythonInterpreterFactoryProvider, IDictionary<string, object>>[] providers, [ImportMany]Lazy<IInterpreterLog>[] loggers) { public InterpreterRegistryService([ImportMany]Lazy<IPythonInterpreterFactoryProvider, IDictionary<string, object>>[] providers, [ImportMany]Lazy<IInterpreterLog>[] loggers) {
_providers = providers; _providers = providers;
@ -72,12 +76,21 @@ namespace Microsoft.PythonTools.Interpreter {
} }
} }
public event EventHandler AsyncInterpreterDiscoveryCompleted;
private void EnsureFactoryChangesWatched() { private void EnsureFactoryChangesWatched() {
if (!_factoryChangesWatched) { if (!_factoryChangesWatched) {
BeginSuppressInterpretersChangedEvent(); BeginSuppressInterpretersChangedEvent();
try { try {
foreach (var provider in GetProviders()) { foreach (var provider in GetProviders()) {
provider.InterpreterFactoriesChanged += Provider_InterpreterFactoriesChanged; provider.InterpreterFactoriesChanged += Provider_InterpreterFactoriesChanged;
// if the provider is async, listen for the completed event
if (provider is IPythonInterpreterFactoryProviderAsync asyncProvider) {
_asyncProviderCount++;
asyncProvider.InterpreterDiscoveryCompleted += Provider_AsyncInterpreterDiscoveryCompleted;
}
} }
} finally { } finally {
EndSuppressInterpretersChangedEvent(); EndSuppressInterpretersChangedEvent();
@ -86,6 +99,23 @@ namespace Microsoft.PythonTools.Interpreter {
} }
} }
// Called when a single async interpreter factory provider finishes discovering interpreters
private void Provider_AsyncInterpreterDiscoveryCompleted(object sender, EventArgs e) {
lock (_asyncInterpreterDiscoveryCompletedCountLock) {
// Since we know how many async providers we have, keep track of how many times this callback is hit.
// Once the number of calls == the number of providers, we know all async interpreter discovery is done.
_asyncInterpreterDiscoveryCompletedCount++;
if (_asyncInterpreterDiscoveryCompletedCount < _asyncProviderCount) {
return;
}
}
// if we get here, interpreter discovery is finished
AsyncInterpreterDiscoveryCompleted?.Invoke(this, EventArgs.Empty);
}
public void BeginSuppressInterpretersChangedEvent() { public void BeginSuppressInterpretersChangedEvent() {
lock (_suppressInterpretersChangedLock) { lock (_suppressInterpretersChangedLock) {
_suppressInterpretersChanged += 1; _suppressInterpretersChanged += 1;

Просмотреть файл

@ -108,6 +108,7 @@
</Compile> </Compile>
<Compile Include="Interpreter\InterpreterUIMode.cs" /> <Compile Include="Interpreter\InterpreterUIMode.cs" />
<Compile Include="Interpreter\IPythonInterpreterFactory.cs" /> <Compile Include="Interpreter\IPythonInterpreterFactory.cs" />
<Compile Include="Interpreter\IPythonInterpreterFactoryProviderAsync.cs" />
<Compile Include="Interpreter\LaunchConfiguration.cs" /> <Compile Include="Interpreter\LaunchConfiguration.cs" />
<Compile Include="Interpreter\NoInterpretersException.cs" /> <Compile Include="Interpreter\NoInterpretersException.cs" />
<Compile Include="Interpreter\LaunchConfigurationUtils.cs" /> <Compile Include="Interpreter\LaunchConfigurationUtils.cs" />

Просмотреть файл

@ -126,6 +126,10 @@ namespace PythonToolsTests {
public IPythonInterpreterFactory NoInterpretersValue => throw new NotImplementedException(); public IPythonInterpreterFactory NoInterpretersValue => throw new NotImplementedException();
public event EventHandler InterpretersChanged; public event EventHandler InterpretersChanged;
public event EventHandler AsyncInterpreterDiscoveryCompleted {
add { }
remove { }
}
public void BeginSuppressInterpretersChangedEvent() { public void BeginSuppressInterpretersChangedEvent() {
throw new NotImplementedException(); throw new NotImplementedException();

Просмотреть файл

@ -124,6 +124,10 @@ namespace TestUtilities.Python {
} }
public event EventHandler DefaultInterpreterChanged; public event EventHandler DefaultInterpreterChanged;
public event EventHandler AsyncInterpreterDiscoveryCompleted {
add { }
remove { }
}
public bool IsInterpreterGeneratingDatabase(IPythonInterpreterFactory interpreter) { public bool IsInterpreterGeneratingDatabase(IPythonInterpreterFactory interpreter) {
throw new NotImplementedException(); throw new NotImplementedException();