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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" Version="$(MicrosoftCodeAnalysisPackageVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="$(MicrosoftCodeAnalysisPackageVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="$(MicrosoftCodeAnalysisWorkspacesMSBuildPackageVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost" Version="$(MicrosoftCodeAnalysisWorkspacesMSBuildBuildHostPackageVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.ExternalAccess.HotReload" Version="$(MicrosoftCodeAnalysisExternalAccessHotReloadPackageVersion)" />

<!-- roslyn-sdk dependencies-->
Expand Down
1 change: 1 addition & 0 deletions eng/Version.Details.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ This file should be imported by eng/Versions.props
<MicrosoftCodeAnalysisRazorToolingInternalPackageVersion>10.0.0-preview.26118.105</MicrosoftCodeAnalysisRazorToolingInternalPackageVersion>
<MicrosoftCodeAnalysisWorkspacesCommonPackageVersion>5.5.0-2.26118.105</MicrosoftCodeAnalysisWorkspacesCommonPackageVersion>
<MicrosoftCodeAnalysisWorkspacesMSBuildPackageVersion>5.5.0-2.26118.105</MicrosoftCodeAnalysisWorkspacesMSBuildPackageVersion>
<MicrosoftCodeAnalysisWorkspacesMSBuildBuildHostPackageVersion>5.5.0-2.26118.105</MicrosoftCodeAnalysisWorkspacesMSBuildBuildHostPackageVersion>
<MicrosoftDotNetArcadeSdkPackageVersion>10.0.0-beta.26118.105</MicrosoftDotNetArcadeSdkPackageVersion>
<MicrosoftDotNetBuildTasksInstallersPackageVersion>10.0.0-beta.26118.105</MicrosoftDotNetBuildTasksInstallersPackageVersion>
<MicrosoftDotNetBuildTasksTemplatingPackageVersion>10.0.0-beta.26118.105</MicrosoftDotNetBuildTasksTemplatingPackageVersion>
Expand Down
4 changes: 4 additions & 0 deletions eng/Version.Details.xml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@
<Uri>https://github.com/dotnet/dotnet</Uri>
<Sha>d346a57fc93fb42eb26f9da2aa66bfdeaa3372a5</Sha>
</Dependency>
<Dependency Name="Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost" Version="5.5.0-2.26113.102">
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>46a48b8c1dfce7c35da115308bedd6a5954fd78a</Sha>
</Dependency>
<Dependency Name="Microsoft.Build.NuGetSdkResolver" Version="7.5.0-rc.11905">
<Uri>https://github.com/dotnet/dotnet</Uri>
<Sha>d346a57fc93fb42eb26f9da2aa66bfdeaa3372a5</Sha>
Expand Down
1 change: 1 addition & 0 deletions sdk.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<File Path="test.sh" />
</Folder>
<Folder Name="/src/">
<Project Path="src/Cli/Microsoft.DotNet.FileBasedPrograms/Microsoft.DotNet.FileBasedPrograms.shproj" Id="374c251e-bf99-45b2-a58e-40229ed8aaca" />
<Project Path="src/Microsoft.DotNet.ProjectTools/Microsoft.DotNet.ProjectTools.csproj" />
<Project Path="src/Microsoft.DotNet.TemplateLocator/Microsoft.DotNet.TemplateLocator.csproj" />
<Project Path="src/Microsoft.Net.Sdk.Compilers.Toolset/Microsoft.Net.Sdk.Compilers.Toolset.csproj" />
Expand Down
4 changes: 2 additions & 2 deletions src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

namespace Microsoft.DotNet.HotReload
{
internal sealed class DefaultHotReloadClient(ILogger logger, ILogger agentLogger, string startupHookPath, bool enableStaticAssetUpdates, ClientTransport transport)
internal sealed class DefaultHotReloadClient(ILogger logger, ILogger agentLogger, string startupHookPath, bool handlesStaticAssetUpdates, ClientTransport transport)
: HotReloadClient(logger, agentLogger)
{
private Task<ImmutableArray<string>>? _capabilitiesTask;
Expand Down Expand Up @@ -212,7 +212,7 @@ static ImmutableArray<RuntimeManagedCodeUpdate> ToRuntimeUpdates(IEnumerable<Hot

public override async Task<Task<bool>> ApplyStaticAssetUpdatesAsync(ImmutableArray<HotReloadStaticAssetUpdate> updates, CancellationToken processExitedCancellationToken, CancellationToken cancellationToken)
{
if (!enableStaticAssetUpdates)
if (!handlesStaticAssetUpdates)
{
// The client has no concept of static assets.
return Task.FromResult(true);
Expand Down
72 changes: 51 additions & 21 deletions src/BuiltInTools/HotReloadClient/HotReloadClients.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
Expand All @@ -16,13 +17,25 @@

namespace Microsoft.DotNet.HotReload;

internal sealed class HotReloadClients(ImmutableArray<(HotReloadClient client, string name)> clients, AbstractBrowserRefreshServer? browserRefreshServer) : IDisposable
/// <summary>
/// Facilitates Hot Reload updates across multiple clients/processes.
/// </summary>
/// <param name="clients">
/// Clients that handle managed updates and static asset updates if <paramref name="useRefreshServerToApplyStaticAssets"/> is false.
/// </param>
/// <param name="browserRefreshServer">
/// Browser refresh server used to communicate managed code update status and errors to the browser,
/// and to apply static asset updates if <paramref name="useRefreshServerToApplyStaticAssets"/> is true.
/// </param>
/// <param name="useRefreshServerToApplyStaticAssets">
/// True to use <paramref name="browserRefreshServer"/> to apply static asset updates (if available).
/// False to use the <paramref name="clients"/> to apply static asset updates.
/// </param>
internal sealed class HotReloadClients(
ImmutableArray<(HotReloadClient client, string name)> clients,
AbstractBrowserRefreshServer? browserRefreshServer,
bool useRefreshServerToApplyStaticAssets) : IDisposable
{
public HotReloadClients(HotReloadClient client, AbstractBrowserRefreshServer? browserRefreshServer)
: this([(client, "")], browserRefreshServer)
{
}

/// <summary>
/// Disposes all clients. Can occur unexpectedly whenever the process exits.
/// </summary>
Expand All @@ -34,6 +47,16 @@ public void Dispose()
}
}

/// <summary>
/// True if Hot Reload is implemented via managed agents.
/// The update itself might not be managed code update, it may be a static asset update implemented via a managed agent.
/// </summary>
public bool IsManagedAgentSupported
=> !clients.IsEmpty;

public bool UseRefreshServerToApplyStaticAssets
=> useRefreshServerToApplyStaticAssets;

public AbstractBrowserRefreshServer? BrowserRefreshServer
=> browserRefreshServer;

Expand All @@ -59,18 +82,6 @@ public event Action<int, string> OnRuntimeRudeEdit
}
}

/// <summary>
/// All clients share the same loggers.
/// </summary>
public ILogger ClientLogger
=> clients.First().client.Logger;

/// <summary>
/// All clients share the same loggers.
/// </summary>
public ILogger AgentLogger
=> clients.First().client.AgentLogger;

internal void ConfigureLaunchEnvironment(IDictionary<string, string> environmentBuilder)
{
foreach (var (client, _) in clients)
Expand Down Expand Up @@ -99,6 +110,12 @@ internal async ValueTask WaitForConnectionEstablishedAsync(CancellationToken can
/// <param name="cancellationToken">Cancellation token. The cancellation should trigger on process terminatation.</param>
public async ValueTask<ImmutableArray<string>> GetUpdateCapabilitiesAsync(CancellationToken cancellationToken)
{
if (!IsManagedAgentSupported)
{
// empty capabilities will cause rude edit ENC0097: NotSupportedByRuntime.
return [];
}

if (clients is [var (singleClient, _)])
{
return await singleClient.GetUpdateCapabilitiesAsync(cancellationToken);
Expand All @@ -114,6 +131,9 @@ public async ValueTask<ImmutableArray<string>> GetUpdateCapabilitiesAsync(Cancel
/// <param name="cancellationToken">Cancellation token. The cancellation should trigger on process terminatation.</param>
public async Task<Task> ApplyManagedCodeUpdatesAsync(ImmutableArray<HotReloadManagedCodeUpdate> updates, CancellationToken applyOperationCancellationToken, CancellationToken cancellationToken)
{
// shouldn't be called if there are no clients
Debug.Assert(IsManagedAgentSupported);

// Apply to all processes.
// The module the change is for does not need to be loaded to any of the processes, yet we still consider it successful if the application does not fail.
// In each process we store the deltas for application when/if the module is loaded to the process later.
Expand All @@ -137,6 +157,9 @@ async Task CompleteApplyOperationAsync()
/// <param name="cancellationToken">Cancellation token. The cancellation should trigger on process terminatation.</param>
public async ValueTask InitialUpdatesAppliedAsync(CancellationToken cancellationToken)
{
// shouldn't be called if there are no clients
Debug.Assert(IsManagedAgentSupported);

if (clients is [var (singleClient, _)])
{
await singleClient.InitialUpdatesAppliedAsync(cancellationToken);
Expand All @@ -150,23 +173,26 @@ public async ValueTask InitialUpdatesAppliedAsync(CancellationToken cancellation
/// <param name="cancellationToken">Cancellation token. The cancellation should trigger on process terminatation.</param>
public async Task<Task> ApplyStaticAssetUpdatesAsync(IEnumerable<StaticWebAsset> assets, CancellationToken applyOperationCancellationToken, CancellationToken cancellationToken)
{
if (browserRefreshServer != null)
if (useRefreshServerToApplyStaticAssets)
{
Debug.Assert(browserRefreshServer != null);
return browserRefreshServer.UpdateStaticAssetsAsync(assets.Select(static a => a.RelativeUrl), applyOperationCancellationToken).AsTask();
}

// shouldn't be called if there are no clients
Debug.Assert(IsManagedAgentSupported);

var updates = new List<HotReloadStaticAssetUpdate>();

foreach (var asset in assets)
{
try
{
ClientLogger.LogDebug("Loading asset '{Url}' from '{Path}'.", asset.RelativeUrl, asset.FilePath);
updates.Add(await HotReloadStaticAssetUpdate.CreateAsync(asset, cancellationToken));
}
catch (Exception e) when (e is not OperationCanceledException)
{
ClientLogger.LogError("Failed to read file {FilePath}: {Message}", asset.FilePath, e.Message);
clients.First().client.Logger.LogError("Failed to read file {FilePath}: {Message}", asset.FilePath, e.Message);
continue;
}
}
Expand All @@ -177,6 +203,10 @@ public async Task<Task> ApplyStaticAssetUpdatesAsync(IEnumerable<StaticWebAsset>
/// <param name="cancellationToken">Cancellation token. The cancellation should trigger on process terminatation.</param>
public async ValueTask<Task> ApplyStaticAssetUpdatesAsync(ImmutableArray<HotReloadStaticAssetUpdate> updates, CancellationToken applyOperationCancellationToken, CancellationToken cancellationToken)
{
// shouldn't be called if there are no clients
Debug.Assert(IsManagedAgentSupported);
Debug.Assert(!useRefreshServerToApplyStaticAssets);

var applyTasks = await Task.WhenAll(clients.Select(c => c.client.ApplyStaticAssetUpdatesAsync(updates, applyOperationCancellationToken, cancellationToken)));

return Task.WhenAll(applyTasks);
Expand Down
83 changes: 62 additions & 21 deletions src/BuiltInTools/HotReloadClient/Logging/LogEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,78 @@

#nullable enable

using System.Diagnostics;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Logging;

namespace Microsoft.DotNet.HotReload;

internal readonly record struct LogEvent(EventId Id, LogLevel Level, string Message);
internal readonly record struct LogEvent<TArgs>(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)
private static LogEvent<None> Create(LogLevel level, string message)
=> Create<None>(level, message);

private static LogEvent<TArgs> Create<TArgs>(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 SendingUpdateBatch = Create(LogLevel.Debug, "Sending update batch #{0}");
public static readonly LogEvent UpdateBatchCompleted = Create(LogLevel.Debug, "Update batch #{0} completed.");
public static readonly LogEvent UpdateBatchFailed = Create(LogLevel.Debug, "Update batch #{0} failed.");
public static readonly LogEvent UpdateBatchCanceled = Create(LogLevel.Debug, "Update batch #{0} canceled.");
public static readonly LogEvent UpdateBatchFailedWithError = Create(LogLevel.Debug, "Update batch #{0} failed with error: {1}");
public static readonly LogEvent UpdateBatchExceptionStackTrace = Create(LogLevel.Debug, "Update batch #{0} exception stack trace: {1}");
public static readonly LogEvent Capabilities = Create(LogLevel.Debug, "Capabilities: '{0}'.");
public static readonly LogEvent RefreshingBrowser = Create(LogLevel.Debug, "Refreshing browser.");
public static readonly LogEvent ReloadingBrowser = Create(LogLevel.Debug, "Reloading browser.");
public static readonly LogEvent SendingWaitMessage = Create(LogLevel.Debug, "Sending wait message.");
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}.");
public static readonly LogEvent ConnectedToRefreshServer = Create(LogLevel.Debug, "Connected to refresh server.");
public static void Log(this ILogger logger, LogEvent<None> logEvent)
=> logger.Log(logEvent.Level, logEvent.Id, logEvent.Message);

public static void Log<TArgs>(this ILogger logger, LogEvent<TArgs> logEvent, TArgs args)
{
if (logger.IsEnabled(logEvent.Level))
{
logger.Log(logEvent.Level, logEvent.Id, logEvent.Message, GetArgumentValues(args));
}
}

public static void Log<TArg1, TArg2>(this ILogger logger, LogEvent<(TArg1, TArg2)> logEvent, TArg1 arg1, TArg2 arg2)
=> Log(logger, logEvent, (arg1, arg2));

public static void Log<TArg1, TArg2, TArg3>(this ILogger logger, LogEvent<(TArg1, TArg2, TArg3)> logEvent, TArg1 arg1, TArg2 arg2, TArg3 arg3)
=> Log(logger, logEvent, (arg1, arg2, arg3));

public static object?[] GetArgumentValues<TArgs>(TArgs args)
{
if (args?.GetType() == typeof(None))
{
return [];
}

if (args is ITuple tuple)
{
var values = new object?[tuple.Length];
for (int i = 0; i < tuple.Length; i++)
{
values[i] = tuple[i];
}

return values;
}

return [args];
}

public static readonly LogEvent<int> SendingUpdateBatch = Create<int>(LogLevel.Debug, "Sending update batch #{0}");
public static readonly LogEvent<int> UpdateBatchCompleted = Create<int>(LogLevel.Debug, "Update batch #{0} completed.");
public static readonly LogEvent<int> UpdateBatchFailed = Create<int>(LogLevel.Debug, "Update batch #{0} failed.");
public static readonly LogEvent<int> UpdateBatchCanceled = Create<int>(LogLevel.Debug, "Update batch #{0} canceled.");
public static readonly LogEvent<(int, string)> UpdateBatchFailedWithError = Create<(int, string)>(LogLevel.Debug, "Update batch #{0} failed with error: {1}");
public static readonly LogEvent<(int, string)> UpdateBatchExceptionStackTrace = Create<(int, string)>(LogLevel.Debug, "Update batch #{0} exception stack trace: {1}");
public static readonly LogEvent<string> Capabilities = Create<string>(LogLevel.Debug, "Capabilities: '{0}'.");
public static readonly LogEvent<None> RefreshingBrowser = Create(LogLevel.Debug, "Refreshing browser.");
public static readonly LogEvent<None> ReloadingBrowser = Create(LogLevel.Debug, "Reloading browser.");
public static readonly LogEvent<None> SendingWaitMessage = Create(LogLevel.Debug, "Sending wait message.");
public static readonly LogEvent<None> NoBrowserConnected = Create(LogLevel.Debug, "No browser is connected.");
public static readonly LogEvent<None> FailedToReceiveResponseFromConnectedBrowser = Create(LogLevel.Debug, "Failed to receive response from a connected browser.");
public static readonly LogEvent<None> UpdatingDiagnostics = Create(LogLevel.Debug, "Updating diagnostics.");
public static readonly LogEvent<string> SendingStaticAssetUpdateRequest = Create<string>(LogLevel.Debug, "Sending static asset update request to connected browsers: '{0}'.");
public static readonly LogEvent<string> RefreshServerRunningAt = Create<string>(LogLevel.Debug, "Refresh server running at {0}.");
public static readonly LogEvent<None> ConnectedToRefreshServer = Create(LogLevel.Debug, "Connected to refresh server.");
}

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

#nullable enable

namespace Microsoft.DotNet.HotReload;

internal readonly struct StaticAsset(string filePath, string relativeUrl, string assemblyName, bool isApplicationProject)
{
public string FilePath => filePath;
public string RelativeUrl => relativeUrl;
public string AssemblyName => assemblyName;
public bool IsApplicationProject => isApplicationProject;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,4 @@

namespace Microsoft.DotNet.HotReload;

internal readonly struct VoidResult
{
}
internal readonly struct None;
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ internal abstract class AbstractBrowserRefreshServer(string middlewareAssemblyPa
private static readonly JsonSerializerOptions s_jsonSerializerOptions = new(JsonSerializerDefaults.Web);

private readonly List<BrowserConnection> _activeConnections = [];
private readonly TaskCompletionSource<VoidResult> _browserConnected = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly TaskCompletionSource<None> _browserConnected = new(TaskCreationOptions.RunContinuationsAsynchronously);

private readonly SharedSecretProvider _sharedSecretProvider = new();

Expand Down Expand Up @@ -241,7 +241,7 @@ public ValueTask SendWaitMessageAsync(CancellationToken cancellationToken)

private async ValueTask SendAsync(ReadOnlyMemory<byte> messageBytes, CancellationToken cancellationToken)
{
await SendAndReceiveAsync<ReadOnlyMemory<byte>, VoidResult>(request: _ => messageBytes, response: null, cancellationToken);
await SendAndReceiveAsync<ReadOnlyMemory<byte>, None>(request: _ => messageBytes, response: null, cancellationToken);
}

public async ValueTask<TResult?> SendAndReceiveAsync<TRequest, TResult>(
Expand Down
2 changes: 1 addition & 1 deletion src/BuiltInTools/HotReloadClient/Web/BrowserConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ namespace Microsoft.DotNet.HotReload;
public ILogger ServerLogger { get; }
public ILogger AgentLogger { get; }

public readonly TaskCompletionSource<VoidResult> Disconnected = new(TaskCreationOptions.RunContinuationsAsynchronously);
public readonly TaskCompletionSource<None> Disconnected = new(TaskCreationOptions.RunContinuationsAsynchronously);

public BrowserConnection(WebSocket clientSocket, string? sharedSecret, ILoggerFactory loggerFactory)
{
Expand Down
Loading
Loading