Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aspire notifications #44348

Merged
merged 4 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}
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
Loading