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

#if !CLI_AOT
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Diagnostics;
using Microsoft.DotNet.Cli.CommandFactory;
using Microsoft.DotNet.Cli.CommandFactory.CommandResolution;
using Microsoft.DotNet.Cli.Commands.Hidden.InternalReportInstallSuccess;
using Microsoft.DotNet.Cli.Commands.Workload;
using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Cli.ShellShim;
using Microsoft.DotNet.Cli.Telemetry;
#endif
using Microsoft.DotNet.Cli.Utils;
#if !CLI_AOT
using Microsoft.DotNet.Cli.Utils.Extensions;
using Microsoft.DotNet.Configurer;
using Microsoft.DotNet.ProjectTools;
using Microsoft.DotNet.Utilities;
using Microsoft.Extensions.EnvironmentAbstractions;
using NuGet.Frameworks;
using CommandResult = System.CommandLine.Parsing.CommandResult;
#endif

namespace Microsoft.DotNet.Cli;

public class Program
{
#if CLI_AOT
    public static int Main(string[] args)
    {
        var parseResult = Parser.Parse(args);
        return Parser.Invoke(parseResult);
    }
#else
    private static readonly string s_toolPathSentinelFileName = $"{Product.Version}.toolpath.sentinel";

    private static readonly Activity? s_mainActivity;
    private static readonly PosixSignalRegistration s_sigIntRegistration;
    private static readonly PosixSignalRegistration s_sigQuitRegistration;
    private static readonly PosixSignalRegistration s_sigTermRegistration;
    private static readonly string? s_globalJsonState;

    public static ITelemetryClient TelemetryInstance { get; private set; }

    static Program()
    {
        var mainTimeStamp = DateTime.Now;
        s_sigIntRegistration = PosixSignalRegistration.Create(PosixSignal.SIGINT, Shutdown);
        s_sigQuitRegistration = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, Shutdown);
        s_sigTermRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, Shutdown);

        // Note: This TelemetryClient instance needs to be created prior to calculating ActivityKind and ParentActivityContext,
        // used in the main activity creation below.
        TelemetryInstance = new TelemetryClient();
        TelemetryEventEntry.Subscribe(TelemetryInstance.TrackEvent);
        TelemetryEventEntry.TelemetryFilter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing);

        s_mainActivity = Activities.Source.CreateActivity("main", TelemetryClient.ActivityKind, TelemetryClient.ParentActivityContext)
            ?.Start()
            ?.SetStartTime(Process.GetCurrentProcess().StartTime)
            ?.AddTag("process.pid", Process.GetCurrentProcess().Id)
            ?.AddTag("process.executable.name", "dotnet");

        if (CommandLoggingContext.IsVerbose)
        {
            Reporter.Verbose.WriteLine($"Telemetry is: {(TelemetryInstance.Enabled ? "Enabled" : "Disabled")}");
        }

        // Creates a host-startup activity which includes the global.json state.
        using (var hostStartupActivity = Activities.Source.StartActivity("host-startup"))
        {
            hostStartupActivity?.SetStartTime(Process.GetCurrentProcess().StartTime);
            if (TelemetryInstance.Enabled && hostStartupActivity is not null)
            {
                // Get the global.json state to report in telemetry along with this command invocation.
                s_globalJsonState = NativeWrapper.NETCoreSdkResolverNativeWrapper.GetGlobalJsonState(Environment.CurrentDirectory);
                hostStartupActivity?.AddTag("dotnet.globalJson", s_globalJsonState);
            }
            hostStartupActivity?.SetEndTime(mainTimeStamp)?.SetStatus(ActivityStatusCode.Ok);
        }

        // We have some behaviors in MSBuild that we want to enforce (either when using MSBuild API or by shelling out to it),
        // so we set those ASAP as globally as possible.
        if (string.IsNullOrEmpty(Env.GetEnvironmentVariable("MSBUILDFAILONDRIVEENUMERATINGWILDCARD")))
        {
            Environment.SetEnvironmentVariable("MSBUILDFAILONDRIVEENUMERATINGWILDCARD", "1");
        }
    }

    public static int Main(string[] args)
    {
        // Register a handler for SIGTERM to allow graceful shutdown of the application on Unix.
        // See https://github.com/dotnet/docs/issues/46226.
        using var termSignalRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, _ => Environment.Exit(0));

        using AutomaticEncodingRestorer _ = new();

        if (Env.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_CONSOLE_USE_DEFAULT_ENCODING) != "1"
            // Setting output encoding is not available on those platforms
            && UILanguageOverride.OperatingSystemSupportsUtf8())
        {
            Console.OutputEncoding = Encoding.UTF8;
        }

        DebugHelper.HandleDebugSwitch(ref args);
        // By default, .NET Core doesn't have all code pages needed for Console apps.
        // See the .NET Core Notes: https://docs.microsoft.com/dotnet/api/system.diagnostics.process#-notes
        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
        UILanguageOverride.Setup();

        var exitCode = 1;
        try
        {
            exitCode = ProcessArgsAndExecute(args);
            s_mainActivity?.AddTag("process.exit.code", exitCode)?.SetStatus(ActivityStatusCode.Ok);
            return exitCode;
        }
        catch (Exception e) when (e.ShouldBeDisplayedAsError())
        {
            Reporter.Error.WriteLine(CommandLoggingContext.IsVerbose
                ? e.ToString().Red().Bold()
                : e.Message.Red().Bold());

            if (e is CommandParsingException { ParseResult: {} exceptionParseResult } )
            {
                exceptionParseResult.ShowHelp();
            }
            s_mainActivity?.AddTag("process.exit.code", exitCode)?.SetStatus(ActivityStatusCode.Error);
            return exitCode;
        }
        catch (Exception e) when (!e.ShouldBeDisplayedAsError())
        {
            TelemetryEventEntry.SendFiltered(e);
            Reporter.Error.WriteLine(e.ToString().Red().Bold());
            s_mainActivity?.AddTag("process.exit.code", exitCode)?.SetStatus(ActivityStatusCode.Error);
            return exitCode;
        }
        finally
        {
            TelemetryInstance.TrackEvent("command/finish", new Dictionary<string, string?> { { "exitCode", exitCode.ToString() } });
            Shutdown(default!);
            TelemetryClient.WriteLogIfNecessary();
        }
    }

    internal static int ProcessArgsAndExecute(string[] args)
    {
        ParseResult parseResult = ParseArgs(args);
        // Options that perform terminating actions are considered to essentially be subcommands.
        // These are special as they should not run the first-run setup.
        // Example: dotnet --version
        if (!(parseResult.Action is InvocableOptionAction { Terminating: true }))
        {
            SetupFirstRun(parseResult);
        }

        TelemetryEventEntry.SendFiltered(new ParseResultWithGlobalJsonState(parseResult, s_globalJsonState));
        if (parseResult.CanBeInvoked())
        {
            return ExecuteInternalCommand(parseResult);
        }

        try
        {
            return ExecuteExternalCommand(args, parseResult);
        }
        catch (CommandUnknownException e)
        {
            Reporter.Error.WriteLine(e.Message.Red());
            Reporter.Output.WriteLine(e.InstructionMessage);
            return 1;
        }

        static ParseResult ParseArgs(string[] args)
        {
            ParseResult parseResult;
            using (var parseActivity = Activities.Source.StartActivity("parse"))
            {
                parseResult = Parser.Parse(args);

                // Avoid create temp directory with root permission and later prevent access in non sudo
                // This method need to be run very early before temp folder get created
                // https://github.com/dotnet/sdk/issues/20195
                SudoEnvironmentDirectoryOverride.OverrideEnvironmentVariableToTmp(parseResult);
            }
            s_mainActivity.SetDisplayName(parseResult);
            return parseResult;
        }
    }

    private static void SetupFirstRun(ParseResult parseResult)
    {
        using var _ = Activities.Source.StartActivity("first-time-use");
        IFirstTimeUseNoticeSentinel firstTimeUseNoticeSentinel = new FirstTimeUseNoticeSentinel();
        IAspNetCertificateSentinel aspNetCertificateSentinel = new AspNetCertificateSentinel();
        string toolPath = Path.Combine(CliFolderPathCalculator.DotnetUserProfileFolderPath, s_toolPathSentinelFileName);
        IFileSentinel toolPathSentinel = new FileSentinel(new FilePath(toolPath));

        var environmentProvider = new EnvironmentProvider();
        bool generateAspNetCertificate = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_GENERATE_ASPNET_CERTIFICATE, defaultValue: true);
        bool telemetryOptout = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.TELEMETRY_OPTOUT, defaultValue: CompileOptions.TelemetryOptOutDefault);
        bool addGlobalToolsToPath = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_ADD_GLOBAL_TOOLS_TO_PATH, defaultValue: true);
        bool nologo = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_NOLOGO, defaultValue: false);
        bool skipWorkloadIntegrityCheck = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK,
            // Default the workload integrity check skip to true if the command is being ran in CI. Otherwise, false.
            defaultValue: new CIEnvironmentDetectorForTelemetry().IsCIEnvironment());

        var isDotnetBeingInvokedFromNativeInstaller = false;
        // Note: This should not be special cased like this. Determine if we can skip first run setup entirely for this command.
        if (parseResult.CommandResult.Command is InternalReportInstallSuccessCommandDefinition)
        {
            aspNetCertificateSentinel = new NoOpAspNetCertificateSentinel();
            firstTimeUseNoticeSentinel = new NoOpFirstTimeUseNoticeSentinel();
            toolPathSentinel = new NoOpFileSentinel(exists: false);
            isDotnetBeingInvokedFromNativeInstaller = true;
        }

        var dotnetFirstRunConfiguration = new DotnetFirstRunConfiguration(
            generateAspNetCertificate,
            telemetryOptout,
            addGlobalToolsToPath,
            nologo,
            skipWorkloadIntegrityCheck);

        string[] getStarOperators = ["getProperty", "getItem", "getTargetResult"];
        char[] switchIndicators = ['-', '/'];
        var skipFirstTimeUseCheck = parseResult.CommandResult.Tokens.Any(t =>
            getStarOperators.Any(o =>
                switchIndicators.Any(i => t.Value.StartsWith(i + o, StringComparison.OrdinalIgnoreCase))));

        var isFirstTimeUse = !firstTimeUseNoticeSentinel.Exists() && !skipFirstTimeUseCheck;
        var environmentPath = EnvironmentPathFactory.CreateEnvironmentPath(isDotnetBeingInvokedFromNativeInstaller, environmentProvider);
        // Note: Not sure why this unused instance type is created.
        var __ = new DotNetCommandFactory(alwaysRunOutOfProc: true);
        var aspnetCertificateGenerator = new AspNetCoreCertificateGenerator();
        var reporter = Reporter.Error;
        var dotnetConfigurer = new DotnetFirstTimeUseConfigurer(
            firstTimeUseNoticeSentinel,
            aspNetCertificateSentinel,
            aspnetCertificateGenerator,
            toolPathSentinel,
            dotnetFirstRunConfiguration,
            reporter,
            environmentPath,
            skipFirstTimeUseCheck);

        dotnetConfigurer.Configure();

#if TARGET_WINDOWS
        if (isDotnetBeingInvokedFromNativeInstaller && OperatingSystem.IsWindows())
        {
            DotDefaultPathCorrector.Correct();
        }
#endif

        if (isFirstTimeUse && !skipWorkloadIntegrityCheck)
        {
            try
            {
                WorkloadIntegrityChecker.RunFirstUseCheck(reporter);
            }
            catch (Exception)
            {
                // If the workload check fails for any reason, we want to eat the failure and continue running the command.
                reporter.WriteLine(CliStrings.WorkloadIntegrityCheckError.Yellow());
            }
        }
    }

    private static int ExecuteInternalCommand(ParseResult parseResult)
    {
        Debug.Assert(parseResult.CanBeInvoked());
        int exitCode;
        using var _ = Activities.Source.StartActivity("invocation");
        try
        {
            exitCode = Parser.Invoke(parseResult);
            if (parseResult.Errors.Any())
            {
                exitCode = AdjustExitCodeForNew();
            }
        }
        catch (Exception exception)
        {
            exitCode = Parser.ExceptionHandler(exception, parseResult);
        }
        return exitCode;

        int AdjustExitCodeForNew()
        {
            var commandResult = parseResult.CommandResult;
            while (commandResult is not null)
            {
                if (commandResult.Command.Name == "new")
                {
                    // Default parse error exit code is 1.
                    // For the "new" command and its subcommands, it needs to be 127.
                    return 127;
                }
                commandResult = commandResult.Parent as CommandResult;
            }
            return exitCode;
        }
    }

    private static int ExecuteExternalCommand(string[] args, ParseResult parseResult)
    {
        string commandName = "dotnet-" + parseResult.GetValue(Parser.RootCommand.DotnetSubCommand);
        CommandSpec? resolvedCommandSpec = null;
        using (var _ = Activities.Source.StartActivity("lookup-external-command"))
        {
            resolvedCommandSpec = CommandResolver.TryResolveCommandSpec(
                new DefaultCommandResolverPolicy(),
                commandName,
                args.GetSubArguments(),
                FrameworkConstants.CommonFrameworks.NetStandardApp15);
        }

        if (resolvedCommandSpec is null && TryRunFileBasedApp(parseResult) is { } fileBasedAppExitCode)
        {
            return fileBasedAppExitCode;
        }

        var resolvedCommand = CommandFactoryUsingResolver.CreateOrThrow(commandName, resolvedCommandSpec);
        using var __ = Activities.Source.StartActivity("execute-extensible-command");
        return resolvedCommand.Execute().ExitCode;
    }

    private static int? TryRunFileBasedApp(ParseResult parseResult)
    {
        // If we didn't match any built-in commands, and a C# file path is the first argument,
        // parse as `dotnet run file.cs ..rest_of_args` instead.
        if (parseResult.GetResult(Parser.RootCommand.DotnetSubCommand) is { Tokens: [{ Type: TokenType.Argument, Value: { } } unmatchedCommandOrFile] }
            && VirtualProjectBuilder.IsValidEntryPointPath(unmatchedCommandOrFile.Value))
        {
            List<string> otherTokens = new(parseResult.Tokens.Count - 1);
            foreach (var token in parseResult.Tokens)
            {
                if (token.Type != TokenType.Argument || token != unmatchedCommandOrFile)
                {
                    otherTokens.Add(token.Value);
                }
            }
            parseResult = Parser.Parse(["run", "--file", unmatchedCommandOrFile.Value, .. otherTokens]);
            return ExecuteInternalCommand(parseResult);
        }

        return null;
    }

    public static void Shutdown(PosixSignalContext context)
    {
        s_sigIntRegistration.Dispose();
        s_sigQuitRegistration.Dispose();
        s_sigTermRegistration.Dispose();
        s_mainActivity?.Stop();
        TelemetryClient.FlushProviders();
        Activities.Source.Dispose();
    }
#endif
}