Skip to content

Commit 85bf7bf

Browse files
committed
Notify DCP of terminated session when process exits on its own
1 parent d1c9c0c commit 85bf7bf

File tree

23 files changed

+332
-171
lines changed

23 files changed

+332
-171
lines changed

src/BuiltInTools/AspireService/AspireServerService.cs

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ public List<KeyValuePair<string, string>> GetServerConnectionEnvironment()
123123
new(DebugSessionServerCertEnvVar, _certificateEncodedBytes),
124124
];
125125

126+
/// <exception cref="OperationCanceledException"/>
126127
public ValueTask NotifySessionEndedAsync(string dcpId, string sessionId, int processId, int? exitCode, CancellationToken cancelationToken)
127128
=> SendNotificationAsync(
128129
new SessionTerminatedNotification()
@@ -136,6 +137,7 @@ public ValueTask NotifySessionEndedAsync(string dcpId, string sessionId, int pro
136137
sessionId,
137138
cancelationToken);
138139

140+
/// <exception cref="OperationCanceledException"/>
139141
public ValueTask NotifySessionStartedAsync(string dcpId, string sessionId, int processId, CancellationToken cancelationToken)
140142
=> SendNotificationAsync(
141143
new ProcessRestartedNotification()
@@ -148,6 +150,7 @@ public ValueTask NotifySessionStartedAsync(string dcpId, string sessionId, int p
148150
sessionId,
149151
cancelationToken);
150152

153+
/// <exception cref="OperationCanceledException"/>
151154
public ValueTask NotifyLogMessageAsync(string dcpId, string sessionId, bool isStdErr, string data, CancellationToken cancelationToken)
152155
=> SendNotificationAsync(
153156
new ServiceLogsNotification()
@@ -161,23 +164,28 @@ public ValueTask NotifyLogMessageAsync(string dcpId, string sessionId, bool isSt
161164
sessionId,
162165
cancelationToken);
163166

164-
private async ValueTask SendNotificationAsync<TNotification>(TNotification notification, string dcpId, string sessionId, CancellationToken cancelationToken)
167+
/// <exception cref="OperationCanceledException"/>
168+
private async ValueTask SendNotificationAsync<TNotification>(TNotification notification, string dcpId, string sessionId, CancellationToken cancellationToken)
165169
where TNotification : SessionNotification
166170
{
167171
try
168172
{
169-
Log($"[#{sessionId}] Sending '{notification.NotificationType}'");
173+
Log($"[#{sessionId}] Sending '{notification.NotificationType}': {notification}");
170174
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-
}
175+
var success = await SendMessageAsync(dcpId, jsonSerialized, cancellationToken);
176176

177-
bool LogAndPropagate(Exception e)
177+
if (!success)
178+
{
179+
cancellationToken.ThrowIfCancellationRequested();
180+
Log($"[#{sessionId}] Failed to send message: Connection not found (dcpId='{dcpId}').");
181+
}
182+
}
183+
catch (Exception e) when (e is not OperationCanceledException)
178184
{
179-
Log($"[#{sessionId}] Sending '{notification.NotificationType}' failed: {e.Message}");
180-
return false;
185+
if (!cancellationToken.IsCancellationRequested)
186+
{
187+
Log($"[#{sessionId}] Failed to send message: {e.Message}");
188+
}
181189
}
182190
}
183191

@@ -373,15 +381,13 @@ private async Task WriteResponseTextAsync(HttpResponse response, Exception ex, b
373381
}
374382
}
375383

376-
private async Task SendMessageAsync(string dcpId, byte[] messageBytes, CancellationToken cancellationToken)
384+
private async ValueTask<bool> SendMessageAsync(string dcpId, byte[] messageBytes, CancellationToken cancellationToken)
377385
{
378386
// Find the connection for the passed in dcpId
379387
WebSocketConnection? connection = _socketConnectionManager.GetSocketConnection(dcpId);
380388
if (connection is null)
381389
{
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;
390+
return false;
385391
}
386392

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

406412
_webSocketAccess.Release();
407413
}
414+
415+
return success;
408416
}
409417

410418
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: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,7 @@ 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+
await Task.WhenAll(sessions.Select(TerminateSessionAsync)).WaitAsync(cancellationToken);
8986
}
9087

9188
public IEnumerable<(string name, string value)> GetEnvironmentVariables()
@@ -113,14 +110,31 @@ public async ValueTask<RunningProject> StartProjectAsync(string dcpId, string se
113110
var processTerminationSource = new CancellationTokenSource();
114111
var outputChannel = Channel.CreateUnbounded<OutputLine>(s_outputChannelOptions);
115112

116-
var runningProject = await _projectLauncher.TryLaunchProcessAsync(
113+
RunningProject? runningProject = null;
114+
115+
runningProject = await _projectLauncher.TryLaunchProcessAsync(
117116
projectOptions,
118117
processTerminationSource,
119118
onOutput: line =>
120119
{
121120
var writeResult = outputChannel.Writer.TryWrite(line);
122121
Debug.Assert(writeResult);
123122
},
123+
onExit: async (processId, exitCode) =>
124+
{
125+
// The process might have been terminated before initialized, in which case runningProject is null.
126+
if (runningProject?.IsRestarting == false)
127+
{
128+
try
129+
{
130+
await _service.NotifySessionEndedAsync(dcpId, sessionId, processId, exitCode, cancellationToken);
131+
}
132+
catch (OperationCanceledException)
133+
{
134+
// canceled on shutdown, ignore
135+
}
136+
}
137+
},
124138
restartOperation: cancellationToken =>
125139
StartProjectAsync(dcpId, sessionId, projectOptions, isRestart: true, cancellationToken),
126140
cancellationToken);
@@ -134,7 +148,7 @@ public async ValueTask<RunningProject> StartProjectAsync(string dcpId, string se
134148
await _service.NotifySessionStartedAsync(dcpId, sessionId, runningProject.ProcessId, cancellationToken);
135149

136150
// cancel reading output when the process terminates:
137-
var outputReader = StartChannelReader(processTerminationSource.Token);
151+
var outputReader = StartChannelReader(runningProject.ProcessExitedSource.Token);
138152

139153
lock (_guard)
140154
{
@@ -159,7 +173,7 @@ async Task StartChannelReader(CancellationToken cancellationToken)
159173
}
160174
catch (Exception e)
161175
{
162-
if (e is not OperationCanceledException)
176+
if (!cancellationToken.IsCancellationRequested)
163177
{
164178
_logger.LogError("Unexpected error reading output of session '{SessionId}': {Exception}", sessionId, e);
165179
}
@@ -185,18 +199,15 @@ async ValueTask<bool> IAspireServerEvents.StopSessionAsync(string dcpId, string
185199
_sessions.Remove(sessionId);
186200
}
187201

188-
await TerminateSessionAsync(session, cancellationToken);
202+
await TerminateSessionAsync(session);
189203
return true;
190204
}
191205

192-
private async ValueTask TerminateSessionAsync(Session session, CancellationToken cancellationToken)
206+
private async Task TerminateSessionAsync(Session session)
193207
{
194208
_logger.LogDebug("Stop session #{SessionId}", session.Id);
195209

196-
var exitCode = await _projectLauncher.TerminateProcessAsync(session.RunningProject, cancellationToken);
197-
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);
210+
await session.RunningProject.TerminateAsync();
200211

201212
// process termination should cancel output reader task:
202213
await session.OutputReader;

0 commit comments

Comments
 (0)