diff --git a/.editorconfig b/.editorconfig index ab7b4daf9fc..2ed1c6ef1ba 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 2f5b5404ffa..f68601c2502 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/Microsoft.VisualStudio.ProjectSystem.Managed.VS.csproj b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/Microsoft.VisualStudio.ProjectSystem.Managed.VS.csproj index edba9e4f44a..edb2843c7b1 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 65bfcf089fd..b559da7301a 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 00000000000..6d633cce05c --- /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/ProjectHotReloadSessionManager.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/HotReload/ProjectHotReloadSessionManager.cs index a9f01259f05..a9280999153 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/ProjectSystem/VS/HotReload/VisualStudioBrowserRefreshServer.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/HotReload/VisualStudioBrowserRefreshServer.cs new file mode 100644 index 00000000000..d4ee1abdc4c --- /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($"http://localhost:{port}/{projectName}/"); + if (sslPort >= 0) + { + httpListener.Prefixes.Add($"https://localhost:{sslPort}/{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 b7384df462f..61b744b3b5d 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 00000000000..cc5bb729e9b --- /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 73a6d5a4cad..f6f7e803ba5 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/PublicAPI.Shipped.txt +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/PublicAPI.Shipped.txt @@ -427,4 +427,8 @@ 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 +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 bbdc44fa2ab..e69de29bb2d 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 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 f119b1a6fc6..40485cddcb2 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,31 @@ + + + + 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 +746,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 00000000000..8eeb4502727 --- /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 00000000000..72afc0b22d7 --- /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 00000000000..1d0abc73d17 --- /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 00000000000..e9222cea7c0 --- /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) + { + _ = 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 00000000000..f8ee1a7a414 --- /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 00000000000..14dd577853f --- /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 00000000000..ea87322e897 --- /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 00000000000..60d3bca8399 --- /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 640a18312d5..85e28edc34b 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 898b0c6c6c1..73ca8d1bc37 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 24e41ff6415..2563e74629d 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 abb3c33ed03..c445b72b703 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 ba08727496e..e2b0199e546 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 9ed80f41f09..9e18d13ac1c 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.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/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/IManagedDeltaApplierCreatorFactory.cs b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/IManagedDeltaApplierCreatorFactory.cs deleted file mode 100644 index 49378651859..00000000000 --- 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 818c5cd2481..9e9400b91a3 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") diff --git a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Setup/PackageContentTests.cs b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Setup/PackageContentTests.cs index acbcaed9294..f352fe2f6bd 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 7675fbda243..9a397dae821 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",