Introducing a in progress sentinel that gets verified before running the first time experience. If we can get a handle for this sentinel, we proceed with the first time run, otherwise, it means there is a first time experience running already, in which case we continue running dotnet normally, even though the final (real) sentinel is not present yet. This prevents multiple dotnet commands from running the first time experience in parallel and prevents us from running into parallel nuget restores.

This commit is contained in:
Livar Cunha 2016-06-10 15:06:48 -07:00
parent ed7e583ab6
commit 105e5ab051
12 changed files with 240 additions and 25 deletions

View file

@ -10,12 +10,15 @@ namespace Microsoft.DotNet.Cli.Utils
{
public static class DotnetFiles
{
private static string SdkRootFolder => Path.Combine(typeof(DotnetFiles).GetTypeInfo().Assembly.Location, "..");
/// <summary>
/// The CLI ships with a .version file that stores the commit information and CLI version
/// </summary>
public static string VersionFile => Path.GetFullPath(Path.Combine(typeof(DotnetFiles).GetTypeInfo().Assembly.Location, "..", ".version"));
public static string VersionFile => Path.GetFullPath(Path.Combine(SdkRootFolder, ".version"));
public static string NuGetPackagesArchive => Path.GetFullPath(Path.Combine(typeof(DotnetFiles).GetTypeInfo().Assembly.Location, "..", "nuGetPackagesArchive.lzma"));
public static string NuGetPackagesArchive =>
Path.GetFullPath(Path.Combine(SdkRootFolder, "nuGetPackagesArchive.lzma"));
/// <summary>
/// Reads the version file and adds runtime specific information

View file

@ -45,7 +45,7 @@ You can opt out of telemetry by setting a DOTNET_CLI_TELEMETRY_OPTOUT environmen
You can read more about .NET Core tools telemetry @ https://aka.ms/dotnet-cli-telemetry.
Configuring...
-------------------
A command is running to initially populate your local package cache, to improve restorespeed and enable offline access. This command will take up to a minute to complete and will only happen once.";
A command is running to initially populate your local package cache, to improve restore speed and enable offline access. This command will take up to a minute to complete and will only happen once.";
Reporter.Output.WriteLine();
Reporter.Output.WriteLine(firstTimeUseWelcomeMessage);
@ -54,9 +54,11 @@ A command is running to initially populate your local package cache, to improve
private bool ShouldPrimeNugetCache()
{
var skipFirstTimeExperience =
_environmentProvider.GetEnvironmentVariableAsBool("DOTNET_SKIP_FIRST_TIME_EXPERIENCE", false));
_environmentProvider.GetEnvironmentVariableAsBool("DOTNET_SKIP_FIRST_TIME_EXPERIENCE", false);
return !skipFirstTimeExperience && !_nugetCacheSentinel.Exists();
return !skipFirstTimeExperience &&
!_nugetCacheSentinel.Exists() &&
!_nugetCacheSentinel.InProgressSentinelAlreadyExists();
}
}
}

View file

@ -1,10 +1,14 @@
// 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;
namespace Microsoft.DotNet.Configurer
{
public interface INuGetCacheSentinel
public interface INuGetCacheSentinel : IDisposable
{
bool InProgressSentinelAlreadyExists();
bool Exists();
void CreateIfNotExists();

View file

@ -10,7 +10,6 @@ namespace Microsoft.DotNet.Configurer
{
public class NuGetCachePrimer : INuGetCachePrimer
{
private const string NUGET_SOURCE_PARAMETER = "-s";
private readonly ICommandFactory _commandFactory;
private readonly IDirectory _directory;
private readonly IFile _file;
@ -88,7 +87,7 @@ namespace Microsoft.DotNet.Configurer
{
return RunCommand(
"restore",
new[] {NUGET_SOURCE_PARAMETER, $"{extractedPackagesArchiveDirectory}"},
new[] {"-s", $"{extractedPackagesArchiveDirectory}"},
workingDirectory);
}

View file

@ -11,6 +11,7 @@ namespace Microsoft.DotNet.Configurer
public class NuGetCacheSentinel : INuGetCacheSentinel
{
public static readonly string SENTINEL = $"{Product.Version}.dotnetSentinel";
public static readonly string INPROGRESS_SENTINEL = $"{Product.Version}.inprogress.dotnetSentinel";
private readonly IFile _file;
@ -29,7 +30,10 @@ namespace Microsoft.DotNet.Configurer
}
}
private string Sentinel => Path.Combine(NuGetCachePath, SENTINEL);
private string SentinelPath => Path.Combine(NuGetCachePath, SENTINEL);
private string InProgressSentinelPath => Path.Combine(NuGetCachePath, INPROGRESS_SENTINEL);
private Stream InProgressSentinel { get; set; }
public NuGetCacheSentinel() : this(string.Empty, FileSystemWrapper.Default.File)
{
@ -39,18 +43,58 @@ namespace Microsoft.DotNet.Configurer
{
_file = file;
_nugetCachePath = nugetCachePath;
SetInProgressSentinel();
}
public bool InProgressSentinelAlreadyExists()
{
return CouldNotGetAHandleToTheInProgressSentinel();
}
public bool Exists()
{
return _file.Exists(Sentinel);
return _file.Exists(SentinelPath);
}
public void CreateIfNotExists()
{
if (!Exists())
{
_file.CreateEmptyFile(Sentinel);
_file.CreateEmptyFile(SentinelPath);
}
}
private bool CouldNotGetAHandleToTheInProgressSentinel()
{
return InProgressSentinel == null;
}
private void SetInProgressSentinel()
{
try
{
// open an exclusive handle to the in-progress sentinel and mark it for delete on close.
// we open with exclusive FileShare.None access to indicate that the operation is in progress.
// buffer size is minimum since we won't be reading or writing from the file.
// delete on close is to indicate that the operation is no longer in progress when we dispose
// this.
InProgressSentinel = _file.OpenFile(
InProgressSentinelPath,
FileMode.OpenOrCreate,
FileAccess.ReadWrite,
FileShare.None,
1,
FileOptions.DeleteOnClose);
}
catch { }
}
public void Dispose()
{
if (InProgressSentinel != null)
{
InProgressSentinel.Dispose();
}
}
}

View file

@ -23,9 +23,28 @@ namespace Microsoft.Extensions.EnvironmentAbstractions
return File.OpenRead(path);
}
public Stream OpenFile(
string path,
FileMode fileMode,
FileAccess fileAccess,
FileShare fileShare,
int bufferSize,
FileOptions fileOptions)
{
return new FileStream(path, fileMode, fileAccess, fileShare, bufferSize, fileOptions);
}
public void CreateEmptyFile(string path)
{
File.Create(path).Dispose();
try
{
var emptyFile = File.Create(path);
if (emptyFile != null)
{
emptyFile.Dispose();
}
}
catch { }
}
}
}

View file

@ -13,6 +13,14 @@ namespace Microsoft.Extensions.EnvironmentAbstractions
Stream OpenRead(string path);
Stream OpenFile(
string path,
FileMode fileMode,
FileAccess fileAccess,
FileShare fileShare,
int bufferSize,
FileOptions fileOptions);
void CreateEmptyFile(string path);
}
}

View file

@ -166,17 +166,19 @@ namespace Microsoft.DotNet.Cli
{
using (var nugetPackagesArchiver = new NuGetPackagesArchiver())
{
var environmentProvider = new EnvironmentProvider();
var nugetCacheSentinel = new NuGetCacheSentinel();
var commandFactory = new DotNetCommandFactory();
var nugetCachePrimer =
new NuGetCachePrimer(commandFactory, nugetPackagesArchiver, nugetCacheSentinel);
var dotnetConfigurer = new DotnetFirstTimeUseConfigurer(
nugetCachePrimer,
nugetCacheSentinel,
environmentProvider);
using (var nugetCacheSentinel = new NuGetCacheSentinel())
{
var environmentProvider = new EnvironmentProvider();
var commandFactory = new DotNetCommandFactory();
var nugetCachePrimer =
new NuGetCachePrimer(commandFactory, nugetPackagesArchiver, nugetCacheSentinel);
var dotnetConfigurer = new DotnetFirstTimeUseConfigurer(
nugetCachePrimer,
nugetCacheSentinel,
environmentProvider);
dotnetConfigurer.Configure();
dotnetConfigurer.Configure();
}
}
}
}

View file

@ -43,6 +43,21 @@ namespace Microsoft.DotNet.Configurer.UnitTests
_nugetCachePrimerMock.Verify(r => r.PrimeCache(), Times.Never);
}
[Fact]
public void It_does_not_prime_the_cache_if_first_run_experience_is_already_happening()
{
_nugetCacheSentinelMock.Setup(n => n.InProgressSentinelAlreadyExists()).Returns(true);
var dotnetFirstTimeUseConfigurer = new DotnetFirstTimeUseConfigurer(
_nugetCachePrimerMock.Object,
_nugetCacheSentinelMock.Object,
_environmentProviderMock.Object);
dotnetFirstTimeUseConfigurer.Configure();
_nugetCachePrimerMock.Verify(r => r.PrimeCache(), Times.Never);
}
[Fact]
public void It_does_not_prime_the_cache_if_the_sentinel_exists_but_the_user_has_set_the_DOTNET_SKIP_FIRST_TIME_EXPERIENCE_environemnt_variable()
{
@ -74,6 +89,6 @@ namespace Microsoft.DotNet.Configurer.UnitTests
dotnetFirstTimeUseConfigurer.Configure();
_nugetCachePrimerMock.Verify(r => r.PrimeCache(), Times.Once);
}
}
}
}

View file

@ -1,25 +1,68 @@
// 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;
using System.IO;
using FluentAssertions;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Configurer;
using Microsoft.Extensions.DependencyModel.Tests;
using Microsoft.Extensions.EnvironmentAbstractions;
using Xunit;
namespace Microsoft.DotNet.Configurer.UnitTests
{
public class GivenANuGetCacheSentinel
{
private const string NUGET_CACHE_PATH = "some path";
private FileSystemMockBuilder _fileSystemMockBuilder;
public GivenANuGetCacheSentinel()
{
_fileSystemMockBuilder = FileSystemMockBuilder.Create();
}
[Fact]
public void As_soon_as_it_gets_created_it_tries_to_get_handle_of_the_InProgress_sentinel()
{
var fileMock = new FileMock();
var nugetCacheSentinel = new NuGetCacheSentinel(NUGET_CACHE_PATH, fileMock);
fileMock.OpenFileWithRightParamsCalled.Should().BeTrue();
}
private const string NUGET_CACHE_PATH = "some path";
[Fact]
public void It_returns_true_to_the_in_progress_sentinel_already_exists_when_it_fails_to_get_a_handle_to_it()
{
var fileMock = new FileMock();
fileMock.InProgressSentinel = null;
var nugetCacheSentinel = new NuGetCacheSentinel(NUGET_CACHE_PATH, fileMock);
nugetCacheSentinel.InProgressSentinelAlreadyExists().Should().BeTrue();
}
[Fact]
public void It_returns_false_to_the_in_progress_sentinel_already_exists_when_it_succeeds_in_getting_a_handle_to_it()
{
var fileMock = new FileMock();
fileMock.InProgressSentinel = new MemoryStream();
var nugetCacheSentinel = new NuGetCacheSentinel(NUGET_CACHE_PATH, fileMock);
nugetCacheSentinel.InProgressSentinelAlreadyExists().Should().BeFalse();
}
[Fact]
public void It_disposes_of_the_handle_to_the_InProgressSentinel_when_NuGetCacheSentinel_is_disposed()
{
var mockStream = new MockStream();
var fileMock = new FileMock();
fileMock.InProgressSentinel = mockStream;
using (var nugetCacheSentinel = new NuGetCacheSentinel(NUGET_CACHE_PATH, fileMock))
{}
mockStream.IsDisposed.Should().BeTrue();
}
[Fact]
public void The_sentinel_has_the_current_version_in_its_name()
@ -79,5 +122,70 @@ namespace Microsoft.DotNet.Configurer.UnitTests
fileSystemMock.File.ReadAllText(sentinel).Should().Be(contentToValidateSentinalWasNotReplaced);
}
private class FileMock : IFile
{
public bool OpenFileWithRightParamsCalled { get; private set; }
public Stream InProgressSentinel { get; set;}
public bool Exists(string path)
{
throw new NotImplementedException();
}
public string ReadAllText(string path)
{
throw new NotImplementedException();
}
public Stream OpenRead(string path)
{
throw new NotImplementedException();
}
public Stream OpenFile(
string path,
FileMode fileMode,
FileAccess fileAccess,
FileShare fileShare,
int bufferSize,
FileOptions fileOptions)
{
Stream fileStream = null;
var inProgressSentinel =
Path.Combine(GivenANuGetCacheSentinel.NUGET_CACHE_PATH, NuGetCacheSentinel.INPROGRESS_SENTINEL);
if (path.Equals(inProgressSentinel) &&
fileMode == FileMode.OpenOrCreate &&
fileAccess == FileAccess.ReadWrite &&
fileShare == FileShare.None &&
bufferSize == 1 &&
fileOptions == FileOptions.DeleteOnClose)
{
OpenFileWithRightParamsCalled = true;
fileStream = InProgressSentinel;
}
return fileStream;
}
public void CreateEmptyFile(string path)
{
throw new NotImplementedException();
}
}
private class MockStream : MemoryStream
{
public bool IsDisposed { get; private set;}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
IsDisposed = true;
}
}
}
}

View file

@ -85,6 +85,17 @@ namespace Microsoft.Extensions.DependencyModel.Tests
return new MemoryStream(Encoding.UTF8.GetBytes(ReadAllText(path)));
}
public Stream OpenFile(
string path,
FileMode fileMode,
FileAccess fileAccess,
FileShare fileShare,
int bufferSize,
FileOptions fileOptions)
{
throw new NotImplementedException();
}
public void CreateEmptyFile(string path)
{
_files.Add(path, string.Empty);

View file

@ -51,7 +51,7 @@ You can opt out of telemetry by setting a DOTNET_CLI_TELEMETRY_OPTOUT environmen
You can read more about .NET Core tools telemetry @ https://aka.ms/dotnet-cli-telemetry.
Configuring...
-------------------
A command is running to initially populate your local package cache, to improve restorespeed and enable offline access. This command will take up to a minute to complete and will only happen once.";
A command is running to initially populate your local package cache, to improve restore speed and enable offline access. This command will take up to a minute to complete and will only happen once.";
_firstDotnetUseCommandResult.StdOut.Should().StartWith(firstTimeUseWelcomeMessage);
}