diff --git a/src/Dotnet.Watch/.editorconfig b/src/Dotnet.Watch/.editorconfig
index 54d2f34da8e4..09b2d5adee8b 100644
--- a/src/Dotnet.Watch/.editorconfig
+++ b/src/Dotnet.Watch/.editorconfig
@@ -14,13 +14,15 @@ dotnet_diagnostic.CA2008.severity = none # Do not create tasks without passing a
# CS - C# compiler warnings/errors
dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member
-dotnet_diagnostic.CS1573.severity = none # Parameter 'sourceFile' has no matching param tag in the XML comment
+dotnet_diagnostic.CS1573.severity = none # Parameter has no matching param tag in the XML comment
+dotnet_diagnostic.CS1572.severity = warning # XML comment has a param tag for '...', but there is no parameter by that name
# IDE - IDE/Style warnings
dotnet_diagnostic.IDE0005.severity = none # Using directive is unnecessary
dotnet_diagnostic.IDE0011.severity = none # Add braces
dotnet_diagnostic.IDE0036.severity = none # Order modifiers
+dotnet_diagnostic.IDE0044.severity = none # Make field readonly
dotnet_diagnostic.IDE0060.severity = none # Remove unused parameter
dotnet_diagnostic.IDE0073.severity = none # File header does not match required text
dotnet_diagnostic.IDE0161.severity = none # Convert to file-scoped namespace
-dotnet_diagnostic.IDE1006.severity = none # Naming rule violation
+dotnet_diagnostic.IDE1006.severity = none # Naming rule violation
\ No newline at end of file
diff --git a/src/Dotnet.Watch/AspireService/AspireServerService.cs b/src/Dotnet.Watch/AspireService/AspireServerService.cs
index 064de0ee7a50..5183e05a8ffc 100644
--- a/src/Dotnet.Watch/AspireService/AspireServerService.cs
+++ b/src/Dotnet.Watch/AspireService/AspireServerService.cs
@@ -42,7 +42,12 @@ internal partial class AspireServerService : IAsyncDisposable
private readonly string _currentSecret;
private readonly string _displayName;
- private readonly CancellationTokenSource _shutdownCancellationTokenSource = new();
+ ///
+ /// Triggered when the shutdown process has been initiated.
+ /// During this time all requests respond with .
+ ///
+ private readonly CancellationTokenSource _shutdownCancellationSource = new();
+
private readonly int _port;
private readonly X509Certificate2 _certificate;
private readonly string _certificateEncodedBytes;
@@ -91,12 +96,20 @@ public AspireServerService(IAspireServerEvents aspireServerEvents, string displa
_requestListener = StartListeningAsync();
}
+ public void InitiateShutdown()
+ {
+ Log("Server shutdown initiated.");
+ _shutdownCancellationSource.Cancel();
+
+ // TODO: send message to DCP
+ // https://github.com/dotnet/aspire/issues/14987
+ }
+
public async ValueTask DisposeAsync()
{
- // Shutdown the service:
- _shutdownCancellationTokenSource.Cancel();
+ Log("Disposing server ...");
- Log("Waiting for server to shutdown ...");
+ _shutdownCancellationSource.Cancel();
try
{
@@ -111,7 +124,7 @@ public async ValueTask DisposeAsync()
_socketConnectionManager.Dispose();
_certificate.Dispose();
- _shutdownCancellationTokenSource.Dispose();
+ _shutdownCancellationSource.Dispose();
}
///
@@ -214,14 +227,14 @@ private Task StartListeningAsync()
var app = builder.Build();
app.MapGet("/", () => _displayName);
- app.MapGet(InfoResponse.Url, GetInfoAsync);
+ app.MapGet(InfoResponse.Url, HandleInfoRequestAsync);
// Set up the run session endpoints
var runSessionApi = app.MapGroup(RunSessionRequest.Url);
- runSessionApi.MapPut("/", RunSessionPutAsync);
- runSessionApi.MapDelete("/{sessionId}", RunSessionDeleteAsync);
- runSessionApi.Map(SessionNotification.Url, RunSessionNotifyAsync);
+ runSessionApi.MapPut("/", HandleStartSessionRequestAsync);
+ runSessionApi.MapDelete("/{sessionId}", HandleStopSessionRequestAsync);
+ runSessionApi.Map(SessionNotification.Url, HandleSessionNotifyRequestAsync);
app.UseWebSockets(new WebSocketOptions
{
@@ -229,62 +242,48 @@ private Task StartListeningAsync()
});
// Run the application async. It will shutdown when the cancel token is signaled
- return app.RunAsync(_shutdownCancellationTokenSource.Token);
+ return app.RunAsync(_shutdownCancellationSource.Token);
}
- private async Task RunSessionPutAsync(HttpContext context)
+ private bool TryAcceptRequest(HttpContext context)
{
- // Check the authentication header
- if (!IsValidAuthentication(context))
- {
- Log("Authorization failure");
- context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
- }
- else
+ if (_shutdownCancellationSource.IsCancellationRequested)
{
- await HandleStartSessionRequestAsync(context);
+ Log("Service unavailable -- shutdown in progress.");
+ context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
+ return false;
}
- }
- private async Task RunSessionDeleteAsync(HttpContext context, string sessionId)
- {
// Check the authentication header
if (!IsValidAuthentication(context))
{
Log("Authorization failure");
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
+ return false;
}
- else
- {
- await HandleStopSessionRequestAsync(context, sessionId);
- }
+
+ return true;
}
- private async Task GetInfoAsync(HttpContext context)
+ private async Task HandleInfoRequestAsync(HttpContext context)
{
- // Check the authentication header
- if (!IsValidAuthentication(context))
+ if (!TryAcceptRequest(context))
{
- Log("Authorization failure");
- context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
- }
- else
- {
- context.Response.StatusCode = (int)HttpStatusCode.OK;
- await context.Response.WriteAsJsonAsync(InfoResponse.Instance, JsonSerializerOptions, _shutdownCancellationTokenSource.Token);
+ return;
}
+
+ context.Response.StatusCode = (int)HttpStatusCode.OK;
+ await context.Response.WriteAsJsonAsync(InfoResponse.Instance, JsonSerializerOptions, _shutdownCancellationSource.Token);
}
- private async Task RunSessionNotifyAsync(HttpContext context)
+ private async Task HandleSessionNotifyRequestAsync(HttpContext context)
{
- // Check the authentication header
- if (!IsValidAuthentication(context))
+ if (!TryAcceptRequest(context))
{
- Log("Authorization failure");
- context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return;
}
- else if (!context.WebSockets.IsWebSocketRequest)
+
+ if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
return;
@@ -323,6 +322,11 @@ private bool IsValidAuthentication(HttpContext context)
private async Task HandleStartSessionRequestAsync(HttpContext context)
{
+ if (!TryAcceptRequest(context))
+ {
+ return;
+ }
+
string? projectPath = null;
try
@@ -333,7 +337,7 @@ private async Task HandleStartSessionRequestAsync(HttpContext context)
}
// Get the project launch request data
- var projectLaunchRequest = await context.GetProjectLaunchInformationAsync(_shutdownCancellationTokenSource.Token);
+ var projectLaunchRequest = await context.GetProjectLaunchInformationAsync(_shutdownCancellationSource.Token);
if (projectLaunchRequest == null)
{
// Unknown or unsupported version
@@ -343,7 +347,7 @@ private async Task HandleStartSessionRequestAsync(HttpContext context)
projectPath = projectLaunchRequest.ProjectPath;
- var sessionId = await _aspireServerEvents.StartProjectAsync(context.GetDcpId(), projectLaunchRequest, _shutdownCancellationTokenSource.Token);
+ var sessionId = await _aspireServerEvents.StartProjectAsync(context.GetDcpId(), projectLaunchRequest, _shutdownCancellationSource.Token);
context.Response.StatusCode = (int)HttpStatusCode.Created;
context.Response.Headers.Location = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}/{sessionId}";
@@ -370,14 +374,14 @@ private async Task WriteResponseTextAsync(HttpResponse response, Exception ex, b
Error = new ErrorDetail { ErrorCode = errorCode, Message = ex.GetMessageFromException() }
};
- await response.WriteAsJsonAsync(error, JsonSerializerOptions, _shutdownCancellationTokenSource.Token);
+ await response.WriteAsJsonAsync(error, JsonSerializerOptions, _shutdownCancellationSource.Token);
}
else
{
errorResponse = Encoding.UTF8.GetBytes(ex.GetMessageFromException());
response.ContentType = "text/plain";
response.ContentLength = errorResponse.Length;
- await response.WriteAsync(ex.GetMessageFromException(), _shutdownCancellationTokenSource.Token);
+ await response.WriteAsync(ex.GetMessageFromException(), _shutdownCancellationSource.Token);
}
}
@@ -394,7 +398,7 @@ private async ValueTask SendMessageAsync(string dcpId, byte[] messageBytes
try
{
using var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(
- cancellationToken, _shutdownCancellationTokenSource.Token, connection.HttpRequestAborted);
+ cancellationToken, _shutdownCancellationSource.Token, connection.HttpRequestAborted);
await _webSocketAccess.WaitAsync(cancelTokenSource.Token);
await connection.Socket.SendAsync(new ArraySegment(messageBytes), WebSocketMessageType.Text, endOfMessage: true, cancelTokenSource.Token);
@@ -415,8 +419,13 @@ private async ValueTask SendMessageAsync(string dcpId, byte[] messageBytes
return success;
}
- private async ValueTask HandleStopSessionRequestAsync(HttpContext context, string sessionId)
+ private async Task HandleStopSessionRequestAsync(HttpContext context, string sessionId)
{
+ if (!TryAcceptRequest(context))
+ {
+ return;
+ }
+
try
{
if (_isDisposed)
@@ -424,7 +433,7 @@ private async ValueTask HandleStopSessionRequestAsync(HttpContext context, strin
throw new ObjectDisposedException(nameof(AspireServerService), "Received 'DELETE /run_session' request after the service has been disposed.");
}
- var sessionExists = await _aspireServerEvents.StopSessionAsync(context.GetDcpId(), sessionId, _shutdownCancellationTokenSource.Token);
+ var sessionExists = await _aspireServerEvents.StopSessionAsync(context.GetDcpId(), sessionId, _shutdownCancellationSource.Token);
context.Response.StatusCode = (int)(sessionExists ? HttpStatusCode.OK : HttpStatusCode.NoContent);
}
catch (Exception e) when (e is not OperationCanceledException)
diff --git a/src/Dotnet.Watch/HotReloadClient/ClientTransport.cs b/src/Dotnet.Watch/HotReloadClient/ClientTransport.cs
index 0422348dc203..660c6c4e5510 100644
--- a/src/Dotnet.Watch/HotReloadClient/ClientTransport.cs
+++ b/src/Dotnet.Watch/HotReloadClient/ClientTransport.cs
@@ -44,5 +44,10 @@ internal abstract class ClientTransport : IDisposable
///
public abstract ValueTask ReadAsync(CancellationToken cancellationToken);
+ ///
+ /// True if and indicate an expected connection termination signal.
+ ///
+ public abstract bool IsExpectedConnectionTermination(Exception exception, CancellationToken cancellationToken);
+
public abstract void Dispose();
}
diff --git a/src/Dotnet.Watch/HotReloadClient/DefaultHotReloadClient.cs b/src/Dotnet.Watch/HotReloadClient/DefaultHotReloadClient.cs
index c07dc05444fe..7597541de4ec 100644
--- a/src/Dotnet.Watch/HotReloadClient/DefaultHotReloadClient.cs
+++ b/src/Dotnet.Watch/HotReloadClient/DefaultHotReloadClient.cs
@@ -82,7 +82,7 @@ async Task> ConnectAsync()
{
// Don't report a warning when cancelled. The process has terminated or the host is shutting down in that case.
// Best effort: There is an inherent race condition due to time between the process exiting and the cancellation token triggering.
- if (!cancellationToken.IsCancellationRequested)
+ if (!transport.IsExpectedConnectionTermination(e, cancellationToken))
{
Logger.LogError("Failed to read capabilities: {Message}", e.Message);
}
@@ -127,7 +127,7 @@ private async Task ListenForResponsesAsync(CancellationToken cancellationToken)
}
catch (Exception e) when (e is not OperationCanceledException)
{
- if (!cancellationToken.IsCancellationRequested)
+ if (!transport.IsExpectedConnectionTermination(e, cancellationToken))
{
Logger.LogError("Failed to read response: {Exception}", e.ToString());
}
diff --git a/src/Dotnet.Watch/HotReloadClient/NamedPipeClientTransport.cs b/src/Dotnet.Watch/HotReloadClient/NamedPipeClientTransport.cs
index 0328c5aa4aca..488d4aaa3bde 100644
--- a/src/Dotnet.Watch/HotReloadClient/NamedPipeClientTransport.cs
+++ b/src/Dotnet.Watch/HotReloadClient/NamedPipeClientTransport.cs
@@ -50,30 +50,14 @@ public override void ConfigureEnvironment(IDictionary env)
public override async Task WaitForConnectionAsync(CancellationToken cancellationToken)
{
_logger.LogDebug("Waiting for application to connect to pipe '{NamedPipeName}'.", _namedPipeName);
-
- try
- {
- await _pipe.WaitForConnectionAsync(cancellationToken);
- }
- catch (Exception e) when (e is not OperationCanceledException)
- {
- // The process may die while we're waiting for the connection and the pipe may be disposed.
- // Log and let subsequent ReadAsync return null gracefully.
- if (IsExpectedPipeException(e, cancellationToken))
- {
- _logger.LogDebug("Pipe connection ended: {Message}", e.Message);
- return;
- }
-
- throw;
- }
+ await _pipe.WaitForConnectionAsync(cancellationToken);
}
///
/// Returns true if the exception is expected when the pipe is disposed or the process has terminated.
/// On Unix named pipes can also throw SocketException with ErrorCode 125 (Operation canceled) when disposed.
///
- private static bool IsExpectedPipeException(Exception e, CancellationToken cancellationToken)
+ public override bool IsExpectedConnectionTermination(Exception e, CancellationToken cancellationToken)
{
return e is ObjectDisposedException or EndOfStreamException or SocketException { ErrorCode: 125 }
|| cancellationToken.IsCancellationRequested;
@@ -93,16 +77,8 @@ public override async ValueTask WriteAsync(byte type, Func ReadAsync(CancellationToken cancellationToken)
{
- try
- {
- var type = (ResponseType)await _pipe.ReadByteAsync(cancellationToken);
- return new ClientTransportResponse(type, _pipe, disposeStream: false);
- }
- catch (Exception e) when (e is not OperationCanceledException && IsExpectedPipeException(e, cancellationToken))
- {
- // Pipe has been disposed or the process has terminated.
- return null;
- }
+ var type = (ResponseType)await _pipe.ReadByteAsync(cancellationToken);
+ return new ClientTransportResponse(type, _pipe, disposeStream: false);
}
public override void Dispose()
diff --git a/src/Dotnet.Watch/HotReloadClient/Web/KestrelWebSocketServer.cs b/src/Dotnet.Watch/HotReloadClient/Web/KestrelWebSocketServer.cs
index ec6033f648c6..bfcceeb50a9b 100644
--- a/src/Dotnet.Watch/HotReloadClient/Web/KestrelWebSocketServer.cs
+++ b/src/Dotnet.Watch/HotReloadClient/Web/KestrelWebSocketServer.cs
@@ -39,10 +39,6 @@ public ImmutableArray ServerUrls
///
/// Starts the Kestrel WebSocket server.
///
- /// Host name to bind to
- /// HTTP port to bind to (0 for auto-assign)
- /// HTTPS port to bind to in addition to HTTP port. Null to skip HTTPS.
- /// Cancellation token
public static async ValueTask StartServerAsync(WebSocketConfig config, RequestDelegate requestHandler, CancellationToken cancellationToken)
{
var host = new HostBuilder()
diff --git a/src/Dotnet.Watch/HotReloadClient/WebSocketClientTransport.cs b/src/Dotnet.Watch/HotReloadClient/WebSocketClientTransport.cs
index 91bff33e95e0..6bcc49d6e7fe 100644
--- a/src/Dotnet.Watch/HotReloadClient/WebSocketClientTransport.cs
+++ b/src/Dotnet.Watch/HotReloadClient/WebSocketClientTransport.cs
@@ -43,6 +43,9 @@ public override void Dispose()
_handler.Dispose();
}
+ public override bool IsExpectedConnectionTermination(Exception exception, CancellationToken cancellationToken)
+ => cancellationToken.IsCancellationRequested;
+
///
/// Creates and starts a new instance.
///
diff --git a/src/Dotnet.Watch/Watch.Aspire/Server/AspireServerLauncher.cs b/src/Dotnet.Watch/Watch.Aspire/Server/AspireServerLauncher.cs
index 00d40cfecd85..ed077b3b03e5 100644
--- a/src/Dotnet.Watch/Watch.Aspire/Server/AspireServerLauncher.cs
+++ b/src/Dotnet.Watch/Watch.Aspire/Server/AspireServerLauncher.cs
@@ -3,12 +3,6 @@
using System.Collections.Immutable;
using System.CommandLine;
-using System.Threading.Channels;
-using Microsoft.CodeAnalysis.Elfie.Diagnostics;
-using Microsoft.DotNet.HotReload;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Logging.Abstractions;
-using static System.Runtime.InteropServices.JavaScript.JSType;
namespace Microsoft.DotNet.Watch;
diff --git a/src/Dotnet.Watch/Watch.Aspire/Server/AspireWatcherLauncher.cs b/src/Dotnet.Watch/Watch.Aspire/Server/AspireWatcherLauncher.cs
index 0ccc26894288..389bb872cc07 100644
--- a/src/Dotnet.Watch/Watch.Aspire/Server/AspireWatcherLauncher.cs
+++ b/src/Dotnet.Watch/Watch.Aspire/Server/AspireWatcherLauncher.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Immutable;
-using Microsoft.CodeAnalysis.Elfie.Diagnostics;
using Microsoft.Extensions.Logging;
namespace Microsoft.DotNet.Watch;
diff --git a/src/Dotnet.Watch/Watch.Aspire/Server/ProcessLauncherFactory.cs b/src/Dotnet.Watch/Watch.Aspire/Server/ProcessLauncherFactory.cs
index 8bb1e7548f30..145f90e43d35 100644
--- a/src/Dotnet.Watch/Watch.Aspire/Server/ProcessLauncherFactory.cs
+++ b/src/Dotnet.Watch/Watch.Aspire/Server/ProcessLauncherFactory.cs
@@ -7,7 +7,6 @@
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading.Channels;
-using Aspire.Tools.Service;
using Microsoft.CodeAnalysis;
using Microsoft.DotNet.HotReload;
using Microsoft.Extensions.Logging;
diff --git a/src/Dotnet.Watch/Watch.Aspire/Server/StatusReportingLoggerFactory.cs b/src/Dotnet.Watch/Watch.Aspire/Server/StatusReportingLoggerFactory.cs
index e676a2ff0b7f..0e58d1485213 100644
--- a/src/Dotnet.Watch/Watch.Aspire/Server/StatusReportingLoggerFactory.cs
+++ b/src/Dotnet.Watch/Watch.Aspire/Server/StatusReportingLoggerFactory.cs
@@ -7,6 +7,7 @@ namespace Microsoft.DotNet.Watch;
///
/// Intercepts select log messages reported by watch and forwards them to to be sent to an external listener.
+/// Does not own (dispose) and .
///
internal sealed class StatusReportingLoggerFactory(WatchStatusWriter writer, LoggerFactory underlyingFactory) : ILoggerFactory
{
diff --git a/src/Dotnet.Watch/Watch.Aspire/Server/WatchControlReader.cs b/src/Dotnet.Watch/Watch.Aspire/Server/WatchControlReader.cs
index 858ec4f8a9b5..d48ba798b76c 100644
--- a/src/Dotnet.Watch/Watch.Aspire/Server/WatchControlReader.cs
+++ b/src/Dotnet.Watch/Watch.Aspire/Server/WatchControlReader.cs
@@ -46,6 +46,8 @@ public async ValueTask DisposeAsync()
{
// Pipe may already be broken if the server disconnected
}
+
+ _disposalCancellationSource.Dispose();
}
private async Task ListenAsync(CancellationToken cancellationToken)
diff --git a/src/Dotnet.Watch/Watch.Aspire/Server/WatchStatusWriter.cs b/src/Dotnet.Watch/Watch.Aspire/Server/WatchStatusWriter.cs
index a208be7836d5..4b273f54da96 100644
--- a/src/Dotnet.Watch/Watch.Aspire/Server/WatchStatusWriter.cs
+++ b/src/Dotnet.Watch/Watch.Aspire/Server/WatchStatusWriter.cs
@@ -1,12 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Diagnostics;
using System.IO.Pipes;
-using System.Reflection.Metadata;
using System.Text.Json;
using System.Threading.Channels;
-using Microsoft.CodeAnalysis.Elfie.Diagnostics;
using Microsoft.Extensions.Logging;
namespace Microsoft.DotNet.Watch;
diff --git a/src/Dotnet.Watch/Watch.Aspire/Utilities/AspireEnvironmentVariables.cs b/src/Dotnet.Watch/Watch.Aspire/Utilities/AspireEnvironmentVariables.cs
index ffdd4f424a4b..161c736fa30b 100644
--- a/src/Dotnet.Watch/Watch.Aspire/Utilities/AspireEnvironmentVariables.cs
+++ b/src/Dotnet.Watch/Watch.Aspire/Utilities/AspireEnvironmentVariables.cs
@@ -1,10 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System;
-using System.Collections.Generic;
-using System.Text;
-
namespace Microsoft.DotNet.Watch;
internal static class AspireEnvironmentVariables
diff --git a/src/Dotnet.Watch/Watch/Aspire/AspireServiceFactory.cs b/src/Dotnet.Watch/Watch/Aspire/AspireServiceFactory.cs
index 10d46c189096..bd90577d05aa 100644
--- a/src/Dotnet.Watch/Watch/Aspire/AspireServiceFactory.cs
+++ b/src/Dotnet.Watch/Watch/Aspire/AspireServiceFactory.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Immutable;
+using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.Threading.Channels;
@@ -46,7 +47,7 @@ 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;
+ private volatile int _pendingSessionInitializationCount;
// Blocks disposal until no session initialization is in progress.
private readonly SemaphoreSlim _postDisposalSessionInitializationCompleted = new(initialCount: 0, maxCount: 1);
@@ -69,14 +70,16 @@ public async ValueTask DisposeAsync()
_logger.LogDebug("Disposing service factory ...");
- // stop accepting requests - triggers cancellation token for in-flight operations:
- await _service.DisposeAsync();
-
- // should not receive any more requests at this point:
_isDisposed = true;
+ // should not receive any more requests at this point, triggers cancellation of any in-flight operations:
+ _service.InitiateShutdown();
+
// wait for all in-flight process initialization to complete:
- await _postDisposalSessionInitializationCompleted.WaitAsync(CancellationToken.None);
+ if (_pendingSessionInitializationCount > 0)
+ {
+ await _postDisposalSessionInitializationCompleted.WaitAsync(CancellationToken.None);
+ }
// terminate all active sessions:
ImmutableArray sessions;
@@ -91,6 +94,9 @@ public async ValueTask DisposeAsync()
_postDisposalSessionInitializationCompleted.Dispose();
+ // terminate the web server:
+ await _service.DisposeAsync();
+
_logger.LogDebug("Service factory disposed");
}
@@ -102,8 +108,6 @@ public async ValueTask DisposeAsync()
///
async ValueTask IAspireServerEvents.StartProjectAsync(string dcpId, ProjectLaunchRequest projectLaunchInfo, CancellationToken cancellationToken)
{
- ObjectDisposedException.ThrowIf(_isDisposed, this);
-
var projectOptions = GetProjectOptions(projectLaunchInfo);
var sessionId = Interlocked.Increment(ref _sessionIdDispenser).ToString(CultureInfo.InvariantCulture);
await StartProjectAsync(dcpId, sessionId, projectOptions, isRestart: false, cancellationToken);
@@ -112,18 +116,18 @@ 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("[#{SessionId}] Starting: '{Path}'", sessionId, projectOptions.Representation.ProjectOrEntryPointFilePath);
-
- RunningProject? runningProject = null;
- var outputChannel = Channel.CreateUnbounded(s_outputChannelOptions);
-
Interlocked.Increment(ref _pendingSessionInitializationCount);
-
try
{
+ // Abort before launching if cancellation has been requested.
+ // This is important after shutdown has been initiated to avoid creating orphaned processes.
+ cancellationToken.ThrowIfCancellationRequested();
+
+ _logger.LogDebug("[#{SessionId}] Starting: '{Path}'", sessionId, projectOptions.Representation.ProjectOrEntryPointFilePath);
+
+ RunningProject? runningProject = null;
+ var outputChannel = Channel.CreateUnbounded(s_outputChannelOptions);
+
runningProject = await _projectLauncher.TryLaunchProcessAsync(
projectOptions,
onOutput: line =>
@@ -169,34 +173,34 @@ public async ValueTask StartProjectAsync(string dcpId, string sessionId, Project
_sessions[sessionId] = new Session(dcpId, sessionId, runningProject, outputReader);
}
- }
- finally
- {
- if (Interlocked.Decrement(ref _pendingSessionInitializationCount) == 0 && _isDisposed)
- {
- _postDisposalSessionInitializationCompleted.Release();
- }
- }
- _logger.LogDebug("[#{SessionId}] Session started", sessionId);
+ _logger.LogDebug("[#{SessionId}] Session started", sessionId);
- async Task StartChannelReader(CancellationToken cancellationToken)
- {
- try
+ async Task StartChannelReader(CancellationToken cancellationToken)
{
- await foreach (var line in outputChannel.Reader.ReadAllAsync(cancellationToken))
+ try
{
- await _service.NotifyLogMessageAsync(dcpId, sessionId, isStdErr: line.IsError, data: line.Content, cancellationToken);
+ await foreach (var line in outputChannel.Reader.ReadAllAsync(cancellationToken))
+ {
+ await _service.NotifyLogMessageAsync(dcpId, sessionId, isStdErr: line.IsError, data: line.Content, cancellationToken);
+ }
}
- }
- catch (Exception e)
- {
- if (!cancellationToken.IsCancellationRequested)
+ catch (Exception e)
{
- _logger.LogError("Unexpected error reading output of session '{SessionId}': {Exception}", sessionId, e);
+ if (!cancellationToken.IsCancellationRequested)
+ {
+ _logger.LogError("Unexpected error reading output of session '{SessionId}': {Exception}", sessionId, e);
+ }
}
}
}
+ finally
+ {
+ if (Interlocked.Decrement(ref _pendingSessionInitializationCount) == 0 && _isDisposed)
+ {
+ _postDisposalSessionInitializationCompleted.Release();
+ }
+ }
}
///
diff --git a/test/Microsoft.DotNet.HotReload.Test.Utilities/AwaitableProcess.cs b/test/Microsoft.DotNet.HotReload.Test.Utilities/AwaitableProcess.cs
index 9f38f3af4545..756897145d2b 100644
--- a/test/Microsoft.DotNet.HotReload.Test.Utilities/AwaitableProcess.cs
+++ b/test/Microsoft.DotNet.HotReload.Test.Utilities/AwaitableProcess.cs
@@ -140,11 +140,6 @@ public async Task GetRequiredOutputLineAsync(Predicate selector)
disposalCompletionSource.Token,
timeoutCancellation.Token);
- if (!Debugger.IsAttached)
- {
- outputReadCancellation.CancelAfter(s_timeout);
- }
-
try
{
while (!outputReadCancellation.IsCancellationRequested)
diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherTests.cs b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherTests.cs
index 52ac55d6cbdf..0718b7205d0b 100644
--- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherTests.cs
+++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherTests.cs
@@ -2,9 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Immutable;
-using System.CommandLine;
-using Microsoft.AspNetCore.Mvc.RazorPages;
-using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.Logging;
namespace Microsoft.DotNet.Watch.UnitTests;
diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireLauncherTests.cs b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireLauncherTests.cs
index 101319f315b4..2d64735848d0 100644
--- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireLauncherTests.cs
+++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireLauncherTests.cs
@@ -1,12 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Diagnostics;
using System.IO.Pipes;
-using System.Reflection.Metadata;
using System.Text.Json;
-using Elfie.Serialization;
-using Xunit.Runners;
namespace Microsoft.DotNet.Watch.UnitTests;
diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Utilities/PipeUtilities.cs b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Utilities/PipeUtilities.cs
index 3bc100bc1255..a671cc5a2da1 100644
--- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Utilities/PipeUtilities.cs
+++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Utilities/PipeUtilities.cs
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.ComponentModel.DataAnnotations;
using System.IO.Pipes;
using System.Text.Json;
diff --git a/test/dotnet-watch.Tests/HotReload/AspireHotReloadTests.cs b/test/dotnet-watch.Tests/HotReload/AspireHotReloadTests.cs
index 477657ecc0c7..a9db6da9c8cd 100644
--- a/test/dotnet-watch.Tests/HotReload/AspireHotReloadTests.cs
+++ b/test/dotnet-watch.Tests/HotReload/AspireHotReloadTests.cs
@@ -107,7 +107,7 @@ public async Task Aspire_BuildError_ManualRestart()
await App.WaitUntilOutputContains($"[WatchAspire.Web ({tfm})] Exited");
await App.WaitUntilOutputContains($"[WatchAspire.AppHost ({tfm})] Exited");
- await App.WaitUntilOutputContains("dotnet watch ⭐ Waiting for server to shutdown ...");
+ await App.WaitUntilOutputContains("dotnet watch ⭐ Disposing server ...");
// TODO: these are not reliably reported: https://github.com/dotnet/sdk/issues/53308
//await App.WaitUntilOutputContains("dotnet watch ⭐ [#1] Stop session");