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

using Microsoft.Extensions.Logging;
using Microsoft.TemplateEngine.Abstractions;
using Microsoft.TemplateEngine.Abstractions.Components;
using Microsoft.TemplateEngine.Abstractions.Constraints;
using Microsoft.TemplateEngine.Utils;

namespace Microsoft.TemplateEngine.Edge.Constraints
{
    public sealed class WorkloadConstraintFactory : ITemplateConstraintFactory
    {
        private static readonly SemaphoreSlim Mutex = new(1);

        Guid IIdentifiedComponent.Id { get; } = Guid.Parse("{F8BA5B13-7BD6-47C8-838C-66626526817B}");

        string ITemplateConstraintFactory.Type => "workload";

        async Task<ITemplateConstraint> ITemplateConstraintFactory.CreateTemplateConstraintAsync(IEngineEnvironmentSettings environmentSettings, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            // need to await due to lack of covariance on Tasks
            return await WorkloadConstraint.CreateAsync(environmentSettings, this, cancellationToken).ConfigureAwait(false);
        }

        internal class WorkloadConstraint : ConstraintBase
        {
            private readonly HashSet<string> _installedWorkloads;
            private readonly string _installedWorkloadsString;
            private readonly Func<IReadOnlyList<string>, string> _remedySuggestionFactory;

            private WorkloadConstraint(
                IEngineEnvironmentSettings environmentSettings,
                ITemplateConstraintFactory factory,
                IReadOnlyList<WorkloadInfo> workloads,
                Func<IReadOnlyList<string>, string> remedySuggestionFactory)
                : base(environmentSettings, factory)
            {
                _installedWorkloads = new HashSet<string>(workloads.Select(w => w.Id), StringComparer.InvariantCultureIgnoreCase);
                _installedWorkloadsString = workloads.Select(w => $"{w.Id} \"{w.Description}\"").ToCsvString();
                _remedySuggestionFactory = remedySuggestionFactory;
            }

            public override string DisplayName => LocalizableStrings.WorkloadConstraint_Name;

            internal static async Task<WorkloadConstraint> CreateAsync(IEngineEnvironmentSettings environmentSettings, ITemplateConstraintFactory factory, CancellationToken cancellationToken)
            {
                (IReadOnlyList<WorkloadInfo> workloads, Func<IReadOnlyList<string>, string> remedySuggestionFactory) =
                    await ExtractWorkloadInfoAsync(
                        environmentSettings.Components.OfType<IWorkloadsInfoProvider>(),
                        environmentSettings.Host.Logger,
                        cancellationToken).ConfigureAwait(false);
                return new WorkloadConstraint(environmentSettings, factory, workloads, remedySuggestionFactory);
            }

            protected override TemplateConstraintResult EvaluateInternal(string? args)
            {
                IReadOnlyList<string> supportedWorkloads = ParseArgs(args).ToList();

                bool isSupportedWorkload = supportedWorkloads.Any(_installedWorkloads.Contains);

                if (isSupportedWorkload)
                {
                    return TemplateConstraintResult.CreateAllowed(this);
                }

                return TemplateConstraintResult.CreateRestricted(
                    this,
                    string.Format(
                        LocalizableStrings.WorkloadConstraint_Message_Restricted,
                        string.Join(", ", supportedWorkloads),
                        string.Join(", ", _installedWorkloadsString)),
                    _remedySuggestionFactory(supportedWorkloads));
            }

            //supported configuration:
            // "args": "maui-mobile"  // workload expected
            // "args": [ "maui-mobile", "maui-desktop" ] // any of workloads expected
            private static IEnumerable<string> ParseArgs(string? args)
            {
                return args.ParseArrayOfConstraintStrings();
            }

            private static async Task<(IReadOnlyList<WorkloadInfo> Workloads, Func<IReadOnlyList<string>, string> RemedySuggestionFactory)> ExtractWorkloadInfoAsync(IEnumerable<IWorkloadsInfoProvider> workloadsInfoProviders, ILogger logger, CancellationToken token)
            {
                List<IWorkloadsInfoProvider> providers = workloadsInfoProviders.ToList();
                List<WorkloadInfo>? workloads = null;

                if (providers.Count == 0)
                {
                    throw new ConfigurationException(LocalizableStrings.WorkloadConstraint_Error_MissingProvider);
                }

                if (providers.Count > 1)
                {
                    throw new ConfigurationException(
                        string.Format(LocalizableStrings.WorkloadConstraint_Error_MismatchedProviders, providers.Select(p => p.Id).ToCsvString()));
                }

                token.ThrowIfCancellationRequested();

                await Mutex.WaitAsync(token).ConfigureAwait(false);
                try
                {
                    IEnumerable<WorkloadInfo> currentProviderWorkloads = await providers[0].GetInstalledWorkloadsAsync(token).ConfigureAwait(false);
                    workloads = currentProviderWorkloads.ToList();
                }
                finally
                {
                    Mutex.Release();
                }

                if (workloads.Select(w => w.Id).HasDuplicates(StringComparer.InvariantCultureIgnoreCase))
                {
                    logger.LogWarning(string.Format(
                        LocalizableStrings.WorkloadConstraint_Warning_DuplicateWorkloads,
                        workloads.Select(w => w.Id).GetDuplicates(StringComparer.InvariantCultureIgnoreCase).ToCsvString()));
                    workloads = workloads
                        .GroupBy(w => w.Id, StringComparer.InvariantCultureIgnoreCase)
                        .Select(g => g.First())
                        .ToList();
                }

                return (workloads, providers[0].ProvideConstraintRemedySuggestion);
            }
        }
    }
}