File: LockFileCache.cs
Web Access
Project: src\src\sdk\src\Tasks\Microsoft.NET.Build.Tasks\Microsoft.NET.Build.Tasks.csproj (Microsoft.NET.Build.Tasks)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable

using Microsoft.Build.Framework;
using NuGet.Common;
using NuGet.ProjectModel;

namespace Microsoft.NET.Build.Tasks
{
    internal class LockFileCache
    {
        private IBuildEngine4 _buildEngine;
        private Logger _log;

        public LockFileCache(TaskBase task)
        {
            _buildEngine = task.BuildEngine4;
            _log = task.Log;
        }

        public LockFile GetLockFile(string path)
        {
            if (string.IsNullOrEmpty(path))
            {
                throw new BuildErrorException(Strings.AssetsFileNotSet);
            }
            if (!Path.IsPathRooted(path))
            {
                throw new BuildErrorException(Strings.AssetsFilePathNotRooted, path);
            }

            string lockFileKey = GetTaskObjectKey(path);

            LockFile result;
            object existingLockFileTaskObject = _buildEngine?.GetRegisteredTaskObject(lockFileKey, RegisteredTaskObjectLifetime.Build);
            if (existingLockFileTaskObject == null)
            {
                result = LoadLockFile(path);

                _buildEngine?.RegisterTaskObject(lockFileKey, result, RegisteredTaskObjectLifetime.Build, true);
            }
            else
            {
                result = (LockFile)existingLockFileTaskObject;
            }

            return result;
        }

        private static string GetTaskObjectKey(string lockFilePath)
        {
            return $"{nameof(LockFileCache)}:{lockFilePath}";
        }

        private LockFile LoadLockFile(string path)
        {
            // https://github.com/NuGet/Home/issues/6732
            //
            // LockFileUtilties.GetLockFile has odd error handling:
            //
            //   1. Exceptions creating TextReader from path (after up to 3 tries) will
            //      bubble out.
            //
            //   2. There's an up-front File.Exists that returns null without logging
            //      anything.
            //
            //   3. Any other exception whatsoever is logged by its Message property
            //      alone, and an empty, non-null lock file is returned.
            //
            // This wrapper will never return null or empty lock file and instead throw 
            // if the assets file is not found  or cannot be read for any other reason.

            LockFile lockFile;

            try
            {
                lockFile = LockFileUtilities.GetLockFile(
                    path,
                    new ThrowOnLockFileLoadError(_log));
            }
            catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
            {
                // Case 1
                throw new BuildErrorException(
                    string.Format(Strings.ErrorReadingAssetsFile, ex.Message),
                    ex);
            }

            if (lockFile == null)
            {
                // Case 2
                // NB: Cannot be moved to our own up-front File.Exists check or else there would be
                // a race where we still need to handle null for delete between our check and 
                // NuGet's.
                throw new BuildErrorException(Strings.AssetsFileNotFound, path);
            }

            return lockFile;
        }

        // Case 3
        // Force an exception on errors reading the lock file
        // Non-errors are not logged today, but push them to the build log in case they are in the future.
        private sealed class ThrowOnLockFileLoadError : LoggerBase
        {
            private Logger _log;

            public ThrowOnLockFileLoadError(Logger log)
            {
                _log = log;
            }

            public override void Log(ILogMessage message)
            {
                if (message.Level == LogLevel.Error)
                {
                    throw new BuildErrorException(
                        string.Format(Strings.ErrorReadingAssetsFile, message.Message));
                }

                _log.Log(
                    new Message(
                        level: message.Level == LogLevel.Warning ? MessageLevel.Warning : MessageLevel.NormalImportance,
                        code: message.Code == NuGetLogCode.Undefined ? default : message.Code.ToString(),
                        file: message.ProjectPath,
                        text: message.Message));
            }

            public override System.Threading.Tasks.Task LogAsync(ILogMessage message)
            {
                Log(message);
                return Task.CompletedTask;
            }
        }
    }
}