File: PostActionDispatcher.cs
Web Access
Project: ..\..\..\src\Cli\Microsoft.TemplateEngine.Cli\Microsoft.TemplateEngine.Cli.csproj (Microsoft.TemplateEngine.Cli)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Utils.Extensions;
using Microsoft.TemplateEngine.Abstractions;
using Microsoft.TemplateEngine.Cli.PostActionProcessors;
using Microsoft.TemplateEngine.Edge.Template;
 
namespace Microsoft.TemplateEngine.Cli
{
    internal enum AllowRunScripts
    {
        No,
        Yes,
        Prompt
    }
 
    /// <summary>
    /// Indicates post action execution status.
    /// </summary>
    [Flags]
    internal enum PostActionExecutionStatus
    {
        /// <summary>
        /// All post actions executed successfully.
        /// </summary>
        Success = 0,
 
        /// <summary>
        /// One or more post actions failed.
        /// </summary>
        Failure = 1,
 
        /// <summary>
        /// User has cancelled post action execution.
        /// </summary>
        Cancelled = 2
    }
 
    internal class PostActionDispatcher
    {
        private readonly IEngineEnvironmentSettings _environment;
        private readonly Func<string> _inputGetter;
 
        /// <summary>
        /// Creates the instance.
        /// </summary>
        /// <param name="environment">template engine environment settings.</param>
        /// <param name="inputGetter">the predicate to get user input whether to run the post action.</param>
        internal PostActionDispatcher(IEngineEnvironmentSettings environment, Func<string> inputGetter)
        {
            _environment = environment ?? throw new ArgumentNullException(nameof(environment));
            _inputGetter = inputGetter ?? throw new ArgumentNullException(nameof(inputGetter));
        }
 
        /// <summary>
        /// Process post actions based on <paramref name="creationResult"/>.
        /// </summary>
        /// <param name="creationResult">the result of template creation or dry run.</param>
        /// <param name="isDryRun">true if dry run should be done, false if post actions should be performed.</param>
        /// <param name="canRunScripts">indicates if run script post action is allowed to be run.</param>
        /// <returns>
        /// <see cref="PostActionExecutionStatus"/> containing result of post action execution.<br />
        /// Note that if <see cref="IPostAction.ContinueOnError"/> is set to true, the result will be <see cref="PostActionExecutionStatus.Success"/> even if the post action fails.
        /// If the user cancelled post action with <see cref="IPostAction.ContinueOnError"/> set to true, the result will be <see cref="PostActionExecutionStatus.Cancelled"></see> anyway.<br />
        /// Note that <see cref="PostActionExecutionStatus"/> is a flags enum, and can contain multiple status if multiple post actions failed with different reason.
        /// </returns>
        internal PostActionExecutionStatus Process(ITemplateCreationResult creationResult, bool isDryRun, AllowRunScripts canRunScripts)
        {
            _ = creationResult ?? throw new ArgumentNullException(nameof(creationResult));
            _ = creationResult.CreationEffects ?? throw new ArgumentNullException(nameof(creationResult.CreationEffects));
            if (!isDryRun)
            {
                _ = creationResult.CreationResult ?? throw new ArgumentNullException(nameof(creationResult.CreationResult));
            }
            if (string.IsNullOrWhiteSpace(creationResult.OutputBaseDirectory))
            {
                throw new ArgumentNullException($"{nameof(creationResult.OutputBaseDirectory)} should not be null or whitespace", nameof(creationResult.OutputBaseDirectory));
            }
 
            IReadOnlyList<IPostAction> postActions = isDryRun
                ? creationResult.CreationEffects.CreationResult.PostActions
                : creationResult.CreationResult!.PostActions;
 
            if (postActions.Count > 0)
            {
                Reporter.Output.WriteLine();
                Reporter.Output.WriteLine(LocalizableStrings.ProcessingPostActions);
            }
            else
            {
                return PostActionExecutionStatus.Success;
            }
 
            PostActionExecutionStatus result = PostActionExecutionStatus.Success;
            foreach (IPostAction action in postActions)
            {
                if (isDryRun)
                {
                    Reporter.Output.WriteLine(LocalizableStrings.ActionWouldHaveBeenTakenAutomatically);
                    if (!string.IsNullOrWhiteSpace(action.Description))
                    {
                        Reporter.Output.WriteLine(action.Description.Indent());
                    }
                    continue;
                }
 
                IPostActionProcessor? actionProcessor = null;
                _environment.Components.TryGetComponent(action.ActionId, out actionProcessor);
 
                if (actionProcessor == null)
                {
                    Reporter.Error.WriteLine(LocalizableStrings.PostActionDispatcher_Error_NotSupported, action.ActionId);
                    if (!string.IsNullOrWhiteSpace(action.Description))
                    {
                        Reporter.Error.WriteLine(LocalizableStrings.PostActionDescription, action.Description);
                    }
                    // The host doesn't know how to handle this action, just display manual instructions.
                    DisplayInstructionsForAction(action, useErrorOutput: true);
                    result |= PostActionExecutionStatus.Failure;
                }
                else if (actionProcessor is ProcessStartPostActionProcessor)
                {
                    if (canRunScripts == AllowRunScripts.No)
                    {
                        Reporter.Error.WriteLine(LocalizableStrings.PostActionDispatcher_Error_RunScriptNotAllowed);
                        if (!string.IsNullOrWhiteSpace(action.Description))
                        {
                            Reporter.Error.WriteLine(LocalizableStrings.PostActionDescription, action.Description);
                        }
                        DisplayInstructionsForAction(action, useErrorOutput: true);
                        result |= PostActionExecutionStatus.Cancelled;
                    }
                    else if (canRunScripts == AllowRunScripts.Yes)
                    {
                        result |= ProcessAction(creationResult.CreationEffects, creationResult.CreationResult!, creationResult.OutputBaseDirectory, action, actionProcessor);
                    }
                    else if (canRunScripts == AllowRunScripts.Prompt)
                    {
                        // Ask the user if they want to run the action.
                        // If they do, run it, and return the result.
                        // Otherwise return cancelled, indicating the action was not run.
                        if (AskUserIfActionShouldRun(action))
                        {
                            result |= ProcessAction(creationResult.CreationEffects, creationResult.CreationResult!, creationResult.OutputBaseDirectory, action, actionProcessor);
                        }
                        else
                        {
                            result |= PostActionExecutionStatus.Cancelled;
                        }
                    }
                    // no trailing else - no other cases.
                }
                else // other post action
                {
                    result |= ProcessAction(creationResult.CreationEffects, creationResult.CreationResult!, creationResult.OutputBaseDirectory, action, actionProcessor);
                }
                if (result != PostActionExecutionStatus.Success)
                {
                    if (action.ContinueOnError)
                    {
                        result ^= PostActionExecutionStatus.Failure;
                    }
                    else
                    {
                        break;
                    }
                }
                Reporter.Output.WriteLine();
            }
            return result;
        }
 
        private bool AskUserIfActionShouldRun(IPostAction action)
        {
            const string YesAnswer = "Y";
            const string NoAnswer = "N";
 
            Reporter.Output.WriteLine(LocalizableStrings.PostActionPromptHeader);
            if (!string.IsNullOrWhiteSpace(action.Description))
            {
                Reporter.Output.WriteLine(LocalizableStrings.PostActionDescription, action.Description);
            }
            // actual command that will be run by 'Run script' post action
            if (action.Args != null && action.Args.TryGetValue("executable", out string? executable))
            {
                action.Args.TryGetValue("args", out string? commandArgs);
                Reporter.Output.WriteLine(string.Format(LocalizableStrings.PostActionCommand, $"{executable} {commandArgs}").Bold().Red());
            }
            Reporter.Output.WriteLine(LocalizableStrings.PostActionPromptRequest, YesAnswer, NoAnswer);
 
            do
            {
                string input = _inputGetter();
 
                if (input.Equals(YesAnswer, StringComparison.OrdinalIgnoreCase))
                {
                    return true;
                }
                else if (input.Equals(NoAnswer, StringComparison.OrdinalIgnoreCase))
                {
                    return false;
                }
 
                Reporter.Output.WriteLine(LocalizableStrings.PostActionInvalidInputRePrompt, input, YesAnswer, NoAnswer);
            }
            while (true);
        }
 
        private PostActionExecutionStatus ProcessAction(
            ICreationEffects creationEffects,
            ICreationResult creationResult,
            string outputBaseDirectory,
            IPostAction action,
            IPostActionProcessor actionProcessor)
        {
            //catch all exceptions on post action execution
            //post actions can be added using components and it's not sure if they handle exceptions properly
            try
            {
                if (actionProcessor.Process(_environment, action, creationEffects, creationResult, outputBaseDirectory))
                {
                    return PostActionExecutionStatus.Success;
                }
 
                Reporter.Error.WriteLine(LocalizableStrings.PostActionFailedInstructionHeader);
                DisplayInstructionsForAction(action, useErrorOutput: true);
                return PostActionExecutionStatus.Failure;
            }
            catch (Exception e)
            {
                Reporter.Error.WriteLine(LocalizableStrings.PostActionFailedInstructionHeader);
                Reporter.Verbose.WriteLine(LocalizableStrings.Generic_Details, e.ToString());
                DisplayInstructionsForAction(action, useErrorOutput: true);
                return PostActionExecutionStatus.Failure;
            }
 
        }
 
        private void DisplayInstructionsForAction(IPostAction action, bool useErrorOutput = false)
        {
            if (string.IsNullOrWhiteSpace(action.ManualInstructions))
            {
                //no manual instructions was written by template author for post action
                return;
            }
 
            IReporter stream = useErrorOutput ? Reporter.Error : Reporter.Output;
            stream.WriteLine(LocalizableStrings.PostActionInstructions, action.ManualInstructions);
 
            // if the post action executes the command ('Run script' post action), additionally display command to be executed.
            if (action.Args != null && action.Args.TryGetValue("executable", out string? executable))
            {
                action.Args.TryGetValue("args", out string? commandArgs);
                stream.WriteLine(string.Format(LocalizableStrings.PostActionCommand, $"{executable} {commandArgs}").Bold().Red());
            }
        }
    }
}