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

using System;
using System.CommandLine;
using System.Globalization;
using System.Linq;
using NuGet.CommandLine.XPlat.Commands;
using NuGet.CommandLine.XPlat.Commands.NuGet.Add;
using NuGet.CommandLine.XPlat.Commands.NuGet.Disable;
using NuGet.CommandLine.XPlat.Commands.NuGet.Enable;
using NuGet.CommandLine.XPlat.Commands.NuGet.List;
using NuGet.CommandLine.XPlat.Commands.NuGet.Remove;
using NuGet.CommandLine.XPlat.Commands.NuGet.Update;
using NuGet.CommandLine.XPlat.Commands.Why;
using NuGet.Commands;
using NuGet.Common;

#if DEBUG
using NuGet.CommandLine.XPlat.Commands.Package.Update;
using NuGet.CommandLine.XPlat.Commands.Package.PackageDownload;
#endif

namespace NuGet.CommandLine.XPlat
{
    public static class Program
    {
#if DEBUG
        private const string DebugOption = "--debug";
#endif

        internal static int Main(string[] args)
        {
            return MainInternal(args, virtualProjectBuilder: null);
        }

        public static int Run(string[] args, IVirtualProjectBuilder virtualProjectBuilder)
        {
            return MainInternal(args, virtualProjectBuilder);
        }

        private static int MainInternal(string[] args, IVirtualProjectBuilder? virtualProjectBuilder)
        {
            var log = new CommandOutputLogger(LogLevel.Information);
            return MainInternal(args, log, EnvironmentVariableWrapper.Instance, virtualProjectBuilder);
        }

        /// <summary>
        /// Internal Main. This is used for testing.
        /// </summary>
        internal static int MainInternal(string[] args, CommandOutputLogger log, IEnvironmentVariableReader environmentVariableReader, IVirtualProjectBuilder? virtualProjectBuilder = null)
        {
#if USEMSBUILDLOCATOR
            try
            {
                // .NET JIT compiles one method at a time. If this method calls `MSBuildLocator` directly, the
                // try block is never entered if Microsoft.Build.Locator.dll can't be found. So, run it in a
                // lambda function to ensure we're in the try block. C# IIFE!
                ((Action)(() => Microsoft.Build.Locator.MSBuildLocator.RegisterDefaults()))();
            }
            catch
            {
                // MSBuildLocator is used only to enable Visual Studio debugging.
                // It's not needed when using a patched dotnet sdk, so it doesn't matter if it fails.
            }
#endif

#if DEBUG
            string? debugNuGetXPlat = environmentVariableReader.GetEnvironmentVariable("DEBUG_NUGET_XPLAT");

            if (args.Contains(DebugOption) || string.Equals(bool.TrueString, debugNuGetXPlat, StringComparison.OrdinalIgnoreCase))
            {
                args = args.Where(arg => !StringComparer.OrdinalIgnoreCase.Equals(arg, DebugOption)).ToArray();
                System.Diagnostics.Debugger.Launch();
            }
#endif

            // Optionally disable localization.
            if (args.Any(arg => string.Equals(arg, CommandConstants.ForceEnglishOutputOption, StringComparison.OrdinalIgnoreCase)))
            {
                CultureUtility.DisableLocalization();
            }
            else
            {
                UILanguageOverride.Setup(log, environmentVariableReader);
            }
            log.LogDebug(string.Format(CultureInfo.CurrentCulture, Strings.Debug_CurrentUICulture, CultureInfo.DefaultThreadCurrentUICulture));

            NuGet.Common.Migrations.MigrationRunner.Run();

            Func<ILoggerWithColor> getHidePrefixLogger = () =>
            {
                log.HidePrefixForInfoAndMinimal = true;
                return log;
            };

            Action<LogLevel> setLogLevel = (logLevel) => log.VerbosityLevel = logLevel;

            RootCommand rootCommand = new RootCommand();
            rootCommand.Options.Add(new Option<bool>(CommandConstants.ForceEnglishOutputOption)
            {
                Description = Strings.ForceEnglishOutput_Description,
                Arity = ArgumentArity.Zero,
                Recursive = true
            });
            Option<bool> interactiveOption = new Option<bool>("--interactive")
            {
                Description = Strings.AddPkg_InteractiveDescription,
                DefaultValueFactory = _ => Console.IsOutputRedirected
            };

            if (args.Length > 0 && args[0] == "package")
            {
                var packageCommand = new Command("package");
                rootCommand.Subcommands.Add(packageCommand);

                var msbuild = new MSBuildAPIUtility(log, virtualProjectBuilder);

                PackageSearchCommand.Register(packageCommand, getHidePrefixLogger);
                AddPackageReferenceCommand.Register(packageCommand, () => log, () => new AddPackageReferenceCommandRunner(), () => msbuild.VirtualProjectBuilder);
                RemovePackageReferenceCommand.Register(packageCommand, () => log, () => new RemovePackageReferenceCommandRunner(), () => msbuild.VirtualProjectBuilder);
                ListPackageCommand.Register(packageCommand, getHidePrefixLogger, setLogLevel, () => new ListPackageCommandRunner(msbuild));
#if DEBUG
                PackageUpdateCommand.Register(packageCommand, interactiveOption, virtualProjectBuilder);
                PackageDownloadCommand.Register(packageCommand, interactiveOption);
#endif
            }
            else
            {
                var nugetCommand = new Command("nuget");
                rootCommand.Subcommands.Add(nugetCommand);

                var lazyConsole = new Lazy<Spectre.Console.IAnsiConsole>(() => Spectre.Console.AnsiConsole.Console);

                ConfigCommand.Register(rootCommand, getHidePrefixLogger);
                WhyCommand.Register(rootCommand, lazyConsole, virtualProjectBuilder);
                DeleteCommand.Register(rootCommand, getHidePrefixLogger);
                PushCommand.Register(rootCommand, getHidePrefixLogger);
                LocalsCommand.Register(rootCommand, getHidePrefixLogger);
                VerifyCommand.Register(rootCommand, getHidePrefixLogger, setLogLevel, () => new VerifyCommandRunner());
                SignCommand.Register(rootCommand, getHidePrefixLogger, setLogLevel, () => new SignCommandRunner());
                TrustedSignersCommand.Register(rootCommand, getHidePrefixLogger, setLogLevel);

                // Source/client-cert verb commands
                DotnetNuGetAddCommand.Register(rootCommand, getHidePrefixLogger);
                DotnetNuGetDisableCommand.Register(rootCommand, getHidePrefixLogger);
                DotnetNuGetEnableCommand.Register(rootCommand, getHidePrefixLogger);
                DotnetNuGetListCommand.Register(rootCommand, getHidePrefixLogger);
                DotnetNuGetRemoveCommand.Register(rootCommand, getHidePrefixLogger);
                DotnetNuGetUpdateCommand.Register(rootCommand, getHidePrefixLogger);

                // These commands have the same parser as the dotnet CLI, so they can be used interchangeably with "dotnet nuget *"
                ConfigCommand.Register(nugetCommand, getHidePrefixLogger);
                WhyCommand.Register(nugetCommand, lazyConsole, virtualProjectBuilder);
            }

            NetworkProtocolUtility.SetConnectionLimit();
            XPlatUtility.SetUserAgent();

            int exitCode = 0;
            ParseResult parseResult = rootCommand.Parse(args);
            var invocationConfig = new InvocationConfiguration
            {
                EnableDefaultExceptionHandler = false
            };

            try
            {
                exitCode = parseResult.Invoke(invocationConfig);
            }
            catch (Exception e)
            {
                LogException(e, log);
                // Commands that let exceptions propagate to here (e.g. push, source, client-cert)
                // have historically returned exit code 1 on failure. Preserve that contract rather
                // than returning ExitCodes.Error (2), which is reserved for commands that set it explicitly.
                exitCode = 1;
            }

            // Limit the exit code range to 0-255 to support POSIX
            if (exitCode < 0 || exitCode > 255)
            {
                exitCode = 1;
            }

            return exitCode;
        }

        internal static void LogException(Exception e, ILogger log)
        {
            // Log the error
            if (ExceptionLogger.Instance.ShowStack)
            {
                log.LogError(e.ToString());
            }
            else
            {
                log.LogError(ExceptionUtilities.DisplayMessage(e));
            }

            // Log the stack trace as verbose output.
            log.LogVerbose(e.ToString());
        }
    }
}