Skip to content

Commit

Permalink
Merge pull request unoplatform#16032 from unoplatform/dev/jela/wasm-r…
Browse files Browse the repository at this point in the history
…eload

fix: Reload project when going from/to browserwasm
  • Loading branch information
jeromelaban authored Mar 27, 2024
2 parents 73f5bd7 + abd1c5b commit 6a30fc4
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 66 deletions.
7 changes: 3 additions & 4 deletions src/Uno.Sdk/Sdk/Sdk.targets
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ Copyright (C) Uno Platform Inc. All rights reserved.
-->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<PropertyGroup>
<AfterMicrosoftNETSdkTargets>$(AfterMicrosoftNETSdkTargets);$(_UnoSdkTargetsDirectory)Uno.Sdk.After.targets</AfterMicrosoftNETSdkTargets>
</PropertyGroup>

<!-- Common Includes -->
<Import Project="$(_UnoSdkTargetsDirectory)Uno.Common.targets" />

Expand All @@ -28,5 +24,8 @@ Copyright (C) Uno Platform Inc. All rights reserved.

<!-- Microsoft.NET.Sdk should be loaded last. This ensures our targets are evaluated before all others. -->
<Import Sdk="$(_DefaultMicrosoftNETSdk)" Project="Sdk.targets" />

<!-- Targets and props to be executed last after the default .NET SDK has been imported -->
<Import Project="$(_UnoSdkTargetsDirectory)Uno.Sdk.After.targets" />

</Project>
39 changes: 39 additions & 0 deletions src/Uno.Sdk/targets/Uno.Sdk.After.targets
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,43 @@
<Manifest Remove="@(_IgnorePlatformFiles)" />
<AppxManifest Remove="@(_IgnorePlatformFiles)" />
</ItemGroup>

<!--
Adjust the first target framework to be browserwasm, if the current debugging
target is browserwasm. This portion is linked to the TryReloadWebAssemblyTargetAsync in
the Uno.UI.RemoteControl.VS project.
This is required by a WebAssembly support issue in VS, where both Publishing/Debugging and other
targets hot reload is not supported. See https://aka.platform.uno/singleproject-vs-wasm-reload.
-->
<PropertyGroup Condition=" '$(BuildingInsideVisualStudio)' == 'true' ">
<_UnoTargetFrameworkCount>$(TargetFrameworks.Split(';', System.StringSplitOptions.RemoveEmptyEntries).Length)</_UnoTargetFrameworkCount>
<_UnoFirstOriginalTargetFramework>$(TargetFrameworks.Split(';', System.StringSplitOptions.RemoveEmptyEntries)[0])</_UnoFirstOriginalTargetFramework>
</PropertyGroup>

<PropertyGroup
Condition="
$([MSBuild]::GetTargetPlatformIdentifier($(ActiveDebugFramework))) == 'browserwasm'
AND !$(TargetFrameworks.StartsWith($(ActiveDebugFramework)))
AND '$(BuildingInsideVisualStudio)' == 'true'">

<_UnoTargetFrameworksWasmFiltered>$(TargetFrameworks.Replace($(ActiveDebugFramework),''))</_UnoTargetFrameworksWasmFiltered>

<TargetFrameworks>$([MSBuild]::Unescape('$(ActiveDebugFramework);$(_UnoTargetFrameworksWasmFiltered)'))</TargetFrameworks>
</PropertyGroup>

<Target Name="_UnoVSWarnBrowserNotFirst"
BeforeTargets="_SetBuildInnerTarget;_ComputeTargetFrameworkItems"
Condition="
'$(UnoDisableVSWarnBrowserNotFirst)' != 'true'
AND '$(BuildingInsideVisualStudio)' == 'true'
AND $([MSBuild]::GetTargetPlatformIdentifier($(_UnoFirstOriginalTargetFramework))) == 'browserwasm'">

<Warning Code="UNOB0010"
Text="The browserwasm TargetFramework must not be placed first in the TargetFrameworks property in order for HotReload to work properly. (See https://aka.platform.uno/UNOB0010)" />
</Target>

<!-- Include any additional targets that packages defined by other packages -->
<Import Project="$(AfterUnoSdkTargets)" Condition="'$(AfterUnoSdkTargets)' != ''"/>

</Project>
223 changes: 165 additions & 58 deletions src/Uno.UI.RemoteControl.VS/DebuggerHelper/ProfilesObserver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,77 +20,175 @@
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

internal class ProfilesObserver : IDisposable
{
private readonly AsyncPackage _asyncPackage;
private readonly Action<string> _debugLog;
private readonly DTE _dte;
private readonly Func<string?, string, Task> _onDebugFrameworkChanged;
private readonly Func<string?, string, Task> _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;

public string? CurrentActiveDebugFramework
=> _currentActiveDebugFramework;

public ProfilesObserver(AsyncPackage asyncPackage, EnvDTE.DTE dte, Func<string?, string, Task> onDebugFrameworkChanged, Func<string?, string, Task> onDebugProfileChanged)
public ProfilesObserver(
AsyncPackage asyncPackage
, EnvDTE.DTE dte
, Func<string?, string, Task> onDebugFrameworkChanged
, Func<string?, string, Task> onDebugProfileChanged
, Action<string> 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<IProjectVersionedValue<Tuple<IProjectSubscriptionUpdate, IProjectCapabilitiesSnapshot>>>(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<IProjectVersionedValue<Tuple<IProjectSubscriptionUpdate, IProjectCapabilitiesSnapshot>>>(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<TInput, Task> CaptureAndApplyExecutionContext<TInput>(Func<TInput, Task> function)
Expand All @@ -115,44 +213,55 @@ private static Func<TInput, Task> CaptureAndApplyExecutionContext<TInput>(Func<T

private async Task ProjectRuleBlock_ChangedAsync(IProjectVersionedValue<Tuple<IProjectSubscriptionUpdate, IProjectCapabilitiesSnapshot>> 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;
}
}

public async Task<List<string>?> GetActiveTargetFrameworksAsync()
{
_debugLog($"GetActiveTargetFrameworksAsync()");

EnsureActiveDebugFrameworkServices();

if (_getProjectFrameworksAsyncMethod?.Invoke(_activeDebugFrameworkServices, []) is Task<List<string>?> listTask)
if (_projectFrameworkServices?.GetProjectFrameworksAsyncMethod?.Invoke(_projectFrameworkServices.ActiveDebugFrameworkServices, []) is Task<List<string>?> listTask)
{
return await listTask;
}
Expand Down Expand Up @@ -210,27 +319,25 @@ public async Task<ImmutableList<ILaunchProfile>> 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);
}
}
}
Expand Down
Loading

0 comments on commit 6a30fc4

Please sign in to comment.