File: RestoreCommand\RestoreRunner.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Commands\NuGet.Commands.csproj (NuGet.Commands)
// 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.

#nullable disable

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;

namespace NuGet.Commands
{
    /// <summary>
    /// Shared code to run the "restore" command for dotnet restore, nuget.exe, and VS.
    /// </summary>
    public static class RestoreRunner
    {
        /// <summary>
        /// Create requests, execute requests, and commit restore results.
        /// </summary>
        public static async Task<IReadOnlyList<RestoreSummary>> RunAsync(RestoreArgs restoreContext, CancellationToken token)
        {
            // Create requests
            var requests = await GetRequests(restoreContext);

            // Run requests
            return await RunAsync(requests, restoreContext, token);
        }

        /// <summary>
        /// Create requests, execute requests, and commit restore results.
        /// </summary>
        public static async Task<IReadOnlyList<RestoreSummary>> RunAsync(RestoreArgs restoreContext)
        {
            // Run requests
            return await RunAsync(restoreContext, CancellationToken.None);
        }

        /// <summary>
        /// Execute and commit restore requests.
        /// </summary>
        private static async Task<IReadOnlyList<RestoreSummary>> RunAsync(
            IEnumerable<RestoreSummaryRequest> restoreRequests,
            RestoreArgs restoreArgs,
            CancellationToken token)
        {
            var maxTasks = GetMaxTaskCount(restoreArgs);

            var log = restoreArgs.Log;

            if (maxTasks > 1)
            {
                log.LogVerbose(string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.Log_RunningParallelRestore,
                    maxTasks));
            }
            else
            {
                log.LogVerbose(Strings.Log_RunningNonParallelRestore);
            }

            // Get requests
            var requests = new Queue<RestoreSummaryRequest>(restoreRequests);
            var restoreTasks = new List<Task<RestoreSummary>>(maxTasks);
            var restoreSummaries = new List<RestoreSummary>(requests.Count);

            // Run requests
            while (requests.Count > 0)
            {
                // Throttle and wait for a task to finish if we have hit the limit
                if (restoreTasks.Count == maxTasks)
                {
                    var restoreSummary = await CompleteTaskAsync(restoreTasks);
                    restoreSummaries.Add(restoreSummary);
                }

                var request = requests.Dequeue();

                var task = Task.Run(() => ExecuteAndCommitAsync(request, restoreArgs.ProgressReporter, token), token);
                restoreTasks.Add(task);
            }

            // Wait for all restores to finish
            while (restoreTasks.Count > 0)
            {
                var restoreSummary = await CompleteTaskAsync(restoreTasks);
                restoreSummaries.Add(restoreSummary);
            }

            // Summary
            return restoreSummaries;
        }

        /// <summary>
        /// Execute and commit restore requests.
        /// </summary>
        public static Task<IReadOnlyList<RestoreResultPair>> RunWithoutCommit(
            IEnumerable<RestoreSummaryRequest> restoreRequests,
            RestoreArgs restoreContext)
        {
            return RunWithoutCommitAsync(restoreRequests, restoreContext, CancellationToken.None);
        }

        /// <summary>
        /// Execute and commit restore requests.
        /// </summary>
        public static async Task<IReadOnlyList<RestoreResultPair>> RunWithoutCommitAsync(
            IEnumerable<RestoreSummaryRequest> restoreRequests,
            RestoreArgs restoreContext,
            CancellationToken cancellationToken)
        {
            var maxTasks = GetMaxTaskCount(restoreContext);

            var log = restoreContext.Log;

            if (maxTasks > 1)
            {
                log.LogVerbose(string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.Log_RunningParallelRestore,
                    maxTasks));
            }
            else
            {
                log.LogVerbose(Strings.Log_RunningNonParallelRestore);
            }

            // Get requests
            var requests = new Queue<RestoreSummaryRequest>(restoreRequests);
            var restoreTasks = new List<Task<RestoreResultPair>>(maxTasks);
            var restoreResults = new List<RestoreResultPair>(maxTasks);

            // Run requests
            while (requests.Count > 0)
            {
                // Throttle and wait for a task to finish if we have hit the limit
                if (restoreTasks.Count == maxTasks)
                {
                    var restoreSummary = await CompleteTaskAsync(restoreTasks);
                    restoreResults.Add(restoreSummary);
                }

                var request = requests.Dequeue();

                var task = Task.Run(() => ExecuteAsync(request, CancellationToken.None));
                restoreTasks.Add(task);
            }

            // Wait for all restores to finish
            while (restoreTasks.Count > 0)
            {
                var restoreSummary = await CompleteTaskAsync(restoreTasks);
                restoreResults.Add(restoreSummary);
            }

            // Summary
            return restoreResults;
        }

        /// <summary>
        /// Create restore requests but do not execute them.
        /// </summary>
        public static async Task<IReadOnlyList<RestoreSummaryRequest>> GetRequests(RestoreArgs restoreContext)
        {
            // Get requests
            var requests = new List<RestoreSummaryRequest>();

            var inputs = new List<string>(restoreContext.Inputs);

            // If there are no inputs, use the current directory
            if (restoreContext.PreLoadedRequestProviders.Count < 1 && !inputs.Any())
            {
                inputs.Add(Path.GetFullPath("."));
            }

            var uniqueRequest = new HashSet<string>(PathUtility.GetStringComparerBasedOnOS());

            // Create requests
            // Pre-loaded requests
            foreach (var request in await CreatePreLoadedRequests(restoreContext))
            {
                // De-dupe requests
                if (request.Request.LockFilePath == null
                    || uniqueRequest.Add(request.Request.LockFilePath))
                {
                    requests.Add(request);
                }
            }

            // Input based requests
            foreach (var input in inputs)
            {
                var inputRequests = await CreateRequests(input, restoreContext);
                if (inputRequests.Count == 0)
                {
                    // No need to throw here - the situation is harmless, and we want to report all possible
                    // inputs that don't resolve to a project.
                    var message = string.Format(
                            CultureInfo.CurrentCulture,
                            Strings.Error_UnableToLocateRestoreTarget,
                            Path.GetFullPath(input));

                    await restoreContext.Log.LogAsync(RestoreLogMessage.CreateWarning(NuGetLogCode.NU1501, message));
                }
                foreach (var request in inputRequests)
                {
                    // De-dupe requests
                    if (uniqueRequest.Add(request.Request.LockFilePath))
                    {
                        requests.Add(request);
                    }
                }
            }

            return requests;
        }

        private static int GetMaxTaskCount(RestoreArgs restoreContext)
        {
            var maxTasks = 1;

            if (!restoreContext.DisableParallel && !RuntimeEnvironmentHelper.IsMono)
            {
                maxTasks = Environment.ProcessorCount;
            }

            if (maxTasks < 1)
            {
                maxTasks = 1;
            }

            return maxTasks;
        }

        private static async Task<RestoreSummary> ExecuteAndCommitAsync(RestoreSummaryRequest summaryRequest, IRestoreProgressReporter progressReporter, CancellationToken token)
        {
            RestoreResultPair result = await ExecuteAsync(summaryRequest, token);

            return await CommitAsync(result, progressReporter, token);
        }

        private static async Task<RestoreResultPair> ExecuteAsync(RestoreSummaryRequest summaryRequest, CancellationToken token)
        {
            var log = summaryRequest.Request.Log;

            log.LogVerbose(string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.Log_ReadingProject,
                    summaryRequest.InputPath));

            // Run the restore
            var request = summaryRequest.Request;

            var command = new RestoreCommand(request);
            if (CommandsEventSource.Instance.IsEnabled()) CommandsEventSource.Instance.RestoreRunner_RestoreProjectStart(request.Project.FilePath);
            var result = await command.ExecuteAsync(token);
            if (CommandsEventSource.Instance.IsEnabled()) CommandsEventSource.Instance.RestoreRunner_RestoreProjectStop(request.Project.FilePath);

            return new RestoreResultPair(summaryRequest, result);
        }

        public static Task<RestoreSummary> CommitAsync(RestoreResultPair restoreResult, CancellationToken token) => CommitAsync(restoreResult, progressReporter: null, token);

        private static async Task<RestoreSummary> CommitAsync(RestoreResultPair restoreResult, IRestoreProgressReporter progressReporter, CancellationToken token)
        {
            if (restoreResult == null)
            {
                throw new ArgumentNullException(nameof(restoreResult));
            }

            var summaryRequest = restoreResult.SummaryRequest;
            var result = restoreResult.Result;

            var log = summaryRequest.Request.Log;

            bool isNoOp = result is NoOpRestoreResult;

            IReadOnlyList<string> filesToBeUpdated = result.GetDirtyFiles();
            try
            {
                if (!isNoOp && filesToBeUpdated?.Count > 0)
                {
                    progressReporter?.StartProjectUpdate(summaryRequest.InputPath, filesToBeUpdated);
                }

                // Commit the result
                log.LogVerbose(Strings.Log_Committing);
                if (CommandsEventSource.Instance.IsEnabled()) CommandsEventSource.Instance.RestoreRunner_CommitAsyncStart(summaryRequest.InputPath);
                await result.CommitAsync(log, token);
                if (CommandsEventSource.Instance.IsEnabled()) CommandsEventSource.Instance.RestoreRunner_CommitAsyncStop(summaryRequest.InputPath);
            }
            finally
            {
                if (!isNoOp && filesToBeUpdated?.Count > 0)
                {
                    progressReporter?.EndProjectUpdate(summaryRequest.InputPath, filesToBeUpdated);
                }
            }

            if (result.Success)
            {
                // For no-op results, don't log a minimal message since a summary is logged at the end
                // For regular results, log a minimal message so that users can see which projects were actually restored
                log.Log(
                    result is NoOpRestoreResult ? LogLevel.Information : LogLevel.Minimal,
                    string.Format(
                        CultureInfo.CurrentCulture,
                        Strings.Log_RestoreComplete,
                        summaryRequest.InputPath,
                        DatetimeUtility.ToReadableTimeFormat(result.ElapsedTime)));
            }
            else
            {
                log.LogMinimal(string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.Log_RestoreFailed,
                    summaryRequest.InputPath,
                    DatetimeUtility.ToReadableTimeFormat(result.ElapsedTime)));
            }
            // Remote the summary messages from the assets file.
            var messages = restoreResult.Result.LogMessages
                .Select(e => new RestoreLogMessage(e.Level, e.Code, e.Message)) ?? Enumerable.Empty<RestoreLogMessage>();

            // Build the summary
            return new RestoreSummary(
                result,
                summaryRequest.InputPath,
                summaryRequest.ConfigFiles,
                summaryRequest.Sources,
                messages);
        }

        private static async Task<RestoreSummary> CompleteTaskAsync(List<Task<RestoreSummary>> restoreTasks)
        {
            var doneTask = await Task.WhenAny(restoreTasks);
            restoreTasks.Remove(doneTask);
            return await doneTask;
        }

        private static async Task<RestoreResultPair>
            CompleteTaskAsync(List<Task<RestoreResultPair>> restoreTasks)
        {
            var doneTask = await Task.WhenAny(restoreTasks);
            restoreTasks.Remove(doneTask);
            return await doneTask;
        }

        private static async Task<IReadOnlyList<RestoreSummaryRequest>> CreatePreLoadedRequests(
            RestoreArgs restoreContext)
        {
            var results = new List<RestoreSummaryRequest>();

            foreach (var provider in restoreContext.PreLoadedRequestProviders)
            {
                var requests = await provider.CreateRequests(restoreContext);
                results.AddRange(requests);
            }

            return results;
        }

        private static async Task<IReadOnlyList<RestoreSummaryRequest>> CreateRequests(
            string input,
            RestoreArgs restoreContext)
        {
            foreach (var provider in restoreContext.RequestProviders)
            {
                if (await provider.Supports(input))
                {
                    return await provider.CreateRequests(
                        input,
                        restoreContext);
                }
            }

            if (File.Exists(input) || Directory.Exists(input))
            {
                // Not a file or directory we know about. Try to be helpful without response.
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, GetInvalidInputErrorMessage(input), input));
            }

            throw new FileNotFoundException(input);
        }

        public static string GetInvalidInputErrorMessage(string input)
        {
            Debug.Assert(File.Exists(input) || Directory.Exists(input));
            if (File.Exists(input))
            {
                var fileExtension = Path.GetExtension(input);
                if (".json".Equals(fileExtension, StringComparison.OrdinalIgnoreCase))
                {
                    return Strings.Error_InvalidCommandLineInputJson;
                }

                if (".config".Equals(fileExtension, StringComparison.OrdinalIgnoreCase))
                {
                    return Strings.Error_InvalidCommandLineInputConfig;
                }
            }

            return Strings.Error_InvalidCommandLineInput;
        }
    }
}