зеркало из https://github.com/dotnet/sdk.git
Add implementation of Aspire service (#41319)
This commit is contained in:
Родитель
9ac963c52f
Коммит
24855ca483
|
@ -76,6 +76,7 @@
|
|||
/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/ @dotnet/aspnet-blazor-eng
|
||||
/src/BuiltInTools/* @tmat @arkalyanms @dotnet/roslyn-ide
|
||||
/src/BuiltInTools/BrowserRefresh @dotnet/aspnet-blazor-eng
|
||||
/src/BuiltInTools/AspireService @BillHiebert @dotnet/aspnet-blazor-eng
|
||||
|
||||
# Compatibility tools owned by runtime team
|
||||
/src/Compatibility/ @dotnet/area-infrastructure-libraries
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
<IsShippingPackage>false</IsShippingPackage>
|
||||
<SdkTargetFramework>net9.0</SdkTargetFramework>
|
||||
<ToolsetTargetFramework>$(SdkTargetFramework)</ToolsetTargetFramework>
|
||||
<VisualStudioServiceTargetFramework>net8.0</VisualStudioServiceTargetFramework>
|
||||
|
||||
<!-- VS for Mac may run on a lower version of .NET than the SDK is targeting, but needs to load the resolvers. So the resolvers and dependencies
|
||||
may target a lower version of .NET -->
|
||||
|
|
11
sdk.sln
11
sdk.sln
|
@ -505,6 +505,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Tools.Test
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EndToEnd-Installer.Tests", "test\EndToEnd-Installer.Tests\EndToEnd-Installer.Tests.csproj", "{149E3D40-8115-4965-9305-5A1ADBF04899}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.WebTools.AspireService.Package", "src\BuiltInTools\AspireService\Microsoft.WebTools.AspireService.Package.csproj", "{19014C60-F87C-4CC7-AC0F-C41B6126EBCE}"
|
||||
EndProject
|
||||
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.WebTools.AspireService", "src\BuiltInTools\AspireService\Microsoft.WebTools.AspireService.shproj", "{94C8526E-DCC2-442F-9868-3DD0BA2688BE}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -963,6 +967,10 @@ Global
|
|||
{149E3D40-8115-4965-9305-5A1ADBF04899}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{149E3D40-8115-4965-9305-5A1ADBF04899}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{149E3D40-8115-4965-9305-5A1ADBF04899}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{19014C60-F87C-4CC7-AC0F-C41B6126EBCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{19014C60-F87C-4CC7-AC0F-C41B6126EBCE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{19014C60-F87C-4CC7-AC0F-C41B6126EBCE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{19014C60-F87C-4CC7-AC0F-C41B6126EBCE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -1140,6 +1148,8 @@ Global
|
|||
{21C21975-84C1-4A24-8E21-F7EC790A4584} = {580D1AE7-AA8F-4912-8B76-105594E00B3B}
|
||||
{6E690AC5-8D00-419F-B5BC-C29D099EDE59} = {580D1AE7-AA8F-4912-8B76-105594E00B3B}
|
||||
{149E3D40-8115-4965-9305-5A1ADBF04899} = {580D1AE7-AA8F-4912-8B76-105594E00B3B}
|
||||
{19014C60-F87C-4CC7-AC0F-C41B6126EBCE} = {71A9F549-0EB6-41F9-BC16-4A6C5007FC91}
|
||||
{94C8526E-DCC2-442F-9868-3DD0BA2688BE} = {71A9F549-0EB6-41F9-BC16-4A6C5007FC91}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {FB8F26CE-4DE6-433F-B32A-79183020BBD6}
|
||||
|
@ -1147,6 +1157,7 @@ Global
|
|||
GlobalSection(SharedMSBuildProjectFiles) = preSolution
|
||||
src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{03c5a84a-982b-4f38-ac73-ab832c645c4a}*SharedItemsImports = 5
|
||||
src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{0a3c9afd-f6e6-4a5d-83fb-93bf66732696}*SharedItemsImports = 5
|
||||
src\BuiltInTools\AspireService\Microsoft.WebTools.AspireService.projitems*{94c8526e-dcc2-442f-9868-3dd0ba2688be}*SharedItemsImports = 13
|
||||
src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{9d36039f-d0a1-462f-85b4-81763c6b02cb}*SharedItemsImports = 13
|
||||
src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{a9103b98-d888-4260-8a05-fa36f640698a}*SharedItemsImports = 5
|
||||
EndGlobalSection
|
||||
|
|
|
@ -0,0 +1,414 @@
|
|||
// 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.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.WebSockets;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.WebTools.AspireServer.Contracts;
|
||||
using Microsoft.WebTools.AspireServer.Helpers;
|
||||
using Microsoft.WebTools.AspireServer.Models;
|
||||
using IAsyncDisposable = System.IAsyncDisposable;
|
||||
|
||||
namespace Microsoft.WebTools.AspireServer;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of the AspireServerService. A new instance of this service will be created for each
|
||||
/// each call to IServiceBroker.CreateProxy()
|
||||
/// </summary>
|
||||
internal partial class AspireServerService : IAsyncDisposable
|
||||
{
|
||||
public const string DebugSessionPortEnvVar = "DEBUG_SESSION_PORT";
|
||||
public const string DebugSessionTokenEnvVar = "DEBUG_SESSION_TOKEN";
|
||||
public const string DebugSessionServerCertEnvVar = "DEBUG_SESSION_SERVER_CERTIFICATE";
|
||||
|
||||
public const int PingIntervalInSeconds = 5;
|
||||
|
||||
private readonly IAspireServerEvents _aspireServerEvents;
|
||||
|
||||
private readonly Action<string>? _tracer;
|
||||
|
||||
private readonly string _currentSecret;
|
||||
private readonly string _displayName;
|
||||
|
||||
private readonly CancellationTokenSource _shutdownCancellationTokenSource = new();
|
||||
private readonly int _port;
|
||||
private readonly X509Certificate2 _certificate;
|
||||
private readonly string _certificateEncodedBytes;
|
||||
|
||||
private readonly SemaphoreSlim _webSocketAccess = new(1);
|
||||
|
||||
private readonly SocketConnectionManager _socketConnectionManager = new();
|
||||
|
||||
private static readonly char[] s_charSeparator = { ' ' };
|
||||
private int _isListening;
|
||||
|
||||
public static readonly JsonSerializerOptions JsonSerializerOptions = new()
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Converters =
|
||||
{
|
||||
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, allowIntegerValues: false)
|
||||
}
|
||||
};
|
||||
|
||||
public AspireServerService(IAspireServerEvents aspireServerEvents, string displayName, Action<string>? tracer)
|
||||
{
|
||||
_aspireServerEvents = aspireServerEvents;
|
||||
_tracer = tracer;
|
||||
_displayName = displayName;
|
||||
|
||||
_port = SocketUtilities.GetNextAvailablePort();
|
||||
|
||||
// Set up the encryption so we can use it to generate our secret.
|
||||
var aes = Aes.Create();
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.KeySize = 128;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
aes.GenerateKey();
|
||||
_currentSecret = Convert.ToBase64String(aes.Key);
|
||||
|
||||
_certificate = CertGenerator.GenerateCert();
|
||||
var certBytes = _certificate.Export(X509ContentType.Cert);
|
||||
_certificateEncodedBytes = Convert.ToBase64String(certBytes);
|
||||
|
||||
// Start the server
|
||||
Initialize();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<KeyValuePair<string, string>>> GetServerConnectionEnvironmentAsync(CancellationToken cancelToken)
|
||||
{
|
||||
return new ValueTask<List<KeyValuePair<string, string>>>(new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>(DebugSessionPortEnvVar,$"localhost:{_port}"),
|
||||
new KeyValuePair<string, string>(DebugSessionTokenEnvVar, _currentSecret),
|
||||
new KeyValuePair<string, string>(DebugSessionServerCertEnvVar, _certificateEncodedBytes),
|
||||
});
|
||||
}
|
||||
|
||||
public async ValueTask SessionEndedAsync(string dcpId, string sessionId, int processId, int? exitCode, CancellationToken cancelToken)
|
||||
{
|
||||
var payload = new SessionChangeNotification()
|
||||
{
|
||||
NotificationType = NotificationType.SessionTerminated,
|
||||
SessionId = sessionId,
|
||||
PID = processId,
|
||||
ExitCode = exitCode
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
LogTrace($"Sending SessionEndedAsync for session {sessionId}");
|
||||
var jsonSerialized = JsonSerializer.SerializeToUtf8Bytes(payload, JsonSerializerOptions);
|
||||
await SendMessageAsync(dcpId, jsonSerialized, cancelToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Send messageAsync can fail if the connection is lost
|
||||
LogTrace($"Sending session ended failed: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask SessionStartedAsync(string dcpId, string sessionId, int processId, CancellationToken cancelToken)
|
||||
{
|
||||
var payload = new SessionChangeNotification()
|
||||
{
|
||||
NotificationType = NotificationType.ProcessRestarted,
|
||||
SessionId = sessionId,
|
||||
PID = processId
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
LogTrace($"Sending SessionStartedAsync for session {sessionId}");
|
||||
var jsonSerialized = JsonSerializer.SerializeToUtf8Bytes(payload, JsonSerializerOptions);
|
||||
await SendMessageAsync(dcpId, jsonSerialized, cancelToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogTrace($"Sending session started failed: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask SendLogMessageAsync(string dcpId, string sessionID, bool isStdErr, string data, CancellationToken cancelToken)
|
||||
{
|
||||
var payload = new SessionLogsNotification()
|
||||
{
|
||||
NotificationType = NotificationType.ServiceLogs,
|
||||
SessionId = sessionID,
|
||||
IsStdErr = isStdErr,
|
||||
LogMessage = data
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var jsonSerialized = JsonSerializer.SerializeToUtf8Bytes(payload, JsonSerializerOptions);
|
||||
await SendMessageAsync(dcpId, jsonSerialized, cancelToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogTrace($"Sending service logs failed {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for a connection so that it can get the WebSocket that will be used to send messages tio the client. It accepts messages via Restful http
|
||||
/// calls.
|
||||
/// </summary>
|
||||
private void StartListening()
|
||||
{
|
||||
var builder = WebApplication.CreateSlimBuilder();
|
||||
|
||||
builder.WebHost.ConfigureKestrel(kestrelOptions =>
|
||||
{
|
||||
kestrelOptions.ListenLocalhost(_port, listenOptions =>
|
||||
{
|
||||
listenOptions.UseHttps(_certificate);
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapGet("/", () => _displayName);
|
||||
app.MapGet(InfoResponse.Url, GetInfoAsync);
|
||||
|
||||
// Set up the run session endpoints
|
||||
var runSessionApi = app.MapGroup(RunSessionRequest.Url);
|
||||
|
||||
runSessionApi.MapPut("/", RunSessionPutAsync);
|
||||
runSessionApi.MapDelete("/{sessionId}", RunSessionDeleteAsync);
|
||||
runSessionApi.Map(SessionNotificationBase.Url, RunSessionNotifyAsync);
|
||||
|
||||
app.UseWebSockets(new WebSocketOptions
|
||||
{
|
||||
KeepAliveInterval = TimeSpan.FromSeconds(PingIntervalInSeconds)
|
||||
});
|
||||
|
||||
// Run the application async. It will shutdown when the cancel token is signaled
|
||||
_ = app.RunAsync(_shutdownCancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
private async Task RunSessionPutAsync(HttpContext context)
|
||||
{
|
||||
// Check the authentication header
|
||||
if (!IsValidAuthentication(context))
|
||||
{
|
||||
LogTrace("Authorization failure");
|
||||
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||
}
|
||||
else
|
||||
{
|
||||
await ProcessStartSessionRequestAsync(context);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunSessionDeleteAsync(HttpContext context, string sessionId)
|
||||
{
|
||||
// Check the authentication header
|
||||
if (!IsValidAuthentication(context))
|
||||
{
|
||||
LogTrace("Authorization failure");
|
||||
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.StatusCode = await HandleStopSessionRequestAsync(context.GetDcpId(), sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GetInfoAsync(HttpContext context)
|
||||
{
|
||||
// Check the authentication header
|
||||
if (!IsValidAuthentication(context))
|
||||
{
|
||||
LogTrace("Authorization failure");
|
||||
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.OK;
|
||||
await context.Response.WriteAsJsonAsync(InfoResponse.Instance, JsonSerializerOptions, _shutdownCancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunSessionNotifyAsync(HttpContext context)
|
||||
{
|
||||
// Check the authentication header
|
||||
if (!IsValidAuthentication(context))
|
||||
{
|
||||
LogTrace("Authorization failure");
|
||||
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||
return;
|
||||
}
|
||||
else if (!context.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
return;
|
||||
}
|
||||
|
||||
var webSocket = await context.WebSockets.AcceptWebSocketAsync();
|
||||
var socketTcs = new TaskCompletionSource();
|
||||
|
||||
// Track this connection.
|
||||
_socketConnectionManager.AddSocketConnection(webSocket, socketTcs, context.GetDcpId(), context.RequestAborted);
|
||||
|
||||
// We must keep the middleware pipeline alive for the duration of the socket
|
||||
await socketTcs.Task;
|
||||
}
|
||||
|
||||
private void LogTrace(string traceMsg)
|
||||
{
|
||||
_tracer?.Invoke($"AspireServer - {traceMsg}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// starts the web server running
|
||||
/// </summary>
|
||||
private void Initialize()
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _isListening, 1, 0) == 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Kick of the web server.
|
||||
StartListening();
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
_socketConnectionManager.Dispose();
|
||||
|
||||
_certificate.Dispose();
|
||||
|
||||
// Shutdown the app
|
||||
_shutdownCancellationTokenSource.Cancel();
|
||||
_shutdownCancellationTokenSource.Dispose();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private bool IsValidAuthentication(HttpContext context)
|
||||
{
|
||||
// Check the authentication header
|
||||
var authHeader = context.Request.Headers.Authorization;
|
||||
if (authHeader.Count == 1)
|
||||
{
|
||||
var authTokens = authHeader[0]!.Split(s_charSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
return authTokens.Length == 2 &&
|
||||
string.Equals(authTokens[0], "Bearer", StringComparison.Ordinal) &&
|
||||
string.Equals(authTokens[1], _currentSecret, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task ProcessStartSessionRequestAsync(HttpContext context)
|
||||
{
|
||||
// Get the project launch request data
|
||||
var projectLaunchRequest = await context.GetProjectLaunchInformationAsync(_shutdownCancellationTokenSource.Token);
|
||||
if (projectLaunchRequest is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sessionId = await LaunchProjectAsync(context.GetDcpId(), projectLaunchRequest);
|
||||
context.Response.StatusCode = (int)HttpStatusCode.Created;
|
||||
context.Response.Headers.Location = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}/{sessionId}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogTrace($"Exception thrown starting project {projectLaunchRequest.ProjectPath}: {ex}");
|
||||
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||
await WriteResponseTextAsync(context.Response, ex, context.GetApiVersion() is not null);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unknown or unsupported version
|
||||
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WriteResponseTextAsync(HttpResponse response, Exception ex, bool useRichErrorResponse)
|
||||
{
|
||||
byte[] errorResponse;
|
||||
if (useRichErrorResponse)
|
||||
{
|
||||
// If the exception is a webtools one, use the failure bucket strings as the error Code
|
||||
string? errorCode = null;
|
||||
|
||||
var error = new ErrorResponse()
|
||||
{
|
||||
Error = new ErrorDetail { ErrorCode = errorCode, Message = ex.GetMessageFromException() }
|
||||
};
|
||||
|
||||
await response.WriteAsJsonAsync(error, JsonSerializerOptions, _shutdownCancellationTokenSource.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
errorResponse = Encoding.UTF8.GetBytes(ex.GetMessageFromException());
|
||||
response.ContentType = "text/plain";
|
||||
response.ContentLength = errorResponse.Length;
|
||||
await response.WriteAsync(ex.GetMessageFromException(), _shutdownCancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendMessageAsync(string dcpId,byte[] messageBytes, CancellationToken cancellationToken)
|
||||
{
|
||||
// Find the connection for the passed in dcpId
|
||||
WebSocketConnection? connection = _socketConnectionManager.GetSocketConnection(dcpId);
|
||||
if (connection is null)
|
||||
{
|
||||
// Most likely the connection has already gone away
|
||||
LogTrace($"Send message failure: Connection with the following dcpId was not found {dcpId}");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
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);
|
||||
LogTrace($"Send message failure: {ex.GetMessageFromException()}");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_webSocketAccess.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<int> HandleStopSessionRequestAsync(string dcpId, string sessionId)
|
||||
{
|
||||
bool sessionExists = await _aspireServerEvents.StopSessionAsync(dcpId, sessionId, _shutdownCancellationTokenSource.Token);
|
||||
|
||||
return (int)(sessionExists ? HttpStatusCode.OK : HttpStatusCode.NoContent);
|
||||
}
|
||||
|
||||
/// <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();
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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. Returns false if the session does not exist. Note that the dcpId identifies
|
||||
/// which DCP/AppHost is making the request.
|
||||
/// </summary>
|
||||
ValueTask<bool> StopSessionAsync(string dcpId, string sessionId, CancellationToken cancelToken);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a request to start a project is received. Returns the sessionId of the started project. Note that the dcpId identifies
|
||||
/// which DCP/AppHost is making the request. The format of this string is <appHostAssemblyName>;<unique string>. The first token can
|
||||
/// be used to identify the AppHost project in the solution. The 2nd is just a unique string so that running the same project multiple times
|
||||
/// generates a unique dcpId. Note that for older DCP's the dcpId will be the empty string
|
||||
/// </summary>
|
||||
ValueTask<string> StartProjectAsync(string dcpId, ProjectLaunchRequest projectLaunchInfo, CancellationToken cancelToken);
|
||||
}
|
||||
|
||||
internal class ProjectLaunchRequest
|
||||
{
|
||||
public string ProjectPath { get; set; } = string.Empty;
|
||||
public bool Debug { get; set; }
|
||||
public IEnumerable<KeyValuePair<string, string>>? Environment { get; set; }
|
||||
public IEnumerable<string>? Arguments { get; set; }
|
||||
public string? LaunchProfile { get; set; }
|
||||
public bool DisableLaunchProfile { get; set; }
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// 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.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace Microsoft.WebTools.AspireServer;
|
||||
|
||||
internal static class CertGenerator
|
||||
{
|
||||
public static X509Certificate2 GenerateCert()
|
||||
{
|
||||
const int rsaKeySize = 2048;
|
||||
var rsa = RSA.Create(rsaKeySize); // Create asymmetric RSA key pair.
|
||||
var req = new CertificateRequest(
|
||||
"cn=debug-session.visualstudio.microsoft.com",
|
||||
rsa,
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pss
|
||||
);
|
||||
|
||||
var sanBuilder = new SubjectAlternativeNameBuilder();
|
||||
sanBuilder.AddDnsName("localhost");
|
||||
req.CertificateExtensions.Add(sanBuilder.Build());
|
||||
|
||||
var cert = req.CreateSelfSigned(
|
||||
DateTimeOffset.UtcNow.AddSeconds(-5),
|
||||
DateTimeOffset.UtcNow.AddDays(7)
|
||||
);
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
// Workaround for Windows S/Channel requirement for storing private for the certificate on disk.
|
||||
// The file will be automatically generated by the following call and disposed when the returned cert is disposed.
|
||||
using (cert)
|
||||
{
|
||||
return new X509Certificate2(cert.Export(X509ContentType.Pfx), "", X509KeyStorageFlags.UserKeySet);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return cert;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.WebTools.AspireServer.Helpers;
|
||||
|
||||
internal static class ExceptionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Given an exception, returns a string which has concatenated the ex.message and inner exception message
|
||||
/// if it exits. If it is an aggregate exception it concatenates all the exceptions that are in the aggregate
|
||||
///</summary>
|
||||
public static string GetMessageFromException(this Exception ex)
|
||||
{
|
||||
string msg = string.Empty;
|
||||
if (ex is AggregateException aggException)
|
||||
{
|
||||
foreach (var e in aggException.Flatten().InnerExceptions)
|
||||
{
|
||||
if (msg == string.Empty)
|
||||
{
|
||||
msg = e.Message;
|
||||
}
|
||||
else
|
||||
{
|
||||
msg += " ";
|
||||
msg += e.Message;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
msg = ex.Message;
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
msg += " ";
|
||||
msg += ex.InnerException.Message;
|
||||
}
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.WebTools.AspireServer.Contracts;
|
||||
using Microsoft.WebTools.AspireServer.Models;
|
||||
|
||||
namespace Microsoft.WebTools.AspireServer.Helpers;
|
||||
|
||||
internal static class HttpContextExtensions
|
||||
{
|
||||
public const string VersionQueryString = "api-version";
|
||||
public const string DCPInstanceIDHeader = "Microsoft-Developer-DCP-Instance-ID";
|
||||
public static DateTime SupportedVersionAsDate = DateTime.Parse(RunSessionRequest.SupportedProtocolVersion);
|
||||
|
||||
public static string? GetApiVersion(this HttpContext context)
|
||||
{
|
||||
return context.Request.Query[VersionQueryString];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks for the dcp instance ID header and returns the id, or the empty string
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetDcpId(this HttpContext context)
|
||||
{
|
||||
// return the header value.
|
||||
var dcpHeader = context.Request.Headers[DCPInstanceIDHeader];
|
||||
if (dcpHeader.Count == 1)
|
||||
{
|
||||
return dcpHeader[0]?? string.Empty;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the payload depending on the protocol version and returns the normalized ProjectLaunchRequest. Returns null if the
|
||||
/// protocol version is not known or older. Throws if there is a serialization failure
|
||||
/// </summary>
|
||||
public static async Task<ProjectLaunchRequest?> GetProjectLaunchInformationAsync(this HttpContext context, CancellationToken cancelToken)
|
||||
{
|
||||
// Get the version querystring if there is one. Reject any requests w/o a supported version
|
||||
var versionString = context.GetApiVersion();
|
||||
if (versionString is not null && DateTime.TryParse(versionString, out var version) && version >= SupportedVersionAsDate)
|
||||
{
|
||||
var runSessionRequest = await context.Request.ReadFromJsonAsync<RunSessionRequest>(AspireServerService.JsonSerializerOptions, cancelToken);
|
||||
return runSessionRequest?.ToProjectLaunchInformation();
|
||||
}
|
||||
|
||||
// Unknown or older version.
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
// 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.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.WebTools.AspireServer;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the set of active socket connections. Since it registers to be notified when a socket has gone bad,
|
||||
/// it also tracks those CancellationTokenRegistration objects so they can be disposed
|
||||
/// </summary>
|
||||
internal class SocketConnectionManager : IDisposable
|
||||
{
|
||||
// Track a single connection per Dcp ID
|
||||
private readonly object _socketConnectionsLock = new();
|
||||
private readonly Dictionary<string, WebSocketConnection> _webSocketConnections = new(StringComparer.Ordinal);
|
||||
|
||||
private void CleanupSocketConnections()
|
||||
{
|
||||
lock (_socketConnectionsLock)
|
||||
{
|
||||
foreach (var connection in _webSocketConnections)
|
||||
{
|
||||
connection.Value.Tcs.SetResult();
|
||||
connection.Value.CancelTokenRegistration.Dispose();
|
||||
}
|
||||
|
||||
_webSocketConnections.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddSocketConnection(WebSocket socket, TaskCompletionSource tcs, string dcpId, CancellationToken httpRequestAborted)
|
||||
{
|
||||
// We only support one connection per DCP Id, therefore if there is
|
||||
// already a connection, drop that one before adding this one
|
||||
lock (_socketConnectionsLock)
|
||||
{
|
||||
if (_webSocketConnections.TryGetValue(dcpId, out var existingConnection))
|
||||
{
|
||||
_webSocketConnections.Remove(dcpId);
|
||||
existingConnection.Dispose();
|
||||
}
|
||||
|
||||
// Register with the cancel token so that if the socket goes bad, we
|
||||
// get notified and can remove it from our list. We need to track the registrations as well
|
||||
// so we can dispose of it later
|
||||
var newConnection = new WebSocketConnection(socket, tcs, dcpId, httpRequestAborted);
|
||||
newConnection.CancelTokenRegistration = httpRequestAborted.Register(() =>
|
||||
{
|
||||
RemoveSocketConnection(newConnection);
|
||||
});
|
||||
|
||||
_webSocketConnections[dcpId] = newConnection;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveSocketConnection(WebSocketConnection connection)
|
||||
{
|
||||
lock (_socketConnectionsLock)
|
||||
{
|
||||
_webSocketConnections.Remove(connection.DcpId);
|
||||
connection.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public WebSocketConnection? GetSocketConnection(string dcpId)
|
||||
{
|
||||
lock (_socketConnectionsLock)
|
||||
{
|
||||
_webSocketConnections.TryGetValue(dcpId, out var connection);
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CleanupSocketConnections();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
// 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.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Microsoft.WebTools.AspireServer.Helpers;
|
||||
|
||||
internal class SocketUtilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Unsafe ports as defined by chrome (http://superuser.com/questions/188058/which-ports-are-considered-unsafe-on-chrome)
|
||||
/// </summary>
|
||||
private static readonly int[] s_unsafePorts = new int[] {
|
||||
2049, // nfs
|
||||
3659, // apple-sasl / PasswordServer
|
||||
4045, // lockd
|
||||
6000, // X11
|
||||
6665, // Alternate IRC [Apple addition]
|
||||
6666, // Alternate IRC [Apple addition]
|
||||
6667, // Standard IRC [Apple addition]
|
||||
6668, // Alternate IRC [Apple addition]
|
||||
6669, // Alternate IRC [Apple addition]
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Get the next available dynamic port
|
||||
/// </summary>
|
||||
public static int GetNextAvailablePort()
|
||||
{
|
||||
var ports = GetNextAvailablePorts(1);
|
||||
return ports == null ? 0 : ports[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of available dynamic ports. Max that can be retrieved is 10
|
||||
/// </summary>
|
||||
public static int[]? GetNextAvailablePorts(int countOfPorts)
|
||||
{
|
||||
// Creates the Socket to send data over a TCP connection.
|
||||
var ports = GetNextAvailablePorts(countOfPorts, AddressFamily.InterNetwork);
|
||||
ports ??= GetNextAvailablePorts(countOfPorts, AddressFamily.InterNetworkV6);
|
||||
return ports;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of available dynamic ports for the addressFamily.
|
||||
/// </summary>
|
||||
private static int[]? GetNextAvailablePorts(int countOfPorts, AddressFamily addressFamily)
|
||||
{
|
||||
// Creates the Socket to send data over a TCP connection.
|
||||
var sockets = new List<Socket>();
|
||||
try
|
||||
{
|
||||
var ports = new int[countOfPorts];
|
||||
for (int i = 0; i < countOfPorts; i++)
|
||||
{
|
||||
Socket socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp);
|
||||
sockets.Add(socket);
|
||||
IPEndPoint endPoint = new IPEndPoint(addressFamily == AddressFamily.InterNetworkV6 ? IPAddress.IPv6Any : IPAddress.Any, 0);
|
||||
socket.Bind(endPoint);
|
||||
var endPointUsed = (IPEndPoint?)socket.LocalEndPoint;
|
||||
if (endPointUsed is not null && !s_unsafePorts.Contains(endPointUsed.Port))
|
||||
{
|
||||
ports[i] = endPointUsed.Port;
|
||||
}
|
||||
else
|
||||
{ // Need to try this one again
|
||||
--i;
|
||||
}
|
||||
}
|
||||
|
||||
return ports;
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var socket in sockets)
|
||||
{
|
||||
socket.Dispose();
|
||||
}
|
||||
sockets.Clear();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
// 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.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.WebTools.AspireServer;
|
||||
|
||||
/// <summary>
|
||||
/// Used by the SocketConnectionManager to track one socket connection. It needs to be disposed when done with it
|
||||
/// </summary>
|
||||
internal class WebSocketConnection : IDisposable
|
||||
{
|
||||
public WebSocketConnection(WebSocket socket, TaskCompletionSource tcs, string dcpId, CancellationToken httpRequestAborted)
|
||||
{
|
||||
Socket = socket;
|
||||
Tcs = tcs;
|
||||
DcpId = dcpId;
|
||||
HttpRequestAborted = httpRequestAborted;
|
||||
}
|
||||
|
||||
public WebSocket Socket { get; }
|
||||
public TaskCompletionSource Tcs { get; }
|
||||
public string DcpId { get; }
|
||||
public CancellationToken HttpRequestAborted { get; }
|
||||
public CancellationTokenRegistration CancelTokenRegistration { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Tcs.SetResult();
|
||||
CancelTokenRegistration.Dispose();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<!-- This source package is used by Visual Studio WebTools -->
|
||||
<TargetFramework>$(VisualStudioServiceTargetFramework)</TargetFramework>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
<DebugType>none</DebugType>
|
||||
<GenerateDependencyFile>false</GenerateDependencyFile>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
|
||||
<!-- NuGet -->
|
||||
<IsPackable>true</IsPackable>
|
||||
<IsSourcePackage>true</IsSourcePackage>
|
||||
<PackageId>Aspire.Tools.Service</PackageId>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<PackageDescription>
|
||||
Package containing sources of a service that implements DCP protocol.
|
||||
</PackageDescription>
|
||||
<!-- Remove once https://github.com/NuGet/Home/issues/8583 is fixed -->
|
||||
<NoWarn>$(NoWarn);NU5128</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<MSBuildAllProjects Condition="'$(MSBuildVersion)' == '' Or '$(MSBuildVersion)' < '16.0'">$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
|
||||
<HasSharedItems>true</HasSharedItems>
|
||||
<SharedGUID>94c8526e-dcc2-442f-9868-3dd0ba2688be</SharedGUID>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Configuration">
|
||||
<Import_RootNamespace>Microsoft.WebTools.AspireService</Import_RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)AspireServerService.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Contracts\IAspireServerEvents.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\CertGenerator.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ExceptionExtensions.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\HttpContextExtensions.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\SocketConnectionManager.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\SocketUtilities.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Helpers\WebSocketConnection.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Models\ErrorResponse.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Models\InfoResponse.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Models\RunSessionRequest.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Models\SessionChangeNotification.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>94c8526e-dcc2-442f-9868-3dd0ba2688be</ProjectGuid>
|
||||
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
|
||||
<PropertyGroup />
|
||||
<Import Project="Microsoft.WebTools.AspireService.projitems" Label="Shared" />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
|
||||
</Project>
|
|
@ -0,0 +1,27 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.WebTools.AspireServer.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Detailed error information serialized into the body of the response
|
||||
/// </summary>
|
||||
internal class ErrorResponse
|
||||
{
|
||||
[JsonPropertyName("error")]
|
||||
public ErrorDetail? Error { get; set; }
|
||||
}
|
||||
|
||||
internal class ErrorDetail
|
||||
{
|
||||
[JsonPropertyName("code")]
|
||||
public string? ErrorCode { get; set; }
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public string? Message { get; set; }
|
||||
|
||||
[JsonPropertyName("details")]
|
||||
public ErrorDetail[]? MessageDetails { get; set; }
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.WebTools.AspireServer.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Response when asked for /info
|
||||
/// </summary>
|
||||
internal class InfoResponse
|
||||
{
|
||||
public const string Url = "/info";
|
||||
|
||||
[JsonPropertyName("protocols_supported")]
|
||||
public string[]? ProtocolsSupported { get; set; }
|
||||
|
||||
public static InfoResponse Instance = new () {ProtocolsSupported = new string[]
|
||||
{
|
||||
RunSessionRequest.OurProtocolVersion,
|
||||
RunSessionRequest.SupportedProtocolVersion
|
||||
}};
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
// 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.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.WebTools.AspireServer.Contracts;
|
||||
|
||||
namespace Microsoft.WebTools.AspireServer.Models;
|
||||
|
||||
internal class EnvVar
|
||||
{
|
||||
[Required]
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("value")]
|
||||
public string? Value { get; set; }
|
||||
}
|
||||
|
||||
internal class LaunchConfiguration
|
||||
{
|
||||
[Required]
|
||||
[JsonPropertyName("type")]
|
||||
public string LaunchType { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[JsonPropertyName("project_path")]
|
||||
public string ProjectPath { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("launch_profile")]
|
||||
public string? LaunchProfile { get; set; }
|
||||
|
||||
[JsonPropertyName("disable_launch_profile")]
|
||||
public bool DisableLaunchProfile { get; set; }
|
||||
|
||||
[JsonPropertyName("mode")]
|
||||
public string LaunchMode { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
internal class RunSessionRequest
|
||||
{
|
||||
public const string Url = "/run_session";
|
||||
public const string VersionQuery = "api-version";
|
||||
public const string OurProtocolVersion = "2024-04-23"; // This means we support socket ping-pong keepalive
|
||||
public const string SupportedProtocolVersion = "2024-03-03";
|
||||
public const string ProjectLaunchConfigurationType = "project";
|
||||
public const string NoDebugLaunchMode = "NoDebug";
|
||||
public const string DebugLaunchMode = "Debug";
|
||||
|
||||
[Required]
|
||||
[JsonPropertyName("launch_configurations")]
|
||||
public LaunchConfiguration[] LaunchConfigurations { get; set; } = Array.Empty<LaunchConfiguration>();
|
||||
|
||||
[JsonPropertyName("env")]
|
||||
public EnvVar[] Environment { get; set; } = Array.Empty<EnvVar>();
|
||||
|
||||
[JsonPropertyName("args")]
|
||||
public string[] Arguments { get; set; } = Array.Empty<string>();
|
||||
|
||||
public ProjectLaunchRequest? ToProjectLaunchInformation()
|
||||
{
|
||||
// Only support one launch project request. Ignoring all others
|
||||
Debug.Assert(LaunchConfigurations.Length == 1, $"Unexpected number of launch configurations {LaunchConfigurations.Length}");
|
||||
|
||||
var projectLaunchConfig = LaunchConfigurations.FirstOrDefault(launchConfig => string.Equals(launchConfig.LaunchType, ProjectLaunchConfigurationType, StringComparison.OrdinalIgnoreCase));
|
||||
if (projectLaunchConfig is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ProjectLaunchRequest()
|
||||
{
|
||||
ProjectPath = projectLaunchConfig.ProjectPath,
|
||||
Debug = string.Equals(projectLaunchConfig.LaunchMode, DebugLaunchMode, StringComparison.OrdinalIgnoreCase),
|
||||
Arguments = Arguments,
|
||||
Environment = Environment.Select(envVar => new KeyValuePair<string, string>(envVar.Name, envVar.Value!)),
|
||||
LaunchProfile = projectLaunchConfig.LaunchProfile,
|
||||
DisableLaunchProfile = projectLaunchConfig.DisableLaunchProfile
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// 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.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.WebTools.AspireServer.Models;
|
||||
|
||||
internal static class NotificationType
|
||||
{
|
||||
public const string ProcessRestarted = "processRestarted";
|
||||
public const string SessionTerminated = "sessionTerminated";
|
||||
public const string ServiceLogs = "serviceLogs";
|
||||
}
|
||||
|
||||
internal class SessionNotificationBase
|
||||
{
|
||||
public const string Url = "/notify";
|
||||
|
||||
[Required]
|
||||
[JsonPropertyName("notification_type")]
|
||||
public string NotificationType { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("session_id")]
|
||||
public string SessionId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
internal class SessionChangeNotification : SessionNotificationBase
|
||||
{
|
||||
[JsonPropertyName("pid")]
|
||||
public int PID { get; set; }
|
||||
|
||||
[JsonPropertyName("exit_code")]
|
||||
public int? ExitCode { get; set; }
|
||||
}
|
||||
|
||||
internal class SessionLogsNotification : SessionNotificationBase
|
||||
{
|
||||
[JsonPropertyName("is_std_err")]
|
||||
public bool IsStdErr { get; set; }
|
||||
|
||||
[JsonPropertyName("log_message")]
|
||||
public string LogMessage { get; set; } = string.Empty;
|
||||
}
|
|
@ -2,6 +2,8 @@
|
|||
"solution": {
|
||||
"path": "..\\..\\sdk.sln",
|
||||
"projects": [
|
||||
"src\\BuiltInTools\\AspireService\\Microsoft.WebTools.AspireService.Package.csproj",
|
||||
"src\\BuiltInTools\\AspireService\\Microsoft.WebTools.AspireService.shproj",
|
||||
"src\\BuiltInTools\\BrowserRefresh\\Microsoft.AspNetCore.Watch.BrowserRefresh.csproj",
|
||||
"src\\BuiltInTools\\DotNetDeltaApplier\\Microsoft.Extensions.DotNetDeltaApplier.csproj",
|
||||
"src\\BuiltInTools\\DotNetWatchTasks\\DotNetWatchTasks.csproj",
|
||||
|
|
Загрузка…
Ссылка в новой задаче