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:
parent
a700604b93
commit
b918b2a6b6
10 changed files with 316 additions and 4 deletions
|
@ -124,7 +124,9 @@ namespace Microsoft.DotNet.Cli.Utils
|
||||||
var lockFilePath = GetLockFilePathFromProjectLockFileProperty() ??
|
var lockFilePath = GetLockFilePathFromProjectLockFileProperty() ??
|
||||||
GetLockFilePathFromIntermediateBaseOutputPath();
|
GetLockFilePathFromIntermediateBaseOutputPath();
|
||||||
|
|
||||||
return new LockFileFormat().Read(lockFilePath);
|
return new LockFileFormat()
|
||||||
|
.ReadWithLock(lockFilePath)
|
||||||
|
.Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetLockFilePathFromProjectLockFileProperty()
|
private string GetLockFilePathFromProjectLockFileProperty()
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
src/Microsoft.DotNet.Cli.Utils/FileAccessRetryer.cs
Normal file
51
src/Microsoft.DotNet.Cli.Utils/FileAccessRetryer.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,11 @@
|
||||||
<PrivateAssets>All</PrivateAssets>
|
<PrivateAssets>All</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup Condition=" '$(TargetFramework)' != 'net46' ">
|
||||||
|
<PackageReference Include="System.IO.FileSystem.Primitives">
|
||||||
|
<Version>4.0.1</Version>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net46' ">
|
<ItemGroup Condition=" '$(TargetFramework)' == 'net46' ">
|
||||||
<Reference Include="System" />
|
<Reference Include="System" />
|
||||||
<Reference Include="Microsoft.CSharp" />
|
<Reference Include="Microsoft.CSharp" />
|
||||||
|
|
|
@ -220,8 +220,11 @@ namespace Microsoft.DotNet.TestFramework
|
||||||
if (exitCode != 0)
|
if (exitCode != 0)
|
||||||
{
|
{
|
||||||
Console.WriteLine(commandResult.StdOut);
|
Console.WriteLine(commandResult.StdOut);
|
||||||
|
|
||||||
Console.WriteLine(commandResult.StdErr);
|
Console.WriteLine(commandResult.StdErr);
|
||||||
|
|
||||||
string message = string.Format($"TestAsset Build '{_assetName}' Failed with {exitCode}");
|
string message = string.Format($"TestAsset Build '{_assetName}' Failed with {exitCode}");
|
||||||
|
|
||||||
throw new Exception(message);
|
throw new Exception(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,16 +9,21 @@ using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Tools.Test.Utilities
|
namespace Microsoft.DotNet.Tools.Test.Utilities
|
||||||
{
|
{
|
||||||
public static class FileInfoExtensions
|
public static partial class FileInfoExtensions
|
||||||
{
|
{
|
||||||
public static FileInfoAssertions Should(this FileInfo file)
|
public static FileInfoAssertions Should(this FileInfo file)
|
||||||
{
|
{
|
||||||
return new FileInfoAssertions(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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -221,6 +221,50 @@ namespace Microsoft.DotNet.Tests
|
||||||
result.Should().Fail();
|
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
|
class HelloCommand : TestCommand
|
||||||
{
|
{
|
||||||
public HelloCommand()
|
public HelloCommand()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue