diff --git a/src/Build/Logging/ProjectTrackingLoggerBase.cs b/src/Build/Logging/ProjectTrackingLoggerBase.cs
new file mode 100644
index 00000000000..375257ba117
--- /dev/null
+++ b/src/Build/Logging/ProjectTrackingLoggerBase.cs
@@ -0,0 +1,430 @@
+// 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.Linq;
+using Microsoft.Build.Framework;
+
+namespace Microsoft.Build.Logging;
+
+///
+/// A wrapper over the project context ID passed to us in logger events.
+///
+internal record struct ProjectContext(int Id)
+{
+ public ProjectContext(BuildEventContext context)
+ : this(context.ProjectContextId)
+ {
+ }
+}
+
+///
+/// A wrapper over the evaluation context ID passed to us in logger events.
+///
+internal record struct EvalContext(int Id)
+{
+ public EvalContext(BuildEventContext context)
+ : this(context.EvaluationId)
+ {
+ }
+}
+
+///
+/// Base class that tracks build state (evaluation data, node status, completed project data, and whole-build data) during builds.
+/// Subclasses can specialize the type of data tracked for each area and implement rendering logic.
+///
+/// Data gathered/projected for each evaluation context
+/// Data stored for each live, actively running build worker node
+/// Data stored for each completed project context instance
+/// Data that is aggregated across the entire build session
+public abstract class ProjectTrackingLoggerBase : INodeLogger
+{
+
+ ///
+ /// Tracks the evaluation data for all evaluations seen so far.
+ ///
+ ///
+ /// Keyed by an ID that gets passed to logger callbacks, this allows us to quickly look up the corresponding evaluation.
+ ///
+ private readonly Dictionary _evaluationDataByEvalId = new();
+
+ ///
+ /// Tracks the status of all relevant projects seen so far.
+ ///
+ ///
+ /// Keyed by an ID that gets passed to logger callbacks, this allows us to quickly look up the corresponding project.
+ ///
+ private readonly Dictionary _projectDataByProjectContextId = new();
+
+ ///
+ /// Tracks build-level data for the entire build session.
+ ///
+ private TBuildData? _buildData;
+
+ #region INodeLogger implementation
+
+ ///
+ public abstract LoggerVerbosity Verbosity { get; set; }
+
+ ///
+ public abstract string? Parameters { get; set; }
+
+ ///
+ /// The number of nodes in the build. Handles the case where MSBUILDNOINPROCNODE is set by reserving an extra slot.
+ ///
+ protected int NodeCount { get; private set; }
+
+ ///
+ public virtual void Initialize(IEventSource eventSource, int nodeCount)
+ {
+ // When MSBUILDNOINPROCNODE enabled, NodeId's reported by build start with 2. We need to reserve an extra spot for this case.
+ NodeCount = nodeCount + 1;
+
+ Initialize(eventSource);
+ }
+
+ ///
+ public virtual void Initialize(IEventSource eventSource)
+ {
+ eventSource.BuildStarted += BuildStartedHandler;
+ eventSource.BuildFinished += BuildFinishedHandler;
+ eventSource.ProjectStarted += ProjectStartedHandler;
+ eventSource.ProjectFinished += ProjectFinishedHandler;
+ eventSource.TargetStarted += TargetStartedHandler;
+ eventSource.TargetFinished += TargetFinishedHandler;
+ eventSource.TaskStarted += TaskStartedHandler;
+ eventSource.StatusEventRaised += StatusEventRaisedHandler;
+ eventSource.MessageRaised += MessageRaisedHandler;
+ eventSource.WarningRaised += WarningRaisedHandler;
+ eventSource.ErrorRaised += ErrorRaisedHandler;
+
+ if (eventSource is IEventSource3 eventSource3)
+ {
+ eventSource3.IncludeTaskInputs();
+ }
+
+ if (eventSource is IEventSource4 eventSource4)
+ {
+ eventSource4.IncludeEvaluationPropertiesAndItems();
+ }
+ }
+
+ ///
+ public abstract void Shutdown();
+
+ #endregion
+
+ #region Logger callbacks
+
+ ///
+ /// The callback.
+ ///
+ private void BuildStartedHandler(object sender, BuildStartedEventArgs e)
+ {
+ _buildData = CreateBuildData(e);
+ OnBuildStarted(e, _buildData);
+ }
+
+ ///
+ /// The callback.
+ ///
+ private void BuildFinishedHandler(object sender, BuildFinishedEventArgs e)
+ {
+ OnBuildFinished(e, _projectDataByProjectContextId.Values.ToArray(), _buildData!);
+
+ // Clear tracking data
+ _projectDataByProjectContextId.Clear();
+ _evaluationDataByEvalId.Clear();
+ _buildData = default;
+ }
+
+ ///
+ /// The callback.
+ ///
+ private void StatusEventRaisedHandler(object sender, BuildStatusEventArgs e)
+ {
+ switch (e)
+ {
+ case BuildCanceledEventArgs cancelEvent:
+ OnBuildCanceled(cancelEvent);
+ break;
+ case ProjectEvaluationStartedEventArgs:
+ break;
+ case ProjectEvaluationFinishedEventArgs evalFinish:
+ CaptureEvalContext(evalFinish);
+ break;
+ }
+ }
+
+ ///
+ /// The callback.
+ ///
+ private void ProjectStartedHandler(object sender, ProjectStartedEventArgs e)
+ {
+ if (e.BuildEventContext is null)
+ {
+ return;
+ }
+
+ ProjectContext projectContext = new(e.BuildEventContext);
+ EvalContext evalContext = new(e.BuildEventContext);
+
+ // Get eval data for this project
+ if (_evaluationDataByEvalId.TryGetValue(evalContext, out TEvalData? evalData))
+ {
+ // Create project data using the eval data
+ TProjectData? projectData = CreateProjectData(evalData, e);
+ if (projectData != null)
+ {
+ _projectDataByProjectContextId[projectContext] = projectData;
+ OnProjectStarted(e, evalData, projectData!, _buildData!);
+ }
+ }
+ }
+
+ ///
+ /// The callback.
+ ///
+ private void ProjectFinishedHandler(object sender, ProjectFinishedEventArgs e)
+ {
+ var buildEventContext = e.BuildEventContext;
+ if (buildEventContext is null)
+ {
+ return;
+ }
+
+ ProjectContext projectContext = new(buildEventContext);
+
+ // Get project data
+ if (_projectDataByProjectContextId.TryGetValue(projectContext, out var projectData))
+ {
+ OnProjectFinished(e, projectData, _buildData!);
+ }
+ }
+
+ ///
+ /// The callback.
+ ///
+ private void TargetStartedHandler(object sender, TargetStartedEventArgs e)
+ {
+ var buildEventContext = e.BuildEventContext;
+ if (buildEventContext is not null)
+ {
+ ProjectContext projectContext = new(buildEventContext);
+ if (_projectDataByProjectContextId.TryGetValue(projectContext, out TProjectData? projectData))
+ {
+ OnTargetStarted(e, projectData, _buildData!);
+ }
+ }
+ }
+
+ ///
+ /// The callback.
+ ///
+ private void TargetFinishedHandler(object sender, TargetFinishedEventArgs e)
+ {
+ var buildEventContext = e.BuildEventContext;
+ if (buildEventContext is null)
+ {
+ return;
+ }
+
+ ProjectContext projectContext = new(buildEventContext);
+ if (_projectDataByProjectContextId.TryGetValue(projectContext, out var projectData))
+ {
+ OnTargetFinished(e, projectData, _buildData!);
+ }
+ }
+
+ ///
+ /// The callback.
+ ///
+ private void TaskStartedHandler(object sender, TaskStartedEventArgs e)
+ {
+ var buildEventContext = e.BuildEventContext;
+ if (buildEventContext is not null)
+ {
+ ProjectContext projectContext = new(buildEventContext);
+ if (_projectDataByProjectContextId.TryGetValue(projectContext, out var projectData))
+ {
+ OnTaskStarted(e, projectData, _buildData!);
+ }
+ }
+ }
+
+ ///
+ /// The callback.
+ ///
+ private void MessageRaisedHandler(object sender, BuildMessageEventArgs e)
+ {
+ var buildEventContext = e.BuildEventContext;
+ if (buildEventContext is null)
+ {
+ return;
+ }
+
+ ProjectContext projectContext = new(buildEventContext);
+ TProjectData? projectData = default;
+ _projectDataByProjectContextId.TryGetValue(projectContext, out projectData);
+ OnMessageRaised(e, projectData, _buildData!);
+ }
+
+ ///
+ /// The callback.
+ ///
+ private void WarningRaisedHandler(object sender, BuildWarningEventArgs e)
+ {
+ BuildEventContext? buildEventContext = e.BuildEventContext;
+ if (buildEventContext is null)
+ {
+ OnWarningRaised(e, default, _buildData!);
+ return;
+ }
+
+ ProjectContext projectContext = new(buildEventContext);
+ TProjectData? projectData = default;
+ _projectDataByProjectContextId.TryGetValue(projectContext, out projectData);
+ OnWarningRaised(e, projectData, _buildData!);
+ }
+
+ ///
+ /// The callback.
+ ///
+ private void ErrorRaisedHandler(object sender, BuildErrorEventArgs e)
+ {
+ BuildEventContext? buildEventContext = e.BuildEventContext;
+ if (buildEventContext is null)
+ {
+ OnErrorRaised(e, default, _buildData!);
+ return;
+ }
+
+ ProjectContext projectContext = new(buildEventContext);
+ TProjectData? projectData = default;
+ _projectDataByProjectContextId.TryGetValue(projectContext, out projectData);
+ OnErrorRaised(e, projectData, _buildData!);
+ }
+
+ #endregion
+
+ #region Protected helpers
+
+ protected int? GetNodeIdForEvent(BuildEventArgs args) => args?.BuildEventContext is null ? null : NodeIndexForContext(args.BuildEventContext);
+
+ #endregion
+
+ #region Private helpers
+
+ private int NodeIndexForContext(BuildEventContext context)
+ {
+ // Node IDs reported by the build are 1-based.
+ return context.NodeId - 1;
+ }
+
+ ///
+ /// Captures evaluation context data from the evaluation finished event.
+ ///
+ private void CaptureEvalContext(ProjectEvaluationFinishedEventArgs evalFinish)
+ {
+ var buildEventContext = evalFinish.BuildEventContext;
+ if (buildEventContext is null)
+ {
+ return;
+ }
+
+ EvalContext evalContext = new(buildEventContext);
+
+ if (!_evaluationDataByEvalId.ContainsKey(evalContext))
+ {
+ TEvalData evalData = CreateEvalData(evalFinish);
+ _evaluationDataByEvalId[evalContext] = evalData;
+ }
+ }
+
+ #endregion
+
+ #region Abstract methods - must be implemented by subclasses
+
+ ///
+ /// Creates evaluation data from the evaluation finished event.
+ ///
+ /// The evaluation finished event args.
+ /// The evaluation data to store, or null to not track this evaluation.
+ protected abstract TEvalData CreateEvalData(ProjectEvaluationFinishedEventArgs e);
+
+ ///
+ /// Creates project data from the project started event and evaluation data.
+ ///
+ /// The evaluation data for this project.
+ /// The project started event args.
+ /// The project data to store, or null to not track this project.
+ protected abstract TProjectData? CreateProjectData(TEvalData evalData, ProjectStartedEventArgs e);
+
+ ///
+ /// Creates build data when the build starts.
+ ///
+ /// The build started event args.
+ /// The build data to track for this build session.
+ protected abstract TBuildData CreateBuildData(BuildStartedEventArgs e);
+
+ #endregion
+
+ #region Virtual methods - can be overridden by subclasses
+
+ ///
+ /// Called when the build starts.
+ ///
+ protected virtual void OnBuildStarted(BuildStartedEventArgs e, TBuildData buildData) { }
+
+ ///
+ /// Called when the build finishes.
+ ///
+ protected virtual void OnBuildFinished(BuildFinishedEventArgs e, TProjectData[] projectData, TBuildData buildData) { }
+
+ ///
+ /// Called when the build is canceled.
+ ///
+ protected virtual void OnBuildCanceled(BuildCanceledEventArgs e) { }
+
+ ///
+ /// Called when a project starts.
+ ///
+ protected virtual void OnProjectStarted(ProjectStartedEventArgs e, TEvalData evalData, TProjectData projectData, TBuildData buildData) { }
+
+ ///
+ /// Called when a project finishes.
+ ///
+ protected virtual void OnProjectFinished(ProjectFinishedEventArgs e, TProjectData projectData, TBuildData buildData) { }
+
+ ///
+ /// Called when a target starts.
+ ///
+ protected virtual void OnTargetStarted(TargetStartedEventArgs e, TProjectData projectData, TBuildData buildData) { }
+
+ ///
+ /// Called when a target finishes.
+ ///
+ protected virtual void OnTargetFinished(TargetFinishedEventArgs e, TProjectData projectData, TBuildData buildData) { }
+
+ ///
+ /// Called when a task starts.
+ ///
+ protected virtual void OnTaskStarted(TaskStartedEventArgs e, TProjectData projectData, TBuildData buildData) { }
+
+ ///
+ /// Called when a message is raised.
+ ///
+ protected virtual void OnMessageRaised(BuildMessageEventArgs e, TProjectData? projectData, TBuildData buildData) { }
+
+ ///
+ /// Called when a warning is raised.
+ ///
+ protected virtual void OnWarningRaised(BuildWarningEventArgs e, TProjectData? projectData, TBuildData buildData) { }
+
+ ///
+ /// Called when an error is raised.
+ ///
+ protected virtual void OnErrorRaised(BuildErrorEventArgs e, TProjectData? projectData, TBuildData buildData) { }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Build/Logging/TerminalLogger/StopwatchAbstraction.cs b/src/Build/Logging/TerminalLogger/StopwatchAbstraction.cs
index c4f72f630de..d6c3920910c 100644
--- a/src/Build/Logging/TerminalLogger/StopwatchAbstraction.cs
+++ b/src/Build/Logging/TerminalLogger/StopwatchAbstraction.cs
@@ -3,7 +3,7 @@
namespace Microsoft.Build.Logging;
-internal abstract class StopwatchAbstraction
+public abstract class StopwatchAbstraction
{
public abstract void Start();
public abstract void Stop();
diff --git a/src/Build/Logging/TerminalLogger/TerminalBuildData.cs b/src/Build/Logging/TerminalLogger/TerminalBuildData.cs
new file mode 100644
index 00000000000..9ef619c8443
--- /dev/null
+++ b/src/Build/Logging/TerminalLogger/TerminalBuildData.cs
@@ -0,0 +1,51 @@
+// 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.Build.Logging;
+
+///
+/// Tracks build-level data for the TerminalLogger across an entire build session.
+///
+public sealed class TerminalBuildData
+{
+ ///
+ /// The timestamp of the build start event.
+ ///
+ public DateTime BuildStartTime { get; set; }
+
+ ///
+ /// Number of build errors encountered during the build.
+ ///
+ public int BuildErrorsCount { get; set; }
+
+ ///
+ /// Number of build warnings encountered during the build.
+ ///
+ public int BuildWarningsCount { get; set; }
+
+ ///
+ /// The project build context corresponding to the Restore initial target, or null if the build is currently not restoring.
+ ///
+ public int? RestoreContext { get; set; }
+
+ ///
+ /// True if restore failed and this failure has already been reported.
+ ///
+ public bool RestoreFailed { get; set; }
+
+ ///
+ /// True if restore happened and finished.
+ ///
+ public bool RestoreFinished { get; set; }
+
+ ///
+ /// Initializes a new instance of TerminalBuildData.
+ ///
+ /// The timestamp when the build started.
+ public TerminalBuildData(DateTime buildStartTime)
+ {
+ BuildStartTime = buildStartTime;
+ }
+}
\ No newline at end of file
diff --git a/src/Build/Logging/TerminalLogger/TerminalBuildMessage.cs b/src/Build/Logging/TerminalLogger/TerminalBuildMessage.cs
index 8e90b6f85e2..1097ff85b61 100644
--- a/src/Build/Logging/TerminalLogger/TerminalBuildMessage.cs
+++ b/src/Build/Logging/TerminalLogger/TerminalBuildMessage.cs
@@ -6,5 +6,5 @@ namespace Microsoft.Build.Logging;
///
/// Represents a piece of diagnostic output (message/warning/error).
///
-internal record struct TerminalBuildMessage(TerminalMessageSeverity Severity, string Message)
+public record struct TerminalBuildMessage(TerminalMessageSeverity Severity, string Message)
{ }
diff --git a/src/Build/Logging/TerminalLogger/TerminalLogger.cs b/src/Build/Logging/TerminalLogger/TerminalLogger.cs
index b64fefb705e..ef9a99704c8 100644
--- a/src/Build/Logging/TerminalLogger/TerminalLogger.cs
+++ b/src/Build/Logging/TerminalLogger/TerminalLogger.cs
@@ -30,7 +30,7 @@ namespace Microsoft.Build.Logging;
///
/// Uses ANSI/VT100 control codes to erase and overwrite lines as the build is progressing.
///
-public sealed partial class TerminalLogger : INodeLogger
+public sealed partial class TerminalLogger : ProjectTrackingLoggerBase
{
private const string FilePathPattern = " -> ";
@@ -42,27 +42,7 @@ public sealed partial class TerminalLogger : INodeLogger
private static readonly string[] newLineStrings = { "\r\n", "\n" };
- ///
- /// A wrapper over the project context ID passed to us in logger events.
- ///
- internal record struct ProjectContext(int Id)
- {
- public ProjectContext(BuildEventContext context)
- : this(context.ProjectContextId)
- {
- }
- }
-
- ///
- /// A wrapper over the evaluation context ID passed to us in logger events.
- ///
- internal record struct EvalContext(int Id)
- {
- public EvalContext(BuildEventContext context)
- : this(context.EvaluationId)
- {
- }
- }
+ // ProjectContext and EvalContext are now inherited from BuildTrackerLogger
private readonly record struct TestSummary(int Total, int Passed, int Skipped, int Failed);
@@ -80,31 +60,6 @@ public EvalContext(BuildEventContext context)
internal Func? CreateStopwatch = null;
- ///
- /// Name of target that identifies the project cache plugin run has just started.
- ///
- private const string CachePluginStartTarget = "_CachePluginRunStart";
-
- ///
- /// Protects access to state shared between the logger callbacks and the rendering thread.
- ///
- private readonly LockType _lock = new LockType();
-
- ///
- /// A cancellation token to signal the rendering thread that it should exit.
- ///
- private readonly CancellationTokenSource _cts = new();
-
- ///
- /// Tracks the status of all relevant projects seen so far.
- ///
- ///
- /// Keyed by an ID that gets passed to logger callbacks, this allows us to quickly look up the corresponding project.
- ///
- private readonly Dictionary _projects = new();
-
- private readonly Dictionary _evals = new();
-
///
/// Tracks the work currently being done by build nodes. Null means the node is not doing any work worth reporting.
///
@@ -112,43 +67,33 @@ public EvalContext(BuildEventContext context)
/// There is no locking around access to this data structure despite it being accessed concurrently by multiple threads.
/// However, reads and writes to locations in an array is atomic, so locking is not required.
///
- private TerminalNodeStatus?[] _nodes = Array.Empty();
+ private TerminalNodeStatus?[] _nodes = [];
///
- /// The timestamp of the event.
+ /// Name of target that identifies the project cache plugin run has just started.
///
- private DateTime _buildStartTime;
+ private const string CachePluginStartTarget = "_CachePluginRunStart";
///
- /// The working directory when the build starts, to trim relative output paths.
+ /// Protects access to state shared between the logger callbacks and the rendering thread.
///
- private readonly string _initialWorkingDirectory = Environment.CurrentDirectory;
+ private readonly LockType _lock = new LockType();
///
- /// Number of build errors.
+ /// A cancellation token to signal the rendering thread that it should exit.
///
- private int _buildErrorsCount;
+ private readonly CancellationTokenSource _cts = new();
- ///
- /// Number of build warnings.
- ///
- private int _buildWarningsCount;
+ // Tracking dictionaries and node array are now inherited from BuildTrackerLogger
- ///
- /// True if restore failed and this failure has already been reported.
- ///
- private bool _restoreFailed;
+ // BuildStartTime is now inherited from BuildTrackerLogger
///
- /// True if restore happened and finished.
+ /// The working directory when the build starts, to trim relative output paths.
///
- private bool _restoreFinished = false;
+ private readonly string _initialWorkingDirectory = Environment.CurrentDirectory;
- ///
- /// The project build context corresponding to the Restore initial target, or null if the build is currently
- /// not restoring.
- ///
- private ProjectContext? _restoreContext;
+ // Build error/warning counts and restore state are now inherited from BuildTrackerLogger
///
/// The thread that performs periodic refresh of the console output.
@@ -347,49 +292,19 @@ private static bool IsTerminalLoggerDisabled(string? value) =>
#region INodeLogger implementation
///
- public LoggerVerbosity Verbosity { get; set; } = LoggerVerbosity.Minimal;
-
- ///
- public string? Parameters { get; set; } = null;
+ public override LoggerVerbosity Verbosity { get; set; } = LoggerVerbosity.Minimal;
///
- public void Initialize(IEventSource eventSource, int nodeCount)
- {
- // When MSBUILDNOINPROCNODE enabled, NodeId's reported by build start with 2. We need to reserve an extra spot for this case.
- _nodes = new TerminalNodeStatus[nodeCount + 1];
-
- Initialize(eventSource);
- }
+ public override string? Parameters { get; set; } = null;
///
- public void Initialize(IEventSource eventSource)
+ public override void Initialize(IEventSource eventSource)
{
ParseParameters();
-
- eventSource.BuildStarted += BuildStarted;
- eventSource.BuildFinished += BuildFinished;
- eventSource.ProjectStarted += ProjectStarted;
- eventSource.ProjectFinished += ProjectFinished;
- eventSource.TargetStarted += TargetStarted;
- eventSource.TargetFinished += TargetFinished;
- eventSource.TaskStarted += TaskStarted;
- eventSource.StatusEventRaised += StatusEventRaised;
- eventSource.MessageRaised += MessageRaised;
- eventSource.WarningRaised += WarningRaised;
- eventSource.ErrorRaised += ErrorRaised;
-
- if (eventSource is IEventSource3 eventSource3)
- {
- eventSource3.IncludeTaskInputs();
- }
-
- if (eventSource is IEventSource4 eventSource4)
- {
- eventSource4.IncludeEvaluationPropertiesAndItems();
- }
+ base.Initialize(eventSource);
+ _nodes = new TerminalNodeStatus[NodeCount];
}
-
///
/// Parses out the logger parameters from the Parameters string.
///
@@ -467,7 +382,7 @@ private bool TryApplyShowCommandLineParameter(string? parameterValue)
}
///
- public void Shutdown()
+ public override void Shutdown()
{
NativeMethodsShared.RestoreConsoleMode(_originalConsoleMode);
@@ -489,12 +404,77 @@ public MessageImportance GetMinimumMessageImportance()
#endregion
- #region Logger callbacks
+ #region BuildTrackerLogger implementation
- ///
- /// The callback.
- ///
- private void BuildStarted(object sender, BuildStartedEventArgs e)
+ ///
+ protected override TerminalBuildData CreateBuildData(BuildStartedEventArgs e)
+ {
+ return new TerminalBuildData(e.Timestamp);
+ }
+
+ ///
+ protected override EvalProjectInfo CreateEvalData(ProjectEvaluationFinishedEventArgs e)
+ {
+ string? tfm = null;
+ string? rid = null;
+ foreach (var property in e.EnumerateProperties())
+ {
+ if (tfm is not null && rid is not null)
+ {
+ // We already have both properties, no need to continue.
+ break;
+ }
+ switch (property.Name)
+ {
+ case "TargetFramework":
+ tfm = property.Value;
+ break;
+ case "RuntimeIdentifier":
+ rid = property.Value;
+ break;
+ }
+ }
+ return new EvalProjectInfo(e.ProjectFile!, tfm, rid);
+ }
+
+ ///
+ protected override TerminalProjectInfo? CreateProjectData(EvalProjectInfo evalData, ProjectStartedEventArgs e)
+ {
+ return new TerminalProjectInfo(evalData, CreateStopwatch?.Invoke());
+ }
+
+ private TerminalNodeStatus? CreateNodeData(TargetStartedEventArgs e, TerminalProjectInfo projectData)
+ {
+ projectData.Stopwatch.Start();
+ string projectFile = Path.GetFileNameWithoutExtension(e.ProjectFile);
+ string targetName = e.TargetName;
+
+ if (targetName == CachePluginStartTarget)
+ {
+ projectData.IsCachePluginProject = true;
+ _hasUsedCache = true;
+ }
+
+ if (targetName == _testStartTarget)
+ {
+ targetName = "Testing";
+ _testStartTime = _testStartTime == null
+ ? e.Timestamp
+ : e.Timestamp < _testStartTime
+ ? e.Timestamp : _testStartTime;
+ projectData.IsTestProject = true;
+ }
+
+ return new TerminalNodeStatus(projectFile, projectData.TargetFramework, projectData.RuntimeIdentifier, targetName, projectData.Stopwatch);
+ }
+
+
+ #endregion
+
+ #region Logger event overrides
+
+ ///
+ protected override void OnBuildStarted(BuildStartedEventArgs e, TerminalBuildData buildData)
{
if (!_manualRefresh && _showNodesDisplay)
{
@@ -503,18 +483,14 @@ private void BuildStarted(object sender, BuildStartedEventArgs e)
_refresher.Start();
}
- _buildStartTime = e.Timestamp;
-
if (Terminal.SupportsProgressReporting && Verbosity != LoggerVerbosity.Quiet)
{
Terminal.Write(AnsiCodes.SetProgressIndeterminate);
}
}
- ///
- /// The callback.
- ///
- private void BuildFinished(object sender, BuildFinishedEventArgs e)
+ ///
+ protected override void OnBuildFinished(BuildFinishedEventArgs e, TerminalProjectInfo[] projectInfos, TerminalBuildData buildData)
{
_cts.Cancel();
_refresher?.Join();
@@ -524,8 +500,8 @@ private void BuildFinished(object sender, BuildFinishedEventArgs e)
{
if (Verbosity > LoggerVerbosity.Quiet)
{
- string duration = (e.Timestamp - _buildStartTime).TotalSeconds.ToString("F1");
- string buildResult = GetBuildResultString(e.Succeeded, _buildErrorsCount, _buildWarningsCount);
+ string duration = (e.Timestamp - buildData.BuildStartTime).TotalSeconds.ToString("F1");
+ string buildResult = GetBuildResultString(e.Succeeded, buildData.BuildErrorsCount, buildData.BuildWarningsCount);
Terminal.WriteLine("");
if (_testRunSummaries.Any())
@@ -537,8 +513,8 @@ private void BuildFinished(object sender, BuildFinishedEventArgs e)
var testDuration = (_testStartTime != null && _testEndTime != null ? (_testEndTime - _testStartTime).Value.TotalSeconds : 0).ToString("F1");
var colorizeFailed = failed > 0;
- var colorizePassed = passed > 0 && _buildErrorsCount == 0 && failed == 0;
- var colorizeSkipped = skipped > 0 && skipped == total && _buildErrorsCount == 0 && failed == 0;
+ var colorizePassed = passed > 0 && buildData.BuildErrorsCount == 0 && failed == 0;
+ var colorizeSkipped = skipped > 0 && skipped == total && buildData.BuildErrorsCount == 0 && failed == 0;
string summaryAndTotalText = ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("TestSummary_BannerAndTotal", total);
string failedText = ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("TestSummary_Failed", failed);
@@ -555,10 +531,10 @@ private void BuildFinished(object sender, BuildFinishedEventArgs e)
if (_showSummary == true)
{
- RenderBuildSummary();
+ RenderBuildSummary(buildData, projectInfos);
}
- if (_restoreFailed)
+ if (buildData?.RestoreFailed == true)
{
Terminal.WriteLine(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("RestoreCompleteWithMessage",
buildResult,
@@ -582,18 +558,14 @@ private void BuildFinished(object sender, BuildFinishedEventArgs e)
Terminal.EndUpdate();
}
- _projects.Clear();
_testRunSummaries.Clear();
- _buildErrorsCount = 0;
- _buildWarningsCount = 0;
- _restoreFailed = false;
_testStartTime = null;
_testEndTime = null;
}
- private void RenderBuildSummary()
+ private void RenderBuildSummary(TerminalBuildData buildData, TerminalProjectInfo[] projectInfos)
{
- if (_buildErrorsCount == 0 && _buildWarningsCount == 0)
+ if (buildData.BuildErrorsCount == 0 && buildData.BuildWarningsCount == 0)
{
// No errors/warnings to display.
return;
@@ -601,7 +573,7 @@ private void RenderBuildSummary()
Terminal.WriteLine(ResourceUtilities.GetResourceString("BuildSummary"));
- foreach (TerminalProjectInfo project in _projects.Values.Where(p => p.HasErrorsOrWarnings))
+ foreach (TerminalProjectInfo project in projectInfos.Where(p => p.HasErrorsOrWarnings))
{
string duration = project.Stopwatch.ElapsedSeconds.ToString("F1");
string buildResult = GetBuildResultString(project.Succeeded, project.ErrorCount, project.WarningCount);
@@ -618,62 +590,25 @@ private void RenderBuildSummary()
Terminal.WriteLine(string.Empty);
}
- private void StatusEventRaised(object sender, BuildStatusEventArgs e)
+ ///
+ protected override void OnBuildCanceled(BuildCanceledEventArgs e)
{
- switch (e)
- {
- case BuildCanceledEventArgs cancelEvent:
- RenderImmediateMessage(cancelEvent.Message!);
- break;
- case ProjectEvaluationStartedEventArgs _evalStart:
- break;
- case ProjectEvaluationFinishedEventArgs evalFinish:
- CaptureEvalContext(evalFinish);
- break;
- }
+ RenderImmediateMessage(e.Message!);
}
- ///
- /// The callback.
- ///
- private void ProjectStarted(object sender, ProjectStartedEventArgs e)
+ ///
+ protected override void OnProjectStarted(ProjectStartedEventArgs e, EvalProjectInfo evalData, TerminalProjectInfo projectData, TerminalBuildData buildData)
{
- if (e.BuildEventContext is null)
+ // Handle restore case
+ if (buildData.RestoreContext is null && e.TargetNames == "Restore" && !buildData.RestoreFinished && e.BuildEventContext is not null)
{
- return;
- }
-
- ProjectContext c = new(e.BuildEventContext);
-
- if (_restoreContext is null)
- {
- EvalContext evalContext = new(e.BuildEventContext);
- string? targetFramework = null;
- string? runtimeIdentifier = null;
- if (_evals.TryGetValue(evalContext, out EvalProjectInfo evalInfo))
- {
- targetFramework = evalInfo.TargetFramework;
- runtimeIdentifier = evalInfo.RuntimeIdentifier;
- }
- System.Diagnostics.Debug.Assert(evalInfo != default, "EvalProjectInfo should have been captured before ProjectStarted");
-
- TerminalProjectInfo projectInfo = new(c, evalInfo, CreateStopwatch?.Invoke());
- _projects[c] = projectInfo;
-
- // First ever restore in the build is starting.
- if (e.TargetNames == "Restore" && !_restoreFinished)
- {
- _restoreContext = c;
- int nodeIndex = NodeIndexForContext(e.BuildEventContext);
- _nodes[nodeIndex] = new TerminalNodeStatus(e.ProjectFile!, targetFramework, runtimeIdentifier, "Restore", _projects[c].Stopwatch);
- }
+ buildData.RestoreContext = e.BuildEventContext.ProjectContextId;
+ StartNode(e, new TerminalNodeStatus(e.ProjectFile!, evalData.TargetFramework, evalData.RuntimeIdentifier, "Restore", projectData.Stopwatch));
}
}
- ///
- /// The callback.
- ///
- private void ProjectFinished(object sender, ProjectFinishedEventArgs e)
+ ///
+ protected override void OnProjectFinished(ProjectFinishedEventArgs e, TerminalProjectInfo? projectData, TerminalBuildData buildData)
{
var buildEventContext = e.BuildEventContext;
if (buildEventContext is null)
@@ -682,9 +617,9 @@ private void ProjectFinished(object sender, ProjectFinishedEventArgs e)
}
// Mark node idle until something uses it again
- if (_restoreContext is null)
+ if (buildData.RestoreContext is null)
{
- UpdateNodeStatus(buildEventContext, null);
+ YieldNode(e);
}
// Continue execution and add project summary to the static part of the Console only if verbosity is higher than Quiet.
@@ -693,12 +628,20 @@ private void ProjectFinished(object sender, ProjectFinishedEventArgs e)
return;
}
- ProjectContext c = new(buildEventContext);
-
- if (_projects.TryGetValue(c, out TerminalProjectInfo? project))
+ if (projectData != null)
{
- project.Succeeded = e.Succeeded;
- project.Stopwatch.Stop();
+ projectData.Succeeded = e.Succeeded;
+ projectData.Stopwatch.Stop();
+
+ // Handle restore completion
+ if (buildEventContext.ProjectContextId == buildData.RestoreContext)
+ {
+ buildData.RestoreContext = null;
+ buildData.RestoreFinished = true;
+ buildData.RestoreFailed = !e.Succeeded;
+ OnRestoreFinished(e, projectData, buildData);
+ }
+
lock (_lock)
{
Terminal.BeginUpdate();
@@ -706,52 +649,26 @@ private void ProjectFinished(object sender, ProjectFinishedEventArgs e)
{
EraseNodes();
- string duration = project.Stopwatch.ElapsedSeconds.ToString("F1");
- ReadOnlyMemory? outputPath = project.OutputPath;
+ string duration = projectData.Stopwatch.ElapsedSeconds.ToString("F1");
+ ReadOnlyMemory? outputPath = projectData.OutputPath;
// Build result. One of 'failed', 'succeeded with warnings', or 'succeeded' depending on the build result and diagnostic messages
// reported during build.
- string buildResult = GetBuildResultString(project.Succeeded, project.ErrorCount, project.WarningCount);
-
- // Check if we're done restoring.
- if (c == _restoreContext)
- {
- if (e.Succeeded)
- {
- if (project.HasErrorsOrWarnings)
- {
- Terminal.WriteLine(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("RestoreCompleteWithMessage",
- buildResult,
- duration));
- }
- else
- {
- Terminal.WriteLine(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("RestoreComplete",
- duration));
- }
- }
- else
- {
- // It will be reported after build finishes.
- _restoreFailed = true;
- }
+ string buildResult = GetBuildResultString(projectData.Succeeded, projectData.ErrorCount, projectData.WarningCount);
- _restoreContext = null;
- _restoreFinished = true;
- }
// If this was a notable project build, we print it as completed only if it's produced an output or warnings/error.
// If this is a test project, print it always, so user can see either a success or failure, otherwise success is hidden
// and it is hard to see if project finished, or did not run at all.
- else if (project.OutputPath is not null || project.BuildMessages is not null || project.IsTestProject)
+ if (projectData.OutputPath is not null || projectData.BuildMessages is not null || projectData.IsTestProject)
{
// Show project build complete and its output
- string projectFinishedHeader = GetProjectFinishedHeader(project, buildResult, duration);
+ string projectFinishedHeader = GetProjectFinishedHeader(projectData, buildResult, duration);
Terminal.Write(projectFinishedHeader);
// Print the output path as a link if we have it.
if (outputPath is { } outputPathSpan)
{
- (var projectDisplayPath, var urlLink) = DetermineOutputPathToRender(outputPathSpan, _initialWorkingDirectory.AsMemory(), project.SourceRoot);
+ (var projectDisplayPath, var urlLink) = DetermineOutputPathToRender(outputPathSpan, _initialWorkingDirectory.AsMemory(), projectData.SourceRoot);
Terminal.WriteLine(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("ProjectFinished_OutputPath", CreateLink(urlLink, projectDisplayPath.ToString())));
}
else
@@ -761,16 +678,20 @@ private void ProjectFinished(object sender, ProjectFinishedEventArgs e)
}
// Print diagnostic output under the Project -> Output line.
- if (project.BuildMessages is not null)
+ if (projectData.BuildMessages is not null)
{
- foreach (TerminalBuildMessage buildMessage in project.BuildMessages)
+ foreach (TerminalBuildMessage buildMessage in projectData.BuildMessages)
{
Terminal.WriteLine($"{DoubleIndentation}{buildMessage.Message}");
}
}
- _buildErrorsCount += project.ErrorCount;
- _buildWarningsCount += project.WarningCount;
+ // Track errors and warnings in build data
+ if (buildData != null)
+ {
+ buildData.BuildErrorsCount += projectData.ErrorCount;
+ buildData.BuildWarningsCount += projectData.WarningCount;
+ }
if (_showNodesDisplay)
{
@@ -785,39 +706,27 @@ private void ProjectFinished(object sender, ProjectFinishedEventArgs e)
}
}
- private void CaptureEvalContext(ProjectEvaluationFinishedEventArgs evalFinish)
+ private void OnRestoreFinished(ProjectFinishedEventArgs e, TerminalProjectInfo? projectData, TerminalBuildData buildData)
{
- var buildEventContext = evalFinish.BuildEventContext;
- if (buildEventContext is null)
+ if (Verbosity > LoggerVerbosity.Quiet && projectData != null)
{
- return;
- }
-
- EvalContext c = new(buildEventContext);
+ string duration = projectData.Stopwatch.ElapsedSeconds.ToString("F1");
+ string buildResult = GetBuildResultString(projectData.Succeeded, projectData.ErrorCount, projectData.WarningCount);
- if (!_evals.TryGetValue(c, out EvalProjectInfo _))
- {
- string? tfm = null;
- string? rid = null;
- foreach (var property in evalFinish.EnumerateProperties())
+ if (e.Succeeded)
{
- if (tfm is not null && rid is not null)
+ if (projectData.HasErrorsOrWarnings)
{
- // We already have both properties, no need to continue.
- break;
+ Terminal.WriteLine(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("RestoreCompleteWithMessage",
+ buildResult,
+ duration));
}
- switch (property.Name)
+ else
{
- case "TargetFramework":
- tfm = property.Value;
- break;
- case "RuntimeIdentifier":
- rid = property.Value;
- break;
+ Terminal.WriteLine(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("RestoreComplete",
+ duration));
}
}
- var evalInfo = new EvalProjectInfo(c, evalFinish.ProjectFile!, tfm, rid);
- _evals[c] = evalInfo;
}
}
@@ -941,123 +850,64 @@ private static string GetProjectFinishedHeader(TerminalProjectInfo project, stri
};
}
- ///
- /// The callback.
- ///
- private void TargetStarted(object sender, TargetStartedEventArgs e)
+ ///
+ protected override void OnTargetStarted(TargetStartedEventArgs e, TerminalProjectInfo projectData, TerminalBuildData buildData)
{
- var buildEventContext = e.BuildEventContext;
- if (_restoreContext is null && buildEventContext is not null && _projects.TryGetValue(new ProjectContext(buildEventContext), out TerminalProjectInfo? project))
+ TerminalNodeStatus? nodeData = CreateNodeData(e, projectData);
+ if (nodeData != null)
{
- project.Stopwatch.Start();
-
- string projectFile = Path.GetFileNameWithoutExtension(e.ProjectFile);
-
- string targetName = e.TargetName;
- if (targetName == CachePluginStartTarget)
- {
- project.IsCachePluginProject = true;
- _hasUsedCache = true;
- }
-
- if (targetName == _testStartTarget)
- {
- targetName = "Testing";
-
- // Use the minimal start time, so if we run tests in parallel, we can calculate duration
- // as this start time, minus time when tests finished.
- _testStartTime = _testStartTime == null
- ? e.Timestamp
- : e.Timestamp < _testStartTime
- ? e.Timestamp : _testStartTime;
- project.IsTestProject = true;
- }
-
- TerminalNodeStatus nodeStatus = new(projectFile, project.TargetFramework, project.RuntimeIdentifier, targetName, project.Stopwatch);
- UpdateNodeStatus(buildEventContext, nodeStatus);
+ StartNode(e, nodeData);
}
}
- private void UpdateNodeStatus(BuildEventContext buildEventContext, TerminalNodeStatus? nodeStatus)
- {
- int nodeIndex = NodeIndexForContext(buildEventContext);
- _nodes[nodeIndex] = nodeStatus;
- }
-
- ///
- /// The callback. Unused.
- ///
- private void TargetFinished(object sender, TargetFinishedEventArgs e)
+ ///
+ protected override void OnTargetFinished(TargetFinishedEventArgs e, TerminalProjectInfo? projectData, TerminalBuildData buildData)
{
// For cache plugin projects which result in a cache hit, ensure the output path is set
// to the item spec corresponding to the GetTargetPath target upon completion.
- var buildEventContext = e.BuildEventContext;
var targetOutputs = e.TargetOutputs;
- if (_restoreContext is not null || buildEventContext is null)
+ if (projectData is null || targetOutputs is null)
{
return;
}
- if (targetOutputs is not null
- && _hasUsedCache
- && e.TargetName == "GetTargetPath"
- && _projects.TryGetValue(new ProjectContext(buildEventContext), out TerminalProjectInfo? project))
+ if (_hasUsedCache && e.TargetName == "GetTargetPath" && projectData.IsCachePluginProject)
{
- if (project is not null && project.IsCachePluginProject)
+ foreach (ITaskItem output in targetOutputs)
{
- foreach (ITaskItem output in targetOutputs)
- {
- project.OutputPath = output.ItemSpec.AsMemory();
- break;
- }
+ projectData.OutputPath = output.ItemSpec.AsMemory();
+ break;
}
}
- else if (targetOutputs is not null
- && e.TargetName == "InitializeSourceRootMappedPaths"
- && _projects.TryGetValue(new ProjectContext(buildEventContext), out project)
- && project.SourceRoot is null)
+ else if (e.TargetName == "InitializeSourceRootMappedPaths" && projectData.SourceRoot is null)
{
- project.SourceRoot =
+ projectData.SourceRoot =
(targetOutputs as IEnumerable)?
.FirstOrDefault(root => !string.IsNullOrEmpty(root.GetMetadata("SourceControl")))
?.ItemSpec.AsMemory();
}
}
- ///
- /// The callback.
- ///
- private void TaskStarted(object sender, TaskStartedEventArgs e)
+ ///
+ protected override void OnTaskStarted(TaskStartedEventArgs e, TerminalProjectInfo projectData, TerminalBuildData buildData)
{
- var buildEventContext = e.BuildEventContext;
- if (_restoreContext is null && buildEventContext is not null && e.TaskName == "MSBuild")
+ if (e.TaskName == "MSBuild")
{
// This will yield the node, so preemptively mark it idle
- UpdateNodeStatus(buildEventContext, null);
+ YieldNode(e);
- if (_projects.TryGetValue(new ProjectContext(buildEventContext), out TerminalProjectInfo? project))
- {
- project.Stopwatch.Stop();
- }
+ projectData.Stopwatch.Stop();
}
}
- ///
- /// The callback.
- ///
- private void MessageRaised(object sender, BuildMessageEventArgs e)
+ ///
+ protected override void OnMessageRaised(BuildMessageEventArgs e, TerminalProjectInfo? projectData, TerminalBuildData buildData)
{
- var buildEventContext = e.BuildEventContext;
- if (buildEventContext is null)
- {
- return;
- }
-
string? message = e.Message;
if (message is not null && e.Importance == MessageImportance.High)
{
- var hasProject = _projects.TryGetValue(new ProjectContext(buildEventContext), out TerminalProjectInfo? project);
+ var hasProject = projectData != null;
// Detect project output path by matching high-importance messages against the "$(MSBuildProjectName) -> ..."
// pattern used by the CopyFilesToOutputDirectory target.
@@ -1066,10 +916,10 @@ private void MessageRaised(object sender, BuildMessageEventArgs e)
{
var projectFileName = Path.GetFileName(e.ProjectFile.AsSpan());
if (!projectFileName.IsEmpty &&
- message.AsSpan().StartsWith(Path.GetFileNameWithoutExtension(projectFileName)) && hasProject)
+ message.AsSpan().StartsWith(Path.GetFileNameWithoutExtension(projectFileName)) && hasProject && projectData != null)
{
ReadOnlyMemory outputPath = e.Message.AsMemory().Slice(index + 4);
- project!.OutputPath = outputPath;
+ projectData.OutputPath = outputPath;
return;
}
}
@@ -1098,10 +948,9 @@ private void MessageRaised(object sender, BuildMessageEventArgs e)
}
}
- if (hasProject && project!.IsTestProject)
+ if (hasProject && projectData != null && projectData.IsTestProject)
{
- var node = _nodes[NodeIndexForContext(buildEventContext)];
-
+ var node = GetNodeForEvent(e);
// Consumes test update messages produced by VSTest and MSTest runner.
if (node != null && e is IExtendedBuildEventArgs extendedMessage)
{
@@ -1112,8 +961,11 @@ private void MessageRaised(object sender, BuildMessageEventArgs e)
var indicator = extendedMessage.ExtendedMetadata!["localizedResult"]!;
var displayName = extendedMessage.ExtendedMetadata!["displayName"]!;
- var status = new TerminalNodeStatus(node.Project, node.TargetFramework, node.RuntimeIdentifier, TerminalColor.Green, indicator, displayName, project.Stopwatch);
- UpdateNodeStatus(buildEventContext, status);
+ var status = new TerminalNodeStatus(node.Project, node.TargetFramework, node.RuntimeIdentifier, TerminalColor.Green, indicator, displayName, projectData.Stopwatch);
+ if (e.BuildEventContext != null)
+ {
+ StartNode(e, status);
+ }
break;
}
@@ -1122,8 +974,11 @@ private void MessageRaised(object sender, BuildMessageEventArgs e)
var indicator = extendedMessage.ExtendedMetadata!["localizedResult"]!;
var displayName = extendedMessage.ExtendedMetadata!["displayName"]!;
- var status = new TerminalNodeStatus(node.Project, node.TargetFramework, node.RuntimeIdentifier, TerminalColor.Yellow, indicator, displayName, project.Stopwatch);
- UpdateNodeStatus(buildEventContext, status);
+ var status = new TerminalNodeStatus(node.Project, node.TargetFramework, node.RuntimeIdentifier, TerminalColor.Yellow, indicator, displayName, projectData.Stopwatch);
+ if (e.BuildEventContext != null)
+ {
+ StartNode(e, status);
+ }
break;
}
@@ -1168,9 +1023,9 @@ private void MessageRaised(object sender, BuildMessageEventArgs e)
return;
}
- if (hasProject)
+ if (hasProject && projectData != null)
{
- project!.AddBuildMessage(TerminalMessageSeverity.Message, FormatInformationalMessage(e));
+ projectData.AddBuildMessage(TerminalMessageSeverity.Message, FormatInformationalMessage(e));
}
else
{
@@ -1194,13 +1049,9 @@ private void MessageRaised(object sender, BuildMessageEventArgs e)
}
}
- ///
- /// The callback.
- ///
- private void WarningRaised(object sender, BuildWarningEventArgs e)
+ ///
+ protected override void OnWarningRaised(BuildWarningEventArgs e, TerminalProjectInfo? projectData, TerminalBuildData buildData)
{
- BuildEventContext? buildEventContext = e.BuildEventContext;
-
// auth provider messages are 'global' in nature and should be a) immediate reported, and b) not re-reported in the summary.
if (IsAuthProviderMessage(e.Message))
{
@@ -1208,9 +1059,7 @@ private void WarningRaised(object sender, BuildWarningEventArgs e)
return;
}
- if (buildEventContext is not null
- && _projects.TryGetValue(new ProjectContext(buildEventContext), out TerminalProjectInfo? project)
- && Verbosity > LoggerVerbosity.Quiet)
+ if (projectData != null && Verbosity > LoggerVerbosity.Quiet)
{
// If the warning is not a 'global' auth provider message, but is immediate, we render it immediately
// but we don't early return so that the project also tracks it.
@@ -1221,17 +1070,17 @@ private void WarningRaised(object sender, BuildWarningEventArgs e)
// This is the general case - _most_ warnings are not immediate, so we add them to the project summary
// and display them in the per-project and final summary.
- project.AddBuildMessage(TerminalMessageSeverity.Warning, FormatWarningMessage(e, TripleIndentation));
+ projectData.AddBuildMessage(TerminalMessageSeverity.Warning, FormatWarningMessage(e, TripleIndentation));
}
else
{
// It is necessary to display warning messages reported by MSBuild,
- // even if it's not tracked in _projects collection or the verbosity is Quiet.
+ // even if it's not tracked in projects collection or the verbosity is Quiet.
// The idea here (similar to the implementation in ErrorRaised) is that
// even in Quiet scenarios we need to show warnings/errors, even if not in
// full project-tree view
RenderImmediateMessage(FormatWarningMessage(e, Indentation));
- _buildWarningsCount++;
+ buildData.BuildWarningsCount++;
}
}
@@ -1286,26 +1135,20 @@ private static bool IsAuthProviderMessage(string? message) =>
}
}
- ///
- /// The callback.
- ///
- private void ErrorRaised(object sender, BuildErrorEventArgs e)
+ ///
+ protected override void OnErrorRaised(BuildErrorEventArgs e, TerminalProjectInfo? projectData, TerminalBuildData buildData)
{
- BuildEventContext? buildEventContext = e.BuildEventContext;
-
- if (buildEventContext is not null
- && _projects.TryGetValue(new ProjectContext(buildEventContext), out TerminalProjectInfo? project)
- && Verbosity > LoggerVerbosity.Quiet)
+ if (projectData != null && Verbosity > LoggerVerbosity.Quiet)
{
- project.AddBuildMessage(TerminalMessageSeverity.Error, FormatErrorMessage(e, TripleIndentation));
+ projectData.AddBuildMessage(TerminalMessageSeverity.Error, FormatErrorMessage(e, TripleIndentation));
}
else
{
- // It is necessary to display error messages reported by MSBuild, even if it's not tracked in _projects collection or the verbosity is Quiet.
+ // It is necessary to display error messages reported by MSBuild, even if it's not tracked in projects collection or the verbosity is Quiet.
// For nicer formatting, any messages from the engine we strip the file portion from.
bool hasMSBuildPlaceholderLocation = e.File.Equals("MSBUILD", StringComparison.Ordinal);
RenderImmediateMessage(FormatErrorMessage(e, Indentation, requireFileAndLinePortion: !hasMSBuildPlaceholderLocation));
- _buildErrorsCount++;
+ buildData.BuildErrorsCount++;
}
}
@@ -1391,6 +1234,35 @@ private void EraseNodes()
#region Helpers
+ private TerminalNodeStatus? GetNodeForEvent(BuildEventArgs e)
+ {
+ var node = GetNodeIdForEvent(e);
+ if (node is int nodeId && _nodes[nodeId] is TerminalNodeStatus status)
+ {
+ return status;
+ }
+
+ return null;
+ }
+
+ private void StartNode(BuildEventArgs e, TerminalNodeStatus status)
+ {
+ var node = GetNodeIdForEvent(e);
+ if (node is int nodeId)
+ {
+ _nodes[nodeId] = status;
+ }
+ }
+
+ public void YieldNode(BuildEventArgs e)
+ {
+ var node = GetNodeIdForEvent(e);
+ if (node is int nodeId)
+ {
+ _nodes[nodeId] = null;
+ }
+ }
+
///
/// Construct a build result summary string.
///
@@ -1437,14 +1309,7 @@ private void RenderImmediateMessage(string message)
}
}
- ///
- /// Returns the index corresponding to the given .
- ///
- private int NodeIndexForContext(BuildEventContext context)
- {
- // Node IDs reported by the build are 1-based.
- return context.NodeId - 1;
- }
+ // NodeIndexForContext is now inherited from base class
///
/// Colorizes the filename part of the given path.
diff --git a/src/Build/Logging/TerminalLogger/TerminalMessageSeverity.cs b/src/Build/Logging/TerminalLogger/TerminalMessageSeverity.cs
index 40fafcea1c6..976db15823f 100644
--- a/src/Build/Logging/TerminalLogger/TerminalMessageSeverity.cs
+++ b/src/Build/Logging/TerminalLogger/TerminalMessageSeverity.cs
@@ -6,4 +6,4 @@ namespace Microsoft.Build.Logging;
///
/// Enumerates the supported message severities.
///
-internal enum TerminalMessageSeverity { Message, Warning, Error }
+public enum TerminalMessageSeverity { Message, Warning, Error }
diff --git a/src/Build/Logging/TerminalLogger/TerminalNodeStatus.cs b/src/Build/Logging/TerminalLogger/TerminalNodeStatus.cs
index dd14f8586ee..eb78f3719e3 100644
--- a/src/Build/Logging/TerminalLogger/TerminalNodeStatus.cs
+++ b/src/Build/Logging/TerminalLogger/TerminalNodeStatus.cs
@@ -11,7 +11,7 @@ namespace Microsoft.Build.Logging;
///
/// Encapsulates the per-node data shown in live node output.
///
-internal class TerminalNodeStatus
+public class TerminalNodeStatus
{
public string Project { get; }
public string? TargetFramework { get; }
diff --git a/src/Build/Logging/TerminalLogger/TerminalProjectInfo.cs b/src/Build/Logging/TerminalLogger/TerminalProjectInfo.cs
index b69c2c515e9..d1b4944e922 100644
--- a/src/Build/Logging/TerminalLogger/TerminalProjectInfo.cs
+++ b/src/Build/Logging/TerminalLogger/TerminalProjectInfo.cs
@@ -10,32 +10,25 @@ namespace Microsoft.Build.Logging;
///
/// A struct containing relevant evaluation-time data that may not be knowable just from ProjectStart events.
///
-///
-///
-///
-///
-internal record struct EvalProjectInfo(TerminalLogger.EvalContext context, string? ProjectFile, string? TargetFramework, string? RuntimeIdentifier)
+public record struct EvalProjectInfo(string? ProjectFile, string? TargetFramework, string? RuntimeIdentifier)
{
- public readonly int Id => context.Id;
}
///
/// Represents a project being built.
///
-internal sealed class TerminalProjectInfo
+public sealed class TerminalProjectInfo
{
private List? _buildMessages;
///
/// Initialized a new with the given .
///
- /// The ProjectContext of this project execution.
/// A subset of the interesting eval-time data for this running project
/// A stopwatch to time the build of the project.
- public TerminalProjectInfo(TerminalLogger.ProjectContext context, EvalProjectInfo evalInfo, StopwatchAbstraction? stopwatch)
+ public TerminalProjectInfo(EvalProjectInfo evalInfo, StopwatchAbstraction? stopwatch)
{
_evalInfo = evalInfo;
- _context = context;
if (stopwatch is not null)
{
@@ -48,11 +41,6 @@ public TerminalProjectInfo(TerminalLogger.ProjectContext context, EvalProjectInf
}
}
- ///
- /// The int value of the ProjectContext id of this project execution.
- ///
- public int Id => _context.Id;
-
///
/// The full path to the project file.
///
@@ -82,7 +70,6 @@ public TerminalProjectInfo(TerminalLogger.ProjectContext context, EvalProjectInf
/// The runtime identifier of the project or null if platform-agnostic.
///
public string? RuntimeIdentifier => _evalInfo.RuntimeIdentifier;
- private readonly TerminalLogger.ProjectContext _context;
private readonly EvalProjectInfo _evalInfo;
///
diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj
index d11075fd13b..45f18e67f5e 100644
--- a/src/Build/Microsoft.Build.csproj
+++ b/src/Build/Microsoft.Build.csproj
@@ -178,6 +178,7 @@
+
diff --git a/src/Framework/Logging/TerminalColor.cs b/src/Framework/Logging/TerminalColor.cs
index 10e66d8f719..102eacba511 100644
--- a/src/Framework/Logging/TerminalColor.cs
+++ b/src/Framework/Logging/TerminalColor.cs
@@ -6,7 +6,7 @@ namespace Microsoft.Build.Framework.Logging;
///
/// Enumerates the text colors supported by VT100 terminal.
///
-internal enum TerminalColor
+public enum TerminalColor
{
Black = 30,
Red = 31,