| File: FirstRunExperience.cs | Web Access |
| Project: 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. using System.CommandLine; using Microsoft.DotNet.Cli.Commands.Hidden.InternalReportInstallSuccess; using Microsoft.DotNet.Cli.Commands.Workload; using Microsoft.DotNet.Cli.ShellShim; #if !CLI_AOT using Microsoft.DotNet.Cli.Utils.Extensions; #endif using Microsoft.DotNet.Cli.Telemetry; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Configurer; using Microsoft.Extensions.EnvironmentAbstractions; namespace Microsoft.DotNet.Cli; /// <summary> /// The cross-cutting "first run" experience: first-time-use notice, telemetry opt-out message, /// ASP.NET Core developer certificate, global-tools PATH registration, and the workload integrity /// check. This lives in one place so the managed CLI entry point (<see cref="Program"/>) and the /// NativeAOT bridge (<c>NativeEntryPoint</c>) share one source of truth for first-run setup. /// /// <para> /// Both entry points build the <em>same</em> <see cref="DotnetFirstTimeUseConfigurer"/> in the same /// order, so the two paths are easy to compare for parity. The managed CLI performs the full /// experience in-process. The NativeAOT binary performs almost all of it in-process too - including /// ASP.NET Core developer-certificate generation, whose <c>CertificateManager</c> uses only /// NativeAOT-safe BCL cryptography (the crypto PAL is resolved from the host's already-loaded native /// library). The one first-run action it cannot perform is workload repair, which needs the NuGet /// engine. When workload repair is still pending, <see cref="Setup"/> defers the whole invocation to /// the managed CLI - before writing any sentinel - so the managed CLI performs the complete first-run /// atomically, exactly once. Everything else (global-tools PATH, the first-time-use notice + telemetry /// message, the NuGet state migration, and the dev certificate) is AOT-safe and runs in-process on both /// paths. Subsequent invocations observe the sentinels and run fully in-process on the AOT fast path. /// </para> /// </summary> internal static class FirstRunExperience { private static readonly string s_toolPathSentinelFileName = $"{Product.Version}.toolpath.sentinel"; /// <summary> /// Runs (or, on the AOT path, arranges) the first-run experience for the given parse result. /// </summary> /// <returns> /// <see langword="true"/> when first-run setup is complete and the caller may proceed to execute /// the command in-process; <see langword="false"/> (AOT only) when the invocation must be deferred /// to the managed CLI so it can perform the parts of first-run that are unavailable in NativeAOT. /// The managed CLI always returns <see langword="true"/>. /// </returns> public static bool Setup(ParseResult parseResult) { // 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 }) { return true; } using var activity = 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 addGlobalToolsToPath = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_ADD_GLOBAL_TOOLS_TO_PATH, defaultValue: true); 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; } 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; #if CLI_AOT // The NativeAOT binary omits only the NuGet engine needed to repair workloads. When workload // repair is still pending we defer this entire invocation to the managed CLI - before mutating // any state - so it performs the complete first-run atomically, exactly once. The workload repair // is detected precisely in-process via WorkloadInstallDetector, so we only defer when workloads // are actually installed for this feature band instead of on every first run. // // Dev-certificate generation IS available in NativeAOT: the ASP.NET Core CertificateManager only // uses BCL X509/RSA crypto (no reflection/dynamic code), and the underlying crypto PAL is the // host's already-loaded native library, so it runs in-process just like the managed CLI. bool workloadRepairPending = isFirstTimeUse && !skipWorkloadIntegrityCheck && WorkloadInstallDetector.HasInstalledWorkloadsForCurrentBand(); if (workloadRepairPending) { return false; } #endif IAspNetCoreCertificateGenerator aspnetCertificateGenerator = new AspNetCoreCertificateGenerator(); bool telemetryOptout = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.TELEMETRY_OPTOUT, defaultValue: CompileOptions.TelemetryOptOutDefault); bool nologo = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_NOLOGO, defaultValue: false); var dotnetFirstRunConfiguration = new DotnetFirstRunConfiguration( generateAspNetCertificate, telemetryOptout, addGlobalToolsToPath, nologo, skipWorkloadIntegrityCheck); var environmentPath = EnvironmentPathFactory.CreateEnvironmentPath(isDotnetBeingInvokedFromNativeInstaller, environmentProvider); 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) { #if CLI_AOT // Reaching here means WorkloadInstallDetector found no installed workloads for this feature // band, so the NuGet-based integrity repair has nothing to do. Any actual repair is handled // by the managed CLI, which the AOT bridge falls back to for workload commands. #else 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()); } #endif } return true; } }