diff --git a/dev/VSIX/Extension/Cpp/Common/VSPackage.Designer.cs b/dev/VSIX/Extension/Cpp/Common/VSPackage.Designer.cs index 751ef7436c..cc432a8ff8 100644 --- a/dev/VSIX/Extension/Cpp/Common/VSPackage.Designer.cs +++ b/dev/VSIX/Extension/Cpp/Common/VSPackage.Designer.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation and Contributors. -// Licensed under the MIT License +// Licensed under the MIT License. //------------------------------------------------------------------------------ // @@ -22,7 +22,7 @@ namespace WindowsAppSDK.Cpp.Extension.Dev17 { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class VSPackage { @@ -471,5 +471,32 @@ internal static string _1054 { return ResourceManager.GetString("1054", resourceCulture); } } + + /// + /// Looks up a localized string similar to [{0}] Builds are currently paused while NuGet packages are being installed: {1}. The build will begin once package installation is complete.. + /// + internal static string _1055 { + get { + return ResourceManager.GetString("1055", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [{0}] NuGet package auto-download is disabled. Unable to install: {1}. Click "Manage NuGet Packages" below to install the required packages manually.. + /// + internal static string _1056 { + get { + return ResourceManager.GetString("1056", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [{0}] NuGet package auto-download is disabled. Unable to install: {1}. Click "Manage NuGet Packages" below to install the required packages manually.. + /// + internal static string _1057 { + get { + return ResourceManager.GetString("1057", resourceCulture); + } + } } } diff --git a/dev/VSIX/Extension/Cpp/Common/VSPackage.resx b/dev/VSIX/Extension/Cpp/Common/VSPackage.resx index 826705f735..ef4d58510c 100644 --- a/dev/VSIX/Extension/Cpp/Common/VSPackage.resx +++ b/dev/VSIX/Extension/Cpp/Common/VSPackage.resx @@ -256,4 +256,13 @@ For more details on the error, check the General tab in the Output window. No output information available. + + [{0}] Builds are currently paused while NuGet packages are being installed: {1}. The build will begin once package installation is complete. + + + [{0}] NuGet package auto-download is disabled. Unable to install: {1}. Click "Manage NuGet Packages" below to install the required packages manually. + + + [{0}] NuGet package auto-download is disabled. Unable to install: {1}. Click "Manage NuGet Packages" below to install the required packages manually. + \ No newline at end of file diff --git a/dev/VSIX/Extension/Cpp/Dev17/WindowsAppSDK.Cpp.Extension.Dev17.csproj b/dev/VSIX/Extension/Cpp/Dev17/WindowsAppSDK.Cpp.Extension.Dev17.csproj index 688c0f484e..66c2a5e4e9 100644 --- a/dev/VSIX/Extension/Cpp/Dev17/WindowsAppSDK.Cpp.Extension.Dev17.csproj +++ b/dev/VSIX/Extension/Cpp/Dev17/WindowsAppSDK.Cpp.Extension.Dev17.csproj @@ -85,7 +85,8 @@ - + + True True VSPackage.resx diff --git a/dev/VSIX/Extension/Cs/Common/VSPackage.Designer.cs b/dev/VSIX/Extension/Cs/Common/VSPackage.Designer.cs index 1167253e0d..973bf46b51 100644 --- a/dev/VSIX/Extension/Cs/Common/VSPackage.Designer.cs +++ b/dev/VSIX/Extension/Cs/Common/VSPackage.Designer.cs @@ -1,7 +1,4 @@ -// Copyright (c) Microsoft Corporation and Contributors. -// Licensed under the MIT License - -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 @@ -22,7 +19,7 @@ namespace WindowsAppSDK.Cs.Extension.Dev17 { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class VSPackage { @@ -467,5 +464,32 @@ internal static string _1054 { return ResourceManager.GetString("1054", resourceCulture); } } + + /// + /// Looks up a localized string similar to [{0}] Builds are currently paused while NuGet packages are being installed: {1}. The build will begin once package installation is complete.. + /// + internal static string _1055 { + get { + return ResourceManager.GetString("1055", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [{0}] NuGet package auto-download is disabled. Unable to install: {1}. To resolve, enable "Allow NuGet to download missing packages" and "Automatically check for missing packages during build in Visual Studio" in Tools > Options > NuGet Package Manager and restore the solution, or install the packages manually via the NuGet Package Manager.. + /// + internal static string _1056 { + get { + return ResourceManager.GetString("1056", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [{0}] NuGet package auto-download is disabled. Unable to install: {1}. Click "Manage NuGet Packages" below to install the required packages manually.. + /// + internal static string _1057 { + get { + return ResourceManager.GetString("1057", resourceCulture); + } + } } } diff --git a/dev/VSIX/Extension/Cs/Common/VSPackage.resx b/dev/VSIX/Extension/Cs/Common/VSPackage.resx index 7ceb082481..2c03526371 100644 --- a/dev/VSIX/Extension/Cs/Common/VSPackage.resx +++ b/dev/VSIX/Extension/Cs/Common/VSPackage.resx @@ -252,4 +252,13 @@ No output information available. - \ No newline at end of file + + [{0}] Builds are currently paused while NuGet packages are being installed: {1}. The build will begin once package installation is complete. + + + [{0}] NuGet package auto-download is disabled. Unable to install: {1}. To resolve, enable "Allow NuGet to download missing packages" and "Automatically check for missing packages during build in Visual Studio" in Tools > Options > NuGet Package Manager and restore the solution, or install the packages manually via the NuGet Package Manager. + + + [{0}] NuGet package auto-download is disabled. Unable to install: {1}. Click "Manage NuGet Packages" below to install the required packages manually. + + diff --git a/dev/VSIX/Extension/Cs/Dev17/WindowsAppSDK.Cs.Extension.Dev17.csproj b/dev/VSIX/Extension/Cs/Dev17/WindowsAppSDK.Cs.Extension.Dev17.csproj index 39348a4bd3..c8c6d6e7cf 100644 --- a/dev/VSIX/Extension/Cs/Dev17/WindowsAppSDK.Cs.Extension.Dev17.csproj +++ b/dev/VSIX/Extension/Cs/Dev17/WindowsAppSDK.Cs.Extension.Dev17.csproj @@ -62,7 +62,8 @@ - + + True True VSPackage.resx diff --git a/dev/VSIX/Shared/BuildGuard.cs b/dev/VSIX/Shared/BuildGuard.cs new file mode 100644 index 0000000000..061dac3b63 --- /dev/null +++ b/dev/VSIX/Shared/BuildGuard.cs @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License + +using System; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.Imaging; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; + +#if CSHARP_EXTENSION +using Resources = WindowsAppSDK.Cs.Extension.Dev17.VSPackage; +#elif CPP_EXTENSION +using Resources = WindowsAppSDK.Cpp.Extension.Dev17.VSPackage; +#endif + +namespace WindowsAppSDK.TemplateUtilities +{ + internal sealed class BuildGuard : IVsUpdateSolutionEvents4, IVsInfoBarUIEvents, IVsShellPropertyEvents, IDisposable + { + private IVsSolutionBuildManager5 _solutionBuildManager5; + private uint _adviseCookie4; + private bool _isBlocking; + private bool _isAdvised4; + private bool _disposed; + private bool _infoBarShown; + private IVsInfoBarUIElement _infoBarUIElement; + private Func _shouldRelease; + private string _infoBarMessage; + private IVsShell _shell; + private uint _shellPropertyCookie; + private bool _isShellPropertyAdvised; + private bool _wasJustReleased; + + public bool IsBlocking => _isBlocking; + + public void SetReleaseCondition(Func condition) + { + _shouldRelease = condition; + } + + public void SetInfoBarMessage(string message) + { + ThreadHelper.ThrowIfNotOnUIThread(); + _infoBarMessage = message; + if (_isBlocking) + { + ShowInfoBarWhenShellReady(); + } + } + + public void DisableBuilds() + { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (_isBlocking) + { + return; + } + + _solutionBuildManager5 = ServiceProvider.GlobalProvider.GetService(typeof(SVsSolutionBuildManager)) as IVsSolutionBuildManager5; + if (_solutionBuildManager5 != null) + { + _solutionBuildManager5.AdviseUpdateSolutionEvents4(this, out _adviseCookie4); + _isAdvised4 = true; + _isBlocking = true; + _infoBarShown = false; + } + } + + public void EnableBuilds() + { + ThreadHelper.ThrowIfNotOnUIThread(); + + _isBlocking = false; + _wasJustReleased = true; + + UnadviseShellPropertyChanges(); + + // Don't unadvise from build events yet - we need to stay subscribed + // so we can dismiss the info bar when the build actually starts + } + + public int UpdateSolution_Begin(ref int pfCancelUpdate) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (_wasJustReleased) + { + _wasJustReleased = false; + DismissInfoBar(); + UnadviseFromBuildEvents(); + return VSConstants.S_OK; + } + + if (_isBlocking) + { + if (_shouldRelease != null && _shouldRelease()) + { + EnableBuilds(); + return VSConstants.S_OK; + } + + ShowInfoBar(); + } + + return VSConstants.S_OK; + } + + private void ShowInfoBarWhenShellReady() + { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (_shell == null) + { + _shell = ServiceProvider.GlobalProvider.GetService(typeof(SVsShell)) as IVsShell; + } + + if (_shell == null) + { + return; + } + + if (_shell.GetProperty((int)__VSSPROPID.VSSPROPID_Zombie, out object zombie) == VSConstants.S_OK + && zombie is bool isZombie && !isZombie) + { + ShowInfoBar(); + return; + } + + // Shell not yet initialized; defer until it is + if (!_isShellPropertyAdvised) + { + _shell.AdviseShellPropertyChanges(this, out _shellPropertyCookie); + _isShellPropertyAdvised = true; + } + } + + public int OnShellPropertyChange(int propid, object var) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (propid == (int)__VSSPROPID.VSSPROPID_Zombie && var is bool isZombie && !isZombie) + { + ShowInfoBar(); + UnadviseShellPropertyChanges(); + } + + return VSConstants.S_OK; + } + + private void UnadviseShellPropertyChanges() + { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (_isShellPropertyAdvised && _shell != null) + { + _shell.UnadviseShellPropertyChanges(_shellPropertyCookie); + _isShellPropertyAdvised = false; + } + } + + private void ShowInfoBar() + { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (_infoBarShown) + { + return; + } + + try + { + var infoBarModel = new InfoBarModel( + textSpans: new[] + { + new InfoBarTextSpan(_infoBarMessage ?? Resources._1055) + }, + image: KnownMonikers.StatusInformation, + isCloseButtonVisible: true); + + IVsInfoBarUIFactory infoBarUIFactory = ServiceProvider.GlobalProvider.GetService(typeof(SVsInfoBarUIFactory)) as IVsInfoBarUIFactory; + if (infoBarUIFactory == null) + { + return; + } + + _infoBarUIElement = infoBarUIFactory.CreateInfoBar(infoBarModel); + if (_infoBarUIElement == null) + { + return; + } + + _infoBarUIElement.Advise(this, out _); + + IVsShell shell = _shell ?? ServiceProvider.GlobalProvider.GetService(typeof(SVsShell)) as IVsShell; + if (shell == null) + { + return; + } + + shell.GetProperty((int)__VSSPROPID7.VSSPROPID_MainWindowInfoBarHost, out object infoBarHostObj); + if (infoBarHostObj is IVsInfoBarHost infoBarHost) + { + infoBarHost.AddInfoBar(_infoBarUIElement); + _infoBarShown = true; + } + } + catch (Exception) + { + // Best-effort: if we can't show the InfoBar, the build is still canceled + } + } + + private void DismissInfoBar() + { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (_infoBarUIElement != null) + { + _infoBarUIElement.Close(); + _infoBarUIElement = null; + } + } + + private void UnadviseFromBuildEvents() + { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (_isAdvised4 && _solutionBuildManager5 != null) + { + _solutionBuildManager5.UnadviseUpdateSolutionEvents4(_adviseCookie4); + _isAdvised4 = false; + } + } + + public void OnActionItemClicked(IVsInfoBarUIElement infoBarUIElement, IVsInfoBarActionItem actionItem) + { + } + + public void OnClosed(IVsInfoBarUIElement infoBarUIElement) + { + _infoBarUIElement = null; + } + + public void UpdateSolution_QueryDelayFirstUpdateAction(out int pfDelay) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + pfDelay = 0; + + if (_wasJustReleased) + { + _wasJustReleased = false; + DismissInfoBar(); + UnadviseFromBuildEvents(); + return; + } + + if (_isBlocking) + { + if (_shouldRelease != null && _shouldRelease()) + { + EnableBuilds(); + return; + } + else if (!_infoBarShown) + { + ShowInfoBar(); + } + + pfDelay = 1; + } + } + + public void UpdateSolution_BeginFirstUpdateAction() + { + } + + public void UpdateSolution_EndLastUpdateAction() + { + } + + public void UpdateSolution_BeginUpdateAction(uint dwAction) + { + } + + public void UpdateSolution_EndUpdateAction(uint dwAction) + { + } + + public void OnActiveProjectCfgChangeBatchBegin() + { + } + + public void OnActiveProjectCfgChangeBatchEnd() + { + } + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + ThreadHelper.JoinableTaskFactory.Run(async () => + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + _isBlocking = false; + _wasJustReleased = false; + DismissInfoBar(); + UnadviseFromBuildEvents(); + UnadviseShellPropertyChanges(); + }); + } + } + } +} diff --git a/dev/VSIX/Shared/WizardImplementation.cs b/dev/VSIX/Shared/WizardImplementation.cs index 58f11c0d46..6b5ce34ae7 100644 --- a/dev/VSIX/Shared/WizardImplementation.cs +++ b/dev/VSIX/Shared/WizardImplementation.cs @@ -3,19 +3,23 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Resources; using System.Threading.Tasks; using System.Windows.Forms; +using System.Xml.Linq; using EnvDTE; using Microsoft.VisualStudio.ComponentModelHost; using Microsoft.VisualStudio.Imaging; +using Microsoft.VisualStudio; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.TemplateWizard; using Microsoft.VisualStudio.Threading; using NuGet.VisualStudio; + // Although the strings are the same in the wizard for both extensions, // they are included with both their respective VSPackages. // Strings for both extensions can be found in {PathToWindowsAppSDK}\dev\VSIX\Extension\Cs\Common\VSPackage.resx @@ -34,7 +38,7 @@ public enum ErrorMessageFormat InfoBar } - public partial class NuGetPackageInstaller : IWizard + public partial class NuGetPackageInstaller : IWizard, IVsSolutionEvents { internal static Guid SolutionVCProjectGuid = new Guid("8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942"); private Project _project; @@ -42,7 +46,13 @@ public partial class NuGetPackageInstaller : IWizard private IEnumerable _nuGetPackages; private IVsNuGetProjectUpdateEvents _nugetProjectUpdateEvents; private IVsThreadedWaitDialog2 _waitDialog; + private IVsInfoBarUIFactory _infoBarUIFactory; private Dictionary _failedPackageExceptions = new Dictionary(); + private static BuildGuard s_buildGuard = new BuildGuard(); + private static volatile bool s_installationComplete; + private IVsSolution _solution; + private uint _solutionEventsCookie; + private bool _packageRestoreEnabled = true; public void RunStarted(object automationObject, Dictionary replacementsDictionary, WizardRunKind runKind, object[] customParams) { @@ -60,6 +70,12 @@ public void RunStarted(object automationObject, Dictionary repla System.Diagnostics.Debug.WriteLine("Warning: Could not obtain IVsThreadedWaitDialog2 service."); } + _infoBarUIFactory = ServiceProvider.GlobalProvider.GetService(typeof(SVsInfoBarUIFactory)) as IVsInfoBarUIFactory; + if (_infoBarUIFactory == null) + { + System.Diagnostics.Debug.WriteLine("Warning: Could not obtain IVsInfoBarUIFactory service."); + } + if (_componentModel != null) { _nugetProjectUpdateEvents = _componentModel.GetService(); @@ -68,18 +84,72 @@ public void RunStarted(object automationObject, Dictionary repla _nugetProjectUpdateEvents.SolutionRestoreFinished += OnSolutionRestoreFinished; } } + // Assuming package list is passed via a custom parameter in the .vstemplate file if (replacementsDictionary.TryGetValue("$NuGetPackages$", out string packages)) { _nuGetPackages = packages.Split(';').Where(p => !string.IsNullOrEmpty(p)); } + + if (_nuGetPackages != null && _nuGetPackages.Any()) + { + _packageRestoreEnabled = IsNuGetPackageRestoreEnabled(); + + if (!_packageRestoreEnabled) + { + LogError("NuGet package restore is disabled. Skipping automatic package installation."); + return; + } + + if (s_installationComplete) + { + s_buildGuard = new BuildGuard(); + s_installationComplete = false; + } + + s_buildGuard.SetReleaseCondition(() => s_installationComplete); + + _solution = ServiceProvider.GlobalProvider.GetService(typeof(SVsSolution)) as IVsSolution; + if (_solution != null) + { + _solution.AdviseSolutionEvents(this, out _solutionEventsCookie); + } + + s_buildGuard.DisableBuilds(); + } } public void ProjectFinishedGenerating(Project project) { ThreadHelper.ThrowIfNotOnUIThread(); + if (project == null) + { + return; + } + _project = project; Guid _projectGuid = GetProjectGuid(project); + + if (_nuGetPackages != null && _nuGetPackages.Any()) + { + var projectName = _project?.Name ?? "Unknown Project"; + var packageNames = string.Join(", ", _nuGetPackages); + s_buildGuard.SetInfoBarMessage(string.Format(Resources._1055, projectName, packageNames)); + } + + if (!_packageRestoreEnabled && _nuGetPackages != null && _nuGetPackages.Any()) + { + // Write C# package references to the project so that auto-download can install packages + // after project generation completes. + if (!_projectGuid.Equals(SolutionVCProjectGuid)) + { + AddPackageReferencesToProject(); + } + + _ = DisplayInfoBarAsync(GetRestoreDisabledMessage()); + return; + } + if (_projectGuid.Equals(SolutionVCProjectGuid)) { ThreadHelper.JoinableTaskFactory.Run(async () => @@ -96,36 +166,55 @@ await ThreadHelper.JoinableTaskFactory.RunAsync(async () => await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); int canceled = 0; // Initialize as not canceled - // Start the package installation task but do not await it here - var installationTask = StartInstallationAsync(); - - // Start the threaded wait dialog - if (_waitDialog != null) + try { - _waitDialog.StartWaitDialog(null, Resources._1044, null, null, Resources._1045, 0, false, true); - } + // Start the package installation task but do not await it here + var installationTask = StartInstallationAsync(); - // Now await the installation task to complete - await installationTask; + // Start the threaded wait dialog + if (_waitDialog != null) + { + _waitDialog.StartWaitDialog(null, Resources._1044, null, null, Resources._1045, 0, false, true); + } - // Once the installation is complete, end the wait dialog - if (_waitDialog != null) - { - _waitDialog.EndWaitDialog(out canceled); - } + // Now await the installation task to complete + await installationTask; - // If _waitDialog is null, canceled remains 0 (not canceled) - // Check if the process was canceled before proceeding - if (canceled == 0) // If not canceled, finalize the process + // Once the installation is complete, end the wait dialog + if (_waitDialog != null) + { + _waitDialog.EndWaitDialog(out canceled); + } + + // If _waitDialog is null, canceled remains 0 (not canceled) + // Check if the process was canceled before proceeding + if (canceled == 0) // If not canceled, finalize the process + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + SaveAllProjects(); + } + } + finally { + s_installationComplete = true; await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - SaveAllProjects(); + UnadviseSolutionEvents(); + s_buildGuard.EnableBuilds(); } }); } private async Task StartInstallationAsync() { + if (!_packageRestoreEnabled) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + LogError("NuGet package restore is disabled. Skipping package installation."); + var packageNames = string.Join(", ", _nuGetPackages); + _failedPackageExceptions[packageNames] = new InvalidOperationException("NuGet package restore is disabled."); + return; + } + if (_componentModel == null) { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); @@ -158,6 +247,15 @@ private async Task StartInstallationAsync() catch (Exception ex) { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + if (IsNuGetRestoreDisabledException(ex)) + { + LogError($"NuGet restore is disabled. Error: {ex.Message}"); + AddPackageReferencesToProject(); + _ = DisplayInfoBarAsync(GetRestoreDisabledMessage()); + return; + } + LogError($"Failed to install NuGet package: {packageId}. Error: {ex.Message}"); _failedPackageExceptions[packageId] = ex; } @@ -165,11 +263,23 @@ private async Task StartInstallationAsync() if (_failedPackageExceptions.Count > 0) { - // Build error message in the requested format - var errorMessage = CreateErrorMessage(ErrorMessageFormat.InfoBar); await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - LogError(errorMessage); - _ = DisplayInfoBarAsync(errorMessage); + + bool isRestoreDisabled = _failedPackageExceptions.Values + .Any(e => IsNuGetRestoreDisabledException(e)); + + if (isRestoreDisabled) + { + AddPackageReferencesToProject(); + _ = DisplayInfoBarAsync(GetRestoreDisabledMessage()); + } + else + { + // Build error message in the requested format + var errorMessage = CreateErrorMessage(ErrorMessageFormat.InfoBar); + LogError(errorMessage); + _ = DisplayInfoBarAsync(errorMessage); + } } } @@ -187,8 +297,23 @@ public void RunFinished() Guid _projectGuid = GetProjectGuid(_project); if (_projectGuid.Equals(SolutionVCProjectGuid)) { + // For C++ projects, installation is synchronous so builds are already + // re-enabled by the finally block. Dispose as a safety net. + UnadviseSolutionEvents(); + s_buildGuard.Dispose(); + if (_failedPackageExceptions.Count > 0) { + bool isRestoreDisabled = _failedPackageExceptions.Values + .Any(ex => IsNuGetRestoreDisabledException(ex)); + + if (isRestoreDisabled) + { + _ = DisplayInfoBarAsync(GetRestoreDisabledMessage()); + ShowOutputWindow(CreateDetailedErrorMessage()); + return; + } + var errorMessage = CreateErrorMessage(ErrorMessageFormat.MessageBox); LogError(errorMessage); @@ -206,12 +331,207 @@ public void RunFinished() } } + private string GetRestoreDisabledMessage() + { + ThreadHelper.ThrowIfNotOnUIThread(); + var projectName = _project?.Name ?? "Unknown Project"; + var packageNames = string.Join(", ", _nuGetPackages); + + // C++ projects can't use PackageReference, so they need manual install + // via the NuGet Package Manager. C# projects can re-enable auto-download + // and restore the solution. + return GetProjectGuid(_project).Equals(SolutionVCProjectGuid) + ? string.Format(Resources._1057, projectName, packageNames) + : string.Format(Resources._1056, projectName, packageNames); + } + private void ShowOutputWindow(string errorMessage) { ThreadHelper.ThrowIfNotOnUIThread(); OutputWindowHelper.ShowMessageInOutputWindow(errorMessage); } + private void AddPackageReferencesToProject() + { + ThreadHelper.ThrowIfNotOnUIThread(); + + try + { + if (_project == null || _nuGetPackages == null) + { + return; + } + + string projectPath = _project.FullName; + if (!File.Exists(projectPath)) + { + return; + } + + var doc = XDocument.Load(projectPath); + XNamespace ns = doc.Root.GetDefaultNamespace(); + + var itemGroup = doc.Root.Elements(ns + "ItemGroup") + .FirstOrDefault(ig => ig.Elements(ns + "PackageReference").Any()); + + if (itemGroup == null) + { + itemGroup = new XElement(ns + "ItemGroup"); + doc.Root.Add(itemGroup); + } + + foreach (var packageId in _nuGetPackages) + { + bool alreadyExists = itemGroup.Elements(ns + "PackageReference") + .Any(e => string.Equals( + (string)e.Attribute("Include"), packageId, StringComparison.OrdinalIgnoreCase)); + + if (!alreadyExists) + { + itemGroup.Add(new XElement(ns + "PackageReference", + new XAttribute("Include", packageId), + new XAttribute("Version", "*"))); + } + } + + doc.Save(projectPath); + } + catch (Exception ex) + { + LogError($"Failed to add package references to project file: {ex.Message}"); + } + } + + // NuGet package restore is enabled by default, so only return false if we can confirm it is disabled in + // the user's config. + private static bool IsNuGetPackageRestoreEnabled() + { + try + { + string configPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "NuGet", + "NuGet.Config"); + + // If no NuGet.config is provided, package restore is enabled by default + if (!File.Exists(configPath)) + { + System.Diagnostics.Debug.WriteLine($"NuGet.Config not found at: {configPath}. Assuming package restore is enabled."); + return true; + } + + // If package restore is not disabled, then it is enabled by default + var doc = XDocument.Load(configPath); + var packageRestore = doc.Descendants("packageRestore").FirstOrDefault(); + if (packageRestore == null) + { + System.Diagnostics.Debug.WriteLine("No packageRestore section found in NuGet.Config. Assuming enabled."); + return true; + } + + var enabledElement = packageRestore.Elements("add") + .FirstOrDefault(e => string.Equals( + (string)e.Attribute("key"), "enabled", StringComparison.OrdinalIgnoreCase)); + + if (enabledElement != null) + { + string value = (string)enabledElement.Attribute("value"); + + // Only explicit disabling of package restore should return false. + if (string.Equals(value, "False", StringComparison.OrdinalIgnoreCase)) + { + System.Diagnostics.Debug.WriteLine("NuGet package restore is explicitly disabled in NuGet.Config."); + return false; + } + } + + System.Diagnostics.Debug.WriteLine("NuGet package restore is enabled (no explicit disable found)."); + return true; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error reading NuGet.Config: {ex.Message}. Assuming package restore is enabled."); + return true; + } + } + + private static bool IsNuGetRestoreDisabledException(Exception ex) + { + for (var current = ex; current != null; current = current.InnerException) + { + if (current.Message != null && + (current.Message.IndexOf("NuGet restore is currently disabled", StringComparison.OrdinalIgnoreCase) >= 0 || + current.Message.IndexOf("package restore is disabled", StringComparison.OrdinalIgnoreCase) >= 0 || + current.Message.IndexOf("EnableNuGetPackageRestore", StringComparison.OrdinalIgnoreCase) >= 0)) + { + return true; + } + } + + return false; + } + + private void UnadviseSolutionEvents() + { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (_solution != null && _solutionEventsCookie != 0) + { + _solution.UnadviseSolutionEvents(_solutionEventsCookie); + _solutionEventsCookie = 0; + } + } + + public int OnAfterOpenProject(IVsHierarchy pHierarchy, int fAdded) + { + return VSConstants.S_OK; + } + + public int OnQueryCloseProject(IVsHierarchy pHierarchy, int fRemoving, ref int pfCancel) + { + return VSConstants.S_OK; + } + + public int OnBeforeCloseProject(IVsHierarchy pHierarchy, int fRemoved) + { + return VSConstants.S_OK; + } + + public int OnAfterLoadProject(IVsHierarchy pStubHierarchy, IVsHierarchy pRealHierarchy) + { + return VSConstants.S_OK; + } + + public int OnQueryUnloadProject(IVsHierarchy pRealHierarchy, ref int pfCancel) + { + return VSConstants.S_OK; + } + + public int OnBeforeUnloadProject(IVsHierarchy pRealHierarchy, IVsHierarchy pStubHierarchy) + { + return VSConstants.S_OK; + } + + public int OnAfterOpenSolution(object pUnkReserved, int fNewSolution) + { + return VSConstants.S_OK; + } + + public int OnQueryCloseSolution(object pUnkReserved, ref int pfCancel) + { + return VSConstants.S_OK; + } + + public int OnBeforeCloseSolution(object pUnkReserved) + { + return VSConstants.S_OK; + } + + public int OnAfterCloseSolution(object pUnkReserved) + { + return VSConstants.S_OK; + } + private void SaveAllProjects() { ThreadHelper.ThrowIfNotOnUIThread("SaveAllProjects must be called on the UI thread."); @@ -385,39 +705,62 @@ private void ShowLocalizationErrorDialog(MissingManifestResourceException ex) private async Task DisplayInfoBarAsync(string errorMessage) { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - var infoBar = CreateNuGetInfoBar(errorMessage); - var infoBarUi = CreateInfoBarUI(infoBar); + try + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var infoBar = CreateNuGetInfoBar(errorMessage); + var infoBarUi = CreateInfoBarUI(infoBar); + + // Write detailed error message to output window + var detailedErrorMessage = CreateDetailedErrorMessage(); + ShowOutputWindow(detailedErrorMessage); - // Write detailed error message to output window - var detailedErrorMessage = CreateDetailedErrorMessage(); - ShowOutputWindow(detailedErrorMessage); + if (infoBarUi == null) + { + ShowOutputWindow("[InfoBar] Failed: Could not create InfoBar UI element."); + return; + } + + infoBarUi.Advise(new NuGetInfoBarUIEvents(detailedErrorMessage), out uint _); + + // The main window InfoBar host may not be available immediately + // during template wizard execution while VS transitions from the + // New Project dialog. Retry briefly while the main window initializes. + for (int retry = 0; retry < 5; retry++) + { + if (TryAddInfoBarToMainWindow(infoBarUi)) + { + return; + } - if (infoBarUi == null) + await Task.Delay(1000); + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + } + } + catch (Exception ex) { - LogError("Could not create InfoBar UI element. Logged error message to output window."); - return; + ShowOutputWindow($"[InfoBar] Exception: {ex.Message}"); } + } - infoBarUi.Advise(new NuGetInfoBarUIEvents(detailedErrorMessage), out uint _); + private bool TryAddInfoBarToMainWindow(IVsInfoBarUIElement infoBarUi) + { + ThreadHelper.ThrowIfNotOnUIThread(); IVsShell shell = ServiceProvider.GlobalProvider.GetService(typeof(SVsShell)) as IVsShell; if (shell == null) { - LogError("Could not obtain IVsShell service"); - return; + return false; } - // Get the main window's InfoBar host using VSSPROPID_MainWindowInfoBarHost - object infoBarHostObj; - int hr = shell.GetProperty((int)__VSSPROPID7.VSSPROPID_MainWindowInfoBarHost, out infoBarHostObj); - if (!(infoBarHostObj is IVsInfoBarHost infoBarHost)) + shell.GetProperty((int)__VSSPROPID7.VSSPROPID_MainWindowInfoBarHost, out object infoBarHostObj); + if (infoBarHostObj is IVsInfoBarHost infoBarHost) { - LogError("Could not obtain IVsInfoBarHost service"); - return; + infoBarHost.AddInfoBar(infoBarUi); + return true; } - infoBarHost.AddInfoBar(infoBarUi); + return false; } private IVsInfoBarUIElement CreateInfoBarUI(IVsInfoBar infoBar) @@ -429,14 +772,13 @@ private IVsInfoBarUIElement CreateInfoBarUI(IVsInfoBar infoBar) return null; } - IVsInfoBarUIFactory infoBarUIFactory = ServiceProvider.GlobalProvider.GetService(typeof(SVsInfoBarUIFactory)) as IVsInfoBarUIFactory; - if (infoBarUIFactory == null) + if (_infoBarUIFactory == null) { LogError("Could not obtain IVsInfoBarUIFactory service"); return null; } - return infoBarUIFactory.CreateInfoBar(infoBar); + return _infoBarUIFactory.CreateInfoBar(infoBar); } private IVsInfoBar CreateNuGetInfoBar(string message)