Invoking a command waits up to 30s for NuGet or another process (#4657)

* Invoking a command waits up to 30s for NuGet or another process

* PR Feedback
This commit is contained in:
Piotr Puszkiewicz 2016-11-08 23:23:13 -08:00 committed by GitHub
parent a700604b93
commit b918b2a6b6
10 changed files with 316 additions and 4 deletions

View file

@ -124,7 +124,9 @@ namespace Microsoft.DotNet.Cli.Utils
var lockFilePath = GetLockFilePathFromProjectLockFileProperty() ??
GetLockFilePathFromIntermediateBaseOutputPath();
return new LockFileFormat().Read(lockFilePath);
return new LockFileFormat()
.ReadWithLock(lockFilePath)
.Result;
}
private string GetLockFilePathFromProjectLockFileProperty()

View file

@ -0,0 +1,33 @@
// 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.Threading;
using System.Threading.Tasks;
using NuGet.Common;
using NuGet.ProjectModel;
namespace Microsoft.DotNet.Cli.Utils
{
public static class LockFileFormatExtensions
{
private const int NumberOfRetries = 3000;
private static readonly TimeSpan SleepDuration = TimeSpan.FromMilliseconds(10);
public static async Task<LockFile> ReadWithLock(this LockFileFormat subject, string path)
{
return await ConcurrencyUtilities.ExecuteWithFileLockedAsync(
path,
lockedToken =>
{
var lockFile = FileAccessRetrier.RetryOnFileAccessFailure(() => subject.Read(path));
return lockFile;
},
CancellationToken.None);
}
}
}

View file

@ -0,0 +1,51 @@
// 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 System.Threading.Tasks;
namespace Microsoft.DotNet.Cli.Utils
{
public static class FileAccessRetrier
{
public static async Task<T> RetryOnFileAccessFailure<T>(Func<T> func, int maxRetries = 3000, TimeSpan sleepDuration = default(TimeSpan))
{
var attemptsLeft = maxRetries;
if (sleepDuration == default(TimeSpan))
{
sleepDuration = TimeSpan.FromMilliseconds(10);
}
while (true)
{
if (attemptsLeft < 1)
{
throw new InvalidOperationException("Could not access assets file.");
}
attemptsLeft--;
try
{
return func();
}
catch (UnauthorizedAccessException)
{
// This can occur when the file is being deleted
// Or when an admin user has locked the file
await Task.Delay(sleepDuration);
continue;
}
catch (IOException)
{
await Task.Delay(sleepDuration);
continue;
}
}
}
}
}

View file

@ -21,6 +21,11 @@
<PrivateAssets>All</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' != 'net46' ">
<PackageReference Include="System.IO.FileSystem.Primitives">
<Version>4.0.1</Version>
</PackageReference>
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net46' ">
<Reference Include="System" />
<Reference Include="Microsoft.CSharp" />

View file

@ -220,8 +220,11 @@ namespace Microsoft.DotNet.TestFramework
if (exitCode != 0)
{
Console.WriteLine(commandResult.StdOut);
Console.WriteLine(commandResult.StdErr);
string message = string.Format($"TestAsset Build '{_assetName}' Failed with {exitCode}");
throw new Exception(message);
}
}

View file

@ -0,0 +1,29 @@
// 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.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.DotNet.Tools.Test.Utilities
{
public static partial class FileInfoExtensions
{
private class FileInfoLock : IDisposable
{
private FileStream _fileStream;
public FileInfoLock(FileInfo fileInfo)
{
_fileStream = fileInfo.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.None);
}
public void Dispose()
{
_fileStream.Dispose();
}
}
}
}

View file

@ -0,0 +1,57 @@
// 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.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;
namespace Microsoft.DotNet.Tools.Test.Utilities
{
public static partial class FileInfoExtensions
{
private class FileInfoNuGetLock : IDisposable
{
private FileStream _fileStream;
private CancellationTokenSource _cancellationTokenSource;
private Task _task;
public FileInfoNuGetLock(FileInfo fileInfo)
{
_cancellationTokenSource = new CancellationTokenSource();
_task = ConcurrencyUtilities.ExecuteWithFileLockedAsync<int>(
fileInfo.FullName,
lockedToken =>
{
Task.Delay(60000, _cancellationTokenSource.Token).Wait();
return Task.FromResult(0);
},
_cancellationTokenSource.Token);
}
public void Dispose()
{
_cancellationTokenSource.Cancel();
try
{
_task.Wait();
}
catch (AggregateException)
{
}
finally
{
_cancellationTokenSource.Dispose();
}
}
}
}
}

View file

@ -9,16 +9,21 @@ using System.Threading.Tasks;
namespace Microsoft.DotNet.Tools.Test.Utilities
{
public static class FileInfoExtensions
public static partial class FileInfoExtensions
{
public static FileInfoAssertions Should(this FileInfo file)
{
return new FileInfoAssertions(file);
}
public static FileInfo Sub(this FileInfo file, string name)
public static IDisposable Lock(this FileInfo subject)
{
return new FileInfo(Path.Combine(file.FullName, name));
return new FileInfoLock(subject);
}
public static IDisposable NuGetLock(this FileInfo subject)
{
return new FileInfoNuGetLock(subject);
}
}
}

View file

@ -0,0 +1,83 @@
// 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.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.DotNet.Tools.Test.Utilities
{
public static class IDisposableExtensions
{
public static IDisposable DisposeAfter(this IDisposable subject, TimeSpan timeout)
{
return new IDisposableWithTimeout(subject, timeout);
}
private class IDisposableWithTimeout :IDisposable
{
private CancellationTokenSource _cancellationTokenSource;
private Task _timeoutTask;
private bool _isDisposed;
private IDisposable _subject;
public IDisposableWithTimeout(IDisposable subject, TimeSpan timeout)
{
_subject = subject;
_cancellationTokenSource = new CancellationTokenSource();
_timeoutTask = Task.Run(() =>
{
Task.Delay(timeout, _cancellationTokenSource.Token).Wait();
DisposeInternal();
},
_cancellationTokenSource.Token);
}
public void Dispose()
{
DisposeInternal();
CancelTimeout();
}
private void DisposeInternal()
{
lock(this)
{
if (!_isDisposed)
{
_subject.Dispose();
_isDisposed = true;
}
}
}
private void CancelTimeout()
{
_cancellationTokenSource.Cancel();
try
{
_timeoutTask.Wait();
}
catch (AggregateException)
{
}
finally
{
_cancellationTokenSource.Dispose();
}
}
}
}
}

View file

@ -221,6 +221,50 @@ namespace Microsoft.DotNet.Tests
result.Should().Fail();
}
[Fact]
public void When_assets_file_is_in_use_Then_CLI_retries_launching_the_command_for_at_least_one_second()
{
var testInstance = TestAssets.Get("AppWithToolDependency")
.CreateInstance()
.WithSourceFiles()
.WithRestoreFiles();
var assetsFile = testInstance.Root.GetDirectory("obj").GetFile("project.assets.json");
using (assetsFile.Lock()
.DisposeAfter(TimeSpan.FromMilliseconds(1000)))
{
new PortableCommand()
.WithWorkingDirectory(testInstance.Root)
.ExecuteWithCapturedOutput()
.Should().HaveStdOutContaining("Hello Portable World!" + Environment.NewLine)
.And.NotHaveStdErr()
.And.Pass();
}
}
[Fact]
public void When_assets_file_is_locked_by_NuGet_Then_CLI_retries_launching_the_command_for_at_least_one_second()
{
var testInstance = TestAssets.Get("AppWithToolDependency")
.CreateInstance()
.WithSourceFiles()
.WithRestoreFiles();
var assetsFile = testInstance.Root.GetDirectory("obj").GetFile("project.assets.json");
using (assetsFile.NuGetLock()
.DisposeAfter(TimeSpan.FromMilliseconds(1000)))
{
new PortableCommand()
.WithWorkingDirectory(testInstance.Root)
.ExecuteWithCapturedOutput()
.Should().HaveStdOutContaining("Hello Portable World!" + Environment.NewLine)
.And.NotHaveStdErr()
.And.Pass();
}
}
class HelloCommand : TestCommand
{
public HelloCommand()