From d3ab458c6c483848bb8cbd0e0f365a8ee837e5ab Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 13 Jul 2017 15:47:52 -0700 Subject: [PATCH] Add flow logger to help with console output parallelism --- build/RepositoryBuild.targets | 3 +- .../Logger/DefaultPrefixMessageWriter.cs | 32 ++++++ build/tasks/Logger/FlowLogger.cs | 69 +++++++++++ build/tasks/Logger/IWriter.cs | 16 +++ build/tasks/Logger/TeamCityMessageWriter.cs | 107 ++++++++++++++++++ 5 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 build/tasks/Logger/DefaultPrefixMessageWriter.cs create mode 100644 build/tasks/Logger/FlowLogger.cs create mode 100644 build/tasks/Logger/IWriter.cs create mode 100644 build/tasks/Logger/TeamCityMessageWriter.cs diff --git a/build/RepositoryBuild.targets b/build/RepositoryBuild.targets index 52fe532c38..5099e9801d 100644 --- a/build/RepositoryBuild.targets +++ b/build/RepositoryBuild.targets @@ -29,6 +29,7 @@ $(RepositoryBuildArguments) /p:BuildNumber=$(BuildNumber) /p:Configuration=$(Configuration) /p:CommitHash=$(CommitHash) + $(RepositoryBuildArguments) /noconsolelogger '/l:RepoTasks.FlowLogger,$(MSBuildThisFileDirectory)tasks\bin\publish\RepoTasks.dll;Summary;FlowId=$(RepositoryToBuild)' $(_RepositoryBuildTargets) $(RepositoryBuildArguments) $(BuildRepositoryRoot)artifacts @@ -91,4 +92,4 @@ - \ No newline at end of file + diff --git a/build/tasks/Logger/DefaultPrefixMessageWriter.cs b/build/tasks/Logger/DefaultPrefixMessageWriter.cs new file mode 100644 index 0000000000..a5613941f6 --- /dev/null +++ b/build/tasks/Logger/DefaultPrefixMessageWriter.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Build.Framework; +using Microsoft.Build.Logging; + +namespace RepoTasks +{ + internal class DefaultPrefixMessageWriter : IWriter + { + private readonly string _flowId; + + public DefaultPrefixMessageWriter(WriteHandler write, string flowId) + { + _flowId = flowId; + var prefix = $"{_flowId,-22}| "; + WriteHandler = msg => write(prefix + msg); + } + + public WriteHandler WriteHandler { get; } + + public void OnBuildStarted(BuildStartedEventArgs e) + { + WriteHandler(e.Message + Environment.NewLine); + } + + public void OnBuildFinished(BuildFinishedEventArgs e) + { + } + } +} diff --git a/build/tasks/Logger/FlowLogger.cs b/build/tasks/Logger/FlowLogger.cs new file mode 100644 index 0000000000..27491e2e4d --- /dev/null +++ b/build/tasks/Logger/FlowLogger.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Logging; + +namespace RepoTasks +{ + public class FlowLogger : ConsoleLogger + { + private static readonly bool IsTeamCity = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TEAMCITY_PROJECT_NAME")); + + private volatile bool _initialized; + + public FlowLogger() + { + } + + public override void Initialize(IEventSource eventSource, int nodeCount) + { + PreInit(eventSource); + base.Initialize(eventSource, nodeCount); + } + + public override void Initialize(IEventSource eventSource) + { + PreInit(eventSource); + base.Initialize(eventSource); + } + + private void PreInit(IEventSource eventSource) + { + if (_initialized) return; + _initialized = true; + + var _flowId = GetFlowId(); + + var writer = IsTeamCity + ? (IWriter)new TeamCityMessageWriter(WriteHandler, _flowId) + : new DefaultPrefixMessageWriter(WriteHandler, _flowId); + + WriteHandler = writer.WriteHandler; + eventSource.BuildStarted += (o, e) => + { + writer.OnBuildStarted(e); + }; + eventSource.BuildFinished += (o, e) => + { + writer.OnBuildFinished(e); + }; + } + + private string GetFlowId() + { + var parameters = Parameters?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + if (parameters == null || parameters.Length == 0) + { + return null; + } + + const string flowIdParamName = "FlowId="; + return parameters + .FirstOrDefault(p => p.StartsWith(flowIdParamName, StringComparison.Ordinal)) + ?.Substring(flowIdParamName.Length); + } + } +} diff --git a/build/tasks/Logger/IWriter.cs b/build/tasks/Logger/IWriter.cs new file mode 100644 index 0000000000..d21b2f51ca --- /dev/null +++ b/build/tasks/Logger/IWriter.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Build.Framework; +using Microsoft.Build.Logging; + +namespace RepoTasks +{ + internal interface IWriter + { + WriteHandler WriteHandler { get; } + + void OnBuildFinished(BuildFinishedEventArgs e); + void OnBuildStarted(BuildStartedEventArgs e); + } +} diff --git a/build/tasks/Logger/TeamCityMessageWriter.cs b/build/tasks/Logger/TeamCityMessageWriter.cs new file mode 100644 index 0000000000..119376578b --- /dev/null +++ b/build/tasks/Logger/TeamCityMessageWriter.cs @@ -0,0 +1,107 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Logging; + +namespace RepoTasks +{ + /// + /// See https://confluence.jetbrains.com/display/TCD10/Build+Script+Interaction+with+TeamCity + /// + internal class TeamCityMessageWriter : IWriter + { + private const string MessagePrefix = "##teamcity"; + private static readonly string EOL = Environment.NewLine; + private readonly WriteHandler _write; + private readonly string _flowIdAttr; + + public TeamCityMessageWriter(WriteHandler write, string flowId) + { + _write = write; + _flowIdAttr = EscapeTeamCityText(flowId); + + WriteHandler = CreateMessageHandler(); + } + + public WriteHandler WriteHandler { get; } + + public void OnBuildFinished(BuildFinishedEventArgs e) + { + _write($"##teamcity[blockOpened name='Build {_flowIdAttr}' flowId='{_flowIdAttr}']" + EOL); + } + + public void OnBuildStarted(BuildStartedEventArgs e) + { + _write($"##teamcity[blockClosed name='Build {_flowIdAttr}' flowId='{_flowIdAttr}']" + EOL); + } + + private WriteHandler CreateMessageHandler() + { + var format = "##teamcity[message text='{0}' flowId='" + _flowIdAttr + "']" + EOL; + return message => + { + if (string.IsNullOrEmpty(message)) + { + return; + } + + if (message.StartsWith(MessagePrefix, StringComparison.Ordinal)) + { + _write(message); + return; + } + + _write( + string.Format( + System.Globalization.CultureInfo.InvariantCulture, + format, + EscapeTeamCityText(message))); + }; + } + + private static string EscapeTeamCityText(string txt) + { + if (string.IsNullOrEmpty(txt)) + { + return txt; + } + + var sb = new StringBuilder(txt.Length); + for (var i = 0; i < txt.Length; i++) + { + var ch = txt[i]; + switch (ch) + { + case '\'': + case '|': + case '[': + case ']': + sb.Append('|').Append(ch); + break; + case '\n': + sb.Append("|n"); + break; + case '\r': + sb.Append("|r"); + break; + case '\u0085': + sb.Append("|x"); + break; + case '\u2028': + sb.Append("|l"); + break; + case '\u2029': + sb.Append("|p"); + break; + default: + sb.Append(ch); + break; + } + } + return sb.ToString(); + } + } +}