Skip to content

Commit 13fdd29

Browse files
committed
Aspire
1 parent e15f052 commit 13fdd29

File tree

4 files changed

+154
-0
lines changed

4 files changed

+154
-0
lines changed

sdk.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,6 +1157,7 @@ Global
11571157
GlobalSection(SharedMSBuildProjectFiles) = preSolution
11581158
src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{03c5a84a-982b-4f38-ac73-ab832c645c4a}*SharedItemsImports = 5
11591159
src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{0a3c9afd-f6e6-4a5d-83fb-93bf66732696}*SharedItemsImports = 5
1160+
src\BuiltInTools\AspireService\Microsoft.WebTools.AspireService.projitems*{445efbd5-6730-4f09-943d-278e77501ffd}*SharedItemsImports = 5
11601161
src\BuiltInTools\AspireService\Microsoft.WebTools.AspireService.projitems*{94c8526e-dcc2-442f-9868-3dd0ba2688be}*SharedItemsImports = 13
11611162
src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{9d36039f-d0a1-462f-85b4-81763c6b02cb}*SharedItemsImports = 13
11621163
src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{a9103b98-d888-4260-8a05-fa36f640698a}*SharedItemsImports = 5
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.DotNet.Watcher;
5+
6+
namespace Microsoft.WebTools.AspireServer;
7+
8+
internal partial class AspireServerService : IRuntimeProcessLauncher
9+
{
10+
public async ValueTask<IEnumerable<(string name, string value)>> GetEnvironmentVariablesAsync(CancellationToken cancelationToken)
11+
{
12+
var environment = await GetServerConnectionEnvironmentAsync(cancelationToken).ConfigureAwait(false);
13+
return environment.Select(kvp => (kvp.Key, kvp.Value));
14+
}
15+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Globalization;
5+
using Microsoft.Build.Graph;
6+
using Microsoft.DotNet.Watcher.Tools;
7+
using Microsoft.Extensions.Tools.Internal;
8+
using Microsoft.WebTools.AspireServer;
9+
using Microsoft.WebTools.AspireServer.Contracts;
10+
11+
namespace Microsoft.DotNet.Watcher;
12+
13+
internal class AspireServiceFactory : IRuntimeProcessLauncherFactory
14+
{
15+
private sealed class ServerEvents(ProjectLauncher projectLauncher, IReadOnlyList<(string name, string value)> buildProperties) : IAspireServerEvents
16+
{
17+
/// <summary>
18+
/// Lock to access:
19+
/// <see cref="_sessions"/>
20+
/// <see cref="_sessionIdDispenser"/>
21+
/// </summary>
22+
private readonly object _guard = new();
23+
24+
private readonly Dictionary<string, RunningProject> _sessions = [];
25+
private int _sessionIdDispenser;
26+
27+
private IReporter Reporter
28+
=> projectLauncher.Reporter;
29+
30+
/// <summary>
31+
/// Implements https://github.com/dotnet/aspire/blob/445d2fc8a6a0b7ce3d8cc42def4d37b02709043b/docs/specs/IDE-execution.md#create-session-request.
32+
/// </summary>
33+
public async ValueTask<string> StartProjectAsync(string dcpId, ProjectLaunchRequest projectLaunchInfo, CancellationToken cancellationToken)
34+
{
35+
Reporter.Verbose($"Starting project: {projectLaunchInfo.ProjectPath}", MessageEmoji);
36+
37+
var projectOptions = GetProjectOptions(projectLaunchInfo);
38+
39+
var processTerminationSource = new CancellationTokenSource();
40+
41+
var runningProject = await projectLauncher.TryLaunchProcessAsync(projectOptions, processTerminationSource, build: false, cancellationToken);
42+
if (runningProject == null)
43+
{
44+
// detailed error already reported:
45+
throw new ApplicationException($"Failed to launch project '{projectLaunchInfo.ProjectPath}'.");
46+
}
47+
48+
string sessionId;
49+
lock (_guard)
50+
{
51+
sessionId = _sessionIdDispenser++.ToString(CultureInfo.InvariantCulture);
52+
_sessions.Add(sessionId, runningProject);
53+
}
54+
55+
Reporter.Verbose($"Session started: {sessionId}");
56+
return sessionId;
57+
}
58+
59+
/// <summary>
60+
/// Implements https://github.com/dotnet/aspire/blob/445d2fc8a6a0b7ce3d8cc42def4d37b02709043b/docs/specs/IDE-execution.md#stop-session-request.
61+
/// </summary>
62+
public async ValueTask<bool> StopSessionAsync(string dcpId, string sessionId, CancellationToken cancellationToken)
63+
{
64+
Reporter.Verbose($"Stop Session {sessionId}", MessageEmoji);
65+
66+
RunningProject? runningProject;
67+
lock (_guard)
68+
{
69+
if (!_sessions.TryGetValue(sessionId, out runningProject))
70+
{
71+
return false;
72+
}
73+
}
74+
75+
_ = await projectLauncher.TerminateProcessesAsync([runningProject.ProjectNode.ProjectInstance.FullPath], cancellationToken);
76+
return true;
77+
}
78+
79+
private ProjectOptions GetProjectOptions(ProjectLaunchRequest projectLaunchInfo)
80+
{
81+
var arguments = new List<string>
82+
{
83+
"--project",
84+
projectLaunchInfo.ProjectPath,
85+
// TODO: Need to suppress launch profile for now, otherwise it would override the port set via env variable.
86+
"--no-launch-profile",
87+
};
88+
89+
//if (projectLaunchInfo.DisableLaunchProfile)
90+
//{
91+
// arguments.Add("--no-launch-profile");
92+
//}
93+
//else if (!string.IsNullOrEmpty(projectLaunchInfo.LaunchProfile))
94+
//{
95+
// arguments.Add("--launch-profile");
96+
// arguments.Add(projectLaunchInfo.LaunchProfile);
97+
//}
98+
99+
if (projectLaunchInfo.Arguments != null)
100+
{
101+
arguments.AddRange(projectLaunchInfo.Arguments);
102+
}
103+
104+
return new()
105+
{
106+
IsRootProject = false,
107+
ProjectPath = projectLaunchInfo.ProjectPath,
108+
WorkingDirectory = projectLauncher.EnvironmentOptions.WorkingDirectory, // TODO: Should DCP protocol specify?
109+
BuildProperties = buildProperties, // TODO: Should DCP protocol specify?
110+
Command = "run",
111+
CommandArguments = arguments,
112+
LaunchEnvironmentVariables = projectLaunchInfo.Environment?.Select(kvp => (kvp.Key, kvp.Value)).ToArray() ?? [],
113+
LaunchProfileName = projectLaunchInfo.LaunchProfile,
114+
NoLaunchProfile = projectLaunchInfo.DisableLaunchProfile,
115+
TargetFramework = null, // TODO: Should DCP protocol specify?
116+
};
117+
}
118+
}
119+
120+
public const string MessageEmoji = "⭐";
121+
122+
public static readonly AspireServiceFactory Instance = new();
123+
public const string AppHostProjectCapability = "Aspire";
124+
125+
public IRuntimeProcessLauncher? TryCreate(ProjectGraphNode projectNode, ProjectLauncher projectLauncher, IReadOnlyList<(string name, string value)> buildProperties)
126+
{
127+
if (!projectNode.GetCapabilities().Contains(AppHostProjectCapability))
128+
{
129+
return null;
130+
}
131+
132+
// TODO: implement notifications:
133+
// 1) Process restarted notification
134+
// 2) Session terminated notification
135+
return new AspireServerService(new ServerEvents(projectLauncher, buildProperties), displayName: ".NET Watch Aspire Server", m => projectLauncher.Reporter.Verbose(m, MessageEmoji));
136+
}
137+
}

src/BuiltInTools/dotnet-watch/dotnet-watch.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<Project Sdk="Microsoft.NET.Sdk">
2+
<Import Project="..\AspireService\Microsoft.WebTools.AspireService.projitems" Label="Shared" />
23
<Import Project="$(RepoRoot)\src\Layout\redist\targets\PublishDotnetWatch.targets" />
34

45
<PropertyGroup>

0 commit comments

Comments
 (0)