Skip to content

Commit c18433d

Browse files
committed
Improve progress output, fix potential race condition while rebuilding multiple projects
1 parent 5cf1d7c commit c18433d

File tree

9 files changed

+70
-54
lines changed

9 files changed

+70
-54
lines changed

src/BuiltInTools/dotnet-watch/Build/BuildReporter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ private void Write(string message)
102102
public void ReportOutput()
103103
{
104104
_logger.LogInformation("MSBuild output:");
105-
BuildOutput.ReportBuildOutput(_logger, Messages, success: false, projectDisplay: null);
105+
BuildOutput.ReportBuildOutput(_logger, Messages, success: false);
106106
}
107107
}
108108
}

src/BuiltInTools/dotnet-watch/CommandLine/EnvironmentOptions.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,16 @@ internal enum TestFlags
1212
RunningAsTest = 1 << 0,
1313
MockBrowser = 1 << 1,
1414

15-
/// <summary>
16-
/// Elevates the severity of <see cref="MessageDescriptor.WaitingForChanges"/> from <see cref="MessageSeverity.Output"/>.
17-
/// </summary>
18-
ElevateWaitingForChangesMessageSeverity = 1 << 2,
19-
2015
/// <summary>
2116
/// Instead of using <see cref="Console.ReadKey()"/> to watch for Ctrl+C, Ctlr+R, and other keys, read from standard input.
2217
/// This allows tests to trigger key based events.
2318
/// </summary>
24-
ReadKeyFromStdin = 1 << 3,
19+
ReadKeyFromStdin = 1 << 2,
2520

2621
/// <summary>
2722
/// Redirects the output of the launched browser process to watch output.
2823
/// </summary>
29-
RedirectBrowserOutput = 1 << 4,
24+
RedirectBrowserOutput = 1 << 3,
3025
}
3126

3227
internal sealed record EnvironmentOptions(

src/BuiltInTools/dotnet-watch/HotReload/HotReloadDotNetWatcher.cs

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,7 @@ public async Task WatchAsync(CancellationToken shutdownCancellationToken)
8282
{
8383
var rootProjectOptions = _context.RootProjectOptions;
8484

85-
var (buildSucceeded, buildOutput, _) = await BuildProjectAsync(rootProjectOptions.ProjectPath, rootProjectOptions.BuildArguments, iterationCancellationToken);
86-
BuildOutput.ReportBuildOutput(_context.BuildLogger, buildOutput, buildSucceeded, projectDisplay: rootProjectOptions.ProjectPath);
85+
var buildSucceeded = await BuildProjectAsync(rootProjectOptions.ProjectPath, rootProjectOptions.BuildArguments, iterationCancellationToken);
8786
if (!buildSucceeded)
8887
{
8988
continue;
@@ -189,7 +188,7 @@ void FileChangedCallback(ChangedPath change)
189188

190189
fileChangedCallback = FileChangedCallback;
191190
fileWatcher.OnFileChange += fileChangedCallback;
192-
ReportWatchingForChanges();
191+
_context.Logger.Log(MessageDescriptor.WaitingForChanges);
193192

194193
// Hot Reload loop - exits when the root process needs to be restarted.
195194
bool extendTimeout = false;
@@ -328,15 +327,19 @@ void FileChangedCallback(ChangedPath change)
328327
fileWatcher.SuppressEvents = true;
329328
try
330329
{
331-
var buildResults = await Task.WhenAll(
332-
projectsToRebuild.Select(projectPath => BuildProjectAsync(projectPath, rootProjectOptions.BuildArguments, iterationCancellationToken)));
333-
334-
foreach (var (success, output, projectPath) in buildResults)
330+
// Build projects sequentially to avoid failed attempts to overwrite dependent project outputs.
331+
// TODO: Ideally, dotnet build would be able to build multiple projects.
332+
var success = true;
333+
foreach (var projectPath in projectsToRebuild)
335334
{
336-
BuildOutput.ReportBuildOutput(_context.BuildLogger, output, success, projectPath);
335+
success = await BuildProjectAsync(projectPath, rootProjectOptions.BuildArguments, iterationCancellationToken);
336+
if (!success)
337+
{
338+
break;
339+
}
337340
}
338341

339-
if (buildResults.All(result => result.success))
342+
if (success)
340343
{
341344
break;
342345
}
@@ -772,12 +775,6 @@ internal static IEnumerable<ChangedPath> NormalizePathChanges(IEnumerable<Change
772775
.Where(item => item != null)
773776
.Select(item => item!.Value);
774777

775-
private void ReportWatchingForChanges()
776-
{
777-
_context.Logger.Log(MessageDescriptor.WaitingForChanges
778-
.WithSeverityWhen(MessageSeverity.Output, _context.EnvironmentOptions.TestFlags.HasFlag(TestFlags.ElevateWaitingForChangesMessageSeverity)));
779-
}
780-
781778
private void ReportFileChanges(IReadOnlyList<ChangedFile> changedFiles)
782779
{
783780
Report(kind: ChangeKind.Add);
@@ -823,6 +820,9 @@ private async ValueTask<EvaluationResult> EvaluateRootProjectAsync(bool restore,
823820
{
824821
cancellationToken.ThrowIfCancellationRequested();
825822

823+
_context.Logger.LogInformation("Evaluating projects ...");
824+
var stopwatch = Stopwatch.StartNew();
825+
826826
var result = EvaluationResult.TryCreate(
827827
_context.RootProjectOptions.ProjectPath,
828828
_context.RootProjectOptions.BuildArguments,
@@ -832,6 +832,8 @@ private async ValueTask<EvaluationResult> EvaluateRootProjectAsync(bool restore,
832832
restore,
833833
cancellationToken);
834834

835+
_context.Logger.LogInformation("Evaluation completed in {Time}s.", stopwatch.Elapsed.TotalSeconds.ToString("0.0"));
836+
835837
if (result != null)
836838
{
837839
return result;
@@ -846,31 +848,43 @@ await FileWatcher.WaitForFileChangeAsync(
846848
}
847849
}
848850

849-
private async Task<(bool success, ImmutableArray<OutputLine> output, string projectPath)> BuildProjectAsync(
850-
string projectPath, IReadOnlyList<string> buildArguments, CancellationToken cancellationToken)
851+
private async Task<bool> BuildProjectAsync(string projectPath, IReadOnlyList<string> buildArguments, CancellationToken cancellationToken)
851852
{
852-
var buildOutput = new List<OutputLine>();
853+
List<OutputLine>? capturedOutput = _context.EnvironmentOptions.TestFlags != TestFlags.None ? [] : null;
853854

854855
var processSpec = new ProcessSpec
855856
{
856857
Executable = _context.EnvironmentOptions.MuxerPath,
857858
WorkingDirectory = Path.GetDirectoryName(projectPath)!,
858859
IsUserApplication = false,
859-
OnOutput = line =>
860-
{
861-
lock (buildOutput)
860+
861+
// Capture output if running in a test environment.
862+
// If the output is not captured dotnet build will show live build progress.
863+
OnOutput = capturedOutput != null
864+
? line =>
862865
{
863-
buildOutput.Add(line);
866+
lock (capturedOutput)
867+
{
868+
capturedOutput.Add(line);
869+
}
864870
}
865-
},
871+
: null,
872+
866873
// pass user-specified build arguments last to override defaults:
867874
Arguments = ["build", projectPath, "-consoleLoggerParameters:NoSummary;Verbosity=minimal", .. buildArguments]
868875
};
869876

870877
_context.BuildLogger.Log(MessageDescriptor.Building, projectPath);
871878

872-
var exitCode = await _context.ProcessRunner.RunAsync(processSpec, _context.Logger, launchResult: null, cancellationToken);
873-
return (exitCode == 0, buildOutput.ToImmutableArray(), projectPath);
879+
var success = await _context.ProcessRunner.RunAsync(processSpec, _context.Logger, launchResult: null, cancellationToken) == 0;
880+
881+
if (capturedOutput != null)
882+
{
883+
_context.BuildLogger.Log(success ? MessageDescriptor.BuildSucceeded : MessageDescriptor.BuildFailed, projectPath);
884+
BuildOutput.ReportBuildOutput(_context.BuildLogger, capturedOutput, success);
885+
}
886+
887+
return success;
874888
}
875889

876890
private string GetRelativeFilePath(string path)

src/BuiltInTools/dotnet-watch/HotReload/IncrementalMSBuildWorkspace.cs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace Microsoft.DotNet.Watch;
1515
internal sealed class IncrementalMSBuildWorkspace : Workspace
1616
{
1717
private readonly ILogger _logger;
18+
private int _solutionUpdateId;
1819

1920
public IncrementalMSBuildWorkspace(ILogger logger)
2021
: base(MSBuildMefHostServices.DefaultServices, WorkspaceKind.MSBuild)
@@ -35,6 +36,9 @@ public IncrementalMSBuildWorkspace(ILogger logger)
3536

3637
public async Task UpdateProjectConeAsync(string rootProjectPath, CancellationToken cancellationToken)
3738
{
39+
_logger.LogInformation("Loading projects ...");
40+
41+
var stopwatch = Stopwatch.StartNew();
3842
var oldSolution = CurrentSolution;
3943

4044
var loader = new MSBuildProjectLoader(this);
@@ -94,9 +98,11 @@ public async Task UpdateProjectConeAsync(string rootProjectPath, CancellationTok
9498
.WithCompilationOutputInfo(newProjectInfo.CompilationOutputInfo));
9599
}
96100

97-
await ReportSolutionFilesAsync(SetCurrentSolution(newSolution), cancellationToken);
101+
await UpdateSolutionAsync(newSolution, operationDisplayName: "project update", cancellationToken);
98102
UpdateReferencesAfterAdd();
99103

104+
_logger.LogInformation("Projects loaded in {Time}s.", stopwatch.Elapsed.TotalSeconds.ToString("0.0"));
105+
100106
ProjectReference MapProjectReference(ProjectReference pr)
101107
// Only C# and VB projects are loaded by the MSBuildProjectLoader, so some references might be missing.
102108
// When a new project is added along with a new project reference the old project id is also null.
@@ -154,6 +160,8 @@ public async ValueTask UpdateFileContentAsync(IEnumerable<ChangedFile> changedFi
154160

155161
var newText = await GetSourceTextAsync(changedFile.FilePath, oldText.Encoding, oldText.ChecksumAlgorithm, cancellationToken);
156162

163+
_logger.LogDebug("Updating document text of '{FilePath}'.", changedFile.FilePath);
164+
157165
updatedSolution = textDocument switch
158166
{
159167
Document document => document.WithText(newText).Project.Solution,
@@ -166,7 +174,7 @@ public async ValueTask UpdateFileContentAsync(IEnumerable<ChangedFile> changedFi
166174

167175
updatedSolution = RemoveDocuments(updatedSolution, documentsToRemove);
168176

169-
await ReportSolutionFilesAsync(SetCurrentSolution(updatedSolution), cancellationToken);
177+
await UpdateSolutionAsync(updatedSolution, operationDisplayName: "document update", cancellationToken);
170178
}
171179

172180
private static Solution RemoveDocuments(Solution solution, IEnumerable<DocumentId> ids)
@@ -217,10 +225,21 @@ private static async ValueTask<SourceText> GetSourceTextAsync(string filePath, E
217225
return null;
218226
}
219227

220-
public async Task ReportSolutionFilesAsync(Solution solution, CancellationToken cancellationToken)
228+
private Task UpdateSolutionAsync(Solution newSolution, string operationDisplayName, CancellationToken cancellationToken)
229+
=> ReportSolutionFilesAsync(SetCurrentSolution(newSolution), Interlocked.Increment(ref _solutionUpdateId), operationDisplayName, cancellationToken);
230+
231+
private async Task ReportSolutionFilesAsync(Solution solution, int updateId, string operationDisplayName, CancellationToken cancellationToken)
221232
{
222233
#if DEBUG
223234
_logger.LogDebug("Solution: {Path}", solution.FilePath);
235+
236+
if (!_logger.IsEnabled(LogLevel.Debug))
237+
{
238+
return;
239+
}
240+
241+
_logger.LogDebug("Solution after {Operation}: v{Version}", operationDisplayName, updateId);
242+
224243
foreach (var project in solution.Projects)
225244
{
226245
_logger.LogDebug(" Project: {Path}", project.FilePath);

src/BuiltInTools/dotnet-watch/Properties/launchSettings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"profiles": {
33
"dotnet-watch": {
44
"commandName": "Project",
5-
"commandLineArgs": "--verbose -bl",
6-
"workingDirectory": "C:\\bugs\\9756\\aspire-watch-start-issue\\Aspire.AppHost",
5+
"commandLineArgs": "",
6+
"workingDirectory": "C:\\temp\\Razor",
77
"environmentVariables": {
88
"DOTNET_WATCH_DEBUG_SDK_DIRECTORY": "$(RepoRoot)artifacts\\bin\\redist\\$(Configuration)\\dotnet\\sdk\\$(Version)",
99
"DCP_IDE_REQUEST_TIMEOUT_SECONDS": "100000",

src/BuiltInTools/dotnet-watch/UI/BuildOutput.cs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,8 @@ internal static partial class BuildOutput
1313
[GeneratedRegex(@"[^:]+: (error|warning) [A-Za-z]+[0-9]+: .+")]
1414
private static partial Regex GetBuildDiagnosticRegex();
1515

16-
public static void ReportBuildOutput(ILogger logger, IEnumerable<OutputLine> buildOutput, bool success, string? projectDisplay)
16+
public static void ReportBuildOutput(ILogger logger, IEnumerable<OutputLine> buildOutput, bool success)
1717
{
18-
if (projectDisplay != null)
19-
{
20-
if (success)
21-
{
22-
logger.Log(MessageDescriptor.BuildSucceeded, projectDisplay);
23-
}
24-
else
25-
{
26-
logger.Log(MessageDescriptor.BuildFailed, projectDisplay);
27-
}
28-
}
29-
3018
foreach (var (line, isError) in buildOutput)
3119
{
3220
if (isError)

src/BuiltInTools/dotnet-watch/UI/IReporter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ public MessageDescriptor WithSeverityWhen(MessageSeverity severity, bool conditi
186186
public static readonly MessageDescriptor ProjectsRestarted = Create("Projects restarted ({0})", Emoji.HotReload, MessageSeverity.Verbose);
187187
public static readonly MessageDescriptor ProjectDependenciesDeployed = Create("Project dependencies deployed ({0})", Emoji.HotReload, MessageSeverity.Verbose);
188188
public static readonly MessageDescriptor FixBuildError = Create("Fix the error to continue or press Ctrl+C to exit.", Emoji.Watch, MessageSeverity.Warning);
189-
public static readonly MessageDescriptor WaitingForChanges = Create("Waiting for changes", Emoji.Watch, MessageSeverity.Verbose);
189+
public static readonly MessageDescriptor WaitingForChanges = Create("Waiting for changes", Emoji.Watch, MessageSeverity.Output);
190190
public static readonly MessageDescriptor LaunchedProcess = Create("Launched '{0}' with arguments '{1}': process id {2}", Emoji.Launch, MessageSeverity.Verbose);
191191
public static readonly MessageDescriptor HotReloadChangeHandled = Create("Hot reload change handled in {0}ms.", Emoji.HotReload, MessageSeverity.Verbose);
192192
public static readonly MessageDescriptor HotReloadSucceeded = Create(LogEvents.HotReloadSucceeded, Emoji.HotReload);

src/BuiltInTools/dotnet-watch/Watch/MsBuildFileSetFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ internal sealed class EvaluationResult(IReadOnlyDictionary<string, FileItem> fil
7373
Logger.LogInformation("MSBuild output from target '{TargetName}':", TargetName);
7474
}
7575

76-
BuildOutput.ReportBuildOutput(Logger, capturedOutput, success, projectDisplay: null);
76+
BuildOutput.ReportBuildOutput(Logger, capturedOutput, success);
7777
if (!success)
7878
{
7979
return null;

test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,7 @@ class AppUpdateHandler
755755
App.DotnetWatchArgs.Clear();
756756
}
757757

758-
App.Start(testAsset, [], testFlags: TestFlags.ElevateWaitingForChangesMessageSeverity);
758+
App.Start(testAsset, []);
759759

760760
await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges);
761761

0 commit comments

Comments
 (0)