1
1
// Licensed to the .NET Foundation under one or more agreements.
2
2
// The .NET Foundation licenses this file to you under the MIT license.
3
3
4
+ using System . Diagnostics ;
5
+ using System . Text . Json ;
4
6
using Microsoft . Extensions . Logging ;
5
7
6
8
namespace Aspire . Cli . Projects ;
7
9
8
10
internal interface IProjectLocator
9
11
{
10
- FileInfo ? UseOrFindAppHostProjectFile ( FileInfo ? projectFile ) ;
12
+ Task < FileInfo ? > UseOrFindAppHostProjectFileAsync ( FileInfo ? projectFile , CancellationToken cancellationToken = default ) ;
11
13
}
12
14
13
- internal sealed class ProjectLocator ( ILogger < ProjectLocator > logger , string currentDirectory ) : IProjectLocator
15
+ internal sealed class ProjectLocator ( ILogger < ProjectLocator > logger , IDotNetCliRunner runner , DirectoryInfo currentDirectory ) : IProjectLocator
14
16
{
15
- public FileInfo ? UseOrFindAppHostProjectFile ( FileInfo ? projectFile )
17
+ private readonly ActivitySource _activitySource = new ( nameof ( ProjectLocator ) ) ;
18
+
19
+ private async Task < List < FileInfo > > FindAppHostProjectFilesAsync ( DirectoryInfo searchDirectory , CancellationToken cancellationToken )
20
+ {
21
+ using var activity = _activitySource . StartActivity ( ) ;
22
+
23
+ var appHostProjects = new List < FileInfo > ( ) ;
24
+
25
+ logger . LogDebug ( "Searching for project files in {SearchDirectory}" , searchDirectory . FullName ) ;
26
+ var projectFiles = searchDirectory . GetFiles ( "*.csproj" , SearchOption . AllDirectories ) ;
27
+ logger . LogDebug ( "Found {ProjectFileCount} project files in {SearchDirectory}" , projectFiles . Length , searchDirectory . FullName ) ;
28
+
29
+ foreach ( var projectFile in projectFiles )
30
+ {
31
+ logger . LogDebug ( "Checking project file {ProjectFile}" , projectFile . FullName ) ;
32
+ var information = await runner . GetAppHostInformationAsync ( projectFile , cancellationToken ) ;
33
+
34
+ if ( information . ExitCode == 0 && information . IsAspireHost )
35
+ {
36
+ logger . LogDebug ( "Found AppHost project file {ProjectFile} in {SearchDirectory}" , projectFile . FullName , searchDirectory . FullName ) ;
37
+ appHostProjects . Add ( projectFile ) ;
38
+ }
39
+ else
40
+ {
41
+ logger . LogTrace ( "Project file {ProjectFile} in {SearchDirectory} is not an Aspire host" , projectFile . FullName , searchDirectory . FullName ) ;
42
+ }
43
+ }
44
+
45
+ return appHostProjects ;
46
+ }
47
+
48
+ private async Task < FileInfo ? > GetAppHostProjectFileFromSettingsAsync ( CancellationToken cancellationToken )
49
+ {
50
+ var searchDirectory = currentDirectory ;
51
+
52
+ while ( true )
53
+ {
54
+ var settingsFile = new FileInfo ( Path . Combine ( searchDirectory . FullName , ".aspire" , "settings.json" ) ) ;
55
+
56
+ if ( settingsFile . Exists )
57
+ {
58
+ using var stream = settingsFile . OpenRead ( ) ;
59
+ var json = await JsonDocument . ParseAsync ( stream , cancellationToken : cancellationToken ) ;
60
+
61
+ if ( json . RootElement . TryGetProperty ( "appHostPath" , out var appHostPathProperty ) && appHostPathProperty . GetString ( ) is { } appHostPath )
62
+ {
63
+
64
+ var qualifiedAppHostPath = Path . IsPathRooted ( appHostPath ) ? appHostPath : Path . Combine ( settingsFile . Directory ! . FullName , appHostPath ) ;
65
+ var appHostFile = new FileInfo ( qualifiedAppHostPath ) ;
66
+
67
+ if ( appHostFile . Exists )
68
+ {
69
+ return appHostFile ;
70
+ }
71
+ else
72
+ {
73
+ throw new ProjectLocatorException ( $ "AppHost file was specified in '{ settingsFile . FullName } ' but it does not exist.") ;
74
+ }
75
+ }
76
+ }
77
+
78
+ if ( searchDirectory . Parent is not null )
79
+ {
80
+ searchDirectory = searchDirectory . Parent ;
81
+ }
82
+ else
83
+ {
84
+ return null ;
85
+ }
86
+ }
87
+ }
88
+
89
+ public async Task < FileInfo ? > UseOrFindAppHostProjectFileAsync ( FileInfo ? projectFile , CancellationToken cancellationToken = default )
16
90
{
17
91
logger . LogDebug ( "Finding project file in {CurrentDirectory}" , currentDirectory ) ;
18
92
@@ -29,22 +103,52 @@ internal sealed class ProjectLocator(ILogger<ProjectLocator> logger, string curr
29
103
return projectFile ;
30
104
}
31
105
106
+ projectFile = await GetAppHostProjectFileFromSettingsAsync ( cancellationToken ) ;
107
+
108
+ if ( projectFile is not null )
109
+ {
110
+ return projectFile ;
111
+ }
112
+
32
113
logger . LogDebug ( "No project file specified, searching for *.csproj files in {CurrentDirectory}" , currentDirectory ) ;
33
- var projectFilePaths = Directory . GetFiles ( currentDirectory , "*.csproj" ) ;
114
+ var appHostProjects = await FindAppHostProjectFilesAsync ( currentDirectory , cancellationToken ) ;
34
115
35
- logger . LogDebug ( "Found {ProjectFileCount} project files." , projectFilePaths . Length ) ;
116
+ logger . LogDebug ( "Found {ProjectFileCount} project files." , appHostProjects . Count ) ;
36
117
37
- return projectFilePaths switch {
38
- { Length : 0 } => throw new ProjectLocatorException ( "No project file found." ) ,
39
- { Length : > 1 } => throw new ProjectLocatorException ( "Multiple project files found." ) ,
40
- { Length : 1 } => new FileInfo ( projectFilePaths [ 0 ] ) ,
118
+ var selectedAppHost = appHostProjects . Count switch {
119
+ 0 => throw new ProjectLocatorException ( "No project file found." ) ,
120
+ > 1 => throw new ProjectLocatorException ( "Multiple project files found." ) ,
121
+ 1 => appHostProjects [ 0 ] ,
122
+ _ => throw new ProjectLocatorException ( "Unexpected number of project files found." )
41
123
} ;
124
+
125
+ await CreateSettingsFileIfNotExistsAsync ( selectedAppHost , cancellationToken ) ;
126
+ return selectedAppHost ;
127
+ }
128
+
129
+ private async Task CreateSettingsFileIfNotExistsAsync ( FileInfo projectFile , CancellationToken cancellationToken )
130
+ {
131
+ var settingsFile = new FileInfo ( Path . Combine ( currentDirectory . FullName , ".aspire" , "settings.json" ) ) ;
132
+
133
+ if ( ! settingsFile . Exists )
134
+ {
135
+ if ( ! settingsFile . Directory ! . Exists )
136
+ {
137
+ settingsFile . Directory . Create ( ) ;
138
+ }
139
+
140
+ var settings = new CliSettings
141
+ {
142
+ AppHostPath = Path . GetRelativePath ( settingsFile . Directory . FullName , projectFile . FullName )
143
+ } ;
144
+
145
+ using var stream = settingsFile . OpenWrite ( ) ;
146
+ await JsonSerializer . SerializeAsync ( stream , settings , JsonSourceGenerationContext . Default . CliSettings , cancellationToken ) ;
147
+ }
42
148
}
43
149
}
44
150
45
151
internal class ProjectLocatorException : System . Exception
46
152
{
47
- public ProjectLocatorException ( ) { }
48
153
public ProjectLocatorException ( string message ) : base ( message ) { }
49
- public ProjectLocatorException ( string message , System . Exception inner ) : base ( message , inner ) { }
50
- }
154
+ }
0 commit comments