Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<T> because it overrides Equals
Expand Down
7 changes: 7 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<MicrosoftDotNetHotReloadPackageVersion>10.0.100-rc.2.25466.104</MicrosoftDotNetHotReloadPackageVersion>
</PropertyGroup>

<!--
Expand All @@ -30,6 +31,12 @@
<PackageVersion Include="Nerdbank.Streams" Version="2.12.87" />
<PackageVersion Include="System.IO.Pipelines" Version="9.0.0" />
<PackageVersion Include="StreamJsonRpc" Version="2.23.32-alpha" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageVersion Include="Microsoft.DotNet.HotReload.Agent.Data" Version="$(MicrosoftDotNetHotReloadPackageVersion)"/>
<PackageVersion Include="Microsoft.DotNet.HotReload.Agent.PipeRpc" Version="$(MicrosoftDotNetHotReloadPackageVersion)"/>
<PackageVersion Include="Microsoft.DotNet.HotReload.Agent.Host" Version="$(MicrosoftDotNetHotReloadPackageVersion)"/>
<PackageVersion Include="Microsoft.DotNet.HotReload.Client" Version="$(MicrosoftDotNetHotReloadPackageVersion)"/>
<PackageVersion Include="Microsoft.DotNet.HotReload.Web.Middleware" Version="$(MicrosoftDotNetHotReloadPackageVersion)"/>

<!-- VS SDK -->
<!-- https://dev.azure.com/azure-public/vside/_artifacts/feed/vssdk -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<PackageReference Include="Microsoft.VisualStudio.ProjectSystem.VS" />
<PackageReference Include="IsExternalInit" PrivateAssets="all" />
<PackageReference Include="Microsoft.VisualStudio.Debugger.Interop.18.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ public Task<IReadOnlyList<IDebugLaunchSettings>> 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()));
}
}
Original file line number Diff line number Diff line change
@@ -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<SVsShellDebugger, IVsDebugger> debugger) : IHotReloadDebugStateProvider
{
public async ValueTask<bool> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,10 @@ async ValueTask<bool> TryCreatePendingSessionInternalAsync()
{
if (await ProjectSupportsHotReloadAsync())
{
var projectName = _unconfiguredProject.GetProjectName();

if (await ProjectSupportsStartupHooksAsync())
{
string name = Path.GetFileNameWithoutExtension(_unconfiguredProject.FullPath);
HotReloadSessionState hotReloadSessionState = new((HotReloadSessionState sessionState) =>
{
int count;
Expand All @@ -87,7 +88,7 @@ async ValueTask<bool> TryCreatePendingSessionInternalAsync()
}, _threadingService);

IProjectHotReloadSession projectHotReloadSession = _hotReloadAgent.CreateHotReloadSession(
name: name,
name: projectName,
id: _nextUniqueId++,
callback: hotReloadSessionState,
launchProfile: launchProfile,
Expand All @@ -105,7 +106,6 @@ async ValueTask<bool> 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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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<WebServerHost> 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<string> 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}/";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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!
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!
Original file line number Diff line number Diff line change
@@ -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!
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.DotNet.HotReload.Agent.Data" PrivateAssets="all" />
<PackageReference Include="Microsoft.DotNet.HotReload.Agent.PipeRpc" PrivateAssets="all" />
<PackageReference Include="Microsoft.DotNet.HotReload.Agent.Host" PrivateAssets="all" ExcludeAssets="all" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.DotNet.HotReload.Client" PrivateAssets="all" />
<PackageReference Include="Microsoft.DotNet.HotReload.Web.Middleware" PrivateAssets="all" ExcludeAssets="all" GeneratePathProperty="true" />

<!-- Needed to break assembly conflict on StreamJsonRpc between microsoft.visualstudio.projectsystem.query and microsoft.visualstudio.languageservices -->
<PackageReference Include="Microsoft.VisualStudio.RpcContracts" />
<!-- Path property: PkgMicrosoft_CodeAnalysis_Common -->
Expand All @@ -64,6 +71,31 @@
<PackageReference Include="Microsoft.VisualStudio.Internal.MicroBuild.NpmPack" PrivateAssets="all" />
</ItemGroup>

<!--
Assemblies that facilitate Hot Reload within the application process.
When updating TFMs here also update ProjectHotReloadSession.GetStartupHookPath and VisualStudioBrowserRefreshServer.MiddlewareTargetFramework.
-->
<ItemGroup>
<Content Include="$(PkgMicrosoft_DotNet_HotReload_Web_Middleware)\lib\net6.0\Microsoft.AspNetCore.Watch.BrowserRefresh.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Visible>false</Visible>
<TargetPath>HotReload\net6.0\Microsoft.AspNetCore.Watch.BrowserRefresh.dll</TargetPath>
<Pack>false</Pack>
</Content>
<Content Include="$(PkgMicrosoft_DotNet_HotReload_Agent_Host)\lib\net6.0\Microsoft.Extensions.DotNetDeltaApplier.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Visible>false</Visible>
<TargetPath>HotReload\net6.0\Microsoft.Extensions.DotNetDeltaApplier.dll</TargetPath>
<Pack>false</Pack>
</Content>
<Content Include="$(PkgMicrosoft_DotNet_HotReload_Agent_Host)\lib\net10.0\Microsoft.Extensions.DotNetDeltaApplier.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Visible>false</Visible>
<TargetPath>HotReload\net10.0\Microsoft.Extensions.DotNetDeltaApplier.dll</TargetPath>
<Pack>false</Pack>
</Content>
</ItemGroup>

<!-- Dependencies -->
<ItemGroup>
<!-- Analyzer Reference -->
Expand Down Expand Up @@ -714,4 +746,10 @@
</ItemGroup>
</Target>

<ItemGroup>
<Compile Update="@(Compile)">
<Link Condition="'%(NuGetPackageId)' != ''">External\%(NuGetPackageId)\%(Link)</Link>
</Compile>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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<string, string> 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<string> relativeUrls, CancellationToken cancellationToken)
=> Server.UpdateStaticAssetsAsync(relativeUrls, cancellationToken);

internal abstract AbstractBrowserRefreshServer Server { get; }
}
Original file line number Diff line number Diff line change
@@ -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; }
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Allows <see cref="IProjectHotReloadSessionCallback"/> to specify whether to suppresses application of deltas
/// as a workaround for https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2570151
/// </summary>
public interface ISuppressDeltaApplication
{
bool SuppressDeltaApplication { get; }
}
Loading