File: BackEnd\Shared\TargetResult.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using Microsoft.Build.BackEnd;
using Microsoft.Build.Collections;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem;
 
#nullable disable
 
namespace Microsoft.Build.Execution
{
    /// <summary>
    /// Contains the result items for a single target as well as the overall result code.
    /// </summary>
    public class TargetResult : ITargetResult, ITranslatable
    {
        /// <summary>
        /// The result for this target.
        /// </summary>
        private WorkUnitResult _result;
 
        /// <summary>
        /// Flag indicating whether to consider this target failure as having caused a build failure.
        /// </summary>
        private bool _targetFailureDoesntCauseBuildFailure;
 
        /// <summary>
        /// Flag indicating whether at least one target which has run after us (transitively via AfterTargets) failed.
        /// </summary>
        private bool _afterTargetsHaveFailed;
 
        /// <summary>
        /// The store of items in this result.
        /// </summary>
        private TaskItem[] _items;
 
        /// <summary>
        /// The context under which these results have been cached.
        /// </summary>
        private CacheInfo _cacheInfo;
 
        /// <summary>
        /// The (possibly null) <see cref="BuildEventContext"/> from the original target build
        /// </summary>
        private BuildEventContext _originalBuildEventContext;
 
        /// <summary>
        /// Initializes the results with specified items and result.
        /// </summary>
        /// <param name="items">The items produced by the target.</param>
        /// <param name="result">The overall result for the target.</param>
        /// <param name="originalBuildEventContext">The original build event context from when the target was first built, if available.
        /// Non-null when creating a <see cref="TargetResult"/> after building the target initially (or skipping due to false condition).
        /// Null when the <see cref="TargetResult"/> is being created in other scenarios:
        ///  * Target that never ran because a dependency had an error
        ///  * in <see cref="ITargetBuilderCallback.LegacyCallTarget"/> when Cancellation was requested
        ///  * in ProjectCache.CacheResult.ConstructBuildResult
        /// </param>
        internal TargetResult(TaskItem[] items, WorkUnitResult result, BuildEventContext originalBuildEventContext = null)
        {
            ErrorUtilities.VerifyThrowArgumentNull(items);
            ErrorUtilities.VerifyThrowArgumentNull(result);
            _items = items;
            _result = result;
            _originalBuildEventContext = originalBuildEventContext;
        }
 
        /// <summary>
        /// Private constructor for deserialization
        /// </summary>
        private TargetResult(ITranslator translator)
        {
            ((ITranslatable)this).Translate(translator);
        }
 
        /// <summary>
        /// Returns the exception which aborted this target, if any.
        /// </summary>
        /// <value>The exception which aborted this target, if any.</value>
        public Exception Exception
        {
            [DebuggerStepThrough]
            get => _result.Exception;
        }
 
        /// <summary>
        /// Returns the items produced by the target.
        /// These are ITaskItem's, so they have no item-type.
        /// </summary>
        /// <value>The items produced by the target.</value>
        public ITaskItem[] Items
        {
            [DebuggerStepThrough]
            get
            {
                lock (_result)
                {
                    RetrieveItemsFromCache();
 
                    return _items;
                }
            }
        }
 
        /// <summary>
        /// Returns the result code for the target.
        /// </summary>
        /// <value>The result code for the target.</value>
        public TargetResultCode ResultCode
        {
            [DebuggerStepThrough]
            get
            {
                switch (_result.ResultCode)
                {
                    case WorkUnitResultCode.Canceled:
                    case WorkUnitResultCode.Failed:
                        return TargetResultCode.Failure;
 
                    case WorkUnitResultCode.Skipped:
                        return TargetResultCode.Skipped;
 
                    case WorkUnitResultCode.Success:
                        return TargetResultCode.Success;
 
                    default:
                        return TargetResultCode.Skipped;
                }
            }
        }
 
        /// <summary>
        /// Returns the internal result for the target.
        /// </summary>
        internal WorkUnitResult WorkUnitResult
        {
            [DebuggerStepThrough]
            get => _result;
        }
 
        /// <summary>
        /// The (possibly null) <see cref="BuildEventContext"/> from the original target build
        /// </summary>
        internal BuildEventContext OriginalBuildEventContext => _originalBuildEventContext;
 
        /// <summary>
        /// Sets or gets a flag indicating whether or not a failure results should cause the build to fail.
        /// </summary>
        internal bool TargetFailureDoesntCauseBuildFailure
        {
            [DebuggerStepThrough]
            get => _targetFailureDoesntCauseBuildFailure;
 
            [DebuggerStepThrough]
            set => _targetFailureDoesntCauseBuildFailure = value;
        }
 
        /// <summary>
        /// Sets or gets a flag indicating whether at least one target which has run after us (transitively via AfterTargets) failed.
        /// </summary>
        internal bool AfterTargetsHaveFailed
        {
            [DebuggerStepThrough]
            get => _afterTargetsHaveFailed;
 
            [DebuggerStepThrough]
            set => _afterTargetsHaveFailed = value;
        }
 
        #region INodePacketTranslatable Members
 
        /// <summary>
        /// Reads or writes the packet to the serializer.
        /// </summary>
        void ITranslatable.Translate(ITranslator translator)
        {
            if (translator.Mode == TranslationDirection.WriteToStream)
            {
                lock (_result)
                {
                    // Should we have cached these items but now want to send them to another node, we need to
                    // ensure they are loaded before doing so.
                    RetrieveItemsFromCache();
                    InternalTranslate(translator);
                }
            }
            else
            {
                InternalTranslate(translator);
            }
        }
 
        /// <summary>
        /// Factory for serialization.
        /// </summary>
        internal static TargetResult FactoryForDeserialization(ITranslator translator)
        {
            return new TargetResult(translator);
        }
 
        #endregion
 
        /// <summary>
        /// Gets the name of the cache file for this configuration.
        /// </summary>
        internal static string GetCacheFile(int configId, string targetToCache)
        {
            string filename = Path.Combine(FileUtilities.GetCacheDirectory(), String.Format(CultureInfo.InvariantCulture, Path.Combine("Results{0}", "{1}.cache"), configId, targetToCache));
            return filename;
        }
 
        /// <summary>
        /// Gets the name of the cache file for this configuration.
        /// </summary>
        internal static string GetCacheDirectory(int configId, string targetToCache)
        {
            string filename = GetCacheFile(configId, targetToCache);
            string directoryName = Path.GetDirectoryName(filename);
            return directoryName;
        }
 
        /// <summary>
        /// Cache the items.
        /// </summary>
        internal void CacheItems(int configId, string targetName)
        {
            lock (_result)
            {
                if (_items == null)
                {
                    // Already cached.
                    return;
                }
 
                if (_items.Length == 0)
                {
                    // Nothing to cache.
                    return;
                }
 
                string cacheFile = GetCacheFile(configId, targetName);
                Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
 
                // If the file doesn't already exists, then we haven't cached this once before. We need to cache it again since it could have changed.
                if (!FileSystems.Default.FileExists(cacheFile))
                {
                    using Stream stream = File.Create(cacheFile);
                    using ITranslator translator = GetResultsCacheTranslator(TranslationDirection.WriteToStream, stream);
 
                    // If the translator is null, it means these results were cached once before.  Since target results are immutable once they
                    // have been created, there is no point in writing them again.
                    if (translator != null)
                    {
                        TranslateItems(translator);
                        _items = null;
                        _cacheInfo = new CacheInfo(configId, targetName);
                    }
                }
            }
        }
 
        /// <summary>
        /// Performs the actual translation
        /// </summary>
        private void InternalTranslate(ITranslator translator)
        {
            translator.Translate(ref _result, WorkUnitResult.FactoryForDeserialization);
            translator.Translate(ref _targetFailureDoesntCauseBuildFailure);
            translator.Translate(ref _afterTargetsHaveFailed);
            translator.TranslateOptionalBuildEventContext(ref _originalBuildEventContext);
            TranslateItems(translator);
        }
 
        /// <summary>
        /// Retrieve the items from the cache.
        /// </summary>
        private void RetrieveItemsFromCache()
        {
            lock (_result)
            {
                if (_items == null)
                {
                    string cacheFile = GetCacheFile(_cacheInfo.ConfigId, _cacheInfo.TargetName);
                    using Stream stream = File.OpenRead(cacheFile);
                    using ITranslator translator = GetResultsCacheTranslator(TranslationDirection.ReadFromStream, stream);
 
                    TranslateItems(translator);
                    _cacheInfo = new CacheInfo();
                }
            }
        }
 
        private void TranslateItems(ITranslator translator)
        {
            var itemsCount = _items?.Length ?? 0;
            translator.Translate(ref itemsCount);
 
            if (translator.Mode == TranslationDirection.WriteToStream)
            {
                // We will just calculate a very rough starting buffer size for the memory stream based on the number of items and a
                // rough guess for an average number of bytes needed to store them.  This doesn't have to be accurate, just
                // big enough to avoid unnecessary buffer reallocations in most cases.
                var defaultBufferCapacity = _items.Length * 128;
 
                using var itemsStream = new MemoryStream(defaultBufferCapacity);
                var itemTranslator = BinaryTranslator.GetWriteTranslator(itemsStream);
 
                // When creating the interner, we use the number of items as the initial size of the collections since the
                // number of strings will be of the order of the number of items in the collection.  This assumes basically
                // one unique string per item (frequently a path related to the item) with most of the rest of the metadata
                // being the same (and thus interning.)  This is a hueristic meant to get us in the ballpark to avoid
                // too many reallocations when growing the collections.
                var interner = new LookasideStringInterner(StringComparer.Ordinal, _items.Length);
                foreach (TaskItem t in _items)
                {
                    t.TranslateWithInterning(itemTranslator, interner);
                }
 
                interner.Translate(translator);
                var buffer = itemsStream.GetBuffer();
                var bufferSize = (int)itemsStream.Length;
                translator.Translate(ref buffer, ref bufferSize);
            }
            else
            {
                var interner = new LookasideStringInterner(translator);
 
                byte[] buffer = null;
                translator.Translate(ref buffer);
                ErrorUtilities.VerifyThrow(buffer != null, "Unexpected null items buffer during translation.");
 
                using MemoryStream itemsStream = new MemoryStream(buffer, 0, buffer.Length, writable: false, publiclyVisible: true);
                using var itemTranslator = BinaryTranslator.GetReadTranslator(itemsStream, InterningBinaryReader.PoolingBuffer);
                _items = new TaskItem[itemsCount];
                for (int i = 0; i < _items.Length; i++)
                {
                    _items[i] = TaskItem.FactoryForDeserialization(itemTranslator, interner);
                }
            }
        }
 
        /// <summary>
        /// Gets the translator for this configuration.
        /// </summary>
        private static ITranslator GetResultsCacheTranslator(TranslationDirection direction, Stream stream) =>
            direction == TranslationDirection.WriteToStream
                    ? BinaryTranslator.GetWriteTranslator(stream)
                    : BinaryTranslator.GetReadTranslator(stream, InterningBinaryReader.PoolingBuffer);
 
        /// <summary>
        /// Information about where the cache for the items in this result are stored.
        /// </summary>
        private struct CacheInfo
        {
            /// <summary>
            /// Constructor.
            /// </summary>
            public CacheInfo(int configId, string targetName)
            {
                ConfigId = configId;
                TargetName = targetName;
            }
 
            /// <summary>
            /// Retrieves the configuration id.
            /// </summary>
            public int ConfigId { get; }
 
            /// <summary>
            /// Retrieves the target name.
            /// </summary>
            public string TargetName { get; }
        }
    }
}