diff --git a/src/Uno.Sdk/Sdk/Sdk.targets b/src/Uno.Sdk/Sdk/Sdk.targets index e84c5c144834..dd09b86e679c 100644 --- a/src/Uno.Sdk/Sdk/Sdk.targets +++ b/src/Uno.Sdk/Sdk/Sdk.targets @@ -10,10 +10,6 @@ Copyright (C) Uno Platform Inc. All rights reserved. --> - - $(AfterMicrosoftNETSdkTargets);$(_UnoSdkTargetsDirectory)Uno.Sdk.After.targets - - @@ -28,5 +24,8 @@ Copyright (C) Uno Platform Inc. All rights reserved. + + + diff --git a/src/Uno.Sdk/targets/Uno.Sdk.After.targets b/src/Uno.Sdk/targets/Uno.Sdk.After.targets index f77ca91a07df..860a705f80df 100644 --- a/src/Uno.Sdk/targets/Uno.Sdk.After.targets +++ b/src/Uno.Sdk/targets/Uno.Sdk.After.targets @@ -12,4 +12,43 @@ + + + + <_UnoTargetFrameworkCount>$(TargetFrameworks.Split(';', System.StringSplitOptions.RemoveEmptyEntries).Length) + <_UnoFirstOriginalTargetFramework>$(TargetFrameworks.Split(';', System.StringSplitOptions.RemoveEmptyEntries)[0]) + + + + + <_UnoTargetFrameworksWasmFiltered>$(TargetFrameworks.Replace($(ActiveDebugFramework),'')) + + $([MSBuild]::Unescape('$(ActiveDebugFramework);$(_UnoTargetFrameworksWasmFiltered)')) + + + + + + + + + + diff --git a/src/Uno.UI.RemoteControl.VS/DebuggerHelper/ProfilesObserver.cs b/src/Uno.UI.RemoteControl.VS/DebuggerHelper/ProfilesObserver.cs index 6c925557c8ce..7e26eb104f57 100644 --- a/src/Uno.UI.RemoteControl.VS/DebuggerHelper/ProfilesObserver.cs +++ b/src/Uno.UI.RemoteControl.VS/DebuggerHelper/ProfilesObserver.cs @@ -20,6 +20,7 @@ using System.Reflection; using System.Management.Instrumentation; using Microsoft.VisualStudio.RpcContracts.Build; +using EnvDTE80; namespace Uno.UI.RemoteControl.VS.DebuggerHelper; #pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread @@ -27,16 +28,25 @@ namespace Uno.UI.RemoteControl.VS.DebuggerHelper; internal class ProfilesObserver : IDisposable { private readonly AsyncPackage _asyncPackage; + private readonly Action _debugLog; private readonly DTE _dte; private readonly Func _onDebugFrameworkChanged; private readonly Func _onDebugProfileChanged; + + private record FrameworkServices(object? ActiveDebugFrameworkServices, MethodInfo? SetActiveFrameworkMethod, MethodInfo? GetProjectFrameworksAsyncMethod); + private FrameworkServices? _projectFrameworkServices; + private string? _currentActiveDebugProfile; private string? _currentActiveDebugFramework; private IDisposable? _projectRuleSubscriptionLink; private UnconfiguredProject? _unconfiguredProject; - private object? _activeDebugFrameworkServices; - private MethodInfo? _setActiveFrameworkMethod; - private MethodInfo? _getProjectFrameworksAsyncMethod; + + // Keep the handlers below in order to avoid collection + // and allow DTE to call them. + _dispSolutionEvents_ProjectAddedEventHandler? _projectAdded; + _dispSolutionEvents_ProjectRemovedEventHandler? _projectRemoved; + _dispSolutionEvents_ProjectRenamedEventHandler? _projectRenamed; + _dispCommandEvents_AfterExecuteEventHandler? _afterExecute; public string? CurrentActiveDebugProfile => _currentActiveDebugProfile; @@ -44,53 +54,141 @@ public string? CurrentActiveDebugProfile public string? CurrentActiveDebugFramework => _currentActiveDebugFramework; - public ProfilesObserver(AsyncPackage asyncPackage, EnvDTE.DTE dte, Func onDebugFrameworkChanged, Func onDebugProfileChanged) + public ProfilesObserver( + AsyncPackage asyncPackage + , EnvDTE.DTE dte + , Func onDebugFrameworkChanged + , Func onDebugProfileChanged + , Action debugLog) { _asyncPackage = asyncPackage; + _debugLog = debugLog; _dte = dte; _onDebugFrameworkChanged = onDebugFrameworkChanged; _onDebugProfileChanged = onDebugProfileChanged; + + ObserveSolutionEvents(); } - public async Task ObserveProfilesAsync() - { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + object[]? _existingStartupProjects = []; - if (_dte.Solution.SolutionBuild.StartupProjects is object[] startupProjects - && startupProjects.Length > 0) + private void TryUpdateSolution() + { + if (_dte.Solution.SolutionBuild.StartupProjects is object[] newStartupProjects) { - var startupProject = (string)startupProjects[0]; + if (!newStartupProjects.SequenceEqual(_existingStartupProjects)) + { + // log all projects + _existingStartupProjects = newStartupProjects; + } - if ((await _dte.GetProjectsAsync()).FirstOrDefault(p => p.UniqueName == startupProject) is Project dteProject - && (await GetUnconfiguredProjectAsync(dteProject)) is { } unconfiguredProject) + if (_unconfiguredProject is null) { - _unconfiguredProject = unconfiguredProject; + _ = ObserveProfilesAsync(); + } + } + } + + public async Task ObserveProfilesAsync() + { + try + { + _debugLog("Starting observing profile"); - var configuredProject = unconfiguredProject.Services.ActiveConfiguredProjectProvider?.ActiveConfiguredProject; - var projectSubscriptionService = configuredProject?.Services.ActiveConfiguredProjectSubscription; + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - if (projectSubscriptionService is not null) + if ((await _dte.GetStartupProjectsAsync()) is { } startupProjects) + { + if (startupProjects.FirstOrDefault() is Project dteProject + && (await GetUnconfiguredProjectAsync(dteProject)) is { } unconfiguredProject) { - var projectChangesBlock = DataflowBlockSlim.CreateActionBlock( - CaptureAndApplyExecutionContext>>(ProjectRuleBlock_ChangedAsync)); + _debugLog($"Observing {unconfiguredProject.FullPath}"); + + _unconfiguredProject = unconfiguredProject; + _unconfiguredProject.ProjectUnloading += OnUnconfiguredProject_ProjectUnloadingAsync; + + var configuredProject = unconfiguredProject.Services.ActiveConfiguredProjectProvider?.ActiveConfiguredProject; + var projectSubscriptionService = configuredProject?.Services.ActiveConfiguredProjectSubscription; - var evaluationLinkOptions = new StandardRuleDataflowLinkOptions + if (projectSubscriptionService is not null) { - 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 }); + 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 }); + } } } } + catch (Exception ex) + { + _debugLog($"Failed to observe {ex}"); + } + } + + private void ObserveSolutionEvents() + { + _projectAdded = (s) => + { + _debugLog($"_projectAdded: {s}"); + TryUpdateSolution(); + }; + _projectRemoved = (s) => + { + _debugLog($"_projectRemoved: {s}"); + TryUpdateSolution(); + }; + _projectRenamed = (s, v) => + { + _debugLog($"_projectRenamed: {s}"); + TryUpdateSolution(); + }; + _afterExecute = (s, c, o, m) => + { + _debugLog($"_afterExecute: {s} {c} {o} {m}"); + TryUpdateSolution(); + }; + + _debugLog("Observing solution"); + _dte.Events.SolutionEvents.ProjectAdded += _projectAdded; + _dte.Events.SolutionEvents.ProjectRemoved += _projectRemoved; + _dte.Events.SolutionEvents.ProjectRenamed += _projectRenamed; + _dte.Events.CommandEvents.AfterExecute += _afterExecute; + } + + private async Task OnUnconfiguredProject_ProjectUnloadingAsync(object? sender, EventArgs args) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + _debugLog($"unconfiguredProject was unloaded"); + + _currentActiveDebugFramework = null; + _currentActiveDebugProfile = null; + + // Force a refresh of reflection calls + _projectFrameworkServices = null; + + _projectRuleSubscriptionLink?.Dispose(); + _projectRuleSubscriptionLink = null; + + if (_unconfiguredProject is not null) + { + _unconfiguredProject.ProjectUnloading -= OnUnconfiguredProject_ProjectUnloadingAsync; + _unconfiguredProject = null; + } } private static Func CaptureAndApplyExecutionContext(Func function) @@ -115,34 +213,43 @@ private static Func CaptureAndApplyExecutionContext(Func> projectSnapshot) { - if (projectSnapshot.Value.Item1.CurrentState.TryGetValue("ProjectDebugger", out var ruleSnapshot)) + try { - ruleSnapshot.Properties.TryGetValue("ActiveDebugProfile", out var activeDebugProfile); - ruleSnapshot.Properties.TryGetValue("ActiveDebugFramework", out var activeDebugFramework); - - if (!string.IsNullOrEmpty(activeDebugProfile) && activeDebugProfile != _currentActiveDebugProfile) + if (projectSnapshot.Value.Item1.CurrentState.TryGetValue("ProjectDebugger", out var ruleSnapshot)) { - var previousProfile = _currentActiveDebugProfile; - _currentActiveDebugProfile = activeDebugProfile; + ruleSnapshot.Properties.TryGetValue("ActiveDebugProfile", out var activeDebugProfile); + ruleSnapshot.Properties.TryGetValue("ActiveDebugFramework", out var activeDebugFramework); - await _onDebugProfileChanged(previousProfile, _currentActiveDebugProfile); - } + if (!string.IsNullOrEmpty(activeDebugProfile) && activeDebugProfile != _currentActiveDebugProfile) + { + var previousProfile = _currentActiveDebugProfile; + _currentActiveDebugProfile = activeDebugProfile; - if (!string.IsNullOrEmpty(activeDebugFramework) && activeDebugFramework != _currentActiveDebugFramework) - { - var previousDebugFramework = _currentActiveDebugProfile; - _currentActiveDebugFramework = activeDebugFramework; + await _onDebugProfileChanged(previousProfile, _currentActiveDebugProfile); + } + + if (!string.IsNullOrEmpty(activeDebugFramework) && activeDebugFramework != _currentActiveDebugFramework) + { + var previousDebugFramework = _currentActiveDebugFramework; + _currentActiveDebugFramework = activeDebugFramework; - await _onDebugFrameworkChanged(previousDebugFramework, _currentActiveDebugFramework); + await _onDebugFrameworkChanged(previousDebugFramework, _currentActiveDebugFramework); + } } } + catch (Exception e) + { + _debugLog($"Failed to process changedAsync: {e}"); + } } public async Task SetActiveTargetFrameworkAsync(string targetFramework) { + _debugLog($"SetActiveTargetFrameworkAsync({targetFramework})"); + EnsureActiveDebugFrameworkServices(); - if (_setActiveFrameworkMethod?.Invoke(_activeDebugFrameworkServices, [targetFramework]) is Task t) + if (_projectFrameworkServices?.SetActiveFrameworkMethod?.Invoke(_projectFrameworkServices.ActiveDebugFrameworkServices, [targetFramework]) is Task t) { await t; } @@ -150,9 +257,11 @@ public async Task SetActiveTargetFrameworkAsync(string targetFramework) public async Task?> GetActiveTargetFrameworksAsync() { + _debugLog($"GetActiveTargetFrameworksAsync()"); + EnsureActiveDebugFrameworkServices(); - if (_getProjectFrameworksAsyncMethod?.Invoke(_activeDebugFrameworkServices, []) is Task?> listTask) + if (_projectFrameworkServices?.GetProjectFrameworksAsyncMethod?.Invoke(_projectFrameworkServices.ActiveDebugFrameworkServices, []) is Task?> listTask) { return await listTask; } @@ -210,27 +319,25 @@ public async Task> GetLaunchProfilesAsync() private void EnsureActiveDebugFrameworkServices() { - if (_setActiveFrameworkMethod is null) - { - var provider = _unconfiguredProject?.Services.ActiveConfiguredProjectProvider?.ActiveConfiguredProject?.Services.ExportProvider; + var provider = _unconfiguredProject?.Services.ActiveConfiguredProjectProvider?.ActiveConfiguredProject?.Services.ExportProvider; + if (_projectFrameworkServices is null && provider is not null) + { 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]); + var 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; - } + if (activeDebugFrameworkServices.GetType().GetMethod("SetActiveDebuggingFrameworkPropertyAsync") is { } 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) + // https://github.com/dotnet/project-system/blob/34eb57b35962367b71c2a1d79f6c486945586e24/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/IActiveDebugFrameworkServices.cs#L20-L21 + && activeDebugFrameworkServices.GetType().GetMethod("GetProjectFrameworksAsync") is { } getProjectFrameworksAsyncMethod) { - _getProjectFrameworksAsyncMethod = getProjectFrameworksAsyncMethod; + _projectFrameworkServices = new(activeDebugFrameworkServices, setActiveFrameworkMethod, getProjectFrameworksAsyncMethod); } } } diff --git a/src/Uno.UI.RemoteControl.VS/EntryPoint.cs b/src/Uno.UI.RemoteControl.VS/EntryPoint.cs index d598fd975f82..7fb54d035226 100644 --- a/src/Uno.UI.RemoteControl.VS/EntryPoint.cs +++ b/src/Uno.UI.RemoteControl.VS/EntryPoint.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.ComponentModel.Composition; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.IO.Pipes; @@ -19,10 +20,13 @@ using EnvDTE80; using Microsoft.Build.Evaluation; using Microsoft.Build.Framework; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.OLE.Interop; using Microsoft.VisualStudio.ProjectSystem; using Microsoft.VisualStudio.ProjectSystem.Build; using Microsoft.VisualStudio.ProjectSystem.Debug; using Microsoft.VisualStudio.ProjectSystem.Properties; +using Microsoft.VisualStudio.ProjectSystem.VS; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using StreamJsonRpc; @@ -93,7 +97,7 @@ 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 = new ProfilesObserver(asyncPackage, _dte, OnDebugFrameworkChangedAsync, OnDebugProfileChangedAsync, _debugAction); _ = _debuggerObserver.ObserveProfilesAsync(); } @@ -111,6 +115,12 @@ private Task> OnProvideGlobalPropertiesAsync() }); } + [MemberNotNull( + nameof(_debugAction) + , nameof(_infoAction) + , nameof(_verboseAction) + , nameof(_warningAction) + , nameof(_errorAction))] private void SetupOutputWindow() { var ow = _dte2.ToolWindows.OutputWindow; @@ -406,8 +416,9 @@ private bool IsApplication(Microsoft.Build.Evaluation.Project project) 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. + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + // 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})"); @@ -446,6 +457,51 @@ bool IsCompatible(ILaunchProfile profile) await _debuggerObserver.SetActiveLaunchProfileAsync(msixProfile.Name); } } + + await TryReloadWebAssemblyTargetAsync(previousFramework, targetFrameworkIdentifier); + } + } + + private async Task TryReloadWebAssemblyTargetAsync(string? previousFramework, string targetFrameworkIdentifier) + { + try + { + if (previousFramework is not null + && GetTargetFrameworkIdentifier(previousFramework) is { } previousTargetFrameworkIdentifier + && ( + previousTargetFrameworkIdentifier == WasmTargetFrameworkIdentifier + || targetFrameworkIdentifier == WasmTargetFrameworkIdentifier) + && await _dte.GetStartupProjectsAsync() is { Length: > 0 } startupProjects + ) + { + _warningAction?.Invoke($"Detected that the active framework was changed from/to WebAssembly, reloading the project (See https://aka.platform.uno/singleproject-vs-wasm-reload)"); + + // In this context, in order to work around the fact that VS does not handle Wasm + // to be in the same project as other target framework, we're using the `_SelectedTargetFramework` + // to reorder the list and make browser-wasm first or not. + + // Assuming serviceProvider is an IServiceProvider instance available in your context + if (await _asyncPackage.GetServiceAsync(typeof(SVsSolution)) is IVsSolution solution + && solution is IVsSolution4 solution4) + { + if (solution.GetProjectOfUniqueName(startupProjects[0].UniqueName, out var startupProject) == 0) + { + if (startupProject.GetGuidProperty((uint)VSConstants.VSITEMID.Root, (int)__VSHPROPID.VSHPROPID_ProjectIDGuid, out var guidObj) == 0 + && guidObj is Guid projectGuid) + { + // Unload project + solution4.UnloadProject(ref projectGuid, (uint)_VSProjectUnloadStatus.UNLOADSTATUS_UnloadedByUser); + + // Reload project + solution4.ReloadProject(ref projectGuid); + } + } + } + } + } + catch (Exception e) + { + _errorAction?.Invoke($"Failed to reload project {e}"); } } @@ -463,7 +519,7 @@ private async Task OnDebugProfileChangedAsync(string? previousProfile, string ne if (profile.LaunchBrowser && FindTargetFramework(WasmTargetFrameworkIdentifier) is { } targetFramework) { - _debugAction?.Invoke($"Setting framework {targetFramework}"); + _errorAction?.Invoke($"Setting framework {targetFramework}"); await _debuggerObserver.SetActiveTargetFrameworkAsync(targetFramework); } @@ -471,7 +527,7 @@ private async Task OnDebugProfileChangedAsync(string? previousProfile, string ne && compatibleTargetObject is string compatibleTarget && FindTargetFramework(compatibleTarget) is { } compatibleTargetFramework) { - _debugAction?.Invoke($"Setting framework {compatibleTarget}"); + _errorAction?.Invoke($"Setting framework {compatibleTarget}"); await _debuggerObserver.SetActiveTargetFrameworkAsync(compatibleTargetFramework); } diff --git a/src/Uno.UI.RemoteControl.VS/Helpers/DTEHelper.cs b/src/Uno.UI.RemoteControl.VS/Helpers/DTEHelper.cs index b2f9b0e96f4b..75f53ca73321 100644 --- a/src/Uno.UI.RemoteControl.VS/Helpers/DTEHelper.cs +++ b/src/Uno.UI.RemoteControl.VS/Helpers/DTEHelper.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using EnvDTE; @@ -92,4 +93,25 @@ private static IEnumerable EnumSubProjects(Project folder) } } + public static async Task GetStartupProjectsAsync(this DTE dte) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + if (dte.Solution.SolutionBuild.StartupProjects is object[] startupProjects + && startupProjects.Length > 0) + { + var projects = await dte.GetProjectsAsync(); + + return startupProjects + .OfType() + .Select(p => projects.FirstOrDefault(p2 => + { + Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread(); + return p2.UniqueName == p; + })) + .Where(p => p is not null).ToArray(); + } + + return null; + } } 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 d5f43c684020..aa7a63f8933c 100644 --- a/src/Uno.UI.RemoteControl.VS/Uno.UI.RemoteControl.VS.csproj +++ b/src/Uno.UI.RemoteControl.VS/Uno.UI.RemoteControl.VS.csproj @@ -30,6 +30,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive +