Skip to content

Commit

Permalink
Aspire notifications (#44348)
Browse files Browse the repository at this point in the history
  • Loading branch information
tmat authored Oct 31, 2024
2 parents 1ecd257 + e8d5736 commit 2407595
Show file tree
Hide file tree
Showing 24 changed files with 707 additions and 396 deletions.
81 changes: 19 additions & 62 deletions src/BuiltInTools/AspireService/AspireServerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,6 @@ internal partial class AspireServerService : IAsyncDisposable

private readonly SocketConnectionManager _socketConnectionManager = new();

// lock on access:
private readonly HashSet<string> _activeSessions = [];

private volatile bool _isDisposed;

private static readonly char[] s_charSeparator = { ' ' };
Expand Down Expand Up @@ -110,25 +107,6 @@ public async ValueTask DisposeAsync()

_isDisposed = true;

ImmutableArray<string> activeSessions;
lock (_activeSessions)
{
activeSessions = [.. _activeSessions];
_activeSessions.Clear();
}

if (activeSessions is [])
{
Log("All sessions stopped.");
}
else
{
foreach (var activeSession in activeSessions)
{
Log($"DCP failed to stop session ${activeSession}.");
}
}

_socketConnectionManager.Dispose();
_certificate.Dispose();
_shutdownCancellationTokenSource.Dispose();
Expand All @@ -143,7 +121,7 @@ public List<KeyValuePair<string, string>> GetServerConnectionEnvironment()
new(DebugSessionServerCertEnvVar, _certificateEncodedBytes),
];

public ValueTask NotifySessionEndedAsync(string dcpId, string sessionId, int processId, int exitCode, CancellationToken cancelationToken)
public ValueTask NotifySessionEndedAsync(string dcpId, string sessionId, int processId, int? exitCode, CancellationToken cancelationToken)
=> SendNotificationAsync(
new SessionTerminatedNotification()
{
Expand Down Expand Up @@ -186,7 +164,7 @@ private async ValueTask SendNotificationAsync<TNotification>(TNotification notif
{
try
{
Log($"Sending '{notification.NotificationType}' for session {sessionId}");
Log($"[#{sessionId}] Sending '{notification.NotificationType}'");
var jsonSerialized = JsonSerializer.SerializeToUtf8Bytes(notification, JsonSerializerOptions);
await SendMessageAsync(dcpId, jsonSerialized, cancelationToken);
}
Expand All @@ -196,7 +174,7 @@ private async ValueTask SendNotificationAsync<TNotification>(TNotification notif

bool LogAndPropagate(Exception e)
{
Log($"Sending notification '{notification.NotificationType}' failed: {e.Message}");
Log($"[#{sessionId}] Sending '{notification.NotificationType}' failed: {e.Message}");
return false;
}
}
Expand Down Expand Up @@ -355,15 +333,7 @@ private async Task HandleStartSessionRequestAsync(HttpContext context)

projectPath = projectLaunchRequest.ProjectPath;

var sessionId = await LaunchProjectAsync(context.GetDcpId(), projectLaunchRequest);

lock (_activeSessions)
{
if (!_activeSessions.Add(sessionId))
{
throw new InvalidOperationException($"Session '{sessionId}' already started.");
}
}
var sessionId = await _aspireServerEvents.StartProjectAsync(context.GetDcpId(), projectLaunchRequest, _shutdownCancellationTokenSource.Token);

context.Response.StatusCode = (int)HttpStatusCode.Created;
context.Response.Headers.Location = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}/{sessionId}";
Expand Down Expand Up @@ -412,22 +382,25 @@ private async Task SendMessageAsync(string dcpId, byte[] messageBytes, Cancellat
return;
}

var success = false;
try
{
using var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _shutdownCancellationTokenSource.Token,
connection.HttpRequestAborted);
using var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken, _shutdownCancellationTokenSource.Token, connection.HttpRequestAborted);

await _webSocketAccess.WaitAsync(cancelTokenSource.Token);
await connection.Socket.SendAsync(new ArraySegment<byte>(messageBytes), WebSocketMessageType.Text, endOfMessage: true, cancelTokenSource.Token);
}
catch (Exception ex)
{
// If the connection throws it almost certainly means the client has gone away, so clean up that connection
_socketConnectionManager.RemoveSocketConnection(connection);
Log($"Send message failure: {ex.GetMessageFromException()}");
throw;

success = true;
}
finally
{
if (!success)
{
// If the connection throws it almost certainly means the client has gone away, so clean up that connection
_socketConnectionManager.RemoveSocketConnection(connection);
}

_webSocketAccess.Release();
}
}
Expand All @@ -441,31 +414,15 @@ private async ValueTask HandleStopSessionRequestAsync(HttpContext context, strin
throw new ObjectDisposedException(nameof(AspireServerService), "Received 'DELETE /run_session' request after the service has been disposed.");
}

lock (_activeSessions)
{
if (!_activeSessions.Remove(sessionId))
{
context.Response.StatusCode = (int)HttpStatusCode.NoContent;
return;
}
}

await _aspireServerEvents.StopSessionAsync(context.GetDcpId(), sessionId, _shutdownCancellationTokenSource.Token);
context.Response.StatusCode = (int)HttpStatusCode.OK;
var sessionExists = await _aspireServerEvents.StopSessionAsync(context.GetDcpId(), sessionId, _shutdownCancellationTokenSource.Token);
context.Response.StatusCode = (int)(sessionExists ? HttpStatusCode.OK : HttpStatusCode.NoContent);
}
catch (Exception e)
{
Log($"Failed to stop session '{sessionId}': {e}");
Log($"[#{sessionId}] Failed to stop: {e}");

context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
await WriteResponseTextAsync(context.Response, e, context.GetApiVersion() is not null);
}
}

/// <summary>
/// Called to launch the project after first creating a LaunchProfile from the sessionRequest object. Returns the sessionId
/// for the launched process. If it throws an exception most likely the project couldn't be launched
/// </summary>
private Task<string> LaunchProjectAsync(string dcpId, ProjectLaunchRequest projectLaunchInfo)
=> _aspireServerEvents.StartProjectAsync(dcpId, projectLaunchInfo, _shutdownCancellationTokenSource.Token).AsTask();
}
12 changes: 6 additions & 6 deletions src/BuiltInTools/AspireService/Contracts/IAspireServerEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@

namespace Microsoft.WebTools.AspireServer.Contracts;

/// <summary>
/// Interface implemented on the VS side and pass
/// </summary>
internal interface IAspireServerEvents
{
/// <summary>
/// Called when a request to stop a session is received.
/// </summary>
/// <param name="sessionId">The id of the session to terminate. The session might have been stopped already.</param>
/// <param name="dcpId">DCP/AppHost making the request. May be empty for older DCP versions.</param>
ValueTask StopSessionAsync(string dcpId, string sessionId, CancellationToken cancelToken);
/// <returns>Returns false if the session is not active.</returns>
ValueTask<bool> StopSessionAsync(string dcpId, string sessionId, CancellationToken cancellationToken);

/// <summary>
/// Called when a request to start a project is received. Returns the sessionId of the started project.
/// Called when a request to start a project is received. Returns the session id of the started project.
/// </summary>
/// <param name="dcpId">DCP/AppHost making the request. May be empty for older DCP versions.</param>
ValueTask<string> StartProjectAsync(string dcpId, ProjectLaunchRequest projectLaunchInfo, CancellationToken cancelToken);
/// <returns>New unique session id.</returns>
ValueTask<string> StartProjectAsync(string dcpId, ProjectLaunchRequest projectLaunchInfo, CancellationToken cancellationToken);
}

internal class ProjectLaunchRequest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ internal sealed class SessionTerminatedNotification : SessionNotification
/// </summary>
[Required]
[JsonPropertyName("exit_code")]
public required int ExitCode { get; init; }
public required int? ExitCode { get; init; }
}

/// <summary>
Expand Down

This file was deleted.

Loading

0 comments on commit 2407595

Please sign in to comment.