Skip to content

Commit

Permalink
Implement support for Aspire
Browse files Browse the repository at this point in the history
  • Loading branch information
tmat committed Oct 16, 2024
1 parent 66c7799 commit 971fe1d
Show file tree
Hide file tree
Showing 22 changed files with 460 additions and 16 deletions.
1 change: 1 addition & 0 deletions sdk.sln
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,7 @@ Global
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*{1f0b4b3c-dc88-4740-b04f-1707102e9930}*SharedItemsImports = 5
src\BuiltInTools\AspireService\Microsoft.WebTools.AspireService.projitems*{445efbd5-6730-4f09-943d-278e77501ffd}*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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.DotNet.Watcher;

namespace Microsoft.WebTools.AspireServer;

internal partial class AspireServerService : IRuntimeProcessLauncher
{
public bool SupportsPartialRestart => false;

public async ValueTask<IEnumerable<(string name, string value)>> GetEnvironmentVariablesAsync(CancellationToken cancelationToken)
{
var environment = await GetServerConnectionEnvironmentAsync(cancelationToken).ConfigureAwait(false);
return environment.Select(kvp => (kvp.Key, kvp.Value));
}
}
138 changes: 138 additions & 0 deletions src/BuiltInTools/dotnet-watch/Aspire/AspireServiceFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using Microsoft.Build.Graph;
using Microsoft.DotNet.Watcher.Tools;
using Microsoft.Extensions.Tools.Internal;
using Microsoft.WebTools.AspireServer;
using Microsoft.WebTools.AspireServer.Contracts;

namespace Microsoft.DotNet.Watcher;

internal class AspireServiceFactory : IRuntimeProcessLauncherFactory
{
private sealed class ServerEvents(ProjectLauncher projectLauncher, IReadOnlyList<(string name, string value)> buildProperties) : IAspireServerEvents
{
/// <summary>
/// Lock to access:
/// <see cref="_sessions"/>
/// <see cref="_sessionIdDispenser"/>
/// </summary>
private readonly object _guard = new();

private readonly Dictionary<string, RunningProject> _sessions = [];
private int _sessionIdDispenser;

private IReporter Reporter
=> projectLauncher.Reporter;

/// <summary>
/// Implements https://github.com/dotnet/aspire/blob/445d2fc8a6a0b7ce3d8cc42def4d37b02709043b/docs/specs/IDE-execution.md#create-session-request.
/// </summary>
public async ValueTask<string> StartProjectAsync(string dcpId, ProjectLaunchRequest projectLaunchInfo, CancellationToken cancellationToken)
{
Reporter.Verbose($"Starting project: {projectLaunchInfo.ProjectPath}", MessageEmoji);

var projectOptions = GetProjectOptions(projectLaunchInfo);

var processTerminationSource = new CancellationTokenSource();

var runningProject = await projectLauncher.TryLaunchProcessAsync(projectOptions, processTerminationSource, build: false, cancellationToken);
if (runningProject == null)
{
// detailed error already reported:
throw new ApplicationException($"Failed to launch project '{projectLaunchInfo.ProjectPath}'.");
}

string sessionId;
lock (_guard)
{
sessionId = _sessionIdDispenser++.ToString(CultureInfo.InvariantCulture);
_sessions.Add(sessionId, runningProject);
}

Reporter.Verbose($"Session started: {sessionId}");
return sessionId;
}

/// <summary>
/// Implements https://github.com/dotnet/aspire/blob/445d2fc8a6a0b7ce3d8cc42def4d37b02709043b/docs/specs/IDE-execution.md#stop-session-request.
/// </summary>
public async ValueTask<bool> StopSessionAsync(string dcpId, string sessionId, CancellationToken cancellationToken)
{
Reporter.Verbose($"Stop Session {sessionId}", MessageEmoji);

RunningProject? runningProject;
lock (_guard)
{
if (!_sessions.TryGetValue(sessionId, out runningProject))
{
return false;
}
}

_ = await projectLauncher.TerminateProcessesAsync([runningProject.ProjectNode.ProjectInstance.FullPath], cancellationToken);
return true;
}

private ProjectOptions GetProjectOptions(ProjectLaunchRequest projectLaunchInfo)
{
var arguments = new List<string>
{
"--project",
projectLaunchInfo.ProjectPath,
// TODO: https://github.com/dotnet/sdk/issues/43946
// Need to suppress launch profile for now, otherwise it would override the port set via env variable.
"--no-launch-profile",
};

//if (projectLaunchInfo.DisableLaunchProfile)
//{
// arguments.Add("--no-launch-profile");
//}
//else if (!string.IsNullOrEmpty(projectLaunchInfo.LaunchProfile))
//{
// arguments.Add("--launch-profile");
// arguments.Add(projectLaunchInfo.LaunchProfile);
//}

if (projectLaunchInfo.Arguments != null)
{
arguments.AddRange(projectLaunchInfo.Arguments);
}

return new()
{
IsRootProject = false,
ProjectPath = projectLaunchInfo.ProjectPath,
WorkingDirectory = projectLauncher.EnvironmentOptions.WorkingDirectory, // TODO: Should DCP protocol specify?
BuildProperties = buildProperties, // TODO: Should DCP protocol specify?
Command = "run",
CommandArguments = arguments,
LaunchEnvironmentVariables = projectLaunchInfo.Environment?.Select(kvp => (kvp.Key, kvp.Value)).ToArray() ?? [],
LaunchProfileName = projectLaunchInfo.LaunchProfile,
NoLaunchProfile = projectLaunchInfo.DisableLaunchProfile,
TargetFramework = null, // TODO: Should DCP protocol specify?
};
}
}

public const string MessageEmoji = "⭐";

public static readonly AspireServiceFactory Instance = new();
public const string AppHostProjectCapability = "Aspire";

public IRuntimeProcessLauncher? TryCreate(ProjectGraphNode projectNode, ProjectLauncher projectLauncher, IReadOnlyList<(string name, string value)> buildProperties)
{
if (!projectNode.GetCapabilities().Contains(AppHostProjectCapability))
{
return null;
}

// TODO: implement notifications:
// 1) Process restarted notification
// 2) Session terminated notification
return new AspireServerService(new ServerEvents(projectLauncher, buildProperties), displayName: ".NET Watch Aspire Server", m => projectLauncher.Reporter.Verbose(m, MessageEmoji));
}
}
11 changes: 11 additions & 0 deletions src/BuiltInTools/dotnet-watch/Browser/BrowserRefreshServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ public async ValueTask SendJsonWithSecret<TValue>(Func<string?, TValue> valueFac
{
try
{
bool messageSent = false;

for (var i = 0; i < _clientSockets.Count; i++)
{
var (clientSocket, secret) = _clientSockets[i];
Expand All @@ -221,7 +223,10 @@ public async ValueTask SendJsonWithSecret<TValue>(Func<string?, TValue> valueFac
var messageBytes = JsonSerializer.SerializeToUtf8Bytes(value, _jsonSerializerOptions);

await clientSocket.SendAsync(messageBytes, WebSocketMessageType.Text, endOfMessage: true, cancellationToken);
messageSent = true;
}

_reporter.Verbose(messageSent ? "Browser message sent." : "Unable to send message to browser, no socket is open.");
}
catch (TaskCanceledException)
{
Expand All @@ -237,15 +242,21 @@ public async ValueTask SendMessage(ReadOnlyMemory<byte> messageBytes, Cancellati
{
try
{
bool messageSent = false;

for (var i = 0; i < _clientSockets.Count; i++)
{
var (clientSocket, _) = _clientSockets[i];
if (clientSocket.State is not WebSocketState.Open)
{
continue;
}

await clientSocket.SendAsync(messageBytes, WebSocketMessageType.Text, endOfMessage: true, cancellationToken);
messageSent = true;
}

_reporter.Verbose(messageSent ? "Browser message sent." : "Unable to send message to browser, no socket is open.");
}
catch (TaskCanceledException)
{
Expand Down
9 changes: 8 additions & 1 deletion src/BuiltInTools/dotnet-watch/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,15 @@ internal Watcher CreateWatcher(IRuntimeProcessLauncherFactory? runtimeProcessLau
var projectGraph = TryReadProject(rootProjectOptions, reporter);
if (projectGraph != null)
{
var rootProject = projectGraph.GraphRoots.Single();

// use normalized MSBuild path so that we can index into the ProjectGraph
rootProjectOptions = rootProjectOptions with { ProjectPath = projectGraph.GraphRoots.Single().ProjectInstance.FullPath };
rootProjectOptions = rootProjectOptions with { ProjectPath = rootProject.ProjectInstance.FullPath };

if (rootProject.GetCapabilities().Contains(AspireServiceFactory.AppHostProjectCapability))
{
runtimeProcessLauncherFactory ??= AspireServiceFactory.Instance;
}
}

var fileSetFactory = new MSBuildFileSetFactory(
Expand Down
1 change: 1 addition & 0 deletions src/BuiltInTools/dotnet-watch/dotnet-watch.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\AspireService\Microsoft.WebTools.AspireService.projitems" Label="Shared" />
<Import Project="$(RepoRoot)\src\Layout\redist\targets\PublishDotnetWatch.targets" />

<PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
<_Parameter1>MicrosoftAspNetCoreAppRefPackageVersion</_Parameter1>
<_Parameter2>$(MicrosoftAspNetCoreAppRefPackageVersion)</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute" >
<_Parameter1>MicrosoftNETSdkAspireManifest80100PackageVersion</_Parameter1>
<_Parameter2>$(MicrosoftNETSdkAspireManifest80100PackageVersion)</_Parameter2>
</AssemblyAttribute>
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
var builder = WebApplication.CreateBuilder(args);

// Add service defaults & Aspire components.
builder.AddServiceDefaults();

// Add services to the container.
builder.Services.AddProblemDetails();

var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseExceptionHandler();

var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
});

app.MapDefaultEndpoints();

app.Run();

internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "weatherforecast",
"applicationUrl": "http://localhost:5303",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(CurrentTargetFramework)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\WatchAspire.ServiceDefaults\WatchAspire.ServiceDefaults.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
var builder = DistributedApplication.CreateBuilder(args);

builder.AddProject<Projects.WatchAspire_ApiService>("apiservice");

builder.Build().Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17211;http://localhost:15137",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21185",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22024"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15137",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19235",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20033",
"ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(CurrentTargetFramework)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
<UserSecretsId>ad800ccc-954c-40cc-920b-2e09fc9eee7a</UserSecretsId>
<!-- workaround -->
<AspireHostingSDKVersion>9.0.0</AspireHostingSDKVersion>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\WatchAspire.ApiService\WatchAspire.ApiService.csproj" Watch="true" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="$(MicrosoftNETSdkAspireManifest80100PackageVersion)" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
Loading

0 comments on commit 971fe1d

Please sign in to comment.