diff --git a/src/Microsoft.DotNet.Configurer/DotnetFirstTimeUseConfigurer.cs b/src/Microsoft.DotNet.Configurer/DotnetFirstTimeUseConfigurer.cs index e13637c47..9a89bcb6f 100644 --- a/src/Microsoft.DotNet.Configurer/DotnetFirstTimeUseConfigurer.cs +++ b/src/Microsoft.DotNet.Configurer/DotnetFirstTimeUseConfigurer.cs @@ -45,10 +45,12 @@ namespace Microsoft.DotNet.Configurer { PrintUnauthorizedAccessMessage(); } + else + { + PrintNugetCachePrimeMessage(); - PrintNugetCachePrimeMessage(); - - _nugetCachePrimer.PrimeCache(); + _nugetCachePrimer.PrimeCache(); + } } } @@ -81,6 +83,8 @@ namespace Microsoft.DotNet.Configurer private bool ShouldPrimeNugetCache() { return ShouldRunFirstRunExperience() && + !_nugetCacheSentinel.Exists() && + !_nugetCacheSentinel.InProgressSentinelAlreadyExists() && !_nugetCachePrimer.SkipPrimingTheCache(); } @@ -96,9 +100,7 @@ namespace Microsoft.DotNet.Configurer var skipFirstTimeExperience = _environmentProvider.GetEnvironmentVariableAsBool("DOTNET_SKIP_FIRST_TIME_EXPERIENCE", false); - return !skipFirstTimeExperience && - !_nugetCacheSentinel.Exists() && - !_nugetCacheSentinel.InProgressSentinelAlreadyExists(); + return !skipFirstTimeExperience; } } } diff --git a/src/Microsoft.DotNet.Configurer/NoOpFirstTimeUseNoticeSentinel.cs b/src/Microsoft.DotNet.Configurer/NoOpFirstTimeUseNoticeSentinel.cs new file mode 100644 index 000000000..a801d4ee6 --- /dev/null +++ b/src/Microsoft.DotNet.Configurer/NoOpFirstTimeUseNoticeSentinel.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.EnvironmentAbstractions; +using NuGet.Configuration; + +namespace Microsoft.DotNet.Configurer +{ + public class NoOpFirstTimeUseNoticeSentinel : IFirstTimeUseNoticeSentinel + { + public bool Exists() + { + return true; + } + + public void CreateIfNotExists() + { + } + + public void Dispose() + { + } + } +} diff --git a/src/Microsoft.DotNet.Configurer/NuGetCacheSentinel.cs b/src/Microsoft.DotNet.Configurer/NuGetCacheSentinel.cs index def087615..bfc7a29d9 100644 --- a/src/Microsoft.DotNet.Configurer/NuGetCacheSentinel.cs +++ b/src/Microsoft.DotNet.Configurer/NuGetCacheSentinel.cs @@ -45,7 +45,7 @@ namespace Microsoft.DotNet.Configurer public bool InProgressSentinelAlreadyExists() { - return CouldNotGetAHandleToTheInProgressSentinel(); + return CouldNotGetAHandleToTheInProgressSentinel() && !UnauthorizedAccess; } public bool Exists() diff --git a/src/dotnet/Program.cs b/src/dotnet/Program.cs index 4611ec7b7..5f44524d4 100644 --- a/src/dotnet/Program.cs +++ b/src/dotnet/Program.cs @@ -81,8 +81,10 @@ namespace Microsoft.DotNet.Cli var lastArg = 0; var cliFallbackFolderPathCalculator = new CliFallbackFolderPathCalculator(); using (INuGetCacheSentinel nugetCacheSentinel = new NuGetCacheSentinel(cliFallbackFolderPathCalculator)) - using (IFirstTimeUseNoticeSentinel firstTimeUseNoticeSentinel = new FirstTimeUseNoticeSentinel(cliFallbackFolderPathCalculator)) + using (IFirstTimeUseNoticeSentinel disposableFirstTimeUseNoticeSentinel = + new FirstTimeUseNoticeSentinel(cliFallbackFolderPathCalculator)) { + IFirstTimeUseNoticeSentinel firstTimeUseNoticeSentinel = disposableFirstTimeUseNoticeSentinel; for (; lastArg < args.Length; lastArg++) { if (IsArg(args[lastArg], "d", "diagnostics")) @@ -113,10 +115,19 @@ namespace Microsoft.DotNet.Cli } else { - ConfigureDotNetForFirstTimeUse(nugetCacheSentinel, firstTimeUseNoticeSentinel, cliFallbackFolderPathCalculator); - // It's the command, and we're done! command = args[lastArg]; + + if (IsDotnetBeingInvokedFromNativeInstaller(command)) + { + firstTimeUseNoticeSentinel = new NoOpFirstTimeUseNoticeSentinel(); + } + + ConfigureDotNetForFirstTimeUse( + nugetCacheSentinel, + firstTimeUseNoticeSentinel, + cliFallbackFolderPathCalculator); + break; } } @@ -164,7 +175,11 @@ namespace Microsoft.DotNet.Cli } return exitCode; + } + private static bool IsDotnetBeingInvokedFromNativeInstaller(string command) + { + return command == "internal-reportinstallsuccess"; } private static void ConfigureDotNetForFirstTimeUse( diff --git a/src/dotnet/Telemetry.cs b/src/dotnet/Telemetry.cs index 457e6f7af..0f28c449a 100644 --- a/src/dotnet/Telemetry.cs +++ b/src/dotnet/Telemetry.cs @@ -37,7 +37,7 @@ namespace Microsoft.DotNet.Cli public Telemetry(IFirstTimeUseNoticeSentinel sentinel) : this(sentinel, null) { } - public Telemetry(IFirstTimeUseNoticeSentinel sentinel, string sessionId) + public Telemetry(IFirstTimeUseNoticeSentinel sentinel, string sessionId, bool blockThreadInitialization = false) { Enabled = !Env.GetEnvironmentVariableAsBool(TelemetryOptout) && PermissionExists(sentinel); @@ -49,8 +49,15 @@ namespace Microsoft.DotNet.Cli // Store the session ID in a static field so that it can be reused CurrentSessionId = sessionId ?? Guid.NewGuid().ToString(); - //initialize in task to offload to parallel thread - _trackEventTask = Task.Factory.StartNew(() => InitializeTelemetry()); + if (blockThreadInitialization) + { + InitializeTelemetry(); + } + else + { + //initialize in task to offload to parallel thread + _trackEventTask = Task.Factory.StartNew(() => InitializeTelemetry()); + } } private bool PermissionExists(IFirstTimeUseNoticeSentinel sentinel) @@ -126,9 +133,9 @@ namespace Microsoft.DotNet.Cli _client.TrackEvent(eventName, eventProperties, eventMeasurements); _client.Flush(); } - catch (Exception) + catch (Exception e) { - Debug.Fail("Exception during TrackEventTask"); + Debug.Fail(e.ToString()); } } diff --git a/src/dotnet/commands/dotnet-internal-reportinstallsuccess/InternalReportinstallsuccessCommand.cs b/src/dotnet/commands/dotnet-internal-reportinstallsuccess/InternalReportinstallsuccessCommand.cs index f3ac8702f..9da815196 100644 --- a/src/dotnet/commands/dotnet-internal-reportinstallsuccess/InternalReportinstallsuccessCommand.cs +++ b/src/dotnet/commands/dotnet-internal-reportinstallsuccess/InternalReportinstallsuccessCommand.cs @@ -43,7 +43,7 @@ namespace Microsoft.DotNet.Cli { var sessionId = Environment.GetEnvironmentVariable(TelemetrySessionIdEnvironmentVariableName); - telemetry = new Telemetry(new FirstTimeUseNoticeSentinel(new CliFallbackFolderPathCalculator()), sessionId); + telemetry = new Telemetry(new NoOpFirstTimeUseNoticeSentinel(), sessionId, blockThreadInitialization: true); } public bool Enabled => telemetry.Enabled; diff --git a/test/Microsoft.DotNet.Configurer.UnitTests/GivenADotnetFirstTimeUseConfigurer.cs b/test/Microsoft.DotNet.Configurer.UnitTests/GivenADotnetFirstTimeUseConfigurer.cs index 24ed0a67e..a9a0e904b 100644 --- a/test/Microsoft.DotNet.Configurer.UnitTests/GivenADotnetFirstTimeUseConfigurer.cs +++ b/test/Microsoft.DotNet.Configurer.UnitTests/GivenADotnetFirstTimeUseConfigurer.cs @@ -233,5 +233,73 @@ namespace Microsoft.DotNet.Configurer.UnitTests _nugetCachePrimerMock.Verify(r => r.PrimeCache(), Times.Once); _reporterMock.Verify(r => r.Write(It.IsAny()), Times.Never); } + + [Fact] + public void It_prints_the_first_time_use_notice_if_the_cache_sentinel_exists_but_the_first_notice_sentinel_does_not() + { + _nugetCacheSentinelMock.Setup(n => n.Exists()).Returns(true); + _firstTimeUseNoticeSentinelMock.Setup(n => n.Exists()).Returns(false); + + var dotnetFirstTimeUseConfigurer = new DotnetFirstTimeUseConfigurer( + _nugetCachePrimerMock.Object, + _nugetCacheSentinelMock.Object, + _firstTimeUseNoticeSentinelMock.Object, + _environmentProviderMock.Object, + _reporterMock.Object, + CliFallbackFolderPath); + + dotnetFirstTimeUseConfigurer.Configure(); + + _reporterMock.Verify(r => + r.WriteLine(It.Is(str => str == LocalizableStrings.FirstTimeWelcomeMessage))); + _reporterMock.Verify( + r => r.WriteLine(It.Is(str => str == LocalizableStrings.NugetCachePrimeMessage)), + Times.Never); + _reporterMock.Verify(r => r.Write(It.IsAny()), Times.Never); + } + + [Fact] + public void It_prints_the_unauthorized_notice_if_the_cache_sentinel_reports_Unauthorized() + { + _nugetCacheSentinelMock.Setup(n => n.UnauthorizedAccess).Returns(true); + + var dotnetFirstTimeUseConfigurer = new DotnetFirstTimeUseConfigurer( + _nugetCachePrimerMock.Object, + _nugetCacheSentinelMock.Object, + _firstTimeUseNoticeSentinelMock.Object, + _environmentProviderMock.Object, + _reporterMock.Object, + CliFallbackFolderPath); + + dotnetFirstTimeUseConfigurer.Configure(); + + _reporterMock.Verify(r => + r.WriteLine(It.Is(str => str == LocalizableStrings.FirstTimeWelcomeMessage))); + _reporterMock.Verify(r => + r.WriteLine(It.Is(str => + str == string.Format(LocalizableStrings.UnauthorizedAccessMessage, CliFallbackFolderPath)))); + _reporterMock.Verify( + r => r.WriteLine(It.Is(str => str == LocalizableStrings.NugetCachePrimeMessage)), + Times.Never); + _reporterMock.Verify(r => r.Write(It.IsAny()), Times.Never); + } + + [Fact] + public void It_does_not_prime_the_cache_if_the_cache_sentinel_reports_Unauthorized() + { + _nugetCacheSentinelMock.Setup(n => n.UnauthorizedAccess).Returns(true); + + var dotnetFirstTimeUseConfigurer = new DotnetFirstTimeUseConfigurer( + _nugetCachePrimerMock.Object, + _nugetCacheSentinelMock.Object, + _firstTimeUseNoticeSentinelMock.Object, + _environmentProviderMock.Object, + _reporterMock.Object, + CliFallbackFolderPath); + + dotnetFirstTimeUseConfigurer.Configure(); + + _nugetCachePrimerMock.Verify(r => r.PrimeCache(), Times.Never); + } } } diff --git a/test/Microsoft.DotNet.Configurer.UnitTests/GivenANuGetCacheSentinel.cs b/test/Microsoft.DotNet.Configurer.UnitTests/GivenANuGetCacheSentinel.cs index 97397d79d..23ae45c4a 100644 --- a/test/Microsoft.DotNet.Configurer.UnitTests/GivenANuGetCacheSentinel.cs +++ b/test/Microsoft.DotNet.Configurer.UnitTests/GivenANuGetCacheSentinel.cs @@ -70,6 +70,18 @@ namespace Microsoft.DotNet.Configurer.UnitTests nugetCacheSentinel.InProgressSentinelAlreadyExists().Should().BeTrue(); } + [Fact] + public void It_returns_false_to_the_in_progress_sentinel_already_exists_when_it_fails_to_get_a_handle_to_it_but_it_failed_because_it_was_unauthorized() + { + var fileMock = new FileMock(); + var directoryMock = new DirectoryMock(); + fileMock.InProgressSentinel = null; + var nugetCacheSentinel = + new NuGetCacheSentinel(NUGET_CACHE_PATH, fileMock, directoryMock); + + nugetCacheSentinel.InProgressSentinelAlreadyExists().Should().BeFalse(); + } + [Fact] public void It_returns_false_to_the_in_progress_sentinel_already_exists_when_it_succeeds_in_getting_a_handle_to_it() { diff --git a/test/dotnet.Tests/GivenThatTheUserIsRunningDotNetForTheFirstTime.cs b/test/dotnet.Tests/GivenThatTheUserIsRunningDotNetForTheFirstTime.cs index 026053261..f323a7d8d 100644 --- a/test/dotnet.Tests/GivenThatTheUserIsRunningDotNetForTheFirstTime.cs +++ b/test/dotnet.Tests/GivenThatTheUserIsRunningDotNetForTheFirstTime.cs @@ -20,15 +20,17 @@ namespace Microsoft.DotNet.Tests private static CommandResult _firstDotnetNonVerbUseCommandResult; private static CommandResult _firstDotnetVerbUseCommandResult; private static DirectoryInfo _nugetFallbackFolder; + private static DirectoryInfo _dotDotnetFolder; + private static string _testDirectory; static GivenThatTheUserIsRunningDotNetForTheFirstTime() { - var testDirectory = TestAssets.CreateTestDirectory("Dotnet_first_time_experience_tests"); - var testNuGetHome = Path.Combine(testDirectory.FullName, "nuget_home"); + _testDirectory = TestAssets.CreateTestDirectory("Dotnet_first_time_experience_tests").FullName; + var testNuGetHome = Path.Combine(_testDirectory, "nuget_home"); var cliTestFallbackFolder = Path.Combine(testNuGetHome, ".dotnet", "NuGetFallbackFolder"); var command = new DotnetCommand() - .WithWorkingDirectory(testDirectory); + .WithWorkingDirectory(_testDirectory); command.Environment["HOME"] = testNuGetHome; command.Environment["USERPROFILE"] = testNuGetHome; command.Environment["APPDATA"] = testNuGetHome; @@ -40,6 +42,7 @@ namespace Microsoft.DotNet.Tests _firstDotnetVerbUseCommandResult = command.ExecuteWithCapturedOutput("new --debug:ephemeral-hive"); _nugetFallbackFolder = new DirectoryInfo(cliTestFallbackFolder); + _dotDotnetFolder = new DirectoryInfo(Path.Combine(testNuGetHome, ".dotnet")); } [Fact] @@ -77,6 +80,60 @@ namespace Microsoft.DotNet.Tests .HaveFile($"{GetDotnetVersion()}.dotnetSentinel"); } + [Fact] + public void ItCreatesAFirstUseSentinelFileUnderTheDotDotNetFolder() + { + _dotDotnetFolder + .Should() + .HaveFile($"{GetDotnetVersion()}.dotnetFirstUseSentinel"); + } + + [Fact] + public void ItDoesNotCreateAFirstUseSentinelFileUnderTheDotDotNetFolderWhenInternalReportInstallSuccessIsInvoked() + { + var emptyHome = Path.Combine(_testDirectory, "empty_home"); + + var command = new DotnetCommand() + .WithWorkingDirectory(_testDirectory); + command.Environment["HOME"] = emptyHome; + command.Environment["USERPROFILE"] = emptyHome; + command.Environment["APPDATA"] = emptyHome; + command.Environment["DOTNET_CLI_TEST_FALLBACKFOLDER"] = _nugetFallbackFolder.FullName; + command.Environment["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = ""; + // Disable to prevent the creation of the .dotnet folder by optimizationdata. + command.Environment["DOTNET_DISABLE_MULTICOREJIT"] = "true"; + command.Environment["SkipInvalidConfigurations"] = "true"; + + command.ExecuteWithCapturedOutput("internal-reportinstallsuccess test").Should().Pass(); + + var emptyHomeFolder = new DirectoryInfo(Path.Combine(emptyHome, ".dotnet")); + emptyHomeFolder.Should().NotExist(); + } + + [Fact] + public void ItShowsTheTelemetryNoticeWhenInvokingACommandAfterInternalReportInstallSuccessHasBeenInvoked() + { + var newHome = Path.Combine(_testDirectory, "new_home"); + var newHomeFolder = new DirectoryInfo(Path.Combine(newHome, ".dotnet")); + + var command = new DotnetCommand() + .WithWorkingDirectory(_testDirectory); + command.Environment["HOME"] = newHome; + command.Environment["USERPROFILE"] = newHome; + command.Environment["APPDATA"] = newHome; + command.Environment["DOTNET_CLI_TEST_FALLBACKFOLDER"] = _nugetFallbackFolder.FullName; + command.Environment["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = ""; + command.Environment["SkipInvalidConfigurations"] = "true"; + + command.ExecuteWithCapturedOutput("internal-reportinstallsuccess test").Should().Pass(); + + var result = command.ExecuteWithCapturedOutput("new --debug:ephemeral-hive"); + + result.StdOut + .Should() + .ContainVisuallySameFragment(Configurer.LocalizableStrings.FirstTimeWelcomeMessage); + } + [Fact] public void ItRestoresTheNuGetPackagesToTheNuGetCacheFolder() {