From d1771aa5c78c3cfb7bb9d7a830e580052b7686b5 Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Wed, 20 Mar 2024 23:18:16 -0400 Subject: [PATCH] feat: Adjust automatic profile selection --- .../DebuggerHelper/ProfilesObserver.cs | 241 ++++++++++++++++++ src/Uno.UI.RemoteControl.VS/EntryPoint.cs | 167 +++++++----- .../Helpers/DTEHelper.cs | 75 ++++++ .../Uno.UI.RemoteControl.VS.csproj | 11 +- 4 files changed, 421 insertions(+), 73 deletions(-) create mode 100644 src/Uno.UI.RemoteControl.VS/DebuggerHelper/ProfilesObserver.cs diff --git a/src/Uno.UI.RemoteControl.VS/DebuggerHelper/ProfilesObserver.cs b/src/Uno.UI.RemoteControl.VS/DebuggerHelper/ProfilesObserver.cs new file mode 100644 index 000000000000..18f6e9c9c810 --- /dev/null +++ b/src/Uno.UI.RemoteControl.VS/DebuggerHelper/ProfilesObserver.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.ProjectSystem.Properties; +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.Shell; +using System.Collections.Immutable; +using System.Threading.Tasks.Dataflow; +using EnvDTE; +using Microsoft.VisualStudio.LanguageServer.Client; +using Uno.UI.RemoteControl.VS.Helpers; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.RpcContracts.Logging; +using Microsoft.VisualStudio.ProjectSystem.Debug; +using System.Reflection; +using System.Management.Instrumentation; +using Microsoft.VisualStudio.RpcContracts.Build; +namespace Uno.UI.RemoteControl.VS.DebuggerHelper; + +#pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread + +internal class ProfilesObserver : IDisposable +{ + private readonly AsyncPackage _asyncPackage; + private readonly DTE _dte; + private readonly Func _onDebugFrameworkChanged; + private readonly Func _onDebugProfileChanged; + private string? _currentActiveDebugProfile; + private string? _currentActiveDebugFramework; + private IDisposable? _projectRuleSubscriptionLink; + private UnconfiguredProject? _unconfiguredProject; + private object? _activeDebugFrameworkServices; + private MethodInfo? _setActiveFrameworkMethod; + private MethodInfo? _getProjectFrameworksAsyncMethod; + + public string? CurrentActiveDebugProfile + => _currentActiveDebugProfile; + + public string? CurrentActiveDebugFramework + => _currentActiveDebugFramework; + + public ProfilesObserver(AsyncPackage asyncPackage, EnvDTE.DTE dte, Func onDebugFrameworkChanged, Func onDebugProfileChanged) + { + _asyncPackage = asyncPackage; + _dte = dte; + _onDebugFrameworkChanged = onDebugFrameworkChanged; + _onDebugProfileChanged = onDebugProfileChanged; + } + + public async Task ObserveProfilesAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + if (_dte.Solution.SolutionBuild.StartupProjects is object[] startupProjects + && startupProjects.Length > 0) + { + var startupProject = (string)startupProjects[0]; + + if ((await _dte.GetProjectsAsync()).FirstOrDefault(p => p.UniqueName == startupProject) is Project dteProject + && (await GetUnconfiguredProjectAsync(dteProject)) is { } unconfiguredProject) + { + _unconfiguredProject = unconfiguredProject; + + var configuredProject = unconfiguredProject.Services.ActiveConfiguredProjectProvider?.ActiveConfiguredProject; + var projectSubscriptionService = configuredProject?.Services.ActiveConfiguredProjectSubscription; + + if (projectSubscriptionService is not null) + { + var projectChangesBlock = DataflowBlockSlim.CreateActionBlock( + CaptureAndApplyExecutionContext>>(ProjectRuleBlock_ChangedAsync)); + + var evaluationLinkOptions = new StandardRuleDataflowLinkOptions + { + RuleNames = ImmutableHashSet.Create("ProjectDebugger"), + PropagateCompletion = true + }; + + var projectBlock = projectSubscriptionService.ProjectRuleSource.SourceBlock.SyncLinkOptions(evaluationLinkOptions, true); + var unconfiguredProjectBlock = ProjectDataSources.SyncLinkOptions(unconfiguredProject.Capabilities.SourceBlock); + + _projectRuleSubscriptionLink = ProjectDataSources.SyncLinkTo( + projectBlock, + unconfiguredProjectBlock, + projectChangesBlock, + new() { PropagateCompletion = true }); + } + } + } + } + + private static Func CaptureAndApplyExecutionContext(Func function) + { + var context = ExecutionContext.Capture(); + + return (TInput input) => + { + var currentSynchronizationContext = SynchronizationContext.Current; + using var executionContext = context.CreateCopy(); + + Task? result = null; + ExecutionContext.Run(executionContext, delegate + { + SynchronizationContext.SetSynchronizationContext(currentSynchronizationContext); + result = function(input); + }, null); + + return result!; + }; + } + + private async Task ProjectRuleBlock_ChangedAsync(IProjectVersionedValue> projectSnapshot) + { + if (projectSnapshot.Value.Item1.CurrentState.TryGetValue("ProjectDebugger", out var ruleSnapshot)) + { + ruleSnapshot.Properties.TryGetValue("ActiveDebugProfile", out var activeDebugProfile); + ruleSnapshot.Properties.TryGetValue("ActiveDebugFramework", out var activeDebugFramework); + + if (!string.IsNullOrEmpty(activeDebugProfile) && activeDebugProfile != _currentActiveDebugProfile) + { + var previousProfile = _currentActiveDebugProfile; + _currentActiveDebugProfile = activeDebugProfile; + + await _onDebugProfileChanged(previousProfile, _currentActiveDebugProfile); + } + + if (!string.IsNullOrEmpty(activeDebugFramework) && activeDebugFramework != _currentActiveDebugFramework) + { + var previousDebugFramework = _currentActiveDebugProfile; + _currentActiveDebugFramework = activeDebugFramework; + + await _onDebugFrameworkChanged(previousDebugFramework, _currentActiveDebugFramework); + } + } + } + + public async Task SetActiveTargetFrameworkAsync(string targetFramework) + { + EnsureActiveDebugFrameworkServices(); + + if (_setActiveFrameworkMethod?.Invoke(_activeDebugFrameworkServices, [targetFramework]) is Task t) + { + await t; + } + } + + public async Task?> GetActiveTargetFrameworksAsync() + { + EnsureActiveDebugFrameworkServices(); + + if (_getProjectFrameworksAsyncMethod?.Invoke(_activeDebugFrameworkServices, []) is Task?> listTask) + { + return await listTask; + } + + return new(); + } + + public async Task SetActiveLaunchProfileAsync(string launchProfile) + { + var provider = _unconfiguredProject?.Services.ActiveConfiguredProjectProvider?.ActiveConfiguredProject?.Services.ExportProvider; + + if (provider?.GetService() is { } launchSettingsProvider) + { + await launchSettingsProvider.SetActiveProfileAsync(launchProfile); + } + } + + public async Task> GetLaunchProfilesAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + var provider = _unconfiguredProject?.Services.ActiveConfiguredProjectProvider?.ActiveConfiguredProject?.Services.ExportProvider; + + if (provider?.GetService() is { } launchSettingsProvider) + { + return launchSettingsProvider.CurrentSnapshot.Profiles; + } + + return ImmutableList.Create(); + } + + public async Task GetUnconfiguredProjectAsync(Project dteProject) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + // Get the IVsSolution service + if (await _asyncPackage.GetServiceAsync(typeof(SVsSolution)) is IVsSolution solution) + { + // Convert DTE project to IVsHierarchy + solution.GetProjectOfUniqueName(dteProject.UniqueName, out var hierarchy); + + // Get UnconfiguredProject from IVsHierarchy + if (hierarchy is IVsBrowseObjectContext browseContext) + { + return browseContext.UnconfiguredProject; + } + else if (hierarchy.GetProperty((uint)VSConstants.VSITEMID.Root, (int)__VSHPROPID.VSHPROPID_BrowseObject, out object browseObject) >= 0 && browseObject is IVsBrowseObjectContext context) + { + return context.UnconfiguredProject; + } + } + + return null; + } + + private void EnsureActiveDebugFrameworkServices() + { + if (_setActiveFrameworkMethod is null) + { + var provider = _unconfiguredProject?.Services.ActiveConfiguredProjectProvider?.ActiveConfiguredProject?.Services.ExportProvider; + + var type = Type.GetType("Microsoft.VisualStudio.ProjectSystem.Debug.IActiveDebugFrameworkServices, Microsoft.VisualStudio.ProjectSystem.Managed"); + if (typeof(MefExtensions).GetMethods().FirstOrDefault(m => m.Name == "GetService") is { } getServiceMethod) + { + var typedMethod = getServiceMethod.MakeGenericMethod(type); + + _activeDebugFrameworkServices = typedMethod.Invoke(null, [provider, /*allow default*/false]); + + // https://github.com/dotnet/project-system/blob/34eb57b35962367b71c2a1d79f6c486945586e24/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/IActiveDebugFrameworkServices.cs#L20-L21 + if (_activeDebugFrameworkServices.GetType().GetMethod("SetActiveDebuggingFrameworkPropertyAsync") is { } setActiveFrameworkMethod) + { + _setActiveFrameworkMethod = setActiveFrameworkMethod; + } + + // https://github.com/dotnet/project-system/blob/34eb57b35962367b71c2a1d79f6c486945586e24/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/IActiveDebugFrameworkServices.cs#L20-L21 + if (_activeDebugFrameworkServices.GetType().GetMethod("GetProjectFrameworksAsync") is { } getProjectFrameworksAsyncMethod) + { + _getProjectFrameworksAsyncMethod = getProjectFrameworksAsyncMethod; + } + } + } + } + + public void Dispose() + => _projectRuleSubscriptionLink?.Dispose(); +} diff --git a/src/Uno.UI.RemoteControl.VS/EntryPoint.cs b/src/Uno.UI.RemoteControl.VS/EntryPoint.cs index 34181e42c866..d598fd975f82 100644 --- a/src/Uno.UI.RemoteControl.VS/EntryPoint.cs +++ b/src/Uno.UI.RemoteControl.VS/EntryPoint.cs @@ -11,18 +11,23 @@ using System.Net.Sockets; using System.Reflection; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; using EnvDTE; using EnvDTE80; using Microsoft.Build.Evaluation; using Microsoft.Build.Framework; using Microsoft.VisualStudio.ProjectSystem; using Microsoft.VisualStudio.ProjectSystem.Build; +using Microsoft.VisualStudio.ProjectSystem.Debug; +using Microsoft.VisualStudio.ProjectSystem.Properties; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using StreamJsonRpc; using Uno.UI.RemoteControl.Messaging.IdeChannel; +using Uno.UI.RemoteControl.VS.DebuggerHelper; using Uno.UI.RemoteControl.VS.Helpers; using Uno.UI.RemoteControl.VS.IdeChannel; using ILogger = Uno.UI.RemoteControl.VS.Helpers.ILogger; @@ -36,8 +41,12 @@ namespace Uno.UI.RemoteControl.VS; public class EntryPoint : IDisposable { private const string UnoPlatformOutputPane = "Uno Platform"; - private const string FolderKind = "{66A26720-8FB5-11D2-AA7E-00C04F688DDE}"; private const string RemoteControlServerPortProperty = "UnoRemoteControlPort"; + private const string DesktopTargetFrameworkIdentifier = "desktop"; + private const string CompatibleTargetFrameworkProfileKey = "compatibleTargetFramework"; + private const string WindowsTargetFrameworkIdentifier = "windows"; + private const string WasmTargetFrameworkIdentifier = "browserwasm"; + private readonly DTE _dte; private readonly DTE2 _dte2; private readonly string _toolsPath; @@ -54,6 +63,7 @@ public class EntryPoint : IDisposable private bool _closing; private bool _isDisposed; private IdeChannelClient? _ideChannelClient; + private ProfilesObserver _debuggerObserver; private readonly _dispSolutionEvents_BeforeClosingEventHandler _closeHandler; private readonly _dispBuildEvents_OnBuildDoneEventHandler _onBuildDoneHandler; private readonly _dispBuildEvents_OnBuildProjConfigBeginEventHandler _onBuildProjConfigBeginHandler; @@ -82,6 +92,9 @@ public EntryPoint(DTE2 dte2, string toolsPath, AsyncPackage asyncPackage, Action // // This will can possibly be removed when all projects are migrated to the sdk project system. _ = UpdateProjectsAsync(); + + _debuggerObserver = new ProfilesObserver(asyncPackage, _dte, OnDebugFrameworkChangedAsync, OnDebugProfileChangedAsync); + _ = _debuggerObserver.ObserveProfilesAsync(); } private Task> OnProvideGlobalPropertiesAsync() @@ -185,7 +198,7 @@ private async Task UpdateProjectsAsync() { StartServer(); var portString = RemoteControlServerPort.ToString(CultureInfo.InvariantCulture); - foreach (var p in await GetProjectsAsync()) + foreach (var p in await _dte.GetProjectsAsync()) { var filename = string.Empty; try @@ -346,69 +359,6 @@ private static int GetTcpPort() return port; } - private async System.Threading.Tasks.Task> GetProjectsAsync() - { - ThreadHelper.ThrowIfNotOnUIThread(); - - var projectService = await _asyncPackage.GetServiceAsync(typeof(IProjectService)) as IProjectService; - - var solutionProjectItems = _dte.Solution.Projects; - - if (solutionProjectItems != null) - { - return EnumerateProjects(solutionProjectItems); - } - else - { - return Array.Empty(); - } - } - - private IEnumerable EnumerateProjects(EnvDTE.Projects vsSolution) - { - foreach (var project in vsSolution.OfType()) - { - if (project.Kind == FolderKind /* Folder */) - { - foreach (var subProject in EnumSubProjects(project)) - { - yield return subProject; - } - } - else - { - yield return project; - } - } - } - - private IEnumerable EnumSubProjects(EnvDTE.Project folder) - { - if (folder.ProjectItems != null) - { - var subProjects = folder.ProjectItems - .OfType() - .Select(p => p.Object) - .Where(p => p != null) - .Cast(); - - foreach (var project in subProjects) - { - if (project.Kind == FolderKind) - { - foreach (var subProject in EnumSubProjects(project)) - { - yield return subProject; - } - } - else - { - yield return project; - } - } - } - } - public void SetGlobalProperty(string projectFullName, string propertyName, string propertyValue) { var msbuildProject = GetMsbuildProject(projectFullName); @@ -454,6 +404,93 @@ private bool IsApplication(Microsoft.Build.Evaluation.Project project) (outputType.Equals("Exe", StringComparison.OrdinalIgnoreCase) || outputType.Equals("WinExe", StringComparison.OrdinalIgnoreCase)); } + private async Task OnDebugFrameworkChangedAsync(string? previousFramework, string newFramework) + { + // In this case, a new TargetFramework was selected. We need to file a matching launch profile, if any. + + if (GetTargetFrameworkIdentifier(newFramework) is { } targetFrameworkIdentifier) + { + _debugAction?.Invoke($"OnDebugFrameworkChangedAsync({previousFramework}, {newFramework}, {targetFrameworkIdentifier})"); + + var profiles = await _debuggerObserver.GetLaunchProfilesAsync(); + + if (targetFrameworkIdentifier == WasmTargetFrameworkIdentifier) + { + if (profiles.Find(p => p.LaunchBrowser) is { } browserProfile) + { + _debugAction?.Invoke($"Setting profile {browserProfile.Name}"); + + await _debuggerObserver.SetActiveLaunchProfileAsync(browserProfile.Name); + } + } + else if (targetFrameworkIdentifier == DesktopTargetFrameworkIdentifier) + { + bool IsCompatible(ILaunchProfile profile) + => profile.OtherSettings.TryGetValue(CompatibleTargetFrameworkProfileKey, out var compatibleTargetFramework) + && compatibleTargetFramework is string ctfs + && ctfs.Equals(DesktopTargetFrameworkIdentifier, StringComparison.OrdinalIgnoreCase); + + if (profiles.Find(IsCompatible) is { } desktopProfile) + { + _debugAction?.Invoke($"Setting profile {desktopProfile.Name}"); + + await _debuggerObserver.SetActiveLaunchProfileAsync(desktopProfile.Name); + } + } + else if (targetFrameworkIdentifier == WindowsTargetFrameworkIdentifier) + { + if (profiles.Find(p => p.CommandName.Equals("MsixPackage", StringComparison.OrdinalIgnoreCase)) is { } msixProfile) + { + _debugAction?.Invoke($"Setting profile {msixProfile.Name}"); + + await _debuggerObserver.SetActiveLaunchProfileAsync(msixProfile.Name); + } + } + } + } + + private async Task OnDebugProfileChangedAsync(string? previousProfile, string newProfile) + { + // In this case, a new TargetFramework was selected. We need to file a matching target framework, if any. + + _debugAction?.Invoke($"OnDebugProfileChangedAsync({previousProfile},{newProfile})"); + + var targetFrameworks = await _debuggerObserver.GetActiveTargetFrameworksAsync(); + var profiles = await _debuggerObserver.GetLaunchProfilesAsync(); + + if (profiles.FirstOrDefault(p => p.Name == newProfile) is { } profile) + { + if (profile.LaunchBrowser + && FindTargetFramework(WasmTargetFrameworkIdentifier) is { } targetFramework) + { + _debugAction?.Invoke($"Setting framework {targetFramework}"); + + await _debuggerObserver.SetActiveTargetFrameworkAsync(targetFramework); + } + else if (profile.OtherSettings.TryGetValue(CompatibleTargetFrameworkProfileKey, out var compatibleTargetObject) + && compatibleTargetObject is string compatibleTarget + && FindTargetFramework(compatibleTarget) is { } compatibleTargetFramework) + { + _debugAction?.Invoke($"Setting framework {compatibleTarget}"); + + await _debuggerObserver.SetActiveTargetFrameworkAsync(compatibleTargetFramework); + } + } + + string? FindTargetFramework(string identifier) + => targetFrameworks.FirstOrDefault(f => f.IndexOf("-" + identifier, StringComparison.OrdinalIgnoreCase) != -1); + } + + private string? GetTargetFrameworkIdentifier(string newFramework) + { + var regex = new Regex(@"net(\d+\.\d+)-(?\w+)(\d+\.\d+\.\d+)?"); + var match = regex.Match(newFramework); + + return match.Success + ? match.Groups["tfi"].Value + : null; + } + public void Dispose() { if (_isDisposed) diff --git a/src/Uno.UI.RemoteControl.VS/Helpers/DTEHelper.cs b/src/Uno.UI.RemoteControl.VS/Helpers/DTEHelper.cs index 18432f51bcec..b2f9b0e96f4b 100644 --- a/src/Uno.UI.RemoteControl.VS/Helpers/DTEHelper.cs +++ b/src/Uno.UI.RemoteControl.VS/Helpers/DTEHelper.cs @@ -3,13 +3,17 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using EnvDTE; using EnvDTE80; +using Microsoft.VisualStudio.ProjectSystem; using Microsoft.VisualStudio.Shell; namespace Uno.UI.RemoteControl.VS.Helpers; internal static class DTEHelper { + internal const string FolderKind = "{66A26720-8FB5-11D2-AA7E-00C04F688DDE}"; + public static int GetMSBuildOutputVerbosity(this DTE2 dte) { ThreadHelper.ThrowIfNotOnUIThread(); @@ -17,4 +21,75 @@ public static int GetMSBuildOutputVerbosity(this DTE2 dte) var logOutput = properties?.Item("MSBuildOutputVerbosity").Value is int log ? log : 0; return logOutput; } + + public static async Task> GetProjectsAsync(this DTE dte) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + var solutionProjectItems = dte.Solution.Projects; + + if (solutionProjectItems != null) + { + return EnumerateProjects(solutionProjectItems); + } + else + { + return Array.Empty(); + } + } + + private static IEnumerable EnumerateProjects(Projects vsSolution) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + foreach (var project in vsSolution.OfType()) + { + if (project.Kind == FolderKind /* Folder */) + { + foreach (var subProject in EnumSubProjects(project)) + { + yield return subProject; + } + } + else + { + yield return project; + } + } + } + + private static IEnumerable EnumSubProjects(Project folder) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (folder.ProjectItems != null) + { + var subProjects = folder.ProjectItems + .OfType() + .Select(p => + { + Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread(); + + return p.Object; + }) + .Where(p => p != null) + .Cast(); + + foreach (var project in subProjects) + { + if (project.Kind == FolderKind) + { + foreach (var subProject in EnumSubProjects(project)) + { + yield return subProject; + } + } + else + { + yield return project; + } + } + } + } + } diff --git a/src/Uno.UI.RemoteControl.VS/Uno.UI.RemoteControl.VS.csproj b/src/Uno.UI.RemoteControl.VS/Uno.UI.RemoteControl.VS.csproj index 83b687be0edd..d5f43c684020 100644 --- a/src/Uno.UI.RemoteControl.VS/Uno.UI.RemoteControl.VS.csproj +++ b/src/Uno.UI.RemoteControl.VS/Uno.UI.RemoteControl.VS.csproj @@ -16,22 +16,17 @@ - - - - - - - + + - +