From 1131fa39b37213f56a98cbe4ffe7febf8eeeb339 Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Tue, 26 Aug 2025 16:06:53 -0700 Subject: [PATCH 1/5] Include HotReload web middleware and switch to shared implementation of delta appliers --- .editorconfig | 4 +- Directory.Packages.props | 6 + ...sualStudio.ProjectSystem.Managed.VS.csproj | 1 + .../Debug/ErrorProfileDebugTargetsProvider.cs | 4 +- .../HotReload/HotReloadDebugStateProvider.cs | 25 +++ .../VisualStudioBrowserRefreshServer.cs | 104 +++++++++++ ...ateNuGetPackageTopLevelBuildMenuCommand.cs | 2 +- ...isualStudioBrowserRefreshServerAccessor.cs | 26 +++ .../PublicAPI.Shipped.txt | 4 +- ....VisualStudio.ProjectSystem.Managed.csproj | 34 ++++ .../AbstractBrowserRefreshServerAccessor.cs | 38 ++++ ...jectHotReloadSessionWebAssemblyCallback.cs | 8 + .../Contracts/ISuppressDeltaApplication.cs | 12 ++ .../ProjectSystem/HotReload/DeltaApplier.cs | 80 ++++++++ .../HotReload/HotReloadLogger.cs | 48 +++++ .../HotReload/HotReloadLoggerFactory.cs | 29 +++ .../HotReload/IDeltaApplierInternal.cs | 19 ++ .../HotReload/IHotReloadDebugStateProvider.cs | 51 +++++ .../HotReload/ProjectHotReloadAgent.cs | 16 +- .../HotReload/ProjectHotReloadSession.cs | 174 +++++++++++++----- .../ImportedNamespacesValueProvider.cs | 4 +- .../Utilities/ConfiguredProjectExtensions.cs | 6 + .../PublicAPI/net472/PublicAPI.Shipped.txt | 15 +- .../PublicAPI/net9.0/PublicAPI.Shipped.txt | 15 +- .../IManagedDeltaApplierCreatorFactory.cs | 16 -- .../HotReload/ProjectHotReloadSessionTests.cs | 25 ++- 26 files changed, 671 insertions(+), 95 deletions(-) create mode 100644 src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/HotReload/HotReloadDebugStateProvider.cs create mode 100644 src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/HotReload/VisualStudioBrowserRefreshServer.cs create mode 100644 src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/Web/VisualStudioBrowserRefreshServerAccessor.cs create mode 100644 src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/Contracts/AbstractBrowserRefreshServerAccessor.cs create mode 100644 src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/Contracts/IProjectHotReloadSessionWebAssemblyCallback.cs create mode 100644 src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/Contracts/ISuppressDeltaApplication.cs create mode 100644 src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/DeltaApplier.cs create mode 100644 src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/HotReloadLogger.cs create mode 100644 src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/HotReloadLoggerFactory.cs create mode 100644 src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/IDeltaApplierInternal.cs create mode 100644 src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/IHotReloadDebugStateProvider.cs delete mode 100644 tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/IManagedDeltaApplierCreatorFactory.cs diff --git a/.editorconfig b/.editorconfig index ab7b4daf9f..2ed1c6ef1b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -213,7 +213,7 @@ csharp_preserve_single_line_blocks = true csharp_prefer_braces = false:none # Using statements -csharp_using_directive_placement = outside_namespace:error +csharp_using_directive_placement = outside_namespace_ignoring_aliases:error # Modifier settings csharp_prefer_static_local_function = true:warning @@ -263,7 +263,7 @@ dotnet_diagnostic.CA1055.severity = none # Uri return values should not b dotnet_diagnostic.CA1056.severity = none # Uri properties should not be strings dotnet_diagnostic.CA1060.severity = none # Move P/Invokes to NativeMethods class dotnet_diagnostic.CA1062.severity = none # Validate arguments of public methods -dotnet_diagnostic.CA1063.severity = warning # Implement IDisposable Correctly +dotnet_diagnostic.CA1063.severity = none # Implement IDisposable Correctly: https://github.com/dotnet/roslyn-analyzers/issues/4801 dotnet_diagnostic.CA1064.severity = none # Exceptions should be public dotnet_diagnostic.CA1065.severity = none # Do not raise exceptions in unexpected locations dotnet_diagnostic.CA1066.severity = none # Type {0} should implement IEquatable because it overrides Equals diff --git a/Directory.Packages.props b/Directory.Packages.props index 2f5b5404ff..d8922672cb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -30,6 +30,12 @@ + + + + + + diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/Microsoft.VisualStudio.ProjectSystem.Managed.VS.csproj b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/Microsoft.VisualStudio.ProjectSystem.Managed.VS.csproj index edba9e4f44..edb2843c7b 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/Microsoft.VisualStudio.ProjectSystem.Managed.VS.csproj +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/Microsoft.VisualStudio.ProjectSystem.Managed.VS.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/Debug/ErrorProfileDebugTargetsProvider.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/Debug/ErrorProfileDebugTargetsProvider.cs index 65bfcf089f..b559da7301 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/Debug/ErrorProfileDebugTargetsProvider.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/Debug/ErrorProfileDebugTargetsProvider.cs @@ -57,9 +57,9 @@ public Task> QueryDebugTargetsAsync(DebugLau activeProfile.OtherSettings.TryGetValue("ErrorString", out object? objErrorString) && objErrorString is string errorString) { - throw new Exception(string.Format(VSResources.ErrorInProfilesFile2, Path.GetFileNameWithoutExtension(_configuredProject.UnconfiguredProject.FullPath), errorString)); + throw new Exception(string.Format(VSResources.ErrorInProfilesFile2, _configuredProject.GetProjectName(), errorString)); } - throw new Exception(string.Format(VSResources.ErrorInProfilesFile, Path.GetFileNameWithoutExtension(_configuredProject.UnconfiguredProject.FullPath))); + throw new Exception(string.Format(VSResources.ErrorInProfilesFile, _configuredProject.GetProjectName())); } } diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/HotReload/HotReloadDebugStateProvider.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/HotReload/HotReloadDebugStateProvider.cs new file mode 100644 index 0000000000..6d633cce05 --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/HotReload/HotReloadDebugStateProvider.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +using Microsoft.VisualStudio.ProjectSystem.HotReload; +using Microsoft.VisualStudio.Shell.Interop; + +namespace Microsoft.VisualStudio.ProjectSystem.VS.HotReload; + +// TODO: Replace with IDebuggerStateService +// https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2571211 + +[Export(typeof(IHotReloadDebugStateProvider))] +[method: ImportingConstructor] +internal sealed class HotReloadDebugStateProvider( + IProjectThreadingService threadingService, + IVsUIService debugger) : IHotReloadDebugStateProvider +{ + public async ValueTask IsSuspendedAsync(CancellationToken cancellationToken) + { + await threadingService.SwitchToUIThread(cancellationToken); + + var dbgmode = new DBGMODE[1]; + return ErrorHandler.Succeeded(debugger.Value.GetMode(dbgmode)) && + (dbgmode[0] & ~DBGMODE.DBGMODE_EncMask) == DBGMODE.DBGMODE_Break; + } +} diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/HotReload/VisualStudioBrowserRefreshServer.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/HotReload/VisualStudioBrowserRefreshServer.cs new file mode 100644 index 0000000000..76bf83108c --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/HotReload/VisualStudioBrowserRefreshServer.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Net; +using Microsoft.DotNet.HotReload; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Threading; + +namespace Microsoft.VisualStudio.ProjectSystem.HotReload; + +internal sealed class VisualStudioBrowserRefreshServer( + ILogger logger, + ILoggerFactory loggerFactory, + string projectName, + int port, + int sslPort, + string virtualDirectory) + : AbstractBrowserRefreshServer(GetMiddlewareAssemblyPath(), logger, loggerFactory) +{ + private const string MiddlewareTargetFramework = "net6.0"; + + private static string GetMiddlewareAssemblyPath() + => ProjectHotReloadSession.GetInjectedAssemblyPath(MiddlewareTargetFramework, "Microsoft.AspNetCore.Watch.BrowserRefresh"); + + protected override bool SuppressTimeouts + => false; + + // for testing + internal Task? WebSocketListeningTask { get; private set; } + + protected override ValueTask CreateAndStartHostAsync(CancellationToken cancellationToken) + { + var httpListener = CreateListener(projectName, port, sslPort); + WebSocketListeningTask = ListenAsync(cancellationToken); + + return new(new WebServerHost(httpListener, GetWebSocketUrls(projectName, port, sslPort), virtualDirectory)); + + async Task ListenAsync(CancellationToken cancellationToken) + { + try + { + httpListener.Start(); + + while (!cancellationToken.IsCancellationRequested) + { + Logger.LogDebug("Waiting for a browser connection"); + + // wait for incoming request: + var context = await httpListener.GetContextAsync(); + if (!context.Request.IsWebSocketRequest) + { + context.Response.StatusCode = 400; + context.Response.Close(); + continue; + } + + try + { + // Accepting Socket Next request. If the context has a "Sec-WebSocket-Protocol" header it passes back in the AcceptWebSocket + var protocol = context.Request.Headers["Sec-WebSocket-Protocol"]; + var webSocketContext = await context.AcceptWebSocketAsync(subProtocol: protocol).WithCancellation(cancellationToken); + + _ = OnBrowserConnected(webSocketContext.WebSocket, webSocketContext.SecWebSocketProtocols.FirstOrDefault()); + } + catch (Exception e) + { + Logger.LogError("Accepting web socket exception: {Message}", e.Message); + + context.Response.StatusCode = 500; + context.Response.Close(); + } + } + } + catch (OperationCanceledException) + { + // nop + } + catch (Exception e) + { + Logger.LogError("HttpListener exception: {Message}", e.Message); + } + } + } + + private static HttpListener CreateListener(string projectName, int port, int sslPort) + { + var httpListener = new HttpListener(); + + httpListener.Prefixes.Add($"https://localhost:{sslPort}/{projectName}/"); + if (sslPort >= 0) + { + httpListener.Prefixes.Add($"http://localhost:{port}/{projectName}/"); + } + + return httpListener; + } + + private static ImmutableArray GetWebSocketUrls(string projectName, int port, int sslPort) + { + return sslPort >= 0 ? [GetWebSocketUrl(port, isSecure: false), GetWebSocketUrl(sslPort, isSecure: true)] : [GetWebSocketUrl(port, isSecure: false)]; + + string GetWebSocketUrl(int port, bool isSecure) + => $"{(isSecure ? "wss" : "ws")}://localhost:{port}/{projectName}/"; + } +} diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/Input/Commands/GenerateNuGetPackageTopLevelBuildMenuCommand.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/Input/Commands/GenerateNuGetPackageTopLevelBuildMenuCommand.cs index b7384df462..61b744b3b5 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/Input/Commands/GenerateNuGetPackageTopLevelBuildMenuCommand.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/Input/Commands/GenerateNuGetPackageTopLevelBuildMenuCommand.cs @@ -24,5 +24,5 @@ public GenerateNuGetPackageTopLevelBuildMenuCommand( protected override bool ShouldHandle(IProjectTree node) => true; protected override string GetCommandText() - => string.Format(VSResources.PackSelectedProjectCommand, Path.GetFileNameWithoutExtension(Project.FullPath)); + => string.Format(VSResources.PackSelectedProjectCommand, Project.GetProjectName()); } diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/Web/VisualStudioBrowserRefreshServerAccessor.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/Web/VisualStudioBrowserRefreshServerAccessor.cs new file mode 100644 index 0000000000..cc5bb729e9 --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/Web/VisualStudioBrowserRefreshServerAccessor.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +using Microsoft.DotNet.HotReload; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.ProjectSystem.HotReload; +using Microsoft.VisualStudio.ProjectSystem.VS.HotReload; + +namespace Microsoft.VisualStudio.ProjectSystem.VS.Web; + +public sealed class VisualStudioBrowserRefreshServerAccessor( + ILogger logger, + ILoggerFactory loggerFactory, + string projectName, + int port, + int sslPort, + string virtualDirectory) + : AbstractBrowserRefreshServerAccessor +{ + internal override AbstractBrowserRefreshServer Server { get; } = new VisualStudioBrowserRefreshServer( + logger, + loggerFactory, + projectName, + port, + sslPort, + virtualDirectory); +} diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/PublicAPI.Shipped.txt b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/PublicAPI.Shipped.txt index 73a6d5a4ca..a6d814d450 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/PublicAPI.Shipped.txt +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/PublicAPI.Shipped.txt @@ -427,4 +427,6 @@ Microsoft.VisualStudio.ProjectSystem.VS.Query.QueryProjectPropertiesContext.Item Microsoft.VisualStudio.ProjectSystem.VS.Query.QueryProjectPropertiesContext.QueryProjectPropertiesContext(bool isProjectFile, string! file, string? itemType, string? itemName) -> void override Microsoft.VisualStudio.ProjectSystem.VS.Query.QueryProjectPropertiesContext.Equals(object! obj) -> bool override Microsoft.VisualStudio.ProjectSystem.VS.Query.QueryProjectPropertiesContext.GetHashCode() -> int -static readonly Microsoft.VisualStudio.ProjectSystem.VS.Query.QueryProjectPropertiesContext.ProjectFile -> Microsoft.VisualStudio.ProjectSystem.VS.Query.QueryProjectPropertiesContext! \ No newline at end of file +static readonly Microsoft.VisualStudio.ProjectSystem.VS.Query.QueryProjectPropertiesContext.ProjectFile -> Microsoft.VisualStudio.ProjectSystem.VS.Query.QueryProjectPropertiesContext! +Microsoft.VisualStudio.ProjectSystem.VS.Web.VisualStudioBrowserRefreshServerAccessor +Microsoft.VisualStudio.ProjectSystem.VS.Web.VisualStudioBrowserRefreshServerAccessor.VisualStudioBrowserRefreshServerAccessor(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, string! projectName, int port, int sslPort, string! virtualDirectory) -> void \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/Microsoft.VisualStudio.ProjectSystem.Managed.csproj b/src/Microsoft.VisualStudio.ProjectSystem.Managed/Microsoft.VisualStudio.ProjectSystem.Managed.csproj index f119b1a6fc..b19c4e2d29 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/Microsoft.VisualStudio.ProjectSystem.Managed.csproj +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/Microsoft.VisualStudio.ProjectSystem.Managed.csproj @@ -48,6 +48,13 @@ + + + + + + + @@ -64,6 +71,27 @@ + + + PreserveNewest + false + HotReload\net6.0\Microsoft.AspNetCore.Watch.BrowserRefresh.dll + false + + + PreserveNewest + false + HotReload\net6.0\Microsoft.Extensions.DotNetDeltaApplier.dll + false + + + PreserveNewest + false + HotReload\net10.0\Microsoft.Extensions.DotNetDeltaApplier.dll + false + + + @@ -714,4 +742,10 @@ + + + External\%(NuGetPackageId)\%(Link) + + + diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/Contracts/AbstractBrowserRefreshServerAccessor.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/Contracts/AbstractBrowserRefreshServerAccessor.cs new file mode 100644 index 0000000000..8eeb450272 --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/Contracts/AbstractBrowserRefreshServerAccessor.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +using Microsoft.DotNet.HotReload; + +namespace Microsoft.VisualStudio.ProjectSystem.VS.HotReload; + +public abstract class AbstractBrowserRefreshServerAccessor : IDisposable +{ + private protected AbstractBrowserRefreshServerAccessor() + { + } + + public void Dispose() + => Server.Dispose(); + + public ValueTask StartServerAsync(CancellationToken cancellationToken) + => Server.StartAsync(cancellationToken); + + public void ConfigureLaunchEnvironment(IDictionary builder, bool enableHotReload) + => Server.ConfigureLaunchEnvironment(builder, enableHotReload); + + public ValueTask RefreshBrowserAsync(CancellationToken cancellationToken) + => Server.RefreshBrowserAsync(cancellationToken); + + public ValueTask SendPingMessageAsync(CancellationToken cancellationToken) + => Server.SendPingMessageAsync(cancellationToken); + + public ValueTask SendReloadMessageAsync(CancellationToken cancellationToken) + => Server.SendReloadMessageAsync(cancellationToken); + + public ValueTask SendWaitMessageAsync(CancellationToken cancellationToken) + => Server.SendWaitMessageAsync(cancellationToken); + + public ValueTask UpdateStaticAssetsAsync(IEnumerable relativeUrls, CancellationToken cancellationToken) + => Server.UpdateStaticAssetsAsync(relativeUrls, cancellationToken); + + internal abstract AbstractBrowserRefreshServer Server { get; } +} diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/Contracts/IProjectHotReloadSessionWebAssemblyCallback.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/Contracts/IProjectHotReloadSessionWebAssemblyCallback.cs new file mode 100644 index 0000000000..72afc0b22d --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/Contracts/IProjectHotReloadSessionWebAssemblyCallback.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Microsoft.VisualStudio.ProjectSystem.VS.HotReload; + +public interface IProjectHotReloadSessionWebAssemblyCallback : IProjectHotReloadSessionCallback +{ + AbstractBrowserRefreshServerAccessor BrowserRefreshServerAccessor { get; } +} diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/Contracts/ISuppressDeltaApplication.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/Contracts/ISuppressDeltaApplication.cs new file mode 100644 index 0000000000..1d0abc73d1 --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/Contracts/ISuppressDeltaApplication.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Microsoft.VisualStudio.ProjectSystem.VS.HotReload; + +/// +/// Allows to specify whether to suppresses application of deltas +/// as a workaround for https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2570151 +/// +public interface ISuppressDeltaApplication +{ + bool SuppressDeltaApplication { get; } +} diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/DeltaApplier.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/DeltaApplier.cs new file mode 100644 index 0000000000..ecb104c16f --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/DeltaApplier.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +using Microsoft.DotNet.HotReload; +using Microsoft.VisualStudio.Debugger.Contracts.HotReload; +using Microsoft.VisualStudio.HotReload.Components.DeltaApplier; + +namespace Microsoft.VisualStudio.ProjectSystem.HotReload; + +internal sealed class DeltaApplier(HotReloadClient client, IHotReloadDebugStateProvider debugStateProvider, bool suppressDeltaApplication) : IDeltaApplierInternal, IStaticAssetApplier +{ + public void Dispose() + { + client.Dispose(); + } + + public ValueTask ApplyProcessEnvironmentVariablesAsync(IDictionary envVars, CancellationToken cancellationToken) + { + client.ConfigureLaunchEnvironment(envVars); +#if DEBUG + envVars[AgentEnvironmentVariables.HotReloadDeltaClientLogMessages] = "[Agent] "; +#endif + return new(true); + } + + public ValueTask InitiateConnectionAsync(CancellationToken cancellationToken) + { + client.InitiateConnection(cancellationToken); + return new(); + } + + public async ValueTask> GetCapabilitiesAsync(CancellationToken cancellationToken) + => await client.GetUpdateCapabilitiesAsync(cancellationToken); + + public async ValueTask InitializeApplicationAsync(CancellationToken cancellationToken) + { + var _ = await client.GetUpdateCapabilitiesAsync(cancellationToken); + + // TODO: apply initial updates? + // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2571676 + + await client.InitialUpdatesAppliedAsync(cancellationToken); + } + + public async ValueTask ApplyUpdatesAsync(ImmutableArray updates, CancellationToken cancellationToken) + { + var isProcessSuspended = await debugStateProvider.IsSuspendedAsync(cancellationToken); + + var managedCodeUpdates = ImmutableArray.CreateRange(updates, + update => new HotReloadManagedCodeUpdate( + update.Module, + suppressDeltaApplication ? [] : update.MetadataDelta, + suppressDeltaApplication ? [] : update.ILDelta, + suppressDeltaApplication ? [] : update.PdbDelta, + update.UpdatedTypes, + update.RequiredCapabilities)); + + var status = await client.ApplyManagedCodeUpdatesAsync(managedCodeUpdates, isProcessSuspended, cancellationToken); + + return ToResult(status); + } + + public async ValueTask ApplyStaticFileUpdateAsync(string assemblyName, bool isApplicationProject, string relativePath, byte[] contents, CancellationToken cancellationToken) + { + var isProcessSuspended = await debugStateProvider.IsSuspendedAsync(cancellationToken); + + var status = await client.ApplyStaticAssetUpdatesAsync( + [new HotReloadStaticAssetUpdate(assemblyName, relativePath, [.. contents], isApplicationProject)], + isProcessSuspended, + cancellationToken); + + return ToResult(status); + } + + private static ApplyResult ToResult(ApplyStatus status) + => status switch + { + ApplyStatus.AllChangesApplied or ApplyStatus.SomeChangesApplied or ApplyStatus.NoChangesApplied => ApplyResult.Success, + _ => ApplyResult.Failed + }; +} diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/HotReloadLogger.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/HotReloadLogger.cs new file mode 100644 index 0000000000..f8ee1a7a41 --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/HotReloadLogger.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Debugger.Contracts.HotReload; + +namespace Microsoft.VisualStudio.ProjectSystem.HotReload; + +using LogLevel = Extensions.Logging.LogLevel; + +internal sealed class HotReloadLogger(IHotReloadDiagnosticOutputService service, string projectName, string variant, int sessionInstanceId, string categoryName) : ILogger +{ + public bool IsEnabled(LogLevel logLevel) + => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + var message = formatter(state, exception); + + service.WriteLine( + new HotReloadLogMessage( + verbosity: logLevel switch + { + LogLevel.Trace or LogLevel.Debug => HotReloadVerbosity.Diagnostic, + LogLevel.Information => HotReloadVerbosity.Diagnostic, + _ => HotReloadVerbosity.Minimal + }, + message: message, + projectName, + variant: variant, + instanceId: (uint)sessionInstanceId, + errorLevel: logLevel switch + { + LogLevel.Warning => HotReloadDiagnosticErrorLevel.Warning, + LogLevel.Error => HotReloadDiagnosticErrorLevel.Error, + _ => HotReloadDiagnosticErrorLevel.Info, + }, + categoryName), + CancellationToken.None); + + System.Diagnostics.Debug.WriteLine($"{GetPrefix(logLevel)} {message}"); + } + + public string GetPrefix(LogLevel logLevel) + => $"{logLevel}: [{projectName} ({variant}#{sessionInstanceId})]"; + + public IDisposable? BeginScope(TState state) where TState : notnull + => throw new NotImplementedException(); +} diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/HotReloadLoggerFactory.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/HotReloadLoggerFactory.cs new file mode 100644 index 0000000000..14dd577853 --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/HotReloadLoggerFactory.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.VisualStudio.ProjectSystem.HotReload; + +internal sealed class HotReloadLoggerFactory(IHotReloadDiagnosticOutputService service, string projectName, string targetFramework, int sessionInstanceId) : ILoggerFactory +{ + public void Dispose() + { + } + + public string ProjectName + => projectName; + + public string TargetFramework + => targetFramework; + + public ILogger CreateLogger(string categoryName) + => new HotReloadLogger( + service, + projectName, + variant: targetFramework, + sessionInstanceId, + categoryName); + + public void AddProvider(ILoggerProvider provider) + => throw new NotImplementedException(); +} diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/IDeltaApplierInternal.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/IDeltaApplierInternal.cs new file mode 100644 index 0000000000..ea87322e89 --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/IDeltaApplierInternal.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +using Microsoft.VisualStudio.HotReload.Components.DeltaApplier; + +namespace Microsoft.VisualStudio.ProjectSystem.HotReload; + +internal interface IDeltaApplierInternal : IDeltaApplier +{ + /// + /// Initiates connection to the agent in the application. + /// Called before the process is started. + /// + ValueTask InitiateConnectionAsync(CancellationToken cancellationToken); + + /// + /// Initializes the application process after it has started. + /// + ValueTask InitializeApplicationAsync(CancellationToken cancellationToken); +} diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/IHotReloadDebugStateProvider.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/IHotReloadDebugStateProvider.cs new file mode 100644 index 0000000000..60d3bca839 --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/IHotReloadDebugStateProvider.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Microsoft.VisualStudio.ProjectSystem.HotReload; + +internal interface IHotReloadDebugStateProvider +{ + /// + /// True if processes are suspended at a break point. + /// + ValueTask IsSuspendedAsync(CancellationToken cancellationToken); +} + +// TODO: +// IDebuggerStateService is a brokered service but the interface is currently internal. +// We should make it public and implement it in VS Code, then use it here. + +#if TODO +[Export(typeof(IHotReloadDebugStateProvider))] +[method: ImportingConstructor] +internal sealed class HotReloadDebugStateProvider(IServiceBroker serviceBroker) : IHotReloadDebugStateProvider +{ + public async ValueTask IsSuspendedAsync(CancellationToken cancellationToken) + { + var debugStateService = await serviceBroker.GetProxyAsync(VsDebuggerStateServiceDescriptor); + if (debugStateService != null) + { + var mode = await debugStateService.GetShellModeAsync(CancellationToken.None); + + if (debugStateService is IDisposable dispSvc) + { + dispSvc.Dispose(); + } + + return mode == IdeShellMode.Break; + } + + + // Assume not in break mode if no service + return false; + } + + /// + /// Gets the for the BrowserLaunch service. + /// Use the interface for the client proxy for this service. + /// + public static ServiceRpcDescriptor VsDebuggerStateServiceDescriptor { get; } = new ServiceJsonRpcDescriptor( + new ServiceMoniker(VsDebuggerStateService.Moniker, Version.Parse(VsDebuggerStateService.Version)), + ServiceJsonRpcDescriptor.Formatters.MessagePack, + ServiceJsonRpcDescriptor.MessageDelimiters.BigEndianInt32LengthHeader); +} +#endif diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/ProjectHotReloadAgent.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/ProjectHotReloadAgent.cs index 640a18312d..85e28edc34 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/ProjectHotReloadAgent.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/ProjectHotReloadAgent.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. using Microsoft.VisualStudio.Debugger.Contracts.HotReload; -using Microsoft.VisualStudio.HotReload.Components.DeltaApplier; using Microsoft.VisualStudio.ProjectSystem.Debug; using Microsoft.VisualStudio.ProjectSystem.VS.HotReload; @@ -12,7 +11,8 @@ namespace Microsoft.VisualStudio.ProjectSystem.HotReload; internal sealed class ProjectHotReloadAgent( Lazy hotReloadAgentManagerClient, Lazy hotReloadDiagnosticOutputService, - Lazy managedDeltaApplierCreator) : IProjectHotReloadAgent + [Import(AllowDefault = true)] IHotReloadDebugStateProvider? debugStateProvider) // allow default until VS Code is updated: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2571211 + : IProjectHotReloadAgent { public IProjectHotReloadSession CreateHotReloadSession( string name, @@ -27,12 +27,20 @@ public IProjectHotReloadSession CreateHotReloadSession( id, hotReloadAgentManagerClient: hotReloadAgentManagerClient, hotReloadOutputService: hotReloadDiagnosticOutputService, - deltaApplierCreator: managedDeltaApplierCreator, callback: callback, buildManager: configuredProject.GetExportedService(), launchProvider: configuredProject.GetExportedService(), configuredProject: configuredProject, launchProfile: launchProfile, - debugLaunchOptions: debugLaunchOptions); + debugLaunchOptions: debugLaunchOptions, + debugStateProvider ?? DefaultDebugStateProvider.Instance); + } + + private sealed class DefaultDebugStateProvider : IHotReloadDebugStateProvider + { + public static readonly DefaultDebugStateProvider Instance = new(); + + public ValueTask IsSuspendedAsync(CancellationToken cancellationToken) + => new(false); } } diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/ProjectHotReloadSession.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/ProjectHotReloadSession.cs index 898b0c6c6c..73ca8d1bc3 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/ProjectHotReloadSession.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/ProjectHotReloadSession.cs @@ -1,11 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. using System.Diagnostics.CodeAnalysis; +using System.Runtime.Versioning; +using Microsoft.DotNet.HotReload; using Microsoft.VisualStudio.Debugger.Contracts.EditAndContinue; using Microsoft.VisualStudio.Debugger.Contracts.HotReload; using Microsoft.VisualStudio.HotReload.Components.DeltaApplier; using Microsoft.VisualStudio.ProjectSystem.Debug; using Microsoft.VisualStudio.ProjectSystem.VS.HotReload; +using Microsoft.VisualStudio.Threading; namespace Microsoft.VisualStudio.ProjectSystem.HotReload; @@ -17,28 +20,28 @@ internal sealed class ProjectHotReloadSession : IProjectHotReloadSessionInternal private readonly Lazy _hotReloadAgentManagerClient; private readonly ConfiguredProject _configuredProject; private readonly Lazy _hotReloadOutputService; - private readonly Lazy _deltaApplierCreator; private readonly IProjectHotReloadSessionCallback _callback; private readonly IProjectHotReloadBuildManager _buildManager; private readonly ILaunchProfile _launchProfile; private readonly DebugLaunchOptions _debugLaunchOptions; + private readonly IHotReloadDebugStateProvider _debugStateProvider; private readonly IProjectHotReloadLaunchProvider _launchProvider; private bool _sessionActive; - private IDeltaApplier? _deltaApplier; + private IDeltaApplier? _lazyDeltaApplier; public ProjectHotReloadSession( string name, int id, Lazy hotReloadAgentManagerClient, Lazy hotReloadOutputService, - Lazy deltaApplierCreator, IProjectHotReloadSessionCallback callback, IProjectHotReloadBuildManager buildManager, IProjectHotReloadLaunchProvider launchProvider, ConfiguredProject configuredProject, ILaunchProfile launchProfile, - DebugLaunchOptions debugLaunchOptions) + DebugLaunchOptions debugLaunchOptions, + IHotReloadDebugStateProvider debugStateProvider) { Name = name; Id = id; @@ -46,17 +49,38 @@ public ProjectHotReloadSession( _configuredProject = configuredProject; _hotReloadAgentManagerClient = hotReloadAgentManagerClient; _hotReloadOutputService = hotReloadOutputService; - _deltaApplierCreator = deltaApplierCreator; _callback = callback; _launchProfile = launchProfile; _buildManager = buildManager; _launchProvider = launchProvider; _debugLaunchOptions = debugLaunchOptions; + _debugStateProvider = debugStateProvider; } - // IProjectHotReloadSession + /// + /// Returns true and the path to the client agent implementation binary if the application needs the agent to be injected. + /// + private static string GetStartupHookPath(Version applicationTargetFrameworkVersion) + { + var hookTargetFramework = applicationTargetFrameworkVersion.Major >= 10 ? "net10.0" : "net6.0"; + return GetInjectedAssemblyPath(hookTargetFramework, "Microsoft.Extensions.DotNetDeltaApplier"); + } + + internal static string GetInjectedAssemblyPath(string targetFramework, string assemblyName) + => Path.Combine(Path.GetDirectoryName(typeof(DeltaApplier).Assembly.Location)!, "HotReload", targetFramework, assemblyName + ".dll"); - public IDeltaApplier? DeltaApplier => _deltaApplier; + public IDeltaApplier? DeltaApplier + => _lazyDeltaApplier; + + [MemberNotNull(nameof(_lazyDeltaApplier))] + private void RequireActiveSession() + { + if (!_sessionActive) + throw new InvalidOperationException($"Hot Reload session has not started"); + + if (_lazyDeltaApplier is null) + throw new InvalidOperationException(); + } public async Task ApplyChangesAsync(CancellationToken cancellationToken) { @@ -66,11 +90,68 @@ public async Task ApplyChangesAsync(CancellationToken cancellationToken) } } - public async Task ApplyLaunchVariablesAsync(IDictionary envVars, CancellationToken cancellationToken) + private async ValueTask GetOrCreateDeltaApplierAsync(CancellationToken cancellationToken) { - EnsureDeltaApplierForSession(); + var applier = _lazyDeltaApplier; + if (applier is not null) + { + return applier; + } + + // The callback may provide a custom delta applier (e.g. MAUIDeltaApplier) + applier = _callback.GetDeltaApplier(); + if (applier is null) + { + var targetFramework = await _configuredProject.GetProjectPropertyValueAsync(ConfigurationGeneral.TargetFrameworkProperty); + var targetFrameworkMoniker = await _configuredProject.GetProjectPropertyValueAsync(ConfigurationGeneral.TargetFrameworkMonikerProperty); + var targetFrameworkName = new FrameworkName(targetFrameworkMoniker); + + var loggerFactory = new HotReloadLoggerFactory( + _hotReloadOutputService.Value, + projectName: Name, + targetFramework, + sessionInstanceId: Id); - return await _deltaApplier.ApplyProcessEnvironmentVariablesAsync(envVars, cancellationToken); + var clientLogger = loggerFactory.CreateLogger("Project"); + var agentLogger = loggerFactory.CreateLogger("Agent"); + + HotReloadClient client; + if (_callback is IProjectHotReloadSessionWebAssemblyCallback wasmCallback) + { + var hotReloadCapabilitiesStr = await _configuredProject.GetProjectPropertyValueAsync("WebAssemblyHotReloadCapabilities"); + var hotReloadCapabilities = hotReloadCapabilitiesStr.Split([';'], StringSplitOptions.RemoveEmptyEntries).Select(static c => c.Trim()).ToImmutableArray(); + var browserRefreshServer = wasmCallback.BrowserRefreshServerAccessor.Server; + + client = new WebAssemblyHotReloadClient(clientLogger, agentLogger, browserRefreshServer, hotReloadCapabilities, targetFrameworkName.Version, suppressBrowserRequestsForTesting: false); + } + else + { + client = new DefaultHotReloadClient(clientLogger, agentLogger, GetStartupHookPath(targetFrameworkName.Version), enableStaticAssetUpdates: true); + } + + var suppressDeltaApplication = _callback is ISuppressDeltaApplication { SuppressDeltaApplication: true }; + applier = new DeltaApplier(client, _debugStateProvider, suppressDeltaApplication); + } + + if (applier is IDeltaApplierInternal applierInternal) + { + // Have to switch to background thread so that we don't block UI thread reading from the pipe: + await TaskScheduler.Default; + + await applierInternal.InitiateConnectionAsync(cancellationToken); + } + + _lazyDeltaApplier = applier; + return applier; + } + + /// + /// Update environment of the process to be launched. + /// + public async Task ApplyLaunchVariablesAsync(IDictionary envVars, CancellationToken cancellationToken) + { + var applier = await GetOrCreateDeltaApplierAsync(cancellationToken); + return await applier.ApplyProcessEnvironmentVariablesAsync(envVars, cancellationToken); } // TODO: remove @@ -102,50 +183,44 @@ public async Task StartSessionAsync(CancellationToken cancellationToken) } }; - DebugTrace($"start session for project '{_configuredProject.UnconfiguredProject.FullPath}' with TFM '{targetFramework}' and HotReloadRestart {runningProjectInfo.RestartAutomatically}"); + DebugTrace($"Start session for project '{_configuredProject.UnconfiguredProject.FullPath}' with TFM '{targetFramework}' and HotReloadRestart {runningProjectInfo.RestartAutomatically}"); var processInfo = new ManagedEditAndContinueProcessInfo(); await _hotReloadAgentManagerClient.Value.AgentStartedAsync(this, flags, processInfo, runningProjectInfo, cancellationToken); WriteToOutputWindow(Resources.HotReloadStartSession, default); + + if (await GetOrCreateDeltaApplierAsync(cancellationToken) is IDeltaApplierInternal applierInternal) + { + await applierInternal.InitializeApplicationAsync(cancellationToken); + } + _sessionActive = true; - EnsureDeltaApplierForSession(); } public async Task StopSessionAsync(CancellationToken cancellationToken) { - if (_sessionActive) - { - _sessionActive = false; - await _hotReloadAgentManagerClient.Value.AgentTerminatedAsync(this, cancellationToken); + RequireActiveSession(); - WriteToOutputWindow(Resources.HotReloadStopSession, default); - } - } + _sessionActive = false; + _lazyDeltaApplier.Dispose(); + _lazyDeltaApplier = null; - // IManagedHotReloadAgent + await _hotReloadAgentManagerClient.Value.AgentTerminatedAsync(this, cancellationToken); + WriteToOutputWindow(Resources.HotReloadStopSession, default); + } public async ValueTask ApplyUpdatesAsync(ImmutableArray updates, CancellationToken cancellationToken) { - if (!_sessionActive) - { - DebugTrace($"{nameof(ApplyUpdatesAsync)} called but the session is not active."); - return; - } - - if (_deltaApplier is null) - { - DebugTrace($"{nameof(ApplyUpdatesAsync)} called but we have no delta applier."); - return; - } + RequireActiveSession(); try { WriteToOutputWindow(Resources.HotReloadSendingUpdates, cancellationToken, HotReloadVerbosity.Detailed); - ApplyResult result = await _deltaApplier.ApplyUpdatesAsync(updates, cancellationToken); + ApplyResult result = await _lazyDeltaApplier.ApplyUpdatesAsync(updates, cancellationToken); - if (result is ApplyResult.Success or ApplyResult.SuccessRefreshUI) + if (result is ApplyResult.Success) { WriteToOutputWindow(Resources.HotReloadApplyUpdatesSuccessful, cancellationToken, HotReloadVerbosity.Detailed); @@ -155,25 +230,30 @@ public async ValueTask ApplyUpdatesAsync(ImmutableArray } } } - catch (Exception ex) + catch (Exception e) when (LogAndPropagate(e, cancellationToken)) + { + // unreachable + } + } + + private bool LogAndPropagate(Exception e, CancellationToken cancellationToken) + { + if (e is not OperationCanceledException) { WriteToOutputWindow( - string.Format(Resources.HotReloadApplyUpdatesFailure, $"{ex.GetType()}: {ex.Message}"), + string.Format(Resources.HotReloadApplyUpdatesFailure, $"{e.GetType()}: {e.Message}"), cancellationToken, errorLevel: HotReloadDiagnosticErrorLevel.Error); - throw; } + + return false; } - public async ValueTask> GetCapabilitiesAsync(CancellationToken cancellationToken) + public ValueTask> GetCapabilitiesAsync(CancellationToken cancellationToken) { - // Delegate to the delta applier for the session - if (_deltaApplier is not null) - { - return await _deltaApplier.GetCapabilitiesAsync(cancellationToken); - } + RequireActiveSession(); - return []; + return _lazyDeltaApplier.GetCapabilitiesAsync(cancellationToken); } public ValueTask ReportDiagnosticsAsync(ImmutableArray diagnostics, CancellationToken cancellationToken) @@ -245,14 +325,6 @@ private void DebugTrace(string message) CancellationToken.None); } - [MemberNotNull(nameof(_deltaApplier))] - private void EnsureDeltaApplierForSession() - { - _deltaApplier ??= _callback.GetDeltaApplier() ?? _deltaApplierCreator.Value.CreateManagedDeltaApplier(runtimeVersion: "0.0"); // the version is not used, just needs to parse - - Assumes.NotNull(_deltaApplier); - } - public ValueTask GetTargetLocalProcessIdAsync(CancellationToken cancellationToken) { if (_callback is IProjectHotReloadSessionCallback2 callback2) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/ReferencesPage/ImportedNamespacesValueProvider.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/ReferencesPage/ImportedNamespacesValueProvider.cs index 24e41ff641..2563e74629 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/ReferencesPage/ImportedNamespacesValueProvider.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/ReferencesPage/ImportedNamespacesValueProvider.cs @@ -42,7 +42,7 @@ private async Task GetSelectedImportStringAsync() private async Task> GetSelectedImportListAsync() { - string projectName = Path.GetFileNameWithoutExtension(_configuredProject.UnconfiguredProject.FullPath); + string projectName = _configuredProject.GetProjectName(); ImmutableArray<(string Value, bool IsImported)> existingImports = await GetProjectImportsAsync(); @@ -77,7 +77,7 @@ public override async Task OnGetEvaluatedPropertyValueAsync(string prope .Where(pair => bool.TryParse(pair.Value, out bool _)) .ToDictionary(pair => pair.Name, pair => bool.Parse(pair.Value)); - importsToAdd.Remove(Path.GetFileNameWithoutExtension(_configuredProject.UnconfiguredProject.FullPath)); + importsToAdd.Remove(_configuredProject.GetProjectName()); foreach ((string value, bool _) in await GetProjectImportsAsync()) { diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Utilities/ConfiguredProjectExtensions.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Utilities/ConfiguredProjectExtensions.cs index abb3c33ed0..c445b72b70 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Utilities/ConfiguredProjectExtensions.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Utilities/ConfiguredProjectExtensions.cs @@ -6,6 +6,12 @@ namespace Microsoft.VisualStudio.ProjectSystem; internal static class ConfiguredProjectExtensions { + public static string GetProjectName(this ConfiguredProject project) + => GetProjectName(project.UnconfiguredProject); + + public static string GetProjectName(this UnconfiguredProject project) + => Path.GetFileNameWithoutExtension(project.FullPath); + public static async ValueTask GetProjectPropertyValueAsync(this ConfiguredProject configuredProject, string propertyName) { var provider = configuredProject.Services.ProjectPropertiesProvider; diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/PublicAPI/net472/PublicAPI.Shipped.txt b/src/Microsoft.VisualStudio.ProjectSystem.Managed/PublicAPI/net472/PublicAPI.Shipped.txt index ba08727496..e2b0199e54 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/PublicAPI/net472/PublicAPI.Shipped.txt +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/PublicAPI/net472/PublicAPI.Shipped.txt @@ -221,4 +221,17 @@ Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadAgentExtensio static Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadAgentExtensions.CreateHotReloadSession(this Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadAgent! agent, string! id, int variant, Microsoft.VisualStudio.ProjectSystem.ConfiguredProject! configuredProject, Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSessionCallback! callback, Microsoft.VisualStudio.ProjectSystem.Debug.ILaunchProfile! launchProfile, Microsoft.VisualStudio.ProjectSystem.Debug.DebugLaunchOptions debugLaunchOptions) -> Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSession! Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSession.Id.get -> int Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadAgent -Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadAgent.CreateHotReloadSession(string! name, int id, Microsoft.VisualStudio.ProjectSystem.ConfiguredProject! configuredProject, Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSessionCallback! callback, Microsoft.VisualStudio.ProjectSystem.Debug.ILaunchProfile! launchProfile, Microsoft.VisualStudio.ProjectSystem.Debug.DebugLaunchOptions debugLaunchOptions) -> Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSession! \ No newline at end of file +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadAgent.CreateHotReloadSession(string! name, int id, Microsoft.VisualStudio.ProjectSystem.ConfiguredProject! configuredProject, Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSessionCallback! callback, Microsoft.VisualStudio.ProjectSystem.Debug.ILaunchProfile! launchProfile, Microsoft.VisualStudio.ProjectSystem.Debug.DebugLaunchOptions debugLaunchOptions) -> Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSession! +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSessionWebAssemblyCallback +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSessionWebAssemblyCallback.BrowserRefreshServerAccessor.get -> Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor! +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor.ConfigureLaunchEnvironment(System.Collections.Generic.IDictionary! builder, bool enableHotReload) -> void +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor.Dispose() -> void +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor.RefreshBrowserAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor.SendPingMessageAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor.SendReloadMessageAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor.SendWaitMessageAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor.StartServerAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor.UpdateStaticAssetsAsync(System.Collections.Generic.IEnumerable! relativeUrls, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.ISuppressDeltaApplication +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.ISuppressDeltaApplication.SuppressDeltaApplication.get -> bool \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/PublicAPI/net9.0/PublicAPI.Shipped.txt b/src/Microsoft.VisualStudio.ProjectSystem.Managed/PublicAPI/net9.0/PublicAPI.Shipped.txt index 9ed80f41f0..9eb7bb04f3 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/PublicAPI/net9.0/PublicAPI.Shipped.txt +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/PublicAPI/net9.0/PublicAPI.Shipped.txt @@ -215,4 +215,17 @@ Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadAgentExtensio static Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadAgentExtensions.CreateHotReloadSession(this Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadAgent agent, string id, int variant, Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSessionCallback callback, Microsoft.VisualStudio.ProjectSystem.Debug.ILaunchProfile launchProfile, Microsoft.VisualStudio.ProjectSystem.Debug.DebugLaunchOptions debugLaunchOptions) -> Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSession Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSession.Id.get -> int Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadAgent -Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadAgent.CreateHotReloadSession(string name, int id, Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSessionCallback callback, Microsoft.VisualStudio.ProjectSystem.Debug.ILaunchProfile launchProfile, Microsoft.VisualStudio.ProjectSystem.Debug.DebugLaunchOptions debugLaunchOptions) -> Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSession \ No newline at end of file +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadAgent.CreateHotReloadSession(string name, int id, Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSessionCallback callback, Microsoft.VisualStudio.ProjectSystem.Debug.ILaunchProfile launchProfile, Microsoft.VisualStudio.ProjectSystem.Debug.DebugLaunchOptions debugLaunchOptions) -> Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSession +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSessionWebAssemblyCallback +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSessionWebAssemblyCallback.BrowserRefreshServerProvider.get -> Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor.ConfigureLaunchEnvironment(System.Collections.Generic.IDictionary builder, bool enableHotReload) -> void +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor.Dispose() -> void +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor.RefreshBrowserAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor.SendPingMessageAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor.SendReloadMessageAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor.SendWaitMessageAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor.StartServerAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor.UpdateStaticAssetsAsync(System.Collections.Generic.IEnumerable relativeUrls, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.ISuppressDeltaApplication +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.ISuppressDeltaApplication.SuppressDeltaApplication.get -> bool \ No newline at end of file diff --git a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/IManagedDeltaApplierCreatorFactory.cs b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/IManagedDeltaApplierCreatorFactory.cs deleted file mode 100644 index 4937865185..0000000000 --- a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/IManagedDeltaApplierCreatorFactory.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. - -using Microsoft.VisualStudio.HotReload.Components.DeltaApplier; - -namespace Microsoft.VisualStudio.ProjectSystem.VS; -internal static class IManagedDeltaApplierCreatorFactory -{ - public static IManagedDeltaApplierCreator Create() - { - var mock = new Mock(); - - mock.Setup(m => m.CreateManagedDeltaApplier(It.IsAny())) - .Returns(new Mock().Object); - return mock.Object; - } -} diff --git a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/ProjectSystem/HotReload/ProjectHotReloadSessionTests.cs b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/ProjectSystem/HotReload/ProjectHotReloadSessionTests.cs index 818c5cd248..9e9400b91a 100644 --- a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/ProjectSystem/HotReload/ProjectHotReloadSessionTests.cs +++ b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/ProjectSystem/HotReload/ProjectHotReloadSessionTests.cs @@ -325,7 +325,7 @@ public async Task StopSessionAsync_WhenSessionNotActive_DoesNotCallAgentTerminat // Session is not started/active // Act - await session.StopSessionAsync(CancellationToken.None); + await Assert.ThrowsAsync(() => session.StopSessionAsync(CancellationToken.None)); // Assert hotReloadAgentManagerClient.Verify( @@ -381,7 +381,7 @@ public async Task ApplyUpdatesAsync_WhenSessionNotActive_DoesNotCallDeltaApplier var updates = ImmutableArray.Create(); // Act - await session.ApplyUpdatesAsync(updates, CancellationToken.None); + await Assert.ThrowsAsync(() => session.ApplyUpdatesAsync(updates, CancellationToken.None).AsTask()); // Assert deltaApplier.Verify( @@ -555,16 +555,16 @@ private static ProjectHotReloadSession CreateInstance( int id = 0, Lazy? hotReloadAgentManagerClient = null, Lazy? hotReloadOutputService = null, - Lazy? deltaApplierCreator = null, IProjectHotReloadSessionCallback? callback = null, ConfiguredProject? configuredProject = null, ILaunchProfile? launchProfile = null, DebugLaunchOptions debugLaunchOptions = DebugLaunchOptions.NoDebug, IProjectHotReloadBuildManager? buildManager = null, - IProjectHotReloadLaunchProvider? launchProvider = null) + IProjectHotReloadLaunchProvider? launchProvider = null, + IHotReloadDebugStateProvider? debugStateProvider = null) { - hotReloadAgentManagerClient ??= new Lazy(() => Mock.Of()); - hotReloadOutputService ??= new Lazy(() => Mock.Of()); + hotReloadAgentManagerClient ??= new(Mock.Of); + hotReloadOutputService ??= new(Mock.Of); var mockDeltaApplier = new Mock(); mockDeltaApplier.Setup(d => d.ApplyProcessEnvironmentVariablesAsync(It.IsAny>(), It.IsAny())) @@ -574,17 +574,14 @@ private static ProjectHotReloadSession CreateInstance( mockDeltaApplier.Setup(d => d.GetCapabilitiesAsync(It.IsAny())) .ReturnsAsync([]); - var mockDeltaApplierCreator = new Mock(); - mockDeltaApplierCreator.Setup(c => c.CreateManagedDeltaApplier(It.IsAny())) - .Returns(mockDeltaApplier.Object); - - deltaApplierCreator ??= new Lazy(() => mockDeltaApplierCreator.Object); - callback ??= Mock.Of(c => c.GetDeltaApplier() == mockDeltaApplier.Object && c.StopProjectAsync(It.IsAny()) == Task.FromResult(true) && c.OnAfterChangesAppliedAsync(It.IsAny()) == Task.CompletedTask); + debugStateProvider ??= Mock.Of(c => + c.IsSuspendedAsync(It.IsAny()) == new ValueTask(false)); + launchProfile ??= new Mock().Object; buildManager ??= new Mock().Object; launchProvider ??= new Mock().Object; @@ -595,13 +592,13 @@ private static ProjectHotReloadSession CreateInstance( id, hotReloadAgentManagerClient, hotReloadOutputService, - deltaApplierCreator, callback, buildManager, launchProvider, configuredProject, launchProfile, - debugLaunchOptions); + debugLaunchOptions, + debugStateProvider); } private static ConfiguredProject CreateConfiguredProjectWithCommonProperties(string targetFramework = "net6.0", string projectPath = "C:\\Test\\Project.csproj") From 660bafb3d94b1019357c11743c58976da825ea90 Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Tue, 16 Sep 2025 13:23:57 -0700 Subject: [PATCH 2/5] Update package tests --- .../Setup/PackageContentTests.cs | 3 +++ .../Setup/PackageContentTests.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Setup/PackageContentTests.cs b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Setup/PackageContentTests.cs index acbcaed929..f352fe2f6b 100644 --- a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Setup/PackageContentTests.cs +++ b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Setup/PackageContentTests.cs @@ -20,6 +20,9 @@ public void NpmPackage() @"es\Microsoft.VisualStudio.ProjectSystem.Managed.resources.dll", @"exports.json", @"fr\Microsoft.VisualStudio.ProjectSystem.Managed.resources.dll", + @"HotReload\net10.0\Microsoft.Extensions.DotNetDeltaApplier.dll", + @"HotReload\net6.0\Microsoft.AspNetCore.Watch.BrowserRefresh.dll", + @"HotReload\net6.0\Microsoft.Extensions.DotNetDeltaApplier.dll", @"it\Microsoft.VisualStudio.ProjectSystem.Managed.resources.dll", @"ja\Microsoft.VisualStudio.ProjectSystem.Managed.resources.dll", @"ko\Microsoft.VisualStudio.ProjectSystem.Managed.resources.dll", diff --git a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/Setup/PackageContentTests.cs b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/Setup/PackageContentTests.cs index 7675fbda24..9a397dae82 100644 --- a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/Setup/PackageContentTests.cs +++ b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/Setup/PackageContentTests.cs @@ -30,6 +30,9 @@ public void ProjectSystem() @"extension.vsixmanifest", @"fr/Microsoft.VisualStudio.ProjectSystem.Managed.resources.dll", @"fr/Microsoft.VisualStudio.ProjectSystem.Managed.VS.resources.dll", + @"HotReload/net10.0/Microsoft.Extensions.DotNetDeltaApplier.dll", + @"HotReload/net6.0/Microsoft.AspNetCore.Watch.BrowserRefresh.dll", + @"HotReload/net6.0/Microsoft.Extensions.DotNetDeltaApplier.dll", @"it/Microsoft.VisualStudio.ProjectSystem.Managed.resources.dll", @"it/Microsoft.VisualStudio.ProjectSystem.Managed.VS.resources.dll", @"ja/Microsoft.VisualStudio.ProjectSystem.Managed.resources.dll", From 528142e339e1fc4258c24f7ea6197305cf64ea54 Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Tue, 16 Sep 2025 15:01:53 -0700 Subject: [PATCH 3/5] Update to 10.0.100-rc.2.25466.104 --- Directory.Packages.props | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index d8922672cb..5ef3603aad 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -31,11 +31,11 @@ - - - - - + + + + + From 63210bdd688da1a167144fbf67d387ebf12d91af Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Tue, 16 Sep 2025 18:05:09 -0700 Subject: [PATCH 4/5] Feedback --- Directory.Packages.props | 11 ++++++----- .../VS/HotReload/VisualStudioBrowserRefreshServer.cs | 4 ++-- ...icrosoft.VisualStudio.ProjectSystem.Managed.csproj | 4 ++++ .../ProjectSystem/HotReload/DeltaApplier.cs | 2 +- .../PublicAPI/net9.0/PublicAPI.Shipped.txt | 2 +- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5ef3603aad..f68601c250 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,6 +5,7 @@ true + 10.0.100-rc.2.25466.104 diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/HotReload/VisualStudioBrowserRefreshServer.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/HotReload/VisualStudioBrowserRefreshServer.cs index 76bf83108c..d4ee1abdc4 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/HotReload/VisualStudioBrowserRefreshServer.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/HotReload/VisualStudioBrowserRefreshServer.cs @@ -85,10 +85,10 @@ private static HttpListener CreateListener(string projectName, int port, int ssl { var httpListener = new HttpListener(); - httpListener.Prefixes.Add($"https://localhost:{sslPort}/{projectName}/"); + httpListener.Prefixes.Add($"http://localhost:{port}/{projectName}/"); if (sslPort >= 0) { - httpListener.Prefixes.Add($"http://localhost:{port}/{projectName}/"); + httpListener.Prefixes.Add($"https://localhost:{sslPort}/{projectName}/"); } return httpListener; diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/Microsoft.VisualStudio.ProjectSystem.Managed.csproj b/src/Microsoft.VisualStudio.ProjectSystem.Managed/Microsoft.VisualStudio.ProjectSystem.Managed.csproj index b19c4e2d29..40485cddcb 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/Microsoft.VisualStudio.ProjectSystem.Managed.csproj +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/Microsoft.VisualStudio.ProjectSystem.Managed.csproj @@ -71,6 +71,10 @@ + PreserveNewest diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/DeltaApplier.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/DeltaApplier.cs index ecb104c16f..e9222cea7c 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/DeltaApplier.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/HotReload/DeltaApplier.cs @@ -33,7 +33,7 @@ public async ValueTask> GetCapabilitiesAsync(Cancellation public async ValueTask InitializeApplicationAsync(CancellationToken cancellationToken) { - var _ = await client.GetUpdateCapabilitiesAsync(cancellationToken); + _ = await client.GetUpdateCapabilitiesAsync(cancellationToken); // TODO: apply initial updates? // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2571676 diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/PublicAPI/net9.0/PublicAPI.Shipped.txt b/src/Microsoft.VisualStudio.ProjectSystem.Managed/PublicAPI/net9.0/PublicAPI.Shipped.txt index 9eb7bb04f3..9e18d13ac1 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/PublicAPI/net9.0/PublicAPI.Shipped.txt +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/PublicAPI/net9.0/PublicAPI.Shipped.txt @@ -217,7 +217,7 @@ Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSession.Id.ge Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadAgent Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadAgent.CreateHotReloadSession(string name, int id, Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSessionCallback callback, Microsoft.VisualStudio.ProjectSystem.Debug.ILaunchProfile launchProfile, Microsoft.VisualStudio.ProjectSystem.Debug.DebugLaunchOptions debugLaunchOptions) -> Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSession Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSessionWebAssemblyCallback -Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSessionWebAssemblyCallback.BrowserRefreshServerProvider.get -> Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor +Microsoft.VisualStudio.ProjectSystem.VS.HotReload.IProjectHotReloadSessionWebAssemblyCallback.BrowserRefreshServerAccessor.get -> Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor.ConfigureLaunchEnvironment(System.Collections.Generic.IDictionary builder, bool enableHotReload) -> void Microsoft.VisualStudio.ProjectSystem.VS.HotReload.AbstractBrowserRefreshServerAccessor.Dispose() -> void From e11c59eefcfc057a97986430a5ae89fc0a40593a Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Wed, 17 Sep 2025 10:19:58 -0700 Subject: [PATCH 5/5] Merge --- .../VS/HotReload/ProjectHotReloadSessionManager.cs | 6 +++--- .../PublicAPI.Shipped.txt | 4 +++- .../PublicAPI.Unshipped.txt | 2 -- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/HotReload/ProjectHotReloadSessionManager.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/HotReload/ProjectHotReloadSessionManager.cs index a9f01259f0..a928099915 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/HotReload/ProjectHotReloadSessionManager.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/HotReload/ProjectHotReloadSessionManager.cs @@ -68,9 +68,10 @@ async ValueTask TryCreatePendingSessionInternalAsync() { if (await ProjectSupportsHotReloadAsync()) { + var projectName = _unconfiguredProject.GetProjectName(); + if (await ProjectSupportsStartupHooksAsync()) { - string name = Path.GetFileNameWithoutExtension(_unconfiguredProject.FullPath); HotReloadSessionState hotReloadSessionState = new((HotReloadSessionState sessionState) => { int count; @@ -87,7 +88,7 @@ async ValueTask TryCreatePendingSessionInternalAsync() }, _threadingService); IProjectHotReloadSession projectHotReloadSession = _hotReloadAgent.CreateHotReloadSession( - name: name, + name: projectName, id: _nextUniqueId++, callback: hotReloadSessionState, launchProfile: launchProfile, @@ -105,7 +106,6 @@ async ValueTask TryCreatePendingSessionInternalAsync() else { // If startup hooks are not supported then tell the user why Hot Reload isn't available. - string projectName = Path.GetFileNameWithoutExtension(_unconfiguredProject.FullPath); WriteOutputMessage( new HotReloadLogMessage( diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/PublicAPI.Shipped.txt b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/PublicAPI.Shipped.txt index a6d814d450..f6f7e803ba 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/PublicAPI.Shipped.txt +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/PublicAPI.Shipped.txt @@ -429,4 +429,6 @@ override Microsoft.VisualStudio.ProjectSystem.VS.Query.QueryProjectPropertiesCon override Microsoft.VisualStudio.ProjectSystem.VS.Query.QueryProjectPropertiesContext.GetHashCode() -> int static readonly Microsoft.VisualStudio.ProjectSystem.VS.Query.QueryProjectPropertiesContext.ProjectFile -> Microsoft.VisualStudio.ProjectSystem.VS.Query.QueryProjectPropertiesContext! Microsoft.VisualStudio.ProjectSystem.VS.Web.VisualStudioBrowserRefreshServerAccessor -Microsoft.VisualStudio.ProjectSystem.VS.Web.VisualStudioBrowserRefreshServerAccessor.VisualStudioBrowserRefreshServerAccessor(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, string! projectName, int port, int sslPort, string! virtualDirectory) -> void \ No newline at end of file +Microsoft.VisualStudio.ProjectSystem.VS.Web.VisualStudioBrowserRefreshServerAccessor.VisualStudioBrowserRefreshServerAccessor(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, string! projectName, int port, int sslPort, string! virtualDirectory) -> void +Microsoft.VisualStudio.ProjectSystem.VS.Debug.IDebugProfileLaunchTargetsProvider5 +Microsoft.VisualStudio.ProjectSystem.VS.Debug.IDebugProfileLaunchTargetsProvider5.OnAfterLaunchAsync(Microsoft.VisualStudio.ProjectSystem.Debug.DebugLaunchOptions launchOptions, Microsoft.VisualStudio.ProjectSystem.Debug.ILaunchProfile! profile, Microsoft.VisualStudio.ProjectSystem.VS.Debug.IDebugLaunchSettings! debugLaunchSetting, Microsoft.VisualStudio.Debugger.Interop.IVsLaunchedProcess! vsLaunchedProcess, Microsoft.VisualStudio.Shell.Interop.VsDebugTargetProcessInfo processInfo) -> System.Threading.Tasks.Task! \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/PublicAPI.Unshipped.txt index bbdc44fa2a..e69de29bb2 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/PublicAPI.Unshipped.txt @@ -1,2 +0,0 @@ -Microsoft.VisualStudio.ProjectSystem.VS.Debug.IDebugProfileLaunchTargetsProvider5 -Microsoft.VisualStudio.ProjectSystem.VS.Debug.IDebugProfileLaunchTargetsProvider5.OnAfterLaunchAsync(Microsoft.VisualStudio.ProjectSystem.Debug.DebugLaunchOptions launchOptions, Microsoft.VisualStudio.ProjectSystem.Debug.ILaunchProfile! profile, Microsoft.VisualStudio.ProjectSystem.VS.Debug.IDebugLaunchSettings! debugLaunchSetting, Microsoft.VisualStudio.Debugger.Interop.IVsLaunchedProcess! vsLaunchedProcess, Microsoft.VisualStudio.Shell.Interop.VsDebugTargetProcessInfo processInfo) -> System.Threading.Tasks.Task! \ No newline at end of file