Skip to content

Commit 3bca85d

Browse files
committed
2
1 parent 268ff97 commit 3bca85d

File tree

25 files changed

+356
-167
lines changed

25 files changed

+356
-167
lines changed

src/BuiltInTools/AspireService/AspireServerService.cs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -161,23 +161,27 @@ public ValueTask NotifyLogMessageAsync(string dcpId, string sessionId, bool isSt
161161
sessionId,
162162
cancelationToken);
163163

164-
private async ValueTask SendNotificationAsync<TNotification>(TNotification notification, string dcpId, string sessionId, CancellationToken cancelationToken)
164+
private async ValueTask SendNotificationAsync<TNotification>(TNotification notification, string dcpId, string sessionId, CancellationToken cancellationToken)
165165
where TNotification : SessionNotification
166166
{
167167
try
168168
{
169-
Log($"[#{sessionId}] Sending '{notification.NotificationType}'");
169+
Log($"[#{sessionId}] Sending '{notification.NotificationType}': {notification}");
170170
var jsonSerialized = JsonSerializer.SerializeToUtf8Bytes(notification, JsonSerializerOptions);
171-
await SendMessageAsync(dcpId, jsonSerialized, cancelationToken);
172-
}
173-
catch (Exception e) when (e is not OperationCanceledException && LogAndPropagate(e))
174-
{
175-
}
171+
var success = await SendMessageAsync(dcpId, jsonSerialized, cancellationToken);
176172

177-
bool LogAndPropagate(Exception e)
173+
if (!success)
174+
{
175+
cancellationToken.ThrowIfCancellationRequested();
176+
Log($"[#{sessionId}] Failed to send message: Connection not found (dcpId='{dcpId}').");
177+
}
178+
}
179+
catch (Exception e) when (e is not OperationCanceledException)
178180
{
179-
Log($"[#{sessionId}] Sending '{notification.NotificationType}' failed: {e.Message}");
180-
return false;
181+
if (!cancellationToken.IsCancellationRequested)
182+
{
183+
Log($"[#{sessionId}] Failed to send message: {e.Message}");
184+
}
181185
}
182186
}
183187

@@ -373,15 +377,13 @@ private async Task WriteResponseTextAsync(HttpResponse response, Exception ex, b
373377
}
374378
}
375379

376-
private async Task SendMessageAsync(string dcpId, byte[] messageBytes, CancellationToken cancellationToken)
380+
private async ValueTask<bool> SendMessageAsync(string dcpId, byte[] messageBytes, CancellationToken cancellationToken)
377381
{
378382
// Find the connection for the passed in dcpId
379383
WebSocketConnection? connection = _socketConnectionManager.GetSocketConnection(dcpId);
380384
if (connection is null)
381385
{
382-
// Most likely the connection has already gone away
383-
Log($"Send message failure: Connection with the following dcpId was not found {dcpId}");
384-
return;
386+
return false;
385387
}
386388

387389
var success = false;
@@ -405,6 +407,8 @@ private async Task SendMessageAsync(string dcpId, byte[] messageBytes, Cancellat
405407

406408
_webSocketAccess.Release();
407409
}
410+
411+
return success;
408412
}
409413

410414
private async ValueTask HandleStopSessionRequestAsync(HttpContext context, string sessionId)

src/BuiltInTools/AspireService/Models/SessionChangeNotification.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ internal sealed class SessionTerminatedNotification : SessionNotification
5656
[Required]
5757
[JsonPropertyName("exit_code")]
5858
public required int? ExitCode { get; init; }
59+
60+
public override string ToString()
61+
=> $"pid={Pid}, exit_code={ExitCode}";
5962
}
6063

6164
/// <summary>
@@ -70,6 +73,9 @@ internal sealed class ProcessRestartedNotification : SessionNotification
7073
[Required]
7174
[JsonPropertyName("pid")]
7275
public required int PID { get; init; }
76+
77+
public override string ToString()
78+
=> $"pid={PID}";
7379
}
7480

7581
/// <summary>
@@ -91,4 +97,7 @@ internal sealed class ServiceLogsNotification : SessionNotification
9197
[Required]
9298
[JsonPropertyName("log_message")]
9399
public required string LogMessage { get; init; }
100+
101+
public override string ToString()
102+
=> $"log_message='{LogMessage}', is_std_err={IsStdErr}";
94103
}

src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,10 @@ private Task<ImmutableArray<string>> GetCapabilitiesTask()
9696

9797
[MemberNotNull(nameof(_pipe))]
9898
[MemberNotNull(nameof(_capabilitiesTask))]
99-
private void RequireReadyForUpdates()
99+
private void RequireReadyForUpdates(CancellationToken cancellationToken)
100100
{
101+
cancellationToken.ThrowIfCancellationRequested();
102+
101103
// should only be called after connection has been created:
102104
_ = GetCapabilitiesTask();
103105

@@ -126,7 +128,7 @@ private ResponseLoggingLevel ResponseLoggingLevel
126128

127129
public override async Task<ApplyStatus> ApplyManagedCodeUpdatesAsync(ImmutableArray<HotReloadManagedCodeUpdate> updates, bool isProcessSuspended, CancellationToken cancellationToken)
128130
{
129-
RequireReadyForUpdates();
131+
RequireReadyForUpdates(cancellationToken);
130132

131133
if (_managedCodeUpdateFailedOrCancelled)
132134
{
@@ -152,7 +154,12 @@ public override async Task<ApplyStatus> ApplyManagedCodeUpdatesAsync(ImmutableAr
152154
{
153155
if (!success)
154156
{
155-
Logger.LogWarning("Further changes won't be applied to this process.");
157+
// Don't report a warning when cancelled. The process has terminated or the host is shutting down in that case.
158+
if (!cancellationToken.IsCancellationRequested)
159+
{
160+
Logger.LogWarning("Further changes won't be applied to this process.");
161+
}
162+
156163
_managedCodeUpdateFailedOrCancelled = true;
157164
DisposePipe();
158165
}
@@ -183,7 +190,7 @@ public async override Task<ApplyStatus> ApplyStaticAssetUpdatesAsync(ImmutableAr
183190
return ApplyStatus.AllChangesApplied;
184191
}
185192

186-
RequireReadyForUpdates();
193+
RequireReadyForUpdates(cancellationToken);
187194

188195
var appliedUpdateCount = 0;
189196

@@ -241,7 +248,7 @@ async ValueTask<bool> SendAndReceiveAsync(int batchId, CancellationToken cancell
241248

242249
Logger.LogDebug("Update batch #{UpdateId} failed.", batchId);
243250
}
244-
catch (Exception e) when (e is not OperationCanceledException || isProcessSuspended)
251+
catch (Exception e)
245252
{
246253
if (cancellationToken.IsCancellationRequested)
247254
{
@@ -282,7 +289,7 @@ private async ValueTask<bool> ReceiveUpdateResponseAsync(CancellationToken cance
282289

283290
public override async Task InitialUpdatesAppliedAsync(CancellationToken cancellationToken)
284291
{
285-
RequireReadyForUpdates();
292+
RequireReadyForUpdates(cancellationToken);
286293

287294
if (_managedCodeUpdateFailedOrCancelled)
288295
{
@@ -299,7 +306,7 @@ public override async Task InitialUpdatesAppliedAsync(CancellationToken cancella
299306
// pipe might throw another exception when forcibly closed on process termination:
300307
if (!cancellationToken.IsCancellationRequested)
301308
{
302-
Logger.LogError("Failed to send InitialUpdatesCompleted: {Message}", e.Message);
309+
Logger.LogError("Failed to send {RequestType}: {Message}", nameof(RequestType.InitialUpdatesCompleted), e.Message);
303310
}
304311
}
305312
}

src/BuiltInTools/dotnet-watch/Aspire/AspireServiceFactory.cs

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,9 @@ public async ValueTask TerminateLaunchedProcessesAsync(CancellationToken cancell
8282
_sessions.Clear();
8383
}
8484

85-
foreach (var session in sessions)
86-
{
87-
await TerminateSessionAsync(session, cancellationToken);
88-
}
85+
_logger.LogDebug($"[DBG] Terminating {sessions.Length} sessions ...");
86+
await Task.WhenAll(sessions.Select(TerminateSessionAsync)).WaitAsync(cancellationToken);
87+
_logger.LogDebug($"[DBG] All sessions terminated.");
8988
}
9089

9190
public IEnumerable<(string name, string value)> GetEnvironmentVariables()
@@ -113,14 +112,24 @@ public async ValueTask<RunningProject> StartProjectAsync(string dcpId, string se
113112
var processTerminationSource = new CancellationTokenSource();
114113
var outputChannel = Channel.CreateUnbounded<OutputLine>(s_outputChannelOptions);
115114

116-
var runningProject = await _projectLauncher.TryLaunchProcessAsync(
115+
RunningProject? runningProject = null;
116+
117+
runningProject = await _projectLauncher.TryLaunchProcessAsync(
117118
projectOptions,
118119
processTerminationSource,
119120
onOutput: line =>
120121
{
121122
var writeResult = outputChannel.Writer.TryWrite(line);
122123
Debug.Assert(writeResult);
123124
},
125+
onExit: async (processId, exitCode) =>
126+
{
127+
// The process might have been terminated before initialized, in which case runningProject is null.
128+
if (runningProject?.IsRestarting == false)
129+
{
130+
await _service.NotifySessionEndedAsync(dcpId, sessionId, processId, exitCode, cancellationToken);
131+
}
132+
},
124133
restartOperation: cancellationToken =>
125134
StartProjectAsync(dcpId, sessionId, projectOptions, isRestart: true, cancellationToken),
126135
cancellationToken);
@@ -134,7 +143,7 @@ public async ValueTask<RunningProject> StartProjectAsync(string dcpId, string se
134143
await _service.NotifySessionStartedAsync(dcpId, sessionId, runningProject.ProcessId, cancellationToken);
135144

136145
// cancel reading output when the process terminates:
137-
var outputReader = StartChannelReader(processTerminationSource.Token);
146+
var outputReader = StartChannelReader(runningProject);
138147

139148
lock (_guard)
140149
{
@@ -148,8 +157,14 @@ public async ValueTask<RunningProject> StartProjectAsync(string dcpId, string se
148157
_logger.LogDebug("Session started: #{SessionId}", sessionId);
149158
return runningProject;
150159

151-
async Task StartChannelReader(CancellationToken cancellationToken)
160+
async Task StartChannelReader(RunningProject runningProject)
152161
{
162+
_logger.LogDebug("Channel reader started: #{SessionId}", sessionId);
163+
164+
// cancel reading when the process termination is initiated or the process exits:
165+
using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(runningProject.ProcessTerminationSource.Token, runningProject.ProcessExitedSource.Token);
166+
var cancellationToken = cancellationTokenSource.Token;
167+
153168
try
154169
{
155170
await foreach (var line in outputChannel.Reader.ReadAllAsync(cancellationToken))
@@ -159,11 +174,13 @@ async Task StartChannelReader(CancellationToken cancellationToken)
159174
}
160175
catch (Exception e)
161176
{
162-
if (e is not OperationCanceledException)
177+
if (!cancellationToken.IsCancellationRequested)
163178
{
164179
_logger.LogError("Unexpected error reading output of session '{SessionId}': {Exception}", sessionId, e);
165180
}
166181
}
182+
183+
_logger.LogDebug("Channel reader terminated: #{SessionId}", sessionId);
167184
}
168185
}
169186

@@ -185,21 +202,23 @@ async ValueTask<bool> IAspireServerEvents.StopSessionAsync(string dcpId, string
185202
_sessions.Remove(sessionId);
186203
}
187204

188-
await TerminateSessionAsync(session, cancellationToken);
205+
await TerminateSessionAsync(session);
189206
return true;
190207
}
191208

192-
private async ValueTask TerminateSessionAsync(Session session, CancellationToken cancellationToken)
209+
private async Task TerminateSessionAsync(Session session)
193210
{
194-
_logger.LogDebug("Stop session #{SessionId}", session.Id);
195-
196-
var exitCode = await _projectLauncher.TerminateProcessAsync(session.RunningProject, cancellationToken);
211+
_logger.LogDebug("Terminating session #{SessionId}", session.Id);
197212

198-
// Wait until the started notification has been sent so that we don't send out of order notifications:
199-
await _service.NotifySessionEndedAsync(session.DcpId, session.Id, session.RunningProject.ProcessId, exitCode, cancellationToken);
213+
await session.RunningProject.TerminateAsync();
200214

201215
// process termination should cancel output reader task:
216+
217+
_logger.LogDebug("Waiting for output reader of #{SessionId}", session.Id);
218+
202219
await session.OutputReader;
220+
221+
_logger.LogDebug("Session #{SessionId} terminated", session.Id);
203222
}
204223

205224
private ProjectOptions GetProjectOptions(ProjectLaunchRequest projectLaunchInfo)

0 commit comments

Comments
 (0)