TeamCity.MSBuild.Logger/TeamCityHierarchicalMessageWriter.cs (256 lines of code) (raw):

namespace TeamCity.MSBuild.Logger { using System; using System.Collections.Generic; using System.IO; using System.Text; using JetBrains.Annotations; using JetBrains.TeamCity.ServiceMessages; using JetBrains.TeamCity.ServiceMessages.Read; using JetBrains.TeamCity.ServiceMessages.Write.Special; using Microsoft.Build.Framework; // ReSharper disable once ClassNeverInstantiated.Global internal class TeamCityHierarchicalMessageWriter : IHierarchicalMessageWriter, ILogWriter, IDisposable { [NotNull] private readonly ILoggerContext _context; [NotNull] private readonly IColorStorage _colorStorage; [NotNull] private readonly IEventContext _eventContext; [NotNull] private readonly Dictionary<int, Flow> _flows = new Dictionary<int, Flow>(); [NotNull] private readonly IColorTheme _colorTheme; [NotNull] private readonly ITeamCityWriter _writer; [NotNull] private readonly IServiceMessageParser _serviceMessageParser; [NotNull] private readonly Dictionary<Flow, MessageInfo> _messages = new Dictionary<Flow, MessageInfo>(); [NotNull] private readonly List<string> _buildProblems = new List<string>(); private static int FlowId => HierarchicalContext.Current.FlowId; public TeamCityHierarchicalMessageWriter( [NotNull] ILoggerContext context, [NotNull] IColorTheme colorTheme, [NotNull] ITeamCityWriter writer, [NotNull] IServiceMessageParser serviceMessageParser, [NotNull] IColorStorage colorStorage, [NotNull] IEventContext eventContext) { _context = context ?? throw new ArgumentNullException(nameof(context)); _colorStorage = colorStorage ?? throw new ArgumentNullException(nameof(colorStorage)); _eventContext = eventContext; _colorTheme = colorTheme ?? throw new ArgumentNullException(nameof(colorTheme)); _writer = writer ?? throw new ArgumentNullException(nameof(writer)); _serviceMessageParser = serviceMessageParser ?? throw new ArgumentNullException(nameof(serviceMessageParser)); } private bool TryGetFlow(int flowId, out Flow flow, bool forceCreate) { if (_flows.TryGetValue(flowId, out flow)) { return true; } if (!forceCreate) { return false; } flow = new Flow(_writer, _eventContext, flowId == HierarchicalContext.DefaultFlowId); _flows.Add(FlowId, flow); return true; } public void StartBlock(string name) { if (name == null) throw new ArgumentNullException(nameof(name)); if (TryGetFlow(FlowId, out var flow, true)) { flow.StartBlock(name.Trim()); } } public void FinishBlock() { if (!TryGetFlow(FlowId, out var flow, false)) { return; } Write("\n", flow); flow.FinishBlock(); if (!flow.IsFinished) { return; } _flows.Remove(FlowId); flow.Dispose(); } public void Write(string message, IConsole console = null) { if (string.IsNullOrEmpty(message)) { return; } if (TryGetFlow(FlowId, out var flow, false) || TryGetFlow(HierarchicalContext.DefaultFlowId, out flow, true)) { // ReSharper disable once AssignNullToNotNullAttribute Write(message, flow); } } public void SetColor(Color color) { _colorStorage.SetColor(color); } public void ResetColor() { _colorStorage.ResetColor(); } public void Dispose() { foreach (var flow in _flows.Values) { _colorStorage.ResetColor(); Write("\n", flow); flow.Dispose(); } _flows.Clear(); if (_buildProblems.Count <= 0) { return; } _writer.WriteBuildProblem("msbuild", string.Join("\n", _buildProblems)); _buildProblems.Clear(); } private void Write([NotNull] string message, [NotNull] Flow flow) { if (message == null) throw new ArgumentNullException(nameof(message)); if (flow == null) throw new ArgumentNullException(nameof(flow)); if (!_messages.TryGetValue(flow, out var messageInfo)) { messageInfo = new MessageInfo(); _messages.Add(flow, messageInfo); } messageInfo.Text.Append(message); messageInfo.Color = _colorStorage.Color; if (!message.EndsWith("\n")) { return; } _messages.Remove(flow); var messageState = MessageState.Normal; if (messageInfo.Color.HasValue) { // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault switch (messageInfo.Color.Value) { case Color.Error: messageState = MessageState.Error; break; case Color.ErrorSummary: _buildProblems.Add(messageInfo.Text.ToString().TrimEnd()); return; case Color.Warning: case Color.WarningSummary: messageState = MessageState.Warning; break; } } var text = messageInfo.Text.ToString().TrimEnd(); var hasServiceMessage = false; if (_context.Parameters.PlainServiceMessage) { // TeamCity service message var trimmed = text.TrimStart(); if (trimmed.StartsWith("##teamcity[", StringComparison.CurrentCultureIgnoreCase)) { foreach (var serviceMessage in _serviceMessageParser.ParseServiceMessages(trimmed)) { hasServiceMessage = true; flow.Write(serviceMessage); } } } // MSBuild output if (!hasServiceMessage) { flow.Write(FormatMessage(messageState, messageInfo, text), messageState); } } private string FormatMessage(MessageState messageState, MessageInfo messageInfo, string text) => messageState == MessageState.Normal && !string.IsNullOrWhiteSpace(text) && messageInfo.Color.HasValue ? $"\x001B[{_colorTheme.GetAnsiColor(messageInfo.Color.Value)}m{text}" : text; private enum MessageState { Normal, Warning, Error } private class MessageInfo { public readonly StringBuilder Text = new StringBuilder(); public Color? Color; } private class Flow: IDisposable { private ITeamCityWriter _writer; private readonly Stack<ITeamCityWriter> _blocks = new Stack<ITeamCityWriter>(); private readonly IEventContext _eventContext; private readonly bool _isMainFlow; public Flow([NotNull] ITeamCityWriter writer, IEventContext eventContext, bool isMainFlow) { if (writer == null) throw new ArgumentNullException(nameof(writer)); _eventContext = eventContext; _isMainFlow = isMainFlow; _writer = isMainFlow ? writer : writer.OpenFlow(); } public bool IsFinished => !_isMainFlow && _blocks.Count == 0; public void StartBlock(string name) { var newWriter = _writer.OpenBlock(name); _blocks.Push(_writer); _writer = newWriter; } public void FinishBlock() { var prevWriter = _blocks.Pop(); _writer.Dispose(); _writer = prevWriter; } public void Write([NotNull] string message, MessageState messageState) { if (message == null) throw new ArgumentNullException(nameof(message)); // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault switch (messageState) { case MessageState.Warning: _writer.WriteWarning(message); break; case MessageState.Error: if ( _eventContext.TryGetEvent(out var buildEventManager) && buildEventManager is BuildErrorEventArgs buildErrorEventArgs) { var errorId = $"{GetProperty(buildErrorEventArgs.SenderName, string.Empty)}{GetProperty(buildErrorEventArgs.Code)},{buildErrorEventArgs.LineNumber},{buildErrorEventArgs.ColumnNumber}{GetProperty(GetFileName(buildErrorEventArgs.ProjectFile))}{GetProperty(GetFileName(buildErrorEventArgs.File))}"; errorId = errorId.Length >= 60 ? errorId.Substring(0, 59) : errorId; _writer.WriteBuildProblem(errorId, message); } else { _writer.WriteError(message); } break; default: _writer.WriteMessage(message); break; } } public void Write([NotNull] IServiceMessage message) { if (message == null) throw new ArgumentNullException(nameof(message)); _writer.WriteRawMessage(message); } public void Dispose() { if (!_isMainFlow) { _writer.Dispose(); } } [NotNull] private static string GetProperty([CanBeNull] string value, string prefix = ",") => string.IsNullOrWhiteSpace(value) ? string.Empty : $"{prefix}{value}"; [NotNull] private static string GetFileName([CanBeNull] string file) { try { if (!string.IsNullOrWhiteSpace(file)) { return Path.GetFileName(file); } } // ReSharper disable once EmptyGeneralCatchClause catch { } return string.Empty; } } } }