diff --git a/ref/Microsoft.Build.Framework/net/Microsoft.Build.Framework.cs b/ref/Microsoft.Build.Framework/net/Microsoft.Build.Framework.cs index 239f7910b68..03d3c5e61c8 100644 --- a/ref/Microsoft.Build.Framework/net/Microsoft.Build.Framework.cs +++ b/ref/Microsoft.Build.Framework/net/Microsoft.Build.Framework.cs @@ -38,6 +38,7 @@ public abstract partial class BuildEventArgs : System.EventArgs public Microsoft.Build.Framework.BuildEventContext BuildEventContext { get { throw null; } set { } } public string HelpKeyword { get { throw null; } } public virtual string Message { get { throw null; } protected set { } } + protected System.DateTime RawTimestamp { get { throw null; } set { } } public string SenderName { get { throw null; } } public int ThreadId { get { throw null; } } public System.DateTime Timestamp { get { throw null; } } @@ -589,6 +590,22 @@ public partial class TaskFinishedEventArgs : Microsoft.Build.Framework.BuildStat public string TaskName { get { throw null; } } } public delegate void TaskFinishedEventHandler(object sender, Microsoft.Build.Framework.TaskFinishedEventArgs e); + public partial class TaskParameterEventArgs : Microsoft.Build.Framework.BuildMessageEventArgs + { + public TaskParameterEventArgs(Microsoft.Build.Framework.TaskParameterMessageKind kind, string itemType, System.Collections.IList items, bool logItemMetadata, System.DateTime eventTimestamp) { } + public System.Collections.IList Items { get { throw null; } } + public string ItemType { get { throw null; } } + public Microsoft.Build.Framework.TaskParameterMessageKind Kind { get { throw null; } } + public bool LogItemMetadata { get { throw null; } } + public override string Message { get { throw null; } } + } + public enum TaskParameterMessageKind + { + TaskInput = 0, + TaskOutput = 1, + AddItem = 2, + RemoveItem = 3, + } public partial class TaskPropertyInfo { public TaskPropertyInfo(string name, System.Type typeOfParameter, bool output, bool required) { } diff --git a/ref/Microsoft.Build.Framework/netstandard/Microsoft.Build.Framework.cs b/ref/Microsoft.Build.Framework/netstandard/Microsoft.Build.Framework.cs index 9c2d45f2253..31d90e78d65 100644 --- a/ref/Microsoft.Build.Framework/netstandard/Microsoft.Build.Framework.cs +++ b/ref/Microsoft.Build.Framework/netstandard/Microsoft.Build.Framework.cs @@ -38,6 +38,7 @@ public abstract partial class BuildEventArgs : System.EventArgs public Microsoft.Build.Framework.BuildEventContext BuildEventContext { get { throw null; } set { } } public string HelpKeyword { get { throw null; } } public virtual string Message { get { throw null; } protected set { } } + protected System.DateTime RawTimestamp { get { throw null; } set { } } public string SenderName { get { throw null; } } public int ThreadId { get { throw null; } } public System.DateTime Timestamp { get { throw null; } } @@ -588,6 +589,22 @@ public partial class TaskFinishedEventArgs : Microsoft.Build.Framework.BuildStat public string TaskName { get { throw null; } } } public delegate void TaskFinishedEventHandler(object sender, Microsoft.Build.Framework.TaskFinishedEventArgs e); + public partial class TaskParameterEventArgs : Microsoft.Build.Framework.BuildMessageEventArgs + { + public TaskParameterEventArgs(Microsoft.Build.Framework.TaskParameterMessageKind kind, string itemType, System.Collections.IList items, bool logItemMetadata, System.DateTime eventTimestamp) { } + public System.Collections.IList Items { get { throw null; } } + public string ItemType { get { throw null; } } + public Microsoft.Build.Framework.TaskParameterMessageKind Kind { get { throw null; } } + public bool LogItemMetadata { get { throw null; } } + public override string Message { get { throw null; } } + } + public enum TaskParameterMessageKind + { + TaskInput = 0, + TaskOutput = 1, + AddItem = 2, + RemoveItem = 3, + } public partial class TaskPropertyInfo { public TaskPropertyInfo(string name, System.Type typeOfParameter, bool output, bool required) { } diff --git a/src/Build.UnitTests/BackEnd/EventSourceSink_Tests.cs b/src/Build.UnitTests/BackEnd/EventSourceSink_Tests.cs index 3798326b7e2..8dc2e82dbdd 100644 --- a/src/Build.UnitTests/BackEnd/EventSourceSink_Tests.cs +++ b/src/Build.UnitTests/BackEnd/EventSourceSink_Tests.cs @@ -68,6 +68,7 @@ public void ConsumeEventsGoodEventsNoHandlers() eventHelper.RaiseBuildEvent(RaiseEventHelper.NormalMessage); eventHelper.RaiseBuildEvent(RaiseEventHelper.TaskFinished); eventHelper.RaiseBuildEvent(RaiseEventHelper.CommandLine); + eventHelper.RaiseBuildEvent(RaiseEventHelper.TaskParameter); eventHelper.RaiseBuildEvent(RaiseEventHelper.Warning); eventHelper.RaiseBuildEvent(RaiseEventHelper.Error); eventHelper.RaiseBuildEvent(RaiseEventHelper.TargetStarted); @@ -99,6 +100,7 @@ public void LoggerExceptionInEventHandler() RaiseExceptionInEventHandler(RaiseEventHelper.NormalMessage, exception); RaiseExceptionInEventHandler(RaiseEventHelper.TaskFinished, exception); RaiseExceptionInEventHandler(RaiseEventHelper.CommandLine, exception); + RaiseExceptionInEventHandler(RaiseEventHelper.TaskParameter, exception); RaiseExceptionInEventHandler(RaiseEventHelper.Warning, exception); RaiseExceptionInEventHandler(RaiseEventHelper.Error, exception); RaiseExceptionInEventHandler(RaiseEventHelper.TargetStarted, exception); @@ -733,6 +735,11 @@ internal class RaiseEventHelper /// private static TaskCommandLineEventArgs s_taskCommandLine = new TaskCommandLineEventArgs("commandLine", "taskName", MessageImportance.Low); + /// + /// Task Parameter Event + /// + private static TaskParameterEventArgs s_taskParameter = new TaskParameterEventArgs(TaskParameterMessageKind.TaskInput, "ItemName", null, true, DateTime.MinValue); + /// /// Build Warning Event /// @@ -883,6 +890,11 @@ internal static TaskCommandLineEventArgs CommandLine } } + /// + /// Event which can be raised in multiple tests. + /// + internal static TaskParameterEventArgs TaskParameter => s_taskParameter; + /// /// Event which can be raised in multiple tests. /// diff --git a/src/Build.UnitTests/BackEnd/NodePackets_Tests.cs b/src/Build.UnitTests/BackEnd/NodePackets_Tests.cs index 459d835bd25..3e860fc7d5f 100644 --- a/src/Build.UnitTests/BackEnd/NodePackets_Tests.cs +++ b/src/Build.UnitTests/BackEnd/NodePackets_Tests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Linq; using Microsoft.Build.Framework; using Microsoft.Build.BackEnd; using Microsoft.Build.Shared; @@ -44,6 +45,7 @@ public void VerifyEventType() TaskStartedEventArgs taskStarted = new TaskStartedEventArgs("message", "help", "projectFile", "taskFile", "taskName"); TaskFinishedEventArgs taskFinished = new TaskFinishedEventArgs("message", "help", "projectFile", "taskFile", "taskName", true); TaskCommandLineEventArgs commandLine = new TaskCommandLineEventArgs("commandLine", "taskName", MessageImportance.Low); + TaskParameterEventArgs taskParameter = CreateTaskParameter(); BuildWarningEventArgs warning = new BuildWarningEventArgs("SubCategoryForSchemaValidationErrors", "MSB4000", "file", 1, 2, 3, 4, "message", "help", "sender"); BuildErrorEventArgs error = new BuildErrorEventArgs("SubCategoryForSchemaValidationErrors", "MSB4000", "file", 1, 2, 3, 4, "message", "help", "sender"); TargetStartedEventArgs targetStarted = new TargetStartedEventArgs("message", "help", "targetName", "ProjectFile", "targetFile"); @@ -58,6 +60,7 @@ public void VerifyEventType() VerifyLoggingPacket(taskStarted, LoggingEventType.TaskStartedEvent); VerifyLoggingPacket(taskFinished, LoggingEventType.TaskFinishedEvent); VerifyLoggingPacket(commandLine, LoggingEventType.TaskCommandLineEvent); + VerifyLoggingPacket(taskParameter, LoggingEventType.TaskParameterEvent); VerifyLoggingPacket(warning, LoggingEventType.BuildWarningEvent); VerifyLoggingPacket(error, LoggingEventType.BuildErrorEvent); VerifyLoggingPacket(targetStarted, LoggingEventType.TargetStartedEvent); @@ -67,12 +70,41 @@ public void VerifyEventType() VerifyLoggingPacket(externalStartedEvent, LoggingEventType.CustomEvent); } + private static TaskParameterEventArgs CreateTaskParameter() + { + var items = new TaskItemData[] + { + new TaskItemData("ItemSpec1", null), + new TaskItemData("ItemSpec2", Enumerable.Range(1,3).ToDictionary(i => i.ToString(), i => i.ToString() + "value")) + }; + var result = new TaskParameterEventArgs( + TaskParameterMessageKind.TaskInput, + "ItemName", + items, + logItemMetadata: true, + DateTime.MinValue); + + // normalize line endings as we can't rely on the line endings of NodePackets_Tests.cs + Assert.Equal(@"Task Parameter: + ItemName= + ItemSpec1 + ItemSpec2 + 1=1value + 2=2value + 3=3value".Replace("\r\n", "\n"), result.Message); + + return result; + } + /// /// Tests serialization of LogMessagePacket with each kind of event type. /// [Fact] public void TestTranslation() { + // need to touch the type so that the static constructor runs + _ = ItemGroupLoggingHelper.OutputItemParameterMessagePrefix; + TaskItem item = new TaskItem("Hello", "my.proj"); List targetOutputs = new List(); targetOutputs.Add(item); @@ -88,6 +120,7 @@ public void TestTranslation() new TaskStartedEventArgs("message", "help", "projectFile", "taskFile", "taskName"), new TaskFinishedEventArgs("message", "help", "projectFile", "taskFile", "taskName", true), new TaskCommandLineEventArgs("commandLine", "taskName", MessageImportance.Low), + CreateTaskParameter(), new BuildWarningEventArgs("SubCategoryForSchemaValidationErrors", "MSB4000", "file", 1, 2, 3, 4, "message", "help", "sender"), new BuildErrorEventArgs("SubCategoryForSchemaValidationErrors", "MSB4000", "file", 1, 2, 3, 4, "message", "help", "sender"), new TargetStartedEventArgs("message", "help", "targetName", "ProjectFile", "targetFile"), @@ -281,6 +314,19 @@ private void CompareLogMessagePackets(LogMessagePacket left, LogMessagePacket ri Assert.Equal(leftCommand.TaskName, rightCommand.TaskName); break; + case LoggingEventType.TaskParameterEvent: + var leftTaskParameter = left.NodeBuildEvent.Value.Value as TaskParameterEventArgs; + var rightTaskParameter = right.NodeBuildEvent.Value.Value as TaskParameterEventArgs; + Assert.NotNull(leftTaskParameter); + Assert.NotNull(rightTaskParameter); + Assert.Equal(leftTaskParameter.Kind, rightTaskParameter.Kind); + Assert.Equal(leftTaskParameter.ItemType, rightTaskParameter.ItemType); + Assert.Equal(leftTaskParameter.Items.Count, rightTaskParameter.Items.Count); + Assert.Equal(leftTaskParameter.Message, rightTaskParameter.Message); + Assert.Equal(leftTaskParameter.BuildEventContext, rightTaskParameter.BuildEventContext); + Assert.Equal(leftTaskParameter.Timestamp, rightTaskParameter.Timestamp); + break; + case LoggingEventType.TaskFinishedEvent: TaskFinishedEventArgs leftTaskFinished = left.NodeBuildEvent.Value.Value as TaskFinishedEventArgs; TaskFinishedEventArgs rightTaskFinished = right.NodeBuildEvent.Value.Value as TaskFinishedEventArgs; diff --git a/src/Build.UnitTests/BinaryLogger_Tests.cs b/src/Build.UnitTests/BinaryLogger_Tests.cs index ec95009bec1..8deb4db94b7 100644 --- a/src/Build.UnitTests/BinaryLogger_Tests.cs +++ b/src/Build.UnitTests/BinaryLogger_Tests.cs @@ -7,7 +7,7 @@ namespace Microsoft.Build.UnitTests { public class BinaryLoggerTests : IDisposable { - private static string s_testProject = @" + private const string s_testProject = @" Test @@ -22,6 +22,38 @@ public class BinaryLoggerTests : IDisposable "; + + private const string s_testProject2 = @" + + + + + + + fromItemDefinition%61%62%63<> + + + + + + MetadataValue1%61%62%63<> + + + + + custom%61%62%63<> + + + + + + + + + + + "; + private readonly TestEnvironment _env; private string _logFile; @@ -35,8 +67,10 @@ public BinaryLoggerTests(ITestOutputHelper output) _logFile = _env.ExpectFile(".binlog").Path; } - [Fact] - public void TestBinaryLoggerRoundtrip() + [Theory] + [InlineData(s_testProject)] + [InlineData(s_testProject2)] + public void TestBinaryLoggerRoundtrip(string projectText) { var binaryLogger = new BinaryLogger(); @@ -45,14 +79,14 @@ public void TestBinaryLoggerRoundtrip() var mockLogFromBuild = new MockLogger(); // build and log into binary logger and mockLogger1 - ObjectModelHelpers.BuildProjectExpectSuccess(s_testProject, binaryLogger, mockLogFromBuild); + ObjectModelHelpers.BuildProjectExpectSuccess(projectText, binaryLogger, mockLogFromBuild); var mockLogFromPlayback = new MockLogger(); var binaryLogReader = new BinaryLogReplayEventSource(); mockLogFromPlayback.Initialize(binaryLogReader); - // read the binary log and replay into mockLogger2testassembly + // read the binary log and replay into mockLogger2 binaryLogReader.Replay(_logFile); // the binlog will have more information than recorded by the text log diff --git a/src/Build.UnitTests/BuildEventArgsSerialization_Tests.cs b/src/Build.UnitTests/BuildEventArgsSerialization_Tests.cs index afd56aea915..a2874d96c06 100644 --- a/src/Build.UnitTests/BuildEventArgsSerialization_Tests.cs +++ b/src/Build.UnitTests/BuildEventArgsSerialization_Tests.cs @@ -1,9 +1,10 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using Microsoft.Build.BackEnd; using Microsoft.Build.Framework; using Microsoft.Build.Framework.Profiler; using Microsoft.Build.Logging; @@ -70,21 +71,39 @@ public void RoundtripProjectStartedEventArgs() toolsVersion: "Current"); args.BuildEventContext = new BuildEventContext(1, 2, 3, 4, 5, 6); - Roundtrip(args, + Roundtrip(args, e => ToString(e.BuildEventContext), e => ToString(e.GlobalProperties), - e => ToString(e.Items.OfType().ToDictionary(d => d.Key.ToString(), d => ((ITaskItem)d.Value).ItemSpec)), + e => GetItemsString(e.Items), e => e.Message, e => ToString(e.ParentProjectBuildEventContext), e => e.ProjectFile, e => e.ProjectId.ToString(), - e => ToString(e.Properties.OfType().ToDictionary(d => d.Key.ToString(), d => d.Value.ToString())), + e => ToString(e.Properties.OfType().ToDictionary((Func)(d => d.Key.ToString()), (Func)(d => d.Value.ToString()))), e => e.TargetNames, e => e.ThreadId.ToString(), e => e.Timestamp.ToString(), e => e.ToolsVersion); } + private string GetItemsString(IEnumerable items) + { + return ToString(items.OfType().ToDictionary(d => d.Key.ToString(), d => GetTaskItemString((ITaskItem)d.Value))); + } + + private string GetTaskItemString(ITaskItem taskItem) + { + var sb = new StringBuilder(); + sb.Append(taskItem.ItemSpec); + foreach (string name in taskItem.MetadataNames) + { + var value = taskItem.GetMetadata(name); + sb.Append($";{name}={value}"); + } + + return sb.ToString(); + } + [Fact] public void RoundtripProjectFinishedEventArgs() { @@ -309,6 +328,23 @@ public void RoundtripTaskCommandLineEventArgs() e => e.Subcategory); } + [Fact] + public void RoundtripTaskParameterEventArgs() + { + var items = new TaskItemData[] + { + new TaskItemData("ItemSpec1", null), + new TaskItemData("ItemSpec2", Enumerable.Range(1,3).ToDictionary(i => i.ToString(), i => i.ToString() + "value")) + }; + var args = new TaskParameterEventArgs(TaskParameterMessageKind.TaskOutput, "ItemName", items, true, DateTime.MinValue); + + Roundtrip(args, + e => e.Kind.ToString(), + e => e.ItemType, + e => e.LogItemMetadata.ToString(), + e => GetItemsString(e.Items)); + } + [Fact] public void RoundtripProjectEvaluationStartedEventArgs() { diff --git a/src/Build.UnitTests/ConfigureableForwardingLogger_Tests.cs b/src/Build.UnitTests/ConfigureableForwardingLogger_Tests.cs index f584072937b..3698f90ed5d 100644 --- a/src/Build.UnitTests/ConfigureableForwardingLogger_Tests.cs +++ b/src/Build.UnitTests/ConfigureableForwardingLogger_Tests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; using System.Collections.Generic; using Microsoft.Build.Framework; using Microsoft.Build.Logging; @@ -20,6 +21,7 @@ public class ConfigureableForwardingLogger_Tests private readonly TaskStartedEventArgs _taskStarted = new TaskStartedEventArgs("message", "help", "projectFile", "taskFile", "taskName"); private readonly TaskFinishedEventArgs _taskFinished = new TaskFinishedEventArgs("message", "help", "projectFile", "taskFile", "taskName", true); private readonly TaskCommandLineEventArgs _commandLine = new TaskCommandLineEventArgs("commandLine", "taskName", MessageImportance.Low); + private readonly TaskParameterEventArgs _taskParameter = new TaskParameterEventArgs(TaskParameterMessageKind.TaskInput, "ItemName", null, true, DateTime.MinValue); private readonly BuildWarningEventArgs _warning = new BuildWarningEventArgs("SubCategoryForSchemaValidationErrors", "MSB4000", "file", 1, 2, 3, 4, "message", "help", "sender"); private readonly BuildErrorEventArgs _error = new BuildErrorEventArgs("SubCategoryForSchemaValidationErrors", "MSB4000", "file", 1, 2, 3, 4, "message", "help", "sender"); private readonly TargetStartedEventArgs _targetStarted = new TargetStartedEventArgs("message", "help", "targetName", "ProjectFile", "targetFile"); @@ -131,6 +133,7 @@ public void ForwardingLoggingEventsBasedOnVerbosity(LoggerVerbosity? loggerVerbo _normalMessage, _highMessage, _commandLine, + _taskParameter, _warning, _error, _taskFinished, @@ -150,6 +153,7 @@ public void ForwardingLoggingEventsBasedOnVerbosity(LoggerVerbosity? loggerVerbo _normalMessage, _highMessage, _commandLine, + _taskParameter, _externalStartedEvent, _warning, _error, @@ -266,6 +270,7 @@ private void RaiseEvents(EventSourceSink source) source.Consume(_normalMessage); source.Consume(_highMessage); source.Consume(_commandLine); + source.Consume(_taskParameter); source.Consume(_externalStartedEvent); source.Consume(_warning); source.Consume(_error); diff --git a/src/Build/BackEnd/Components/Logging/LoggingService.cs b/src/Build/BackEnd/Components/Logging/LoggingService.cs index 85c95d728d1..074ec86ccca 100644 --- a/src/Build/BackEnd/Components/Logging/LoggingService.cs +++ b/src/Build/BackEnd/Components/Logging/LoggingService.cs @@ -285,6 +285,10 @@ protected LoggingService(LoggerMode loggerMode, int nodeId) CreateLoggingEventQueue(); } + // Ensure the static constructor of ItemGroupLoggingHelper runs. + // It is important to ensure the Message delegate on TaskParameterEventArgs is set. + _ = ItemGroupLoggingHelper.ItemGroupIncludeLogMessagePrefix; + _serviceState = LoggingServiceState.Instantiated; } diff --git a/src/Build/BackEnd/Components/Logging/TargetLoggingContext.cs b/src/Build/BackEnd/Components/Logging/TargetLoggingContext.cs index 6daf7608f20..efde35dd8af 100644 --- a/src/Build/BackEnd/Components/Logging/TargetLoggingContext.cs +++ b/src/Build/BackEnd/Components/Logging/TargetLoggingContext.cs @@ -138,6 +138,11 @@ internal TargetOutputItemsInstanceEnumeratorProxy(IEnumerable backingI _backingItems = backingItems; } + // For performance reasons we need to expose the raw items to BinaryLogger + // as we know we're not going to mutate anything. This allows us to bypass DeepClone + // for each item + internal IEnumerable BackingItems => _backingItems; + /// /// Returns an enumerator that provides copies of the items /// in the backing store. diff --git a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs index 9b24ebd0cec..66195775b22 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs @@ -213,12 +213,12 @@ private void ExecuteAdd(ProjectItemGroupTaskItemInstance child, ItemBucket bucke if (LogTaskInputs && !LoggingContext.LoggingService.OnlyLogCriticalEvents && itemsToAdd?.Count > 0) { - var itemGroupText = ItemGroupLoggingHelper.GetParameterText( - ItemGroupLoggingHelper.ItemGroupIncludeLogMessagePrefix, + ItemGroupLoggingHelper.LogTaskParameter( + LoggingContext, + TaskParameterMessageKind.AddItem, child.ItemType, itemsToAdd, logItemMetadata: true); - LoggingContext.LogCommentFromText(MessageImportance.Low, itemGroupText); } // Now add the items we created to the lookup. @@ -256,12 +256,12 @@ private void ExecuteRemove(ProjectItemGroupTaskItemInstance child, ItemBucket bu { if (LogTaskInputs && !LoggingContext.LoggingService.OnlyLogCriticalEvents && itemsToRemove.Count > 0) { - var itemGroupText = ItemGroupLoggingHelper.GetParameterText( - ItemGroupLoggingHelper.ItemGroupRemoveLogMessage, + ItemGroupLoggingHelper.LogTaskParameter( + LoggingContext, + TaskParameterMessageKind.RemoveItem, child.ItemType, itemsToRemove, logItemMetadata: true); - LoggingContext.LogCommentFromText(MessageImportance.Low, itemGroupText); } bucket.Lookup.RemoveItems(itemsToRemove); diff --git a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupLoggingHelper.cs b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupLoggingHelper.cs index 9aa0bfd4fce..2b8d80d306c 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupLoggingHelper.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupLoggingHelper.cs @@ -5,6 +5,8 @@ using System.Collections; using System.Collections.Generic; using System.Globalization; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Collections; using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.Utilities; @@ -31,6 +33,17 @@ internal static class ItemGroupLoggingHelper internal static string OutputItemParameterMessagePrefix = ResourceUtilities.GetResourceString("OutputItemParameterMessagePrefix"); internal static string TaskParameterPrefix = ResourceUtilities.GetResourceString("TaskParameterPrefix"); + /// + /// by itself doesn't have the implementation + /// to materialize the Message as that's a declaration assembly. We inject the logic + /// here. + /// + static ItemGroupLoggingHelper() + { + TaskParameterEventArgs.MessageGetter = GetTaskParameterText; + TaskParameterEventArgs.DictionaryFactory = ArrayDictionary.Create; + } + /// /// Gets a text serialized value of a parameter for logging. /// @@ -203,5 +216,62 @@ private static void AppendStringFromParameterValue(ReuseableStringBuilder sb, ob ErrorUtilities.ThrowInternalErrorUnreachable(); } } + + internal static void LogTaskParameter( + LoggingContext loggingContext, + TaskParameterMessageKind messageKind, + string itemType, + IList items, + bool logItemMetadata) + { + var args = CreateTaskParameterEventArgs( + loggingContext.BuildEventContext, + messageKind, + itemType, + items, + logItemMetadata, + DateTime.UtcNow); + loggingContext.LogBuildEvent(args); + } + + internal static TaskParameterEventArgs CreateTaskParameterEventArgs( + BuildEventContext buildEventContext, + TaskParameterMessageKind messageKind, + string itemType, + IList items, + bool logItemMetadata, + DateTime timestamp) + { + var args = new TaskParameterEventArgs( + messageKind, + itemType, + items, + logItemMetadata, + timestamp); + args.BuildEventContext = buildEventContext; + return args; + } + + internal static string GetTaskParameterText(TaskParameterEventArgs args) + => GetTaskParameterText(args.Kind, args.ItemType, args.Items, args.LogItemMetadata); + + internal static string GetTaskParameterText(TaskParameterMessageKind messageKind, string itemType, IList items, bool logItemMetadata) + { + var resourceText = messageKind switch + { + TaskParameterMessageKind.AddItem => ItemGroupIncludeLogMessagePrefix, + TaskParameterMessageKind.RemoveItem => ItemGroupRemoveLogMessage, + TaskParameterMessageKind.TaskInput => TaskParameterPrefix, + TaskParameterMessageKind.TaskOutput => OutputItemParameterMessagePrefix, + _ => throw new NotImplementedException($"Unsupported {nameof(TaskParameterMessageKind)} value: {messageKind}") + }; + + var itemGroupText = GetParameterText( + resourceText, + itemType, + items, + logItemMetadata); + return itemGroupText; + } } } diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index 7bf9a69aebc..dc75cd9496e 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -1333,12 +1333,12 @@ private bool InternalSetTaskParameter(TaskPropertyInfo parameter, IList paramete parameterValue.Count > 0 && parameter.Log) { - string parameterText = ItemGroupLoggingHelper.GetParameterText( - ItemGroupLoggingHelper.TaskParameterPrefix, + ItemGroupLoggingHelper.LogTaskParameter( + _taskLoggingContext, + TaskParameterMessageKind.TaskInput, parameter.Name, parameterValue, parameter.LogItemMetadata); - _taskLoggingContext.LogCommentFromText(MessageImportance.Low, parameterText); } return InternalSetTaskParameter(parameter, (object)parameterValue); @@ -1426,11 +1426,16 @@ private void GatherTaskItemOutputs(bool outputTargetIsItem, string outputTargetN { if (outputTargetIsItem) { + // Only count non-null elements. We sometimes have a single-element array where the element is null + bool hasElements = false; + foreach (ITaskItem output in outputs) { // if individual items in the array are null, ignore them if (output != null) { + hasElements = true; + ProjectItemInstance newItem; TaskItem outputAsProjectItem = output as TaskItem; @@ -1474,15 +1479,14 @@ private void GatherTaskItemOutputs(bool outputTargetIsItem, string outputTargetN } } - if (LogTaskInputs && !_taskLoggingContext.LoggingService.OnlyLogCriticalEvents && outputs.Length > 0 && parameter.Log) + if (hasElements && LogTaskInputs && !_taskLoggingContext.LoggingService.OnlyLogCriticalEvents && parameter.Log) { - string parameterText = ItemGroupLoggingHelper.GetParameterText( - ItemGroupLoggingHelper.OutputItemParameterMessagePrefix, + ItemGroupLoggingHelper.LogTaskParameter( + _taskLoggingContext, + TaskParameterMessageKind.TaskOutput, outputTargetName, outputs, parameter.LogItemMetadata); - - _taskLoggingContext.LogCommentFromText(MessageImportance.Low, parameterText); } } else @@ -1553,12 +1557,12 @@ private void GatherArrayStringAndValueOutputs(bool outputTargetIsItem, string ou if (LogTaskInputs && !_taskLoggingContext.LoggingService.OnlyLogCriticalEvents && outputs.Length > 0 && parameter.Log) { - string parameterText = ItemGroupLoggingHelper.GetParameterText( - ItemGroupLoggingHelper.OutputItemParameterMessagePrefix, + ItemGroupLoggingHelper.LogTaskParameter( + _taskLoggingContext, + TaskParameterMessageKind.TaskOutput, outputTargetName, outputs, parameter.LogItemMetadata); - _taskLoggingContext.LogCommentFromText(MessageImportance.Low, parameterText); } } else diff --git a/src/Build/Collections/ArrayDictionary.cs b/src/Build/Collections/ArrayDictionary.cs new file mode 100644 index 00000000000..9cdf1d7a0e1 --- /dev/null +++ b/src/Build/Collections/ArrayDictionary.cs @@ -0,0 +1,261 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Build.Collections +{ + /// + /// Lightweight, read-only IDictionary implementation using two arrays + /// and O(n) lookup. + /// Requires specifying capacity at construction and does not + /// support reallocation to increase capacity. + /// + /// Type of keys + /// Type of values + internal class ArrayDictionary : IDictionary, IDictionary + { + private TKey[] keys; + private TValue[] values; + + private int count; + + public ArrayDictionary(int capacity) + { + keys = new TKey[capacity]; + values = new TValue[capacity]; + } + + public static IDictionary Create(int capacity) + { + return new ArrayDictionary(capacity); + } + + public TValue this[TKey key] + { + get + { + TryGetValue(key, out var value); + return value; + } + + set + { + var comparer = KeyComparer; + for (int i = 0; i < count; i++) + { + if (comparer.Equals(key, keys[i])) + { + values[i] = value; + return; + } + } + + Add(key, value); + } + } + + object IDictionary.this[object key] + { + get => this[(TKey)key]; + set => this[(TKey)key] = (TValue)value; + } + + public ICollection Keys => keys; + + ICollection IDictionary.Keys => keys; + + public ICollection Values => values; + + ICollection IDictionary.Values => values; + + private IEqualityComparer KeyComparer => EqualityComparer.Default; + + private IEqualityComparer ValueComparer => EqualityComparer.Default; + + public int Count => count; + + public bool IsReadOnly => true; + + bool IDictionary.IsFixedSize => true; + + object ICollection.SyncRoot => this; + + bool ICollection.IsSynchronized => false; + + public void Add(TKey key, TValue value) + { + if (count < keys.Length) + { + keys[count] = key; + values[count] = value; + count += 1; + } + else + { + throw new InvalidOperationException($"ArrayDictionary is at capacity {keys.Length}"); + } + } + + public void Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + public void Clear() + { + throw new System.NotImplementedException(); + } + + public bool Contains(KeyValuePair item) + { + var keyComparer = KeyComparer; + var valueComparer = ValueComparer; + for (int i = 0; i < count; i++) + { + if (keyComparer.Equals(item.Key, keys[i]) && valueComparer.Equals(item.Value, values[i])) + { + return true; + } + } + + return false; + } + + public bool ContainsKey(TKey key) + { + var comparer = KeyComparer; + for (int i = 0; i < count; i++) + { + if (comparer.Equals(key, keys[i])) + { + return true; + } + } + + return false; + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + for (int i = 0; i < count; i++) + { + array[arrayIndex + i] = new KeyValuePair(keys[i], values[i]); + } + } + + void ICollection.CopyTo(Array array, int index) + { + throw new NotImplementedException(); + } + + public IEnumerator> GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + IDictionaryEnumerator IDictionary.GetEnumerator() + { + return new Enumerator(this, emitDictionaryEntries: true); + } + + public bool Remove(TKey key) + { + throw new System.NotImplementedException(); + } + + public bool Remove(KeyValuePair item) + { + throw new System.NotImplementedException(); + } + + public bool TryGetValue(TKey key, out TValue value) + { + var comparer = KeyComparer; + for (int i = 0; i < count; i++) + { + if (comparer.Equals(key, keys[i])) + { + value = values[i]; + return true; + } + } + + value = default; + return false; + } + + bool IDictionary.Contains(object key) + { + if (key is not TKey typedKey) + { + return false; + } + + return ContainsKey(typedKey); + } + + void IDictionary.Add(object key, object value) + { + if (key is TKey typedKey && value is TValue typedValue) + { + Add(typedKey, typedValue); + } + + throw new NotSupportedException(); + } + + void IDictionary.Remove(object key) + { + throw new NotImplementedException(); + } + + private struct Enumerator : IEnumerator>, IDictionaryEnumerator + { + private readonly ArrayDictionary _dictionary; + private readonly bool _emitDictionaryEntries; + private int _position; + + public Enumerator(ArrayDictionary dictionary, bool emitDictionaryEntries = false) + { + this._dictionary = dictionary; + this._position = -1; + this._emitDictionaryEntries = emitDictionaryEntries; + } + + public KeyValuePair Current => + new KeyValuePair( + _dictionary.keys[_position], + _dictionary.values[_position]); + + private DictionaryEntry CurrentDictionaryEntry => new DictionaryEntry(_dictionary.keys[_position], _dictionary.values[_position]); + + object IEnumerator.Current => _emitDictionaryEntries ? CurrentDictionaryEntry : Current; + + object IDictionaryEnumerator.Key => _dictionary.keys[_position]; + + object IDictionaryEnumerator.Value => _dictionary.values[_position]; + + DictionaryEntry IDictionaryEnumerator.Entry => CurrentDictionaryEntry; + + public void Dispose() + { + } + + public bool MoveNext() + { + _position += 1; + return _position < _dictionary.Count; + } + + public void Reset() + { + throw new NotImplementedException(); + } + } + } +} \ No newline at end of file diff --git a/src/Build/Instance/ProjectItemInstance.cs b/src/Build/Instance/ProjectItemInstance.cs index 6200da27e43..f9dc6429ec1 100644 --- a/src/Build/Instance/ProjectItemInstance.cs +++ b/src/Build/Instance/ProjectItemInstance.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -28,7 +28,13 @@ namespace Microsoft.Build.Execution /// and evaluation has already been performed, so it is unnecessary bulk. /// [DebuggerDisplay("{ItemType}={EvaluatedInclude} #DirectMetadata={DirectMetadataCount})")] - public class ProjectItemInstance : IItem, ITaskItem2, IMetadataTable, ITranslatable, IDeepCloneable + public class ProjectItemInstance : + IItem, + ITaskItem2, + IMetadataTable, + ITranslatable, + IDeepCloneable, + IMetadataContainer { /// /// The project instance to which this item belongs. @@ -515,6 +521,8 @@ IDictionary ITaskItem2.CloneCustomMetadataEscaped() return ((ITaskItem2)_taskItem).CloneCustomMetadataEscaped(); } + IEnumerable> IMetadataContainer.EnumerateMetadata() => _taskItem.EnumerateMetadata(); + #region IMetadataTable Members /// @@ -723,7 +731,11 @@ internal sealed class TaskItem : #if FEATURE_APPDOMAIN MarshalByRefObject, #endif - ITaskItem2, IItem, ITranslatable, IEquatable + ITaskItem2, + IItem, + ITranslatable, + IEquatable, + IMetadataContainer { /// /// The source file that defined this item. @@ -1045,6 +1057,36 @@ internal int DirectMetadataCount get { return (_directMetadata == null) ? 0 : _directMetadata.Count; } } + /// + /// Efficient way to retrieve metadata used by packet serialization + /// and binary logger. + /// + public IEnumerable> EnumerateMetadata() + { + // If we have item definitions, call the expensive property that does the right thing. + // Otherwise use _directMetadata to avoid allocations caused by DeepClone(). + var list = _itemDefinitions != null ? MetadataCollection : _directMetadata; + if (list != null) + { + return EnumerateMetadata(list); + } + else + { + return Array.Empty>(); + } + } + + private IEnumerable> EnumerateMetadata(CopyOnWritePropertyDictionary list) + { + foreach (var projectMetadataInstance in list) + { + if (projectMetadataInstance != null) + { + yield return new KeyValuePair(projectMetadataInstance.Name, projectMetadataInstance.EvaluatedValue); + } + } + } + /// /// Unordered collection of evaluated metadata on the item. /// If there is no metadata, returns an empty collection. diff --git a/src/Build/Logging/BinaryLogger/BinaryLogRecordKind.cs b/src/Build/Logging/BinaryLogger/BinaryLogRecordKind.cs index d8c90800210..dcb22fc4979 100644 --- a/src/Build/Logging/BinaryLogger/BinaryLogRecordKind.cs +++ b/src/Build/Logging/BinaryLogger/BinaryLogRecordKind.cs @@ -27,5 +27,6 @@ internal enum BinaryLogRecordKind PropertyInitialValueSet, NameValueList, String, + TaskParameter } } diff --git a/src/Build/Logging/BinaryLogger/BinaryLogReplayEventSource.cs b/src/Build/Logging/BinaryLogger/BinaryLogReplayEventSource.cs index c6456c1a759..e4dc9c80b9c 100644 --- a/src/Build/Logging/BinaryLogger/BinaryLogReplayEventSource.cs +++ b/src/Build/Logging/BinaryLogger/BinaryLogReplayEventSource.cs @@ -2,6 +2,7 @@ using System.IO; using System.IO.Compression; using System.Threading; +using Microsoft.Build.BackEnd; using Microsoft.Build.Framework; using Microsoft.Build.Shared; @@ -14,6 +15,14 @@ namespace Microsoft.Build.Logging /// The class is public so that we can call it from MSBuild.exe when replaying a log file. public sealed class BinaryLogReplayEventSource : EventArgsDispatcher { + /// Touches the static constructor + /// to ensure it initializes + /// and + static BinaryLogReplayEventSource() + { + _ = ItemGroupLoggingHelper.ItemGroupIncludeLogMessagePrefix; + } + /// /// Read the provided binary log file and raise corresponding events for each BuildEventArgs /// diff --git a/src/Build/Logging/BinaryLogger/BinaryLogger.cs b/src/Build/Logging/BinaryLogger/BinaryLogger.cs index f9e9cb0b295..5274f54ab3a 100644 --- a/src/Build/Logging/BinaryLogger/BinaryLogger.cs +++ b/src/Build/Logging/BinaryLogger/BinaryLogger.cs @@ -41,7 +41,9 @@ public sealed class BinaryLogger : ILogger // * NameValueList - deduplicate arrays of name-value pairs such as properties, items and metadata // in a separate record and refer to those records from regular records // where a list used to be written in-place - internal const int FileFormatVersion = 10; + // version 11: + // - new record kind: TaskParameterEventArgs + internal const int FileFormatVersion = 11; private Stream stream; private BinaryWriter binaryWriter; diff --git a/src/Build/Logging/BinaryLogger/BuildEventArgsReader.cs b/src/Build/Logging/BinaryLogger/BuildEventArgsReader.cs index bd9ae6e9481..f9af8eed299 100644 --- a/src/Build/Logging/BinaryLogger/BuildEventArgsReader.cs +++ b/src/Build/Logging/BinaryLogger/BuildEventArgsReader.cs @@ -1,11 +1,17 @@ -using System; +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Collections; using Microsoft.Build.Framework; using Microsoft.Build.Framework.Profiler; +using Microsoft.Build.Shared; namespace Microsoft.Build.Logging { @@ -29,6 +35,8 @@ public class BuildEventArgsReader : IDisposable /// /// A list of dictionaries we've encountered so far. Dictionaries are referred to by their order in this list. /// + /// This is designed to not hold on to strings. We just store the string indices and + /// hydrate the dictionary on demand before returning. private readonly List<(int keyIndex, int valueIndex)[]> nameValueListRecords = new List<(int, int)[]>(); /// @@ -146,6 +154,9 @@ public BuildEventArgs Read() case BinaryLogRecordKind.TaskCommandLine: result = ReadTaskCommandLineEventArgs(); break; + case BinaryLogRecordKind.TaskParameter: + result = ReadTaskParameterEventArgs(); + break; case BinaryLogRecordKind.ProjectEvaluationStarted: result = ReadProjectEvaluationStartedEventArgs(); break; @@ -215,14 +226,17 @@ private void ReadNameValueList() { var list = nameValueListRecords[id]; - var dictionary = new Dictionary(list.Length); + // We can't cache these as they would hold on to strings. + // This reader is designed to not hold onto strings, + // so that we can fit in a 32-bit process when reading huge binlogs + var dictionary = ArrayDictionary.Create(list.Length); for (int i = 0; i < list.Length; i++) { string key = GetStringFromRecord(list[i].keyIndex); string value = GetStringFromRecord(list[i].valueIndex); if (key != null) { - dictionary[key] = value; + dictionary.Add(key, value); } } @@ -596,6 +610,27 @@ private BuildEventArgs ReadTaskCommandLineEventArgs() return e; } + private BuildEventArgs ReadTaskParameterEventArgs() + { + var fields = ReadBuildEventArgsFields(); + // Read unused Importance, it defaults to Low + ReadInt32(); + + var kind = (TaskParameterMessageKind)ReadInt32(); + var itemName = ReadDeduplicatedString(); + var items = ReadTaskItemList() as IList; + + var e = ItemGroupLoggingHelper.CreateTaskParameterEventArgs( + fields.BuildEventContext, + kind, + itemName, + items, + true, + fields.Timestamp); + e.ProjectFile = fields.ProjectFile; + return e; + } + private BuildEventArgs ReadCriticalBuildMessageEventArgs() { var fields = ReadBuildEventArgsFields(); @@ -896,65 +931,12 @@ private BuildEventContext ReadBuildEventContext() return result; } - private class TaskItem : ITaskItem - { - private static readonly Dictionary emptyMetadata = new Dictionary(); - - public string ItemSpec { get; set; } - public IDictionary Metadata { get; } - - public TaskItem() - { - Metadata = new Dictionary(); - } - - public TaskItem(string itemSpec, IDictionary metadata) - { - ItemSpec = itemSpec; - Metadata = metadata ?? emptyMetadata; - } - - public int MetadataCount => Metadata.Count; - - public ICollection MetadataNames => (ICollection)Metadata.Keys; - - public IDictionary CloneCustomMetadata() - { - return (IDictionary)Metadata; - } - - public void CopyMetadataTo(ITaskItem destinationItem) - { - throw new NotImplementedException(); - } - - public string GetMetadata(string metadataName) - { - return Metadata[metadataName]; - } - - public void RemoveMetadata(string metadataName) - { - throw new NotImplementedException(); - } - - public void SetMetadata(string metadataName, string metadataValue) - { - throw new NotImplementedException(); - } - - public override string ToString() - { - return $"{ItemSpec} Metadata: {MetadataCount}"; - } - } - private ITaskItem ReadTaskItem() { string itemSpec = ReadDeduplicatedString(); var metadata = ReadStringDictionary(); - var taskItem = new TaskItem(itemSpec, metadata); + var taskItem = new TaskItemData(itemSpec, metadata); return taskItem; } @@ -1079,7 +1061,10 @@ private string GetStringFromRecord(int index) private int ReadInt32() { - return Read7BitEncodedInt(binaryReader); + // on some platforms (net5) this method was added to BinaryReader + // but it's not available on others. Call our own extension method + // explicitly to avoid ambiguity. + return BinaryReaderExtensions.Read7BitEncodedInt(binaryReader); } private long ReadInt64() @@ -1102,30 +1087,6 @@ private TimeSpan ReadTimeSpan() return new TimeSpan(binaryReader.ReadInt64()); } - private int Read7BitEncodedInt(BinaryReader reader) - { - // Read out an Int32 7 bits at a time. The high bit - // of the byte when on means to continue reading more bytes. - int count = 0; - int shift = 0; - byte b; - do - { - // Check for a corrupted stream. Read a max of 5 bytes. - // In a future version, add a DataFormatException. - if (shift == 5 * 7) // 5 bytes max per Int32, shift += 7 - { - throw new FormatException(); - } - - // ReadByte handles end of stream cases for us. - b = reader.ReadByte(); - count |= (b & 0x7F) << shift; - shift += 7; - } while ((b & 0x80) != 0); - return count; - } - private ProfiledLocation ReadProfiledLocation() { var numberOfHits = ReadInt32(); diff --git a/src/Build/Logging/BinaryLogger/BuildEventArgsWriter.cs b/src/Build/Logging/BinaryLogger/BuildEventArgsWriter.cs index 3b37d1404cf..1acc641650b 100644 --- a/src/Build/Logging/BinaryLogger/BuildEventArgsWriter.cs +++ b/src/Build/Logging/BinaryLogger/BuildEventArgsWriter.cs @@ -1,13 +1,19 @@ -using System; +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using Microsoft.Build.BackEnd.Logging; using Microsoft.Build.Exceptions; +using Microsoft.Build.Execution; using Microsoft.Build.Framework; using Microsoft.Build.Framework.Profiler; using Microsoft.Build.Internal; +using Microsoft.Build.Shared; namespace Microsoft.Build.Logging { @@ -128,71 +134,33 @@ public void Write(BuildEventArgs e) private void WriteCore(BuildEventArgs e) { - // the cases are ordered by most used first for performance - if (e is BuildMessageEventArgs) - { - Write((BuildMessageEventArgs)e); - } - else if (e is TaskStartedEventArgs) - { - Write((TaskStartedEventArgs)e); - } - else if (e is TaskFinishedEventArgs) - { - Write((TaskFinishedEventArgs)e); - } - else if (e is TargetStartedEventArgs) - { - Write((TargetStartedEventArgs)e); - } - else if (e is TargetFinishedEventArgs) - { - Write((TargetFinishedEventArgs)e); - } - else if (e is BuildErrorEventArgs) - { - Write((BuildErrorEventArgs)e); - } - else if (e is BuildWarningEventArgs) - { - Write((BuildWarningEventArgs)e); - } - else if (e is ProjectStartedEventArgs) - { - Write((ProjectStartedEventArgs)e); - } - else if (e is ProjectFinishedEventArgs) - { - Write((ProjectFinishedEventArgs)e); - } - else if (e is BuildStartedEventArgs) - { - Write((BuildStartedEventArgs)e); - } - else if (e is BuildFinishedEventArgs) - { - Write((BuildFinishedEventArgs)e); - } - else if (e is ProjectEvaluationStartedEventArgs) - { - Write((ProjectEvaluationStartedEventArgs)e); - } - else if (e is ProjectEvaluationFinishedEventArgs) - { - Write((ProjectEvaluationFinishedEventArgs)e); - } - else - { - // convert all unrecognized objects to message - // and just preserve the message - var buildMessageEventArgs = new BuildMessageEventArgs( - e.Message, - e.HelpKeyword, - e.SenderName, - MessageImportance.Normal, - e.Timestamp); - buildMessageEventArgs.BuildEventContext = e.BuildEventContext ?? BuildEventContext.Invalid; - Write(buildMessageEventArgs); + switch (e) + { + case BuildMessageEventArgs buildMessage: Write(buildMessage); break; + case TaskStartedEventArgs taskStarted: Write(taskStarted); break; + case TaskFinishedEventArgs taskFinished: Write(taskFinished); break; + case TargetStartedEventArgs targetStarted: Write(targetStarted); break; + case TargetFinishedEventArgs targetFinished: Write(targetFinished); break; + case BuildErrorEventArgs buildError: Write(buildError); break; + case BuildWarningEventArgs buildWarning: Write(buildWarning); break; + case ProjectStartedEventArgs projectStarted: Write(projectStarted); break; + case ProjectFinishedEventArgs projectFinished: Write(projectFinished); break; + case BuildStartedEventArgs buildStarted: Write(buildStarted); break; + case BuildFinishedEventArgs buildFinished: Write(buildFinished); break; + case ProjectEvaluationStartedEventArgs projectEvaluationStarted: Write(projectEvaluationStarted); break; + case ProjectEvaluationFinishedEventArgs projectEvaluationFinished: Write(projectEvaluationFinished); break; + default: + // convert all unrecognized objects to message + // and just preserve the message + var buildMessageEventArgs = new BuildMessageEventArgs( + e.Message, + e.HelpKeyword, + e.SenderName, + MessageImportance.Normal, + e.Timestamp); + buildMessageEventArgs.BuildEventContext = e.BuildEventContext ?? BuildEventContext.Invalid; + Write(buildMessageEventArgs); + break; } } @@ -389,51 +357,57 @@ private void Write(BuildWarningEventArgs e) private void Write(BuildMessageEventArgs e) { - if (e is CriticalBuildMessageEventArgs) + if (e is TaskParameterEventArgs taskParameter) { - Write((CriticalBuildMessageEventArgs)e); + Write(taskParameter); return; } - if (e is TaskCommandLineEventArgs) + if (e is CriticalBuildMessageEventArgs criticalBuildMessage) { - Write((TaskCommandLineEventArgs)e); + Write(criticalBuildMessage); return; } - if (e is ProjectImportedEventArgs) + if (e is TaskCommandLineEventArgs taskCommandLine) { - Write((ProjectImportedEventArgs)e); + Write(taskCommandLine); return; } - if (e is TargetSkippedEventArgs) + if (e is ProjectImportedEventArgs projectImported) { - Write((TargetSkippedEventArgs)e); + Write(projectImported); return; } - if (e is PropertyReassignmentEventArgs) + if (e is TargetSkippedEventArgs targetSkipped) { - Write((PropertyReassignmentEventArgs)e); + Write(targetSkipped); return; } - if (e is UninitializedPropertyReadEventArgs) + if (e is PropertyReassignmentEventArgs propertyReassignment) { - Write((UninitializedPropertyReadEventArgs)e); + Write(propertyReassignment); return; } - if (e is EnvironmentVariableReadEventArgs) + if (e is UninitializedPropertyReadEventArgs uninitializedPropertyRead) { - Write((EnvironmentVariableReadEventArgs)e); + Write(uninitializedPropertyRead); return; } - if (e is PropertyInitialValueSetEventArgs) + if (e is EnvironmentVariableReadEventArgs environmentVariableRead) { - Write((PropertyInitialValueSetEventArgs)e); + Write(environmentVariableRead); + return; + } + + if (e is PropertyInitialValueSetEventArgs propertyInitialValueSet) + { + Write(propertyInitialValueSet); return; } @@ -507,9 +481,18 @@ private void Write(TaskCommandLineEventArgs e) WriteDeduplicatedString(e.TaskName); } - private void WriteBuildEventArgsFields(BuildEventArgs e) + private void Write(TaskParameterEventArgs e) { - var flags = GetBuildEventArgsFieldFlags(e); + Write(BinaryLogRecordKind.TaskParameter); + WriteMessageFields(e, writeMessage: false); + Write((int)e.Kind); + WriteDeduplicatedString(e.ItemType); + WriteTaskItemList(e.Items, e.LogItemMetadata); + } + + private void WriteBuildEventArgsFields(BuildEventArgs e, bool writeMessage = true) + { + var flags = GetBuildEventArgsFieldFlags(e, writeMessage); Write((int)flags); WriteBaseFields(e, flags); } @@ -547,9 +530,9 @@ private void WriteBaseFields(BuildEventArgs e, BuildEventArgsFieldFlags flags) } } - private void WriteMessageFields(BuildMessageEventArgs e) + private void WriteMessageFields(BuildMessageEventArgs e, bool writeMessage = true) { - var flags = GetBuildEventArgsFieldFlags(e); + var flags = GetBuildEventArgsFieldFlags(e, writeMessage); flags = GetMessageFlags(e, flags); Write((int)flags); @@ -644,7 +627,7 @@ private static BuildEventArgsFieldFlags GetMessageFlags(BuildMessageEventArgs e, return flags; } - private static BuildEventArgsFieldFlags GetBuildEventArgsFieldFlags(BuildEventArgs e) + private static BuildEventArgsFieldFlags GetBuildEventArgsFieldFlags(BuildEventArgs e, bool writeMessage = true) { var flags = BuildEventArgsFieldFlags.None; if (e.BuildEventContext != null) @@ -657,7 +640,7 @@ private static BuildEventArgsFieldFlags GetBuildEventArgsFieldFlags(BuildEventAr flags |= BuildEventArgsFieldFlags.HelpHeyword; } - if (!string.IsNullOrEmpty(e.Message)) + if (writeMessage) { flags |= BuildEventArgsFieldFlags.Message; } @@ -681,21 +664,68 @@ private static BuildEventArgsFieldFlags GetBuildEventArgsFieldFlags(BuildEventAr return flags; } - private void WriteTaskItemList(IEnumerable items) + private readonly List reusableItemsList = new List(); + + private void WriteTaskItemList(IEnumerable items, bool writeMetadata = true) { - var taskItems = items as IEnumerable; - if (taskItems == null) + if (items == null) { Write(false); return; } - Write(taskItems.Count()); + // For target outputs bypass copying of all items to save on performance. + // The proxy creates a deep clone of each item to protect against writes, + // but since we're not writing we don't need the deep cloning. + // Additionally, it is safe to access the underlying List as it's allocated + // in a single location and noboby else mutates it after that: + // https://github.com/dotnet/msbuild/blob/f0eebf2872d76ab0cd43fdc4153ba636232b222f/src/Build/BackEnd/Components/RequestBuilder/TargetEntry.cs#L564 + if (items is TargetLoggingContext.TargetOutputItemsInstanceEnumeratorProxy proxy) + { + items = proxy.BackingItems; + } + + int count; + + if (items is ICollection arrayList) + { + count = arrayList.Count; + } + else if (items is ICollection genericList) + { + count = genericList.Count; + } + else + { + // enumerate only once + foreach (var item in items) + { + if (item != null) + { + reusableItemsList.Add(item); + } + } + + items = reusableItemsList; + count = reusableItemsList.Count; + } + + Write(count); - foreach (var item in taskItems) + foreach (var item in items) { - Write(item); + if (item is ITaskItem taskItem) + { + Write(taskItem, writeMetadata); + } + else + { + WriteDeduplicatedString(item?.ToString() ?? ""); // itemspec + Write(0); // no metadata + } } + + reusableItemsList.Clear(); } private void WriteProjectItems(IEnumerable items) @@ -721,40 +751,32 @@ private void WriteProjectItems(IEnumerable items) } } - private void Write(ITaskItem item) + private void Write(ITaskItem item, bool writeMetadata = true) { WriteDeduplicatedString(item.ItemSpec); - - nameValueListBuffer.Clear(); - - IDictionary customMetadata = item.CloneCustomMetadata(); - - foreach (string metadataName in customMetadata.Keys) + if (!writeMetadata) { - string valueOrError; + Write((byte)0); + return; + } - try - { - valueOrError = item.GetMetadata(metadataName); - } - catch (InvalidProjectFileException e) - { - valueOrError = e.Message; - } - // Temporarily try catch all to mitigate frequent NullReferenceExceptions in - // the logging code until CopyOnWritePropertyDictionary is replaced with - // ImmutableDictionary. Calling into Debug.Fail to crash the process in case - // the exception occures in Debug builds. - catch (Exception e) - { - valueOrError = e.Message; - Debug.Fail(e.ToString()); - } + // WARNING: Can't use AddRange here because CopyOnWriteDictionary in Microsoft.Build.Utilities.v4.0.dll + // is broken. Microsoft.Build.Utilities.v4.0.dll loads from the GAC by XAML markup tooling and it's + // implementation doesn't work with AddRange because AddRange special-cases ICollection and + // CopyOnWriteDictionary doesn't implement it properly. + foreach (var kvp in item.EnumerateMetadata()) + { + nameValueListBuffer.Add(kvp); + } - nameValueListBuffer.Add(new KeyValuePair(metadataName, valueOrError)); + if (nameValueListBuffer.Count > 1) + { + nameValueListBuffer.Sort((l, r) => StringComparer.OrdinalIgnoreCase.Compare(l.Key, r.Key)); } WriteNameValueList(); + + nameValueListBuffer.Clear(); } private void WriteProperties(IEnumerable properties) @@ -765,8 +787,6 @@ private void WriteProperties(IEnumerable properties) return; } - nameValueListBuffer.Clear(); - // there are no guarantees that the properties iterator won't change, so // take a snapshot and work with the readonly copy var propertiesArray = properties.OfType().ToArray(); @@ -785,6 +805,8 @@ private void WriteProperties(IEnumerable properties) } WriteNameValueList(); + + nameValueListBuffer.Clear(); } private void Write(BuildEventContext buildEventContext) @@ -800,8 +822,6 @@ private void Write(BuildEventContext buildEventContext) private void Write(IEnumerable> keyValuePairs) { - nameValueListBuffer.Clear(); - if (keyValuePairs != null) { foreach (var kvp in keyValuePairs) @@ -811,6 +831,8 @@ private void Write(IEnumerable> keyValuePairs) } WriteNameValueList(); + + nameValueListBuffer.Clear(); } private void WriteNameValueList() @@ -893,7 +915,7 @@ private void Write(BinaryLogRecordKind kind) private void Write(int value) { - Write7BitEncodedInt(binaryWriter, value); + BinaryWriterExtensions.Write7BitEncodedInt(binaryWriter, value); } private void Write(long value) @@ -901,19 +923,6 @@ private void Write(long value) binaryWriter.Write(value); } - private void Write7BitEncodedInt(BinaryWriter writer, int value) - { - // Write out an int 7 bits at a time. The high bit of the byte, - // when on, tells reader to continue reading more bytes. - uint v = (uint)value; // support negative numbers - while (v >= 0x80) - { - writer.Write((byte)(v | 0x80)); - v >>= 7; - } - writer.Write((byte)v); - } - private void Write(byte[] bytes) { binaryWriter.Write(bytes); diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 01a5fd018d2..acf22733730 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -375,6 +375,7 @@ + diff --git a/src/Framework/BuildEventArgs.cs b/src/Framework/BuildEventArgs.cs index 1592e8fd480..58dc81f838a 100644 --- a/src/Framework/BuildEventArgs.cs +++ b/src/Framework/BuildEventArgs.cs @@ -111,6 +111,17 @@ public DateTime Timestamp } } + /// + /// Exposes the private field to derived types. + /// Used for serialization. Avoids the side effects of calling the + /// getter. + /// + protected DateTime RawTimestamp + { + get => timestamp; + set => timestamp = value; + } + /// /// The thread that raised event. /// @@ -155,24 +166,8 @@ internal virtual void WriteToStream(BinaryWriter writer) writer.WriteOptionalString(helpKeyword); writer.WriteOptionalString(senderName); writer.WriteTimestamp(timestamp); - - writer.Write((Int32)threadId); - - if (buildEventContext == null) - { - writer.Write((byte)0); - } - else - { - writer.Write((byte)1); - writer.Write((Int32)buildEventContext.NodeId); - writer.Write((Int32)buildEventContext.ProjectContextId); - writer.Write((Int32)buildEventContext.TargetId); - writer.Write((Int32)buildEventContext.TaskId); - writer.Write((Int32)buildEventContext.SubmissionId); - writer.Write((Int32)buildEventContext.ProjectInstanceId); - writer.Write((Int32)buildEventContext.EvaluationId); - } + writer.Write(threadId); + writer.WriteOptionalBuildEventContext(buildEventContext); } /// @@ -182,9 +177,9 @@ internal virtual void WriteToStream(BinaryWriter writer) /// The version of the runtime the message packet was created from internal virtual void CreateFromStream(BinaryReader reader, int version) { - message = reader.ReadByte() == 0 ? null : reader.ReadString(); - helpKeyword = reader.ReadByte() == 0 ? null : reader.ReadString(); - senderName = reader.ReadByte() == 0 ? null : reader.ReadString(); + message = reader.ReadOptionalString(); + helpKeyword = reader.ReadOptionalString(); + senderName = reader.ReadOptionalString(); long timestampTicks = reader.ReadInt64(); diff --git a/src/Framework/IMetadataContainer.cs b/src/Framework/IMetadataContainer.cs new file mode 100644 index 00000000000..169130cdef3 --- /dev/null +++ b/src/Framework/IMetadataContainer.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +namespace Microsoft.Build.Framework +{ + /// + /// Provides a way to efficiently enumerate item metadata + /// + internal interface IMetadataContainer + { + /// + /// Returns a list of metadata names and unescaped values, including + /// metadata from item definition groups, but not including built-in + /// metadata. Implementations should be low-overhead as the method + /// is used for serialization (in node packet translator) as well as + /// in the binary logger. + /// + IEnumerable> EnumerateMetadata(); + } +} diff --git a/src/Framework/ITaskItemExtensions.cs b/src/Framework/ITaskItemExtensions.cs new file mode 100644 index 00000000000..53a5e0d5b42 --- /dev/null +++ b/src/Framework/ITaskItemExtensions.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.Build.Framework +{ + internal static class ITaskItemExtensions + { + /// + /// Provides a way to efficiently enumerate custom metadata of an item, without built-in metadata. + /// + /// TaskItem implementation to return metadata from + /// WARNING: do NOT use List`1.AddRange to iterate over this collection. + /// CopyOnWriteDictionary from Microsoft.Build.Utilities.v4.0.dll is broken. + /// A non-null (but possibly empty) enumerable of item metadata. + public static IEnumerable> EnumerateMetadata(this ITaskItem taskItem) + { + if (taskItem is IMetadataContainer container) + { + // This is the common case: most implementations should implement this for quick access + return container.EnumerateMetadata(); + } + + // This runs if ITaskItem is Microsoft.Build.Utilities.TaskItem from Microsoft.Build.Utilities.v4.0.dll + // that is loaded from the GAC. + IDictionary customMetadata = taskItem.CloneCustomMetadata(); + if (customMetadata is IEnumerable> enumerableMetadata) + { + return enumerableMetadata; + } + + // In theory this should never be reachable. + var list = new KeyValuePair[customMetadata.Count]; + int i = 0; + + foreach (string metadataName in customMetadata.Keys) + { + string valueOrError; + + try + { + valueOrError = taskItem.GetMetadata(metadataName); + } + // Temporarily try catch all to mitigate frequent NullReferenceExceptions in + // the logging code until CopyOnWritePropertyDictionary is replaced with + // ImmutableDictionary. Calling into Debug.Fail to crash the process in case + // the exception occurres in Debug builds. + catch (Exception e) + { + valueOrError = e.Message; + Debug.Fail(e.ToString()); + } + + list[i] = new KeyValuePair(metadataName, valueOrError); + i += 1; + } + + return list; + } + } +} \ No newline at end of file diff --git a/src/Framework/Microsoft.Build.Framework.csproj b/src/Framework/Microsoft.Build.Framework.csproj index 59296adc225..939d7db72f7 100644 --- a/src/Framework/Microsoft.Build.Framework.csproj +++ b/src/Framework/Microsoft.Build.Framework.csproj @@ -8,9 +8,6 @@ false partial - - - @@ -21,6 +18,9 @@ Shared\Constants.cs + + Shared\BinaryReaderExtensions.cs + Shared\BinaryWriterExtensions.cs diff --git a/src/Framework/TaskItemData.cs b/src/Framework/TaskItemData.cs new file mode 100644 index 00000000000..8441badb819 --- /dev/null +++ b/src/Framework/TaskItemData.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Build.Framework +{ + /// + /// Lightweight specialized implementation of only used for deserializing items. + /// The goal is to minimize overhead when representing deserialized items. + /// Used by node packet translator and binary logger. + /// + internal class TaskItemData : ITaskItem, IMetadataContainer + { + private static readonly Dictionary _emptyMetadata = new Dictionary(); + + public string ItemSpec { get; set; } + public IDictionary Metadata { get; } + + public TaskItemData(string itemSpec, IDictionary metadata) + { + ItemSpec = itemSpec; + Metadata = metadata ?? _emptyMetadata; + } + + IEnumerable> IMetadataContainer.EnumerateMetadata() => Metadata; + + public int MetadataCount => Metadata.Count; + + public ICollection MetadataNames => (ICollection)Metadata.Keys; + + public IDictionary CloneCustomMetadata() + { + // against the guidance for CloneCustomMetadata this returns the original collection. + // Since this is only to be used for serialization and logging, consumers should not + // modify the collection. We need to minimize allocations so avoid cloning here. + return (IDictionary)Metadata; + } + + public void CopyMetadataTo(ITaskItem destinationItem) + { + throw new NotImplementedException(); + } + + public string GetMetadata(string metadataName) + { + Metadata.TryGetValue(metadataName, out var result); + return result; + } + + public void RemoveMetadata(string metadataName) + { + throw new NotImplementedException(); + } + + public void SetMetadata(string metadataName, string metadataValue) + { + throw new NotImplementedException(); + } + + public override string ToString() + { + return $"{ItemSpec} Metadata: {MetadataCount}"; + } + } +} diff --git a/src/Framework/TaskParameterEventArgs.cs b/src/Framework/TaskParameterEventArgs.cs new file mode 100644 index 00000000000..f53363237ce --- /dev/null +++ b/src/Framework/TaskParameterEventArgs.cs @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Framework +{ + public enum TaskParameterMessageKind + { + TaskInput, + TaskOutput, + AddItem, + RemoveItem, + } + + /// + /// This class is used by tasks to log their parameters (input, output). + /// The intrinsic ItemGroupIntrinsicTask to add or remove items also + /// uses this class. + /// + public class TaskParameterEventArgs : BuildMessageEventArgs + { + /// + /// Creates an instance of this class for the given task parameter. + /// + public TaskParameterEventArgs + ( + TaskParameterMessageKind kind, + string itemType, + IList items, + bool logItemMetadata, + DateTime eventTimestamp + ) + : base(null, null, null, MessageImportance.Low, eventTimestamp) + { + Kind = kind; + ItemType = itemType; + Items = items; + LogItemMetadata = logItemMetadata; + } + + public TaskParameterMessageKind Kind { get; private set; } + public string ItemType { get; private set; } + public IList Items { get; private set; } + public bool LogItemMetadata { get; private set; } + + /// + /// The type is declared in Microsoft.Build.Framework.dll + /// which is a declarations assembly. The logic to realize the Message is in Microsoft.Build.dll + /// which is an implementations assembly. This seems like the easiest way to inject the + /// implementation for realizing the Message. + /// + /// + /// Note that the current implementation never runs and is provided merely + /// as a safeguard in case MessageGetter isn't set for some reason. + /// + internal static Func MessageGetter = args => + { + var sb = new StringBuilder(); + sb.AppendLine($"{args.Kind}: {args.ItemType}"); + foreach (var item in args.Items) + { + sb.AppendLine(item.ToString()); + } + + return sb.ToString(); + }; + + /// + /// Provides a way for Microsoft.Build.dll to provide a more efficient dictionary factory + /// (using ArrayDictionary`2). Since that is an implementation detail, it is not included + /// in Microsoft.Build.Framework.dll so we need this extensibility point here. + /// + internal static Func> DictionaryFactory = capacity => new Dictionary(capacity); + + internal override void CreateFromStream(BinaryReader reader, int version) + { + RawTimestamp = reader.ReadTimestamp(); + BuildEventContext = reader.ReadOptionalBuildEventContext(); + Kind = (TaskParameterMessageKind)reader.Read7BitEncodedInt(); + ItemType = reader.ReadString(); + Items = ReadItems(reader); + } + + private IList ReadItems(BinaryReader reader) + { + var list = new ArrayList(); + + int count = reader.Read7BitEncodedInt(); + for (int i = 0; i < count; i++) + { + var item = ReadItem(reader); + list.Add(item); + } + + return list; + } + + private object ReadItem(BinaryReader reader) + { + string itemSpec = reader.ReadString(); + int metadataCount = reader.Read7BitEncodedInt(); + if (metadataCount == 0) + { + return new TaskItemData(itemSpec, metadata: null); + } + + var metadata = DictionaryFactory(metadataCount); + for (int i = 0; i < metadataCount; i++) + { + string key = reader.ReadString(); + string value = reader.ReadString(); + if (key != null) + { + metadata.Add(key, value); + } + } + + var taskItem = new TaskItemData(itemSpec, metadata); + return taskItem; + } + + internal override void WriteToStream(BinaryWriter writer) + { + writer.WriteTimestamp(RawTimestamp); + writer.WriteOptionalBuildEventContext(BuildEventContext); + writer.Write7BitEncodedInt((int)Kind); + writer.Write(ItemType); + WriteItems(writer, Items); + } + + private void WriteItems(BinaryWriter writer, IList items) + { + if (items == null) + { + writer.Write7BitEncodedInt(0); + return; + } + + int count = items.Count; + writer.Write7BitEncodedInt(count); + + for (int i = 0; i < count; i++) + { + var item = items[i]; + WriteItem(writer, item); + } + } + + private void WriteItem(BinaryWriter writer, object item) + { + if (item is ITaskItem taskItem) + { + writer.Write(taskItem.ItemSpec); + if (LogItemMetadata) + { + WriteMetadata(writer, taskItem); + } + else + { + writer.Write7BitEncodedInt(0); + } + } + else // string or ValueType + { + writer.Write(item?.ToString() ?? ""); + writer.Write7BitEncodedInt(0); + } + } + + [ThreadStatic] + private static List> reusableMetadataList; + + private void WriteMetadata(BinaryWriter writer, ITaskItem taskItem) + { + if (reusableMetadataList == null) + { + reusableMetadataList = new List>(); + } + + // WARNING: Can't use AddRange here because CopyOnWriteDictionary in Microsoft.Build.Utilities.v4.0.dll + // is broken. Microsoft.Build.Utilities.v4.0.dll loads from the GAC by XAML markup tooling and it's + // implementation doesn't work with AddRange because AddRange special-cases ICollection and + // CopyOnWriteDictionary doesn't implement it properly. + foreach (var kvp in taskItem.EnumerateMetadata()) + { + reusableMetadataList.Add(kvp); + } + + writer.Write7BitEncodedInt(reusableMetadataList.Count); + if (reusableMetadataList.Count == 0) + { + return; + } + + foreach (var kvp in reusableMetadataList) + { + writer.Write(kvp.Key); + writer.Write(kvp.Value); + } + + reusableMetadataList.Clear(); + } + + public override string Message + { + get + { + lock (this) + { + if (base.Message == null) + { + base.Message = MessageGetter(this); + } + + return base.Message; + } + } + } + } +} diff --git a/src/MSBuildTaskHost/MSBuildTaskHost.csproj b/src/MSBuildTaskHost/MSBuildTaskHost.csproj index 0437fdcbb36..8c4de37e038 100644 --- a/src/MSBuildTaskHost/MSBuildTaskHost.csproj +++ b/src/MSBuildTaskHost/MSBuildTaskHost.csproj @@ -16,7 +16,7 @@ win7-x86;win7-x64 false - $(DefineConstants);CLR2COMPATIBILITY + $(DefineConstants);CLR2COMPATIBILITY;TASKHOST true