diff --git a/Directory.Packages.props b/Directory.Packages.props
index a44ed722182f..3e9b3bd272de 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -37,6 +37,7 @@
+
diff --git a/eng/Version.Details.props b/eng/Version.Details.props
index fc316bc18258..ba78063ca632 100644
--- a/eng/Version.Details.props
+++ b/eng/Version.Details.props
@@ -22,6 +22,7 @@ This file should be imported by eng/Versions.props
10.0.0-preview.26118.105
5.5.0-2.26118.105
5.5.0-2.26118.105
+ 5.5.0-2.26118.105
10.0.0-beta.26118.105
10.0.0-beta.26118.105
10.0.0-beta.26118.105
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index 762e1e184c32..91b93b8c46e6 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -124,6 +124,10 @@
https://github.com/dotnet/dotnet
d346a57fc93fb42eb26f9da2aa66bfdeaa3372a5
+
+ https://github.com/dotnet/roslyn
+ 46a48b8c1dfce7c35da115308bedd6a5954fd78a
+
https://github.com/dotnet/dotnet
d346a57fc93fb42eb26f9da2aa66bfdeaa3372a5
diff --git a/sdk.slnx b/sdk.slnx
index b5f5c625b0c2..6ed9cd703fca 100644
--- a/sdk.slnx
+++ b/sdk.slnx
@@ -23,6 +23,7 @@
+
diff --git a/src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs b/src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs
index 00dfa8f61b14..9ca5fb9cc271 100644
--- a/src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs
+++ b/src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs
@@ -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>? _capabilitiesTask;
@@ -212,7 +212,7 @@ static ImmutableArray ToRuntimeUpdates(IEnumerable> ApplyStaticAssetUpdatesAsync(ImmutableArray updates, CancellationToken processExitedCancellationToken, CancellationToken cancellationToken)
{
- if (!enableStaticAssetUpdates)
+ if (!handlesStaticAssetUpdates)
{
// The client has no concept of static assets.
return Task.FromResult(true);
diff --git a/src/BuiltInTools/HotReloadClient/HotReloadClients.cs b/src/BuiltInTools/HotReloadClient/HotReloadClients.cs
index 1b02eac9de48..1400a43c7e09 100644
--- a/src/BuiltInTools/HotReloadClient/HotReloadClients.cs
+++ b/src/BuiltInTools/HotReloadClient/HotReloadClients.cs
@@ -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;
@@ -16,13 +17,25 @@
namespace Microsoft.DotNet.HotReload;
-internal sealed class HotReloadClients(ImmutableArray<(HotReloadClient client, string name)> clients, AbstractBrowserRefreshServer? browserRefreshServer) : IDisposable
+///
+/// Facilitates Hot Reload updates across multiple clients/processes.
+///
+///
+/// Clients that handle managed updates and static asset updates if is false.
+///
+///
+/// Browser refresh server used to communicate managed code update status and errors to the browser,
+/// and to apply static asset updates if is true.
+///
+///
+/// True to use to apply static asset updates (if available).
+/// False to use the to apply static asset updates.
+///
+internal sealed class HotReloadClients(
+ ImmutableArray<(HotReloadClient client, string name)> clients,
+ AbstractBrowserRefreshServer? browserRefreshServer,
+ bool useRefreshServerToApplyStaticAssets) : IDisposable
{
- public HotReloadClients(HotReloadClient client, AbstractBrowserRefreshServer? browserRefreshServer)
- : this([(client, "")], browserRefreshServer)
- {
- }
-
///
/// Disposes all clients. Can occur unexpectedly whenever the process exits.
///
@@ -34,6 +47,16 @@ public void Dispose()
}
}
+ ///
+ /// 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.
+ ///
+ public bool IsManagedAgentSupported
+ => !clients.IsEmpty;
+
+ public bool UseRefreshServerToApplyStaticAssets
+ => useRefreshServerToApplyStaticAssets;
+
public AbstractBrowserRefreshServer? BrowserRefreshServer
=> browserRefreshServer;
@@ -59,18 +82,6 @@ public event Action OnRuntimeRudeEdit
}
}
- ///
- /// All clients share the same loggers.
- ///
- public ILogger ClientLogger
- => clients.First().client.Logger;
-
- ///
- /// All clients share the same loggers.
- ///
- public ILogger AgentLogger
- => clients.First().client.AgentLogger;
-
internal void ConfigureLaunchEnvironment(IDictionary environmentBuilder)
{
foreach (var (client, _) in clients)
@@ -99,6 +110,12 @@ internal async ValueTask WaitForConnectionEstablishedAsync(CancellationToken can
/// Cancellation token. The cancellation should trigger on process terminatation.
public async ValueTask> GetUpdateCapabilitiesAsync(CancellationToken cancellationToken)
{
+ if (!IsManagedAgentSupported)
+ {
+ // empty capabilities will cause rude edit ENC0097: NotSupportedByRuntime.
+ return [];
+ }
+
if (clients is [var (singleClient, _)])
{
return await singleClient.GetUpdateCapabilitiesAsync(cancellationToken);
@@ -114,6 +131,9 @@ public async ValueTask> GetUpdateCapabilitiesAsync(Cancel
/// Cancellation token. The cancellation should trigger on process terminatation.
public async Task ApplyManagedCodeUpdatesAsync(ImmutableArray 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.
@@ -137,6 +157,9 @@ async Task CompleteApplyOperationAsync()
/// Cancellation token. The cancellation should trigger on process terminatation.
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);
@@ -150,23 +173,26 @@ public async ValueTask InitialUpdatesAppliedAsync(CancellationToken cancellation
/// Cancellation token. The cancellation should trigger on process terminatation.
public async Task ApplyStaticAssetUpdatesAsync(IEnumerable 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();
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;
}
}
@@ -177,6 +203,10 @@ public async Task ApplyStaticAssetUpdatesAsync(IEnumerable
/// Cancellation token. The cancellation should trigger on process terminatation.
public async ValueTask ApplyStaticAssetUpdatesAsync(ImmutableArray 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);
diff --git a/src/BuiltInTools/HotReloadClient/Logging/LogEvents.cs b/src/BuiltInTools/HotReloadClient/Logging/LogEvents.cs
index 8b43cb4bad29..d154e76c4097 100644
--- a/src/BuiltInTools/HotReloadClient/Logging/LogEvents.cs
+++ b/src/BuiltInTools/HotReloadClient/Logging/LogEvents.cs
@@ -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(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 Create(LogLevel level, string message)
+ => Create(level, message);
+
+ 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 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 logEvent)
+ => logger.Log(logEvent.Level, logEvent.Id, logEvent.Message);
+
+ public static void Log(this ILogger logger, LogEvent logEvent, TArgs args)
+ {
+ if (logger.IsEnabled(logEvent.Level))
+ {
+ logger.Log(logEvent.Level, logEvent.Id, logEvent.Message, GetArgumentValues(args));
+ }
+ }
+
+ public static void Log(this ILogger logger, LogEvent<(TArg1, TArg2)> logEvent, TArg1 arg1, TArg2 arg2)
+ => Log(logger, logEvent, (arg1, arg2));
+
+ public static void Log(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 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 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<(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 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.");
}
+
diff --git a/src/BuiltInTools/HotReloadClient/StaticAsset.cs b/src/BuiltInTools/HotReloadClient/StaticAsset.cs
new file mode 100644
index 000000000000..74cf7d1e096d
--- /dev/null
+++ b/src/BuiltInTools/HotReloadClient/StaticAsset.cs
@@ -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;
+}
diff --git a/src/BuiltInTools/HotReloadClient/Utilities/VoidResult.cs b/src/BuiltInTools/HotReloadClient/Utilities/None.cs
similarity index 83%
rename from src/BuiltInTools/HotReloadClient/Utilities/VoidResult.cs
rename to src/BuiltInTools/HotReloadClient/Utilities/None.cs
index d024e9cb07e9..2ecb1bc60294 100644
--- a/src/BuiltInTools/HotReloadClient/Utilities/VoidResult.cs
+++ b/src/BuiltInTools/HotReloadClient/Utilities/None.cs
@@ -5,6 +5,4 @@
namespace Microsoft.DotNet.HotReload;
-internal readonly struct VoidResult
-{
-}
+internal readonly struct None;
diff --git a/src/BuiltInTools/HotReloadClient/Web/AbstractBrowserRefreshServer.cs b/src/BuiltInTools/HotReloadClient/Web/AbstractBrowserRefreshServer.cs
index 79c5d957265b..09b04ab26827 100644
--- a/src/BuiltInTools/HotReloadClient/Web/AbstractBrowserRefreshServer.cs
+++ b/src/BuiltInTools/HotReloadClient/Web/AbstractBrowserRefreshServer.cs
@@ -32,7 +32,7 @@ internal abstract class AbstractBrowserRefreshServer(string middlewareAssemblyPa
private static readonly JsonSerializerOptions s_jsonSerializerOptions = new(JsonSerializerDefaults.Web);
private readonly List _activeConnections = [];
- private readonly TaskCompletionSource _browserConnected = new(TaskCreationOptions.RunContinuationsAsynchronously);
+ private readonly TaskCompletionSource _browserConnected = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly SharedSecretProvider _sharedSecretProvider = new();
@@ -241,7 +241,7 @@ public ValueTask SendWaitMessageAsync(CancellationToken cancellationToken)
private async ValueTask SendAsync(ReadOnlyMemory messageBytes, CancellationToken cancellationToken)
{
- await SendAndReceiveAsync, VoidResult>(request: _ => messageBytes, response: null, cancellationToken);
+ await SendAndReceiveAsync, None>(request: _ => messageBytes, response: null, cancellationToken);
}
public async ValueTask SendAndReceiveAsync(
diff --git a/src/BuiltInTools/HotReloadClient/Web/BrowserConnection.cs b/src/BuiltInTools/HotReloadClient/Web/BrowserConnection.cs
index 74bfabc268d9..101193a7fa39 100644
--- a/src/BuiltInTools/HotReloadClient/Web/BrowserConnection.cs
+++ b/src/BuiltInTools/HotReloadClient/Web/BrowserConnection.cs
@@ -25,7 +25,7 @@ namespace Microsoft.DotNet.HotReload;
public ILogger ServerLogger { get; }
public ILogger AgentLogger { get; }
- public readonly TaskCompletionSource Disconnected = new(TaskCreationOptions.RunContinuationsAsynchronously);
+ public readonly TaskCompletionSource Disconnected = new(TaskCreationOptions.RunContinuationsAsynchronously);
public BrowserConnection(WebSocket clientSocket, string? sharedSecret, ILoggerFactory loggerFactory)
{
diff --git a/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs b/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs
index 72fadfe6f024..dd56ed47d0c3 100644
--- a/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs
+++ b/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs
@@ -24,15 +24,12 @@ public static async Task RunAsync(string workingDirectory, DotNetWatchOpti
commandArguments.AddRange(options.ApplicationArguments);
- var rootProjectOptions = new ProjectOptions()
+ var mainProjectOptions = new ProjectOptions()
{
- IsRootProject = true,
- ProjectPath = options.ProjectPath,
+ IsMainProject = true,
+ Representation = options.Project,
WorkingDirectory = workingDirectory,
- TargetFramework = null,
- BuildArguments = [],
- NoLaunchProfile = options.NoLaunchProfile,
- LaunchProfileName = null,
+ LaunchProfileName = options.NoLaunchProfile ? default : null,
Command = "run",
CommandArguments = [.. commandArguments],
LaunchEnvironmentVariables = [],
@@ -59,7 +56,10 @@ public static async Task RunAsync(string workingDirectory, DotNetWatchOpti
ProcessRunner = processRunner,
Options = globalOptions,
EnvironmentOptions = environmentOptions,
- RootProjectOptions = rootProjectOptions,
+ MainProjectOptions = mainProjectOptions,
+ RootProjects = [mainProjectOptions.Representation],
+ BuildArguments = [],
+ TargetFramework = null,
BrowserRefreshServerFactory = new BrowserRefreshServerFactory(),
BrowserLauncher = new BrowserLauncher(logger, reporter, environmentOptions),
};
diff --git a/src/BuiltInTools/Watch.Aspire/DotNetWatchOptions.cs b/src/BuiltInTools/Watch.Aspire/DotNetWatchOptions.cs
index 1d8f526c1fff..4558d5ba2273 100644
--- a/src/BuiltInTools/Watch.Aspire/DotNetWatchOptions.cs
+++ b/src/BuiltInTools/Watch.Aspire/DotNetWatchOptions.cs
@@ -3,6 +3,7 @@
using System.Collections.Immutable;
using System.CommandLine;
+using System.CommandLine.Parsing;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
@@ -16,7 +17,7 @@ internal sealed class DotNetWatchOptions
///
public required string SdkDirectory { get; init; }
- public required string ProjectPath { get; init; }
+ public required ProjectRepresentation Project { get; init; }
public required ImmutableArray ApplicationArguments { get; init; }
public LogLevel LogLevel { get; init; }
public bool NoLaunchProfile { get; init; }
@@ -24,20 +25,13 @@ internal sealed class DotNetWatchOptions
public static bool TryParse(string[] args, [NotNullWhen(true)] out DotNetWatchOptions? options)
{
var sdkOption = new Option("--sdk") { Arity = ArgumentArity.ExactlyOne, Required = true, AllowMultipleArgumentsPerToken = false };
- var projectOption = new Option("--project") { Arity = ArgumentArity.ExactlyOne, Required = true, AllowMultipleArgumentsPerToken = false };
+ var projectOption = new Option("--project") { Arity = ArgumentArity.ZeroOrOne, AllowMultipleArgumentsPerToken = false };
+ var fileOption = new Option("--file") { Arity = ArgumentArity.ZeroOrOne, AllowMultipleArgumentsPerToken = false };
var quietOption = new Option("--quiet") { Arity = ArgumentArity.Zero };
var verboseOption = new Option("--verbose") { Arity = ArgumentArity.Zero };
var noLaunchProfileOption = new Option("--no-launch-profile") { Arity = ArgumentArity.Zero };
var applicationArguments = new Argument("arguments") { Arity = ArgumentArity.ZeroOrMore };
- verboseOption.Validators.Add(v =>
- {
- if (v.GetValue(quietOption) && v.GetValue(verboseOption))
- {
- v.AddError("Cannot specify both '--quiet' and '--verbose' options.");
- }
- });
-
var rootCommand = new RootCommand()
{
Directives = { new EnvironmentVariablesDirective() },
@@ -45,6 +39,7 @@ public static bool TryParse(string[] args, [NotNullWhen(true)] out DotNetWatchOp
{
sdkOption,
projectOption,
+ fileOption,
quietOption,
verboseOption,
noLaunchProfileOption
@@ -55,6 +50,23 @@ public static bool TryParse(string[] args, [NotNullWhen(true)] out DotNetWatchOp
}
};
+ rootCommand.Validators.Add(v =>
+ {
+ if (HasOption(v, quietOption) && HasOption(v, verboseOption))
+ {
+ v.AddError("Cannot specify both '--quiet' and '--verbose' options.");
+ }
+
+ if (HasOption(v, projectOption) && HasOption(v, fileOption))
+ {
+ v.AddError("Cannot specify both '--file' and '--project' options.");
+ }
+ else if (!HasOption(v, projectOption) && !HasOption(v, fileOption))
+ {
+ v.AddError("Must specify either '--file' or '--project' option.");
+ }
+ });
+
var parseResult = rootCommand.Parse(args);
if (parseResult.Errors.Count > 0)
{
@@ -70,7 +82,7 @@ public static bool TryParse(string[] args, [NotNullWhen(true)] out DotNetWatchOp
options = new DotNetWatchOptions()
{
SdkDirectory = parseResult.GetRequiredValue(sdkOption),
- ProjectPath = parseResult.GetRequiredValue(projectOption),
+ Project = new ProjectRepresentation(projectPath: parseResult.GetValue(projectOption), entryPointFilePath: parseResult.GetValue(fileOption)),
LogLevel = parseResult.GetValue(quietOption) ? LogLevel.Warning : parseResult.GetValue(verboseOption) ? LogLevel.Debug : LogLevel.Information,
ApplicationArguments = [.. parseResult.GetValue(applicationArguments) ?? []],
NoLaunchProfile = parseResult.GetValue(noLaunchProfileOption),
@@ -78,4 +90,7 @@ public static bool TryParse(string[] args, [NotNullWhen(true)] out DotNetWatchOp
return true;
}
+
+ private static bool HasOption(SymbolResult symbolResult, Option option)
+ => symbolResult.GetResult(option) is OptionResult or && !or.Implicit;
}
diff --git a/src/BuiltInTools/Watch/AppModels/BlazorWebAssemblyAppModel.cs b/src/BuiltInTools/Watch/AppModels/BlazorWebAssemblyAppModel.cs
index ea3480f979f6..ef0c112b98fb 100644
--- a/src/BuiltInTools/Watch/AppModels/BlazorWebAssemblyAppModel.cs
+++ b/src/BuiltInTools/Watch/AppModels/BlazorWebAssemblyAppModel.cs
@@ -17,11 +17,11 @@ internal sealed class BlazorWebAssemblyAppModel(DotNetWatchContext context, Proj
{
public override ProjectGraphNode LaunchingProject => clientProject;
- public override bool RequiresBrowserRefresh => true;
+ public override bool ManagedHotReloadRequiresBrowserRefresh => true;
- protected override HotReloadClients CreateClients(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer? browserRefreshServer)
+ protected override ImmutableArray<(HotReloadClient client, string name)> CreateManagedClients(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer? browserRefreshServer)
{
Debug.Assert(browserRefreshServer != null);
- return new(CreateWebAssemblyClient(clientLogger, agentLogger, browserRefreshServer, clientProject), browserRefreshServer);
+ return [(CreateWebAssemblyClient(clientLogger, agentLogger, browserRefreshServer, clientProject), "")];
}
}
diff --git a/src/BuiltInTools/Watch/AppModels/BlazorWebAssemblyHostedAppModel.cs b/src/BuiltInTools/Watch/AppModels/BlazorWebAssemblyHostedAppModel.cs
index d56f69aeccb3..768187dbeda3 100644
--- a/src/BuiltInTools/Watch/AppModels/BlazorWebAssemblyHostedAppModel.cs
+++ b/src/BuiltInTools/Watch/AppModels/BlazorWebAssemblyHostedAppModel.cs
@@ -19,17 +19,15 @@ internal sealed class BlazorWebAssemblyHostedAppModel(DotNetWatchContext context
{
public override ProjectGraphNode LaunchingProject => serverProject;
- public override bool RequiresBrowserRefresh => true;
+ public override bool ManagedHotReloadRequiresBrowserRefresh => true;
- protected override HotReloadClients CreateClients(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer? browserRefreshServer)
+ protected override ImmutableArray<(HotReloadClient client, string name)> CreateManagedClients(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer? browserRefreshServer)
{
Debug.Assert(browserRefreshServer != null);
-
- return new(
- [
- (CreateWebAssemblyClient(clientLogger, agentLogger, browserRefreshServer, clientProject), "client"),
- (new DefaultHotReloadClient(clientLogger, agentLogger, GetStartupHookPath(serverProject), enableStaticAssetUpdates: false, new NamedPipeClientTransport(clientLogger)), "host")
- ],
- browserRefreshServer);
+ return
+ [
+ (CreateWebAssemblyClient(clientLogger, agentLogger, browserRefreshServer, clientProject), "client"),
+ (new DefaultHotReloadClient(clientLogger, agentLogger, GetStartupHookPath(serverProject), handlesStaticAssetUpdates: false, new NamedPipeClientTransport(clientLogger)), "host")
+ ];
}
}
diff --git a/src/BuiltInTools/Watch/AppModels/DefaultAppModel.cs b/src/BuiltInTools/Watch/AppModels/DefaultAppModel.cs
index ebb8e7d9c4b2..ac79fc90d8ac 100644
--- a/src/BuiltInTools/Watch/AppModels/DefaultAppModel.cs
+++ b/src/BuiltInTools/Watch/AppModels/DefaultAppModel.cs
@@ -12,6 +12,11 @@ namespace Microsoft.DotNet.Watch;
///
internal sealed class DefaultAppModel(ProjectGraphNode project) : HotReloadAppModel
{
- public override ValueTask TryCreateClientsAsync(ILogger clientLogger, ILogger agentLogger, CancellationToken cancellationToken)
- => new(new HotReloadClients(new DefaultHotReloadClient(clientLogger, agentLogger, GetStartupHookPath(project), enableStaticAssetUpdates: true, new NamedPipeClientTransport(clientLogger)), browserRefreshServer: null));
+ public override ValueTask CreateClientsAsync(ILogger clientLogger, ILogger agentLogger, CancellationToken cancellationToken)
+ => new(new HotReloadClients(
+ clients: IsManagedAgentSupported(project, clientLogger)
+ ? [(new DefaultHotReloadClient(clientLogger, agentLogger, GetStartupHookPath(project), handlesStaticAssetUpdates: true, new NamedPipeClientTransport(clientLogger)), "")]
+ : [],
+ browserRefreshServer: null,
+ useRefreshServerToApplyStaticAssets: false));
}
diff --git a/src/BuiltInTools/Watch/AppModels/HotReloadAppModel.cs b/src/BuiltInTools/Watch/AppModels/HotReloadAppModel.cs
index 16d837d5c180..5c9810fa3c13 100644
--- a/src/BuiltInTools/Watch/AppModels/HotReloadAppModel.cs
+++ b/src/BuiltInTools/Watch/AppModels/HotReloadAppModel.cs
@@ -9,7 +9,7 @@ namespace Microsoft.DotNet.Watch;
internal abstract partial class HotReloadAppModel()
{
- public abstract ValueTask TryCreateClientsAsync(ILogger clientLogger, ILogger agentLogger, CancellationToken cancellationToken);
+ public abstract ValueTask CreateClientsAsync(ILogger clientLogger, ILogger agentLogger, CancellationToken cancellationToken);
protected static string GetInjectedAssemblyPath(string targetFramework, string assemblyName)
=> Path.Combine(Path.GetDirectoryName(typeof(HotReloadAppModel).Assembly.Location)!, "hotreload", targetFramework, assemblyName + ".dll");
@@ -51,4 +51,42 @@ public static HotReloadAppModel InferFromProject(DotNetWatchContext context, Pro
context.Logger.Log(MessageDescriptor.ApplicationKind_Default);
return new DefaultAppModel(projectNode);
}
+
+ ///
+ /// True if a managed code agent can be injected into the target process.
+ /// The agent is injected either via dotnet startup hook, or via web server middleware for WASM clients.
+ ///
+ internal static bool IsManagedAgentSupported(ProjectGraphNode project, ILogger logger)
+ {
+ if (!project.IsNetCoreApp(Versions.Version6_0))
+ {
+ LogWarning("target framework is older than 6.0");
+ return false;
+ }
+
+ // If property is not specified startup hook is enabled:
+ // https://github.com/dotnet/runtime/blob/4b0b7238ba021b610d3963313b4471517108d2bc/src/libraries/System.Private.CoreLib/src/System/StartupHookProvider.cs#L22
+ // Startup hooks are not used for WASM projects.
+ //
+ // TODO: Remove once implemented: https://github.com/dotnet/runtime/issues/123778
+ if (!project.ProjectInstance.GetBooleanPropertyValue(PropertyNames.StartupHookSupport, defaultValue: true) &&
+ !project.GetCapabilities().Contains(ProjectCapability.WebAssembly))
+ {
+ // Report which property is causing lack of support for startup hooks:
+ var (propertyName, propertyValue) =
+ project.ProjectInstance.GetBooleanPropertyValue(PropertyNames.PublishAot)
+ ? (PropertyNames.PublishAot, true)
+ : project.ProjectInstance.GetBooleanPropertyValue(PropertyNames.PublishTrimmed)
+ ? (PropertyNames.PublishTrimmed, true)
+ : (PropertyNames.StartupHookSupport, false);
+
+ LogWarning(string.Format("'{0}' property is '{1}'", propertyName, propertyValue));
+ return false;
+ }
+
+ return true;
+
+ void LogWarning(string reason)
+ => logger.Log(MessageDescriptor.ProjectDoesNotSupportHotReload, reason);
+ }
}
diff --git a/src/BuiltInTools/Watch/AppModels/MobileAppModel.cs b/src/BuiltInTools/Watch/AppModels/MobileAppModel.cs
index 503481230596..fa3d16509151 100644
--- a/src/BuiltInTools/Watch/AppModels/MobileAppModel.cs
+++ b/src/BuiltInTools/Watch/AppModels/MobileAppModel.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections.Immutable;
using Microsoft.Build.Graph;
using Microsoft.DotNet.HotReload;
using Microsoft.Extensions.Logging;
@@ -13,16 +14,24 @@ internal sealed class MobileAppModel(DotNetWatchContext context, ProjectGraphNod
// Mobile workloads (Android, iOS) add this capability since named pipes don't work over the network.
// Pass the startup hook path so it can be included in the environment variables
// passed via `dotnet run -e` as @(RuntimeEnvironmentVariable) items.
- public override async ValueTask TryCreateClientsAsync(ILogger clientLogger, ILogger agentLogger, CancellationToken cancellationToken)
+ public override async ValueTask CreateClientsAsync(ILogger clientLogger, ILogger agentLogger, CancellationToken cancellationToken)
{
- var transport = await WebSocketClientTransport.CreateAsync(
- context.EnvironmentOptions.AgentWebSocketPort,
- context.EnvironmentOptions.AgentWebSocketSecurePort,
- clientLogger,
- cancellationToken);
+ ImmutableArray<(HotReloadClient client, string name)> clients;
+ if (IsManagedAgentSupported(project, clientLogger))
+ {
+ var transport = await WebSocketClientTransport.CreateAsync(
+ context.EnvironmentOptions.AgentWebSocketPort,
+ context.EnvironmentOptions.AgentWebSocketSecurePort,
+ clientLogger,
+ cancellationToken);
- return new HotReloadClients(
- new DefaultHotReloadClient(clientLogger, agentLogger, GetStartupHookPath(project), enableStaticAssetUpdates: true, transport),
- browserRefreshServer: null);
+ clients = [(new DefaultHotReloadClient(clientLogger, agentLogger, GetStartupHookPath(project), handlesStaticAssetUpdates: true, transport), "")];
+ }
+ else
+ {
+ clients = [];
+ }
+
+ return new HotReloadClients(clients, browserRefreshServer: null, useRefreshServerToApplyStaticAssets: false);
}
}
diff --git a/src/BuiltInTools/Watch/AppModels/WebApplicationAppModel.cs b/src/BuiltInTools/Watch/AppModels/WebApplicationAppModel.cs
index 0f1fbb74d5d8..2460d27a79e7 100644
--- a/src/BuiltInTools/Watch/AppModels/WebApplicationAppModel.cs
+++ b/src/BuiltInTools/Watch/AppModels/WebApplicationAppModel.cs
@@ -16,25 +16,24 @@ internal abstract class WebApplicationAppModel(DotNetWatchContext context) : Hot
public DotNetWatchContext Context => context;
- public abstract bool RequiresBrowserRefresh { get; }
+ public abstract bool ManagedHotReloadRequiresBrowserRefresh { get; }
///
/// Project that's used for launching the application.
///
public abstract ProjectGraphNode LaunchingProject { get; }
- protected abstract HotReloadClients CreateClients(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer? browserRefreshServer);
+ protected abstract ImmutableArray<(HotReloadClient client, string name)> CreateManagedClients(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer? browserRefreshServer);
- public async sealed override ValueTask TryCreateClientsAsync(ILogger clientLogger, ILogger agentLogger, CancellationToken cancellationToken)
+ public async sealed override ValueTask CreateClientsAsync(ILogger clientLogger, ILogger agentLogger, CancellationToken cancellationToken)
{
var browserRefreshServer = await context.BrowserRefreshServerFactory.GetOrCreateBrowserRefreshServerAsync(LaunchingProject, this, cancellationToken);
- if (RequiresBrowserRefresh && browserRefreshServer == null)
- {
- // Error has been reported
- return null;
- }
- return CreateClients(clientLogger, agentLogger, browserRefreshServer);
+ var managedClients = (!ManagedHotReloadRequiresBrowserRefresh || browserRefreshServer != null) && IsManagedAgentSupported(LaunchingProject, clientLogger)
+ ? CreateManagedClients(clientLogger, agentLogger, browserRefreshServer)
+ : [];
+
+ return new HotReloadClients(managedClients, browserRefreshServer, useRefreshServerToApplyStaticAssets: true);
}
protected WebAssemblyHotReloadClient CreateWebAssemblyClient(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer browserRefreshServer, ProjectGraphNode clientProject)
@@ -71,13 +70,29 @@ public bool IsServerSupported(ProjectGraphNode projectNode, ILogger logger)
{
if (context.EnvironmentOptions.SuppressBrowserRefresh)
{
- logger.Log(MessageDescriptor.SkippingConfiguringBrowserRefresh_SuppressedViaEnvironmentVariable.WithLevelWhen(LogLevel.Error, RequiresBrowserRefresh), EnvironmentVariables.Names.SuppressBrowserRefresh);
+ if (ManagedHotReloadRequiresBrowserRefresh)
+ {
+ logger.Log(MessageDescriptor.BrowserRefreshSuppressedViaEnvironmentVariable_ApplicationWillBeRestarted, EnvironmentVariables.Names.SuppressBrowserRefresh);
+ }
+ else
+ {
+ logger.Log(MessageDescriptor.BrowserRefreshSuppressedViaEnvironmentVariable_ManualRefreshRequired, EnvironmentVariables.Names.SuppressBrowserRefresh);
+ }
+
return false;
}
if (!projectNode.IsNetCoreApp(minVersion: s_minimumSupportedVersion))
{
- logger.Log(MessageDescriptor.SkippingConfiguringBrowserRefresh_TargetFrameworkNotSupported.WithLevelWhen(LogLevel.Error, RequiresBrowserRefresh));
+ if (ManagedHotReloadRequiresBrowserRefresh)
+ {
+ logger.Log(MessageDescriptor.BrowserRefreshNotSupportedByProjectTargetFramework_ApplicationWillBeRestarted);
+ }
+ else
+ {
+ logger.Log(MessageDescriptor.BrowserRefreshNotSupportedByProjectTargetFramework_ManualRefreshRequired);
+ }
+
return false;
}
diff --git a/src/BuiltInTools/Watch/AppModels/WebServerAppModel.cs b/src/BuiltInTools/Watch/AppModels/WebServerAppModel.cs
index b1026c5f2d6a..04727dee7984 100644
--- a/src/BuiltInTools/Watch/AppModels/WebServerAppModel.cs
+++ b/src/BuiltInTools/Watch/AppModels/WebServerAppModel.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections.Immutable;
using Microsoft.Build.Graph;
using Microsoft.DotNet.HotReload;
using Microsoft.Extensions.Logging;
@@ -12,9 +13,9 @@ internal sealed class WebServerAppModel(DotNetWatchContext context, ProjectGraph
{
public override ProjectGraphNode LaunchingProject => serverProject;
- public override bool RequiresBrowserRefresh
+ public override bool ManagedHotReloadRequiresBrowserRefresh
=> false;
- protected override HotReloadClients CreateClients(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer? browserRefreshServer)
- => new(new DefaultHotReloadClient(clientLogger, agentLogger, GetStartupHookPath(serverProject), enableStaticAssetUpdates: true, new NamedPipeClientTransport(clientLogger)), browserRefreshServer);
+ protected override ImmutableArray<(HotReloadClient client, string name)> CreateManagedClients(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer? browserRefreshServer)
+ => [(new DefaultHotReloadClient(clientLogger, agentLogger, GetStartupHookPath(serverProject), handlesStaticAssetUpdates: true, new NamedPipeClientTransport(clientLogger)), "")];
}
diff --git a/src/BuiltInTools/Watch/Aspire/AspireServiceFactory.cs b/src/BuiltInTools/Watch/Aspire/AspireServiceFactory.cs
index 06fd0a52b127..99539e56433d 100644
--- a/src/BuiltInTools/Watch/Aspire/AspireServiceFactory.cs
+++ b/src/BuiltInTools/Watch/Aspire/AspireServiceFactory.cs
@@ -6,12 +6,11 @@
using System.Globalization;
using System.Threading.Channels;
using Aspire.Tools.Service;
-using Microsoft.Build.Graph;
using Microsoft.Extensions.Logging;
namespace Microsoft.DotNet.Watch;
-internal class AspireServiceFactory : IRuntimeProcessLauncherFactory
+internal class AspireServiceFactory(ProjectOptions hostProjectOptions) : IRuntimeProcessLauncherFactory
{
internal sealed class SessionManager : IAspireServerEvents, IRuntimeProcessLauncher
{
@@ -30,8 +29,8 @@ private readonly struct Session(string dcpId, string sessionId, RunningProject r
};
private readonly ProjectLauncher _projectLauncher;
- private readonly AspireServerService _service;
private readonly ProjectOptions _hostProjectOptions;
+ private readonly AspireServerService _service;
private readonly ILogger _logger;
///
@@ -46,6 +45,12 @@ private readonly struct Session(string dcpId, string sessionId, RunningProject r
private volatile bool _isDisposed;
+ // The number of sessions whose initialization is in progress.
+ private int _pendingSessionInitializationCount;
+
+ // Blocks disposal until no session initialization is in progress.
+ private readonly SemaphoreSlim _postDisposalSessionInitializationCompleted = new(initialCount: 0, maxCount: 1);
+
public SessionManager(ProjectLauncher projectLauncher, ProjectOptions hostProjectOptions)
{
_projectLauncher = projectLauncher;
@@ -60,21 +65,20 @@ public SessionManager(ProjectLauncher projectLauncher, ProjectOptions hostProjec
public async ValueTask DisposeAsync()
{
-#if DEBUG
- lock (_guard)
- {
- Debug.Assert(_sessions.Count == 0);
- }
-#endif
- _isDisposed = true;
+ ObjectDisposedException.ThrowIf(_isDisposed, this);
+ _logger.LogDebug("Disposing service factory ...");
+
+ // stop accepting requests - triggers cancellation token for in-flight operations:
await _service.DisposeAsync();
- }
- public async ValueTask TerminateLaunchedProcessesAsync(CancellationToken cancellationToken)
- {
- ObjectDisposedException.ThrowIf(_isDisposed, this);
+ // should not receive any more requests at this point:
+ _isDisposed = true;
+ // wait for all in-flight process initialization to complete:
+ await _postDisposalSessionInitializationCompleted.WaitAsync(CancellationToken.None);
+
+ // terminate all active sessions:
ImmutableArray sessions;
lock (_guard)
{
@@ -83,7 +87,11 @@ public async ValueTask TerminateLaunchedProcessesAsync(CancellationToken cancell
_sessions.Clear();
}
- await Task.WhenAll(sessions.Select(TerminateSessionAsync)).WaitAsync(cancellationToken);
+ await Task.WhenAll(sessions.Select(TerminateSessionAsync)).WaitAsync(CancellationToken.None);
+
+ _postDisposalSessionInitializationCompleted.Dispose();
+
+ _logger.LogDebug("Service factory disposed");
}
public IEnumerable<(string name, string value)> GetEnvironmentVariables()
@@ -104,61 +112,73 @@ async ValueTask IAspireServerEvents.StartProjectAsync(string dcpId, Proj
public async ValueTask StartProjectAsync(string dcpId, string sessionId, ProjectOptions projectOptions, bool isRestart, CancellationToken cancellationToken)
{
+ // Neither request from DCP nor restart should happen once the disposal has started.
ObjectDisposedException.ThrowIf(_isDisposed, this);
- _logger.LogDebug("Starting project: {Path}", projectOptions.ProjectPath);
+ _logger.LogDebug("[#{SessionId}] Starting: '{Path}'", sessionId, projectOptions.Representation.ProjectOrEntryPointFilePath);
+ RunningProject? runningProject = null;
var outputChannel = Channel.CreateUnbounded(s_outputChannelOptions);
- RunningProject? runningProject = null;
+ Interlocked.Increment(ref _pendingSessionInitializationCount);
- runningProject = await _projectLauncher.TryLaunchProcessAsync(
- projectOptions,
- onOutput: line =>
- {
- var writeResult = outputChannel.Writer.TryWrite(line);
- Debug.Assert(writeResult);
- },
- onExit: async (processId, exitCode) =>
- {
- // Project can be null if the process exists while it's being initialized.
- if (runningProject?.IsRestarting == false)
+ try
+ {
+ runningProject = await _projectLauncher.TryLaunchProcessAsync(
+ projectOptions,
+ onOutput: line =>
{
- try
- {
- await _service.NotifySessionEndedAsync(dcpId, sessionId, processId, exitCode, cancellationToken);
- }
- catch (OperationCanceledException)
+ var writeResult = outputChannel.Writer.TryWrite(line);
+ Debug.Assert(writeResult);
+ },
+ onExit: async (processId, exitCode) =>
+ {
+ // Project can be null if the process exists while it's being initialized.
+ if (runningProject?.IsRestarting == false)
{
- // canceled on shutdown, ignore
+ try
+ {
+ await _service.NotifySessionEndedAsync(dcpId, sessionId, processId, exitCode, cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ // canceled on shutdown, ignore
+ }
}
- }
- },
- restartOperation: cancellationToken =>
- StartProjectAsync(dcpId, sessionId, projectOptions, isRestart: true, cancellationToken),
- cancellationToken);
+ },
+ restartOperation: cancellationToken =>
+ StartProjectAsync(dcpId, sessionId, projectOptions, isRestart: true, cancellationToken),
+ cancellationToken);
- if (runningProject == null)
- {
- // detailed error already reported:
- throw new ApplicationException($"Failed to launch project '{projectOptions.ProjectPath}'.");
- }
+ if (runningProject == null)
+ {
+ // detailed error already reported:
+ throw new ApplicationException($"Failed to launch '{projectOptions.Representation.ProjectOrEntryPointFilePath}'.");
+ }
- await _service.NotifySessionStartedAsync(dcpId, sessionId, runningProject.ProcessId, cancellationToken);
+ await _service.NotifySessionStartedAsync(dcpId, sessionId, runningProject.Process.Id, cancellationToken);
- // cancel reading output when the process terminates:
- var outputReader = StartChannelReader(runningProject.ProcessExitedCancellationToken);
+ // cancel reading output when the process terminates:
+ var outputReader = StartChannelReader(runningProject.Process.ExitedCancellationToken);
- lock (_guard)
- {
- // When process is restarted we reuse the session id.
- // The session already exists, it needs to be updated with new info.
- Debug.Assert(_sessions.ContainsKey(sessionId) == isRestart);
+ lock (_guard)
+ {
+ // When process is restarted we reuse the session id.
+ // The session already exists, it needs to be updated with new info.
+ Debug.Assert(_sessions.ContainsKey(sessionId) == isRestart);
- _sessions[sessionId] = new Session(dcpId, sessionId, runningProject, outputReader);
+ _sessions[sessionId] = new Session(dcpId, sessionId, runningProject, outputReader);
+ }
+ }
+ finally
+ {
+ if (Interlocked.Decrement(ref _pendingSessionInitializationCount) == 0 && _isDisposed)
+ {
+ _postDisposalSessionInitializationCompleted.Release();
+ }
}
- _logger.LogDebug("Session started: #{SessionId}", sessionId);
+ _logger.LogDebug("[#{SessionId}] Session started", sessionId);
return runningProject;
async Task StartChannelReader(CancellationToken cancellationToken)
@@ -204,32 +224,25 @@ async ValueTask IAspireServerEvents.StopSessionAsync(string dcpId, string
private async Task TerminateSessionAsync(Session session)
{
- _logger.LogDebug("Stop session #{SessionId}", session.Id);
+ _logger.LogDebug("[#{SessionId}] Stop session", session.Id);
- await session.RunningProject.TerminateAsync();
+ await session.RunningProject.Process.TerminateAsync();
// process termination should cancel output reader task:
await session.OutputReader;
}
private ProjectOptions GetProjectOptions(ProjectLaunchRequest projectLaunchInfo)
- {
- var hostLaunchProfile = _hostProjectOptions.NoLaunchProfile ? null : _hostProjectOptions.LaunchProfileName;
-
- return new()
+ => new()
{
- IsRootProject = false,
- ProjectPath = projectLaunchInfo.ProjectPath,
+ IsMainProject = false,
+ Representation = ProjectRepresentation.FromProjectOrEntryPointFilePath(projectLaunchInfo.ProjectPath),
WorkingDirectory = Path.GetDirectoryName(projectLaunchInfo.ProjectPath) ?? throw new InvalidOperationException(),
- BuildArguments = _hostProjectOptions.BuildArguments,
Command = "run",
- CommandArguments = GetRunCommandArguments(projectLaunchInfo, hostLaunchProfile),
+ CommandArguments = GetRunCommandArguments(projectLaunchInfo, _hostProjectOptions.LaunchProfileName.Value),
LaunchEnvironmentVariables = projectLaunchInfo.Environment?.Select(e => (e.Key, e.Value))?.ToArray() ?? [],
- LaunchProfileName = projectLaunchInfo.LaunchProfile,
- NoLaunchProfile = projectLaunchInfo.DisableLaunchProfile,
- TargetFramework = _hostProjectOptions.TargetFramework,
+ LaunchProfileName = projectLaunchInfo.DisableLaunchProfile ? default : projectLaunchInfo.LaunchProfile,
};
- }
// internal for testing
internal static IReadOnlyList GetRunCommandArguments(ProjectLaunchRequest projectLaunchInfo, string? hostLaunchProfile)
@@ -274,13 +287,9 @@ internal static IReadOnlyList GetRunCommandArguments(ProjectLaunchReques
}
}
- public static readonly AspireServiceFactory Instance = new();
-
public const string AspireLogComponentName = "Aspire";
public const string AppHostProjectCapability = ProjectCapability.Aspire;
- public IRuntimeProcessLauncher? TryCreate(ProjectGraphNode projectNode, ProjectLauncher projectLauncher, ProjectOptions hostProjectOptions)
- => projectNode.GetCapabilities().Contains(AppHostProjectCapability)
- ? new SessionManager(projectLauncher, hostProjectOptions)
- : null;
+ public IRuntimeProcessLauncher Create(ProjectLauncher projectLauncher)
+ => new SessionManager(projectLauncher, hostProjectOptions);
}
diff --git a/src/BuiltInTools/Watch/Browser/BrowserLauncher.cs b/src/BuiltInTools/Watch/Browser/BrowserLauncher.cs
index 74ce7a91dde2..cef3c5d54600 100644
--- a/src/BuiltInTools/Watch/Browser/BrowserLauncher.cs
+++ b/src/BuiltInTools/Watch/Browser/BrowserLauncher.cs
@@ -38,7 +38,7 @@ public void InstallBrowserLaunchTrigger(
WebServerProcessStateObserver.Observe(projectNode, processSpec, url =>
{
- if (projectOptions.IsRootProject &&
+ if (projectOptions.IsMainProject &&
ImmutableInterlocked.Update(ref _browserLaunchAttempted, static (set, key) => set.Add(key), projectNode.ProjectInstance.GetId()))
{
// first build iteration of a root project:
@@ -66,7 +66,14 @@ private void LaunchBrowser(string launchUrl, AbstractBrowserRefreshServer? serve
? (browserPath, launchUrl, false)
: (launchUrl, null, true);
- logger.Log(MessageDescriptor.LaunchingBrowser, fileName, arg);
+ if (arg != null)
+ {
+ logger.Log(MessageDescriptor.LaunchingBrowserWithUrl, (fileName, arg));
+ }
+ else
+ {
+ logger.Log(MessageDescriptor.LaunchingBrowser, fileName);
+ }
if (environmentOptions.TestFlags != TestFlags.None && environmentOptions.BrowserPath == null)
{
@@ -127,7 +134,10 @@ private bool CanLaunchBrowser(ProjectOptions projectOptions, [NotNullWhen(true)]
private LaunchSettingsProfile GetLaunchProfile(ProjectOptions projectOptions)
{
- return (projectOptions.NoLaunchProfile == true
- ? null : LaunchSettingsProfile.ReadLaunchProfile(projectOptions.ProjectPath, projectOptions.LaunchProfileName, logger)) ?? new();
+ var profile = projectOptions.LaunchProfileName.HasValue
+ ? LaunchSettingsProfile.ReadLaunchProfile(projectOptions.Representation, projectOptions.LaunchProfileName.Value, logger)
+ : null;
+
+ return profile ?? new();
}
}
diff --git a/src/BuiltInTools/Watch/Build/BuildNames.cs b/src/BuiltInTools/Watch/Build/BuildNames.cs
index e7919c238252..d7ad8b1acbbd 100644
--- a/src/BuiltInTools/Watch/Build/BuildNames.cs
+++ b/src/BuiltInTools/Watch/Build/BuildNames.cs
@@ -23,6 +23,9 @@ internal static class PropertyNames
public const string SkipCompilerExecution = nameof(SkipCompilerExecution);
public const string ProvideCommandLineArgs = nameof(ProvideCommandLineArgs);
public const string NonExistentFile = nameof(NonExistentFile);
+ public const string StartupHookSupport = nameof(StartupHookSupport);
+ public const string PublishTrimmed = nameof(PublishTrimmed);
+ public const string PublishAot = nameof(PublishAot);
}
internal static class ItemNames
diff --git a/src/BuiltInTools/Watch/Build/BuildReporter.cs b/src/BuiltInTools/Watch/Build/BuildReporter.cs
index 6410476dc3c8..de404b1d07f9 100644
--- a/src/BuiltInTools/Watch/Build/BuildReporter.cs
+++ b/src/BuiltInTools/Watch/Build/BuildReporter.cs
@@ -14,12 +14,13 @@ namespace Microsoft.DotNet.Watch;
internal sealed class BuildReporter(ILogger logger, GlobalOptions options, EnvironmentOptions environmentOptions)
{
public ILogger Logger => logger;
+ public GlobalOptions GlobalOptions => options;
public EnvironmentOptions EnvironmentOptions => environmentOptions;
public Loggers GetLoggers(string projectPath, string operationName)
=> new(logger, environmentOptions.GetBinLogPath(projectPath, operationName, options));
- public void ReportWatchedFiles(Dictionary fileItems)
+ public static void ReportWatchedFiles(ILogger logger, IReadOnlyDictionary fileItems)
{
logger.Log(MessageDescriptor.WatchingFilesForChanges, fileItems.Count);
diff --git a/src/BuiltInTools/Watch/Build/BuildRequest.cs b/src/BuiltInTools/Watch/Build/BuildRequest.cs
new file mode 100644
index 000000000000..46e35ff35113
--- /dev/null
+++ b/src/BuiltInTools/Watch/Build/BuildRequest.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Immutable;
+using Microsoft.Build.Execution;
+
+namespace Microsoft.DotNet.Watch;
+
+internal readonly struct BuildRequest(ProjectInstance projectInstance, ImmutableArray targets, T data)
+{
+ public ProjectInstance ProjectInstance { get; } = projectInstance;
+ public ImmutableArray Targets { get; } = targets;
+ public T Data { get; } = data;
+}
+
+internal static class BuildRequest
+{
+ public static BuildRequest