Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
739dbe9
Unindent
tmat Aug 25, 2025
24689ac
Minor improvements to browser refresh logging
tmat Aug 25, 2025
ba0fd12
Simplify app model inference, add WebApplicationAppModel.
tmat Aug 25, 2025
3060d4d
Better logging
tmat Aug 25, 2025
508917d
Separate BrowserLauncher and BrowserConnector
tmat Aug 26, 2025
8b9f221
Renames
tmat Aug 26, 2025
170ddb5
Comments
tmat Aug 26, 2025
b5fb8a9
Cancellation
tmat Aug 26, 2025
fcca0fb
BrowserLauncherTests
tmat Aug 26, 2025
ca49ca9
Env vars
tmat Aug 26, 2025
5bc60b5
Fix tests
tmat Aug 27, 2025
bca735b
Move LoggingUtilities
tmat Aug 27, 2025
f40db3a
EnvOptions
tmat Aug 28, 2025
7c0493e
Fix BRS cancellation
tmat Aug 28, 2025
d9536d4
Move BRS from RunningProject to HotReloadClients
tmat Aug 29, 2025
da751fa
Add HotReloadClient.ConfigureLaunchEnvironment
tmat Aug 29, 2025
df90263
Add BrowserRefreshServer abstraction to Client package
tmat Aug 29, 2025
04c9bdf
Move WebAssemblyHotReloadClient to shared package
tmat Aug 29, 2025
2190db8
Cleanup
tmat Aug 29, 2025
1e0e54c
Share Kestrel-based BRS
tmat Sep 2, 2025
5739efc
Simplify
tmat Sep 2, 2025
635467e
Refactoring and tests
tmat Sep 4, 2025
27ebc87
Update assembly version of Microsoft.Bcl.AsyncInterfaces available in…
tmat Sep 6, 2025
04af27e
Pending tasks for suspended WASM processes
tmat Sep 6, 2025
4e970f1
Simplify dispose
tmat Sep 6, 2025
6718b0f
Cleanup
tmat Sep 6, 2025
727929b
Update SBMSBuildSdkResolver dependencies
tmat Sep 6, 2025
65f4b9d
Fix TFMs
tmat Sep 7, 2025
43a6b90
Fix
tmat Sep 7, 2025
50f4a80
Fix env utils
tmat Sep 7, 2025
4ea877b
Move pending update handling to base class
tmat Sep 7, 2025
201c641
Feedback
tmat Sep 8, 2025
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
3 changes: 2 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
<NetCurrent>net10.0</NetCurrent>
<NetToolMinimum Condition="'$(DotNetBuildSourceOnly)' == 'true'">$(NetCurrent)</NetToolMinimum>
<ToolsetTargetFramework>$(SdkTargetFramework)</ToolsetTargetFramework>
<VisualStudioServiceTargetFramework>net8.0</VisualStudioServiceTargetFramework>
<VisualStudioServiceTargetFramework>net9.0</VisualStudioServiceTargetFramework>
<VisualStudioTargetFramework>net472</VisualStudioTargetFramework>

<!-- We used to have scenarios where the MSBuild host (VSMac) had an older .NET, but don't any more. -->
<ResolverTargetFramework>$(SdkTargetFramework)</ResolverTargetFramework>
Expand Down
4 changes: 2 additions & 2 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
<NETStandardLibraryRefPackageVersion>2.1.0</NETStandardLibraryRefPackageVersion>
<!-- These are minimum versions used for netfx-targeted components that run in Visual Studio because in those cases,
Visual Studio is providing those assemblies, and we should work with whichever version it ships. -->
<MicrosoftBclAsyncInterfacesToolsetPackageVersion>8.0.0</MicrosoftBclAsyncInterfacesToolsetPackageVersion>
<MicrosoftBclAsyncInterfacesToolsetPackageVersion>9.0.0</MicrosoftBclAsyncInterfacesToolsetPackageVersion>
<MicrosoftDeploymentDotNetReleasesToolsetPackageVersion>2.0.0-preview.1.24427.4</MicrosoftDeploymentDotNetReleasesToolsetPackageVersion>
<MicrosoftExtensionsLoggingAbstractionsToolsetPackageVersion>9.0.0</MicrosoftExtensionsLoggingAbstractionsToolsetPackageVersion>
<SystemBuffersToolsetPackageVersion>4.5.1</SystemBuffersToolsetPackageVersion>
Expand All @@ -91,7 +91,7 @@
<SystemReflectionMetadataLoadContextToolsetPackageVersion>9.0.0</SystemReflectionMetadataLoadContextToolsetPackageVersion>
<SystemReflectionMetadataToolsetPackageVersion>9.0.0</SystemReflectionMetadataToolsetPackageVersion>
<SystemDiagnosticsDiagnosticSourceToolsetPackageVersion>9.0.0</SystemDiagnosticsDiagnosticSourceToolsetPackageVersion>
<SystemTextJsonToolsetPackageVersion>8.0.5</SystemTextJsonToolsetPackageVersion>
<SystemTextJsonToolsetPackageVersion>9.0.0</SystemTextJsonToolsetPackageVersion>
<SystemThreadingTasksExtensionsToolsetPackageVersion>4.5.4</SystemThreadingTasksExtensionsToolsetPackageVersion>
<SystemResourcesExtensionsToolsetPackageVersion>8.0.0</SystemResourcesExtensionsToolsetPackageVersion>
</PropertyGroup>
Expand Down
1 change: 1 addition & 0 deletions sdk.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@
<Project Path="test/HelixTasks/HelixTasks.csproj" />
<Project Path="test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj" />
<Project Path="test/Microsoft.DotNet.Cli.Utils.Tests/Microsoft.DotNet.Cli.Utils.Tests.csproj" />
<Project Path="test/Microsoft.DotNet.HotReload.Client.Tests/Microsoft.DotNet.HotReload.Client.Tests.csproj" />
<Project Path="test/Microsoft.DotNet.MSBuildSdkResolver.Tests/Microsoft.DotNet.MSBuildSdkResolver.Tests.csproj" />
<Project Path="test/Microsoft.DotNet.PackageInstall.Tests/Microsoft.DotNet.PackageInstall.Tests.csproj" />
<Project Path="test/Microsoft.DotNet.TemplateLocator.Tests/Microsoft.DotNet.TemplateLocator.Tests.csproj" />
Expand Down
6 changes: 3 additions & 3 deletions src/BuiltInTools/AspireService/Models/RunSessionRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ internal class RunSessionRequest

[Required]
[JsonPropertyName("launch_configurations")]
public LaunchConfiguration[] LaunchConfigurations { get; set; } = Array.Empty<LaunchConfiguration>();
public LaunchConfiguration[] LaunchConfigurations { get; set; } = [];

[JsonPropertyName("env")]
public EnvVar[] Environment { get; set; } = Array.Empty<EnvVar>();
public EnvVar[] Environment { get; set; } = [];

[JsonPropertyName("args")]
public string[]? Arguments { get; set; }
Expand All @@ -78,7 +78,7 @@ internal class RunSessionRequest
ProjectPath = projectLaunchConfig.ProjectPath,
Debug = string.Equals(projectLaunchConfig.LaunchMode, DebugLaunchMode, StringComparison.OrdinalIgnoreCase),
Arguments = Arguments,
Environment = Environment.Select(envVar => new KeyValuePair<string, string>(envVar.Name, envVar.Value!)),
Environment = Environment.Select(envVar => new KeyValuePair<string, string>(envVar.Name, envVar.Value ?? "")),
LaunchProfile = projectLaunchConfig.LaunchProfile,
DisableLaunchProfile = projectLaunchConfig.DisableLaunchProfile
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<!--
This assembly may be loaded .NET 6.0+ web server.
When updating the TFM also update minimal supported version in BrowserConnector.cs.
When updating the TFM also update minimal supported version in dotnet-watch.csproj and WebApplicationAppModel.cs.
-->
<TargetFramework>net6.0</TargetFramework>
<StrongNameKeyId>MicrosoftAspNetCore</StrongNameKeyId>
Expand Down
61 changes: 27 additions & 34 deletions src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,15 @@

namespace Microsoft.DotNet.HotReload
{
internal sealed class DefaultHotReloadClient(ILogger logger, ILogger agentLogger, bool enableStaticAssetUpdates) : HotReloadClient(logger, agentLogger)
internal sealed class DefaultHotReloadClient(ILogger logger, ILogger agentLogger, string startupHookPath, bool enableStaticAssetUpdates)
: HotReloadClient(logger, agentLogger)
{
private readonly string _namedPipeName = Guid.NewGuid().ToString("N");

private Task<ImmutableArray<string>>? _capabilitiesTask;
private NamedPipeServerStream? _pipe;
private bool _managedCodeUpdateFailedOrCancelled;

private int _updateBatchId;

/// <summary>
/// Updates that were sent over to the agent while the process has been suspended.
/// </summary>
private readonly object _pendingUpdatesGate = new();
private Task _pendingUpdates = Task.CompletedTask;

public override void Dispose()
{
DisposePipe();
Expand All @@ -46,17 +41,17 @@ private void DisposePipe()
}

// for testing
internal Task PendingUpdates
=> _pendingUpdates;
internal string NamedPipeName
=> _namedPipeName;

public override void InitiateConnection(string namedPipeName, CancellationToken cancellationToken)
public override void InitiateConnection(CancellationToken cancellationToken)
{
#if NET
var options = PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly;
#else
var options = PipeOptions.Asynchronous;
#endif
_pipe = new NamedPipeServerStream(namedPipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, options);
_pipe = new NamedPipeServerStream(_namedPipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, options);

// It is important to establish the connection (WaitForConnectionAsync) before we return,
// otherwise the client wouldn't be able to connect.
Expand All @@ -67,7 +62,7 @@ async Task<ImmutableArray<string>> ConnectAsync()
{
try
{
Logger.LogDebug("Waiting for application to connect to pipe {NamedPipeName}.", namedPipeName);
Logger.LogDebug("Waiting for application to connect to pipe {NamedPipeName}.", _namedPipeName);

await _pipe.WaitForConnectionAsync(cancellationToken);

Expand Down Expand Up @@ -110,6 +105,16 @@ private void RequireReadyForUpdates()
throw new InvalidOperationException("Pipe has been disposed.");
}

public override void ConfigureLaunchEnvironment(IDictionary<string, string> environmentBuilder)
{
environmentBuilder[AgentEnvironmentVariables.DotNetModifiableAssemblies] = "debug";

// HotReload startup hook should be loaded before any other startup hooks:
environmentBuilder.InsertListItem(AgentEnvironmentVariables.DotNetStartupHooks, startupHookPath, Path.PathSeparator);

environmentBuilder[AgentEnvironmentVariables.DotNetWatchHotReloadNamedPipeName] = _namedPipeName;
}

public override Task WaitForConnectionEstablishedAsync(CancellationToken cancellationToken)
=> GetCapabilitiesTask();

Expand Down Expand Up @@ -192,6 +197,8 @@ public async override Task<ApplyStatus> ApplyStaticAssetUpdatesAsync(ImmutableAr
update.IsApplicationProject),
ResponseLoggingLevel);

Logger.LogDebug("Sending static file update request for asset '{Url}'.", update.RelativePath);

var success = await SendAndReceiveUpdateAsync(request, isProcessSuspended, cancellationToken);
if (success)
{
Expand All @@ -206,31 +213,17 @@ public async override Task<ApplyStatus> ApplyStaticAssetUpdatesAsync(ImmutableAr
(appliedUpdateCount < updates.Length) ? ApplyStatus.SomeChangesApplied : ApplyStatus.AllChangesApplied;
}

private async ValueTask<bool> SendAndReceiveUpdateAsync<TRequest>(TRequest request, bool isProcessSuspended, CancellationToken cancellationToken)
private ValueTask<bool> SendAndReceiveUpdateAsync<TRequest>(TRequest request, bool isProcessSuspended, CancellationToken cancellationToken)
where TRequest : IUpdateRequest
{
// Should not be disposed:
Debug.Assert(_pipe != null);

var batchId = _updateBatchId++;

if (!isProcessSuspended)
{
return await SendAndReceiveAsync(batchId, cancellationToken);
}

lock (_pendingUpdatesGate)
{
var previous = _pendingUpdates;

_pendingUpdates = Task.Run(async () =>
{
await previous;
await SendAndReceiveAsync(batchId, cancellationToken);
}, cancellationToken);
}

return true;
return SendAndReceiveUpdateAsync(
send: SendAndReceiveAsync,
isProcessSuspended,
suspendedResult: true,
cancellationToken);

async ValueTask<bool> SendAndReceiveAsync(int batchId, CancellationToken cancellationToken)
{
Expand Down
46 changes: 45 additions & 1 deletion src/BuiltInTools/HotReloadClient/HotReloadClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,24 @@ internal abstract class HotReloadClient(ILogger logger, ILogger agentLogger) : I
public readonly ILogger Logger = logger;
public readonly ILogger AgentLogger = agentLogger;

private int _updateBatchId;

/// <summary>
/// Updates that were sent over to the agent while the process has been suspended.
/// </summary>
private readonly object _pendingUpdatesGate = new();
private Task _pendingUpdates = Task.CompletedTask;

// for testing
internal Task PendingUpdates
=> _pendingUpdates;

public abstract void ConfigureLaunchEnvironment(IDictionary<string, string> environmentBuilder);

/// <summary>
/// Initiates connection with the agent in the target process.
/// </summary>
public abstract void InitiateConnection(string namedPipeName, CancellationToken cancellationToken);
public abstract void InitiateConnection(CancellationToken cancellationToken);

/// <summary>
/// Waits until the connection with the agent is established.
Expand Down Expand Up @@ -84,4 +98,34 @@ public async Task<IReadOnlyList<HotReloadManagedCodeUpdate>> FilterApplicableUpd

return applicableUpdates;
}

protected async ValueTask<TResult> SendAndReceiveUpdateAsync<TResult>(
Func<int, CancellationToken, ValueTask<TResult>> send,
bool isProcessSuspended,
TResult suspendedResult,
CancellationToken cancellationToken)
where TResult : struct
{
var batchId = _updateBatchId++;

Task previous;
lock (_pendingUpdatesGate)
{
previous = _pendingUpdates;

if (isProcessSuspended)
{
_pendingUpdates = Task.Run(async () =>
{
await previous;
_ = await send(batchId, cancellationToken);
}, cancellationToken);

return suspendedResult;
}
}

await previous;
return await send(batchId, cancellationToken);
}
}
23 changes: 0 additions & 23 deletions src/BuiltInTools/HotReloadClient/LogEvents.cs

This file was deleted.

33 changes: 33 additions & 0 deletions src/BuiltInTools/HotReloadClient/Logging/LogEvents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using Microsoft.Extensions.Logging;

namespace Microsoft.DotNet.HotReload;

internal readonly record struct LogEvent(EventId Id, LogLevel Level, string Message);

internal static class LogEvents
{
// Non-shared event ids start at 0.
private static int s_id = 1000;

private static LogEvent Create(LogLevel level, string message)
=> new(new EventId(s_id++), level, message);

public static void Log(this ILogger logger, LogEvent logEvent, params object[] args)
=> logger.Log(logEvent.Level, logEvent.Id, logEvent.Message, args);

public static readonly LogEvent UpdatesApplied = Create(LogLevel.Debug, "Updates applied: {0} out of {1}.");
public static readonly LogEvent Capabilities = Create(LogLevel.Debug, "Capabilities: '{1}'.");
public static readonly LogEvent HotReloadSucceeded = Create(LogLevel.Information, "Hot reload succeeded.");
public static readonly LogEvent RefreshingBrowser = Create(LogLevel.Debug, "Refreshing browser.");
public static readonly LogEvent ReloadingBrowser = Create(LogLevel.Debug, "Reloading browser.");
public static readonly LogEvent NoBrowserConnected = Create(LogLevel.Debug, "No browser is connected.");
public static readonly LogEvent FailedToReceiveResponseFromConnectedBrowser = Create(LogLevel.Debug, "Failed to receive response from a connected browser.");
public static readonly LogEvent UpdatingDiagnostics = Create(LogLevel.Debug, "Updating diagnostics.");
public static readonly LogEvent SendingStaticAssetUpdateRequest = Create(LogLevel.Debug, "Sending static asset update request to connected browsers: '{0}'.");
public static readonly LogEvent RefreshServerRunningAt = Create(LogLevel.Debug, "Refresh server running at {0}.");
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using Microsoft.Extensions.Logging;

namespace Microsoft.DotNet.Watch;
namespace Microsoft.DotNet.HotReload;

internal static class LoggingUtilities
{
Expand All @@ -14,7 +16,4 @@ public static (string comonentName, string? displayName) ParseCategoryName(strin
=> categoryName.IndexOf('|') is int index && index > 0
? (categoryName[..index], categoryName[(index + 1)..])
: (categoryName, null);

public static string GetPrefix(Emoji emoji)
=> $"dotnet watch {emoji.ToDisplay()} ";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

<PropertyGroup>
<!--
Used in in-proc VS.
Used in in-proc VS and VS Code.
We also need to target $(SdkTargetFramework) to allow tests to run.
-->
<TargetFrameworks>$(SdkTargetFramework);netstandard2.0</TargetFrameworks>
<TargetFrameworks>$(VisualStudioServiceTargetFramework);$(SdkTargetFramework);$(VisualStudioTargetFramework)</TargetFrameworks>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<DebugType>none</DebugType>
<GenerateDependencyFile>false</GenerateDependencyFile>
Expand All @@ -24,22 +25,23 @@
<PackageReference Include="Microsoft.CodeAnalysis.Contracts" PrivateAssets="all" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<!-- netstandard2.0 polyfills
Since this code may be run in Visual Studio / full framework MSBuild, these packages need to use the "toolset" package versions so they
<ItemGroup>
<!--
Since this code may be run in Visual Studio / VS Code / full framework MSBuild, these packages need to use the "toolset" package versions so they
don't depend on higher versions of the packages than are available in those environments.
-->
<PackageReference Include="System.Buffers" VersionOverride="$(SystemBuffersToolsetPackageVersion)" />
<PackageReference Include="System.Collections.Immutable" VersionOverride="$(SystemCollectionsImmutableToolsetPackageVersion)"/>
<PackageReference Include="System.Diagnostics.DiagnosticSource" VersionOverride="$(SystemDiagnosticsDiagnosticSourceToolsetPackageVersion)"/>
<PackageReference Include="System.Collections.Immutable" VersionOverride="$(SystemCollectionsImmutableToolsetPackageVersion)" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" VersionOverride="$(SystemDiagnosticsDiagnosticSourceToolsetPackageVersion)" />
<PackageReference Include="System.Memory" VersionOverride="$(SystemMemoryToolsetPackageVersion)" />
<PackageReference Include="System.Threading.Tasks.Extensions" VersionOverride="$(SystemThreadingTasksExtensionsToolsetPackageVersion)" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" VersionOverride="$(MicrosoftBclAsyncInterfacesToolsetPackageVersion)"/>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" VersionOverride="$(MicrosoftExtensionsLoggingAbstractionsToolsetPackageVersion)"/>
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonToolsetPackageVersion)" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" VersionOverride="$(MicrosoftBclAsyncInterfacesToolsetPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" VersionOverride="$(MicrosoftExtensionsLoggingAbstractionsToolsetPackageVersion)" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != 'netstandard2.0'">
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<!-- Make sure the shared source files do not require any global usings -->
Expand Down
Loading