diff --git a/src/GitVersionCore.Tests/Helpers/TestFileSystem.cs b/src/GitVersionCore.Tests/Helpers/TestFileSystem.cs index ddfac75184..50373495a5 100644 --- a/src/GitVersionCore.Tests/Helpers/TestFileSystem.cs +++ b/src/GitVersionCore.Tests/Helpers/TestFileSystem.cs @@ -78,6 +78,11 @@ public IEnumerable DirectoryEnumerateFiles(string directory, string sear throw new NotImplementedException(); } + public FileStream Open(string path, FileMode mode, FileAccess access, FileShare share) + { + return new FileStream(path, mode, access, share); + } + public Stream OpenWrite(string path) { return new TestStream(path, this); diff --git a/src/GitVersionCore/Core/Abstractions/IFileSystem.cs b/src/GitVersionCore/Core/Abstractions/IFileSystem.cs index bfd32bcbd5..e9357ee027 100644 --- a/src/GitVersionCore/Core/Abstractions/IFileSystem.cs +++ b/src/GitVersionCore/Core/Abstractions/IFileSystem.cs @@ -14,6 +14,7 @@ public interface IFileSystem void WriteAllText(string file, string fileContents); void WriteAllText(string file, string fileContents, Encoding encoding); IEnumerable DirectoryEnumerateFiles(string directory, string searchPattern, SearchOption searchOption); + FileStream Open(string path, FileMode mode, FileAccess access, FileShare share); Stream OpenWrite(string path); Stream OpenRead(string path); void CreateDirectory(string path); diff --git a/src/GitVersionCore/Core/FileSystem.cs b/src/GitVersionCore/Core/FileSystem.cs index cb64fee773..f1b0de945e 100644 --- a/src/GitVersionCore/Core/FileSystem.cs +++ b/src/GitVersionCore/Core/FileSystem.cs @@ -51,6 +51,11 @@ public IEnumerable DirectoryEnumerateFiles(string directory, string sear return Directory.EnumerateFiles(directory, searchPattern, searchOption); } + public FileStream Open(string path, FileMode mode, FileAccess access, FileShare share) + { + return File.Open(path, mode, access, share); + } + public Stream OpenWrite(string path) { return File.OpenWrite(path); diff --git a/src/GitVersionCore/FileLocking/Abstractions/IFileLock.cs b/src/GitVersionCore/FileLocking/Abstractions/IFileLock.cs new file mode 100644 index 0000000000..738e6ee9d8 --- /dev/null +++ b/src/GitVersionCore/FileLocking/Abstractions/IFileLock.cs @@ -0,0 +1,7 @@ +using System; + +namespace GitVersion.FileLocking +{ + public interface IFileLock : IDisposable + { } +} diff --git a/src/GitVersionCore/FileLocking/Abstractions/IFileLocker.cs b/src/GitVersionCore/FileLocking/Abstractions/IFileLocker.cs new file mode 100644 index 0000000000..496db05c19 --- /dev/null +++ b/src/GitVersionCore/FileLocking/Abstractions/IFileLocker.cs @@ -0,0 +1,7 @@ +namespace GitVersion.FileLocking +{ + public interface IFileLocker + { + FileLockUse WaitUntilAcquired(); + } +} diff --git a/src/GitVersionCore/FileLocking/Abstractions/ILockFileApi.cs b/src/GitVersionCore/FileLocking/Abstractions/ILockFileApi.cs new file mode 100644 index 0000000000..99beb92881 --- /dev/null +++ b/src/GitVersionCore/FileLocking/Abstractions/ILockFileApi.cs @@ -0,0 +1,9 @@ +using System.IO; + +namespace GitVersion.FileLocking +{ + public interface ILockFileApi + { + FileStream WaitUntilAcquired(string filePath, int timeoutInMilliseconds, FileMode fileMode = FileMode.OpenOrCreate, FileAccess fileAccess = FileAccess.ReadWrite, FileShare fileShare = FileShare.None, bool noThrowOnTimeout = false); + } +} diff --git a/src/GitVersionCore/FileLocking/FileLockContext.cs b/src/GitVersionCore/FileLocking/FileLockContext.cs new file mode 100644 index 0000000000..f15ada0d83 --- /dev/null +++ b/src/GitVersionCore/FileLocking/FileLockContext.cs @@ -0,0 +1,77 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; + +namespace GitVersion.FileLocking +{ + +#nullable enable + + internal class FileLockContext + { + public FileStream? FileStream { get; } + public Exception? Error { get; } + public ManualResetEvent? ErrorUnlockDone { get; } + + private readonly FileLocker fileLocker; + private object? decreaseLockUseLocker; + + private FileLockContext(FileLocker fileLocker, object decreaseLockUseLocker) + { + this.fileLocker = fileLocker ?? throw new ArgumentNullException(nameof(fileLocker)); + this.decreaseLockUseLocker = decreaseLockUseLocker; + } + + public FileLockContext(FileLocker fileLocker, object decreaseLockUseLocker, FileStream fileStream) + : this(fileLocker, decreaseLockUseLocker) + { + fileStream = fileStream ?? throw new ArgumentNullException(nameof(fileStream)); + FileStream = fileStream; + } + + public FileLockContext(FileLocker fileLocker, object decreaseLockUseLocker, Exception error, ManualResetEvent errorUnlockDone) + : this(fileLocker, decreaseLockUseLocker) + { + Error = error ?? throw new ArgumentNullException(nameof(error)); + ErrorUnlockDone = errorUnlockDone ?? throw new ArgumentNullException(nameof(errorUnlockDone)); + } + + public void DecreaseLockUse(bool decreaseToZero, string lockId) + { + if (FileStream == null) + { + throw new InvalidOperationException("You cannot decrease lock use when no file stream has been assgined."); + } + + var decreaseLockUseLocker = this.decreaseLockUseLocker; + + if (decreaseLockUseLocker == null) + return; + + // Why surround by lock? + // There is a race condition, when number of file lock uses + // is decrased to 0. It may not have invalidated the file + // stream yet. Now it can happen that the number of file lock + // uses is increased to 1 due to file lock, but right after another + // file unlock is about to decrease the number again to 0. + // There is the possiblity that the actual file lock gets released + // two times accidentally. + lock (decreaseLockUseLocker) + { + if (!(FileStream.CanRead || FileStream.CanWrite)) + { + Trace.WriteLine($"{FileLocker.CurrentThreadWithLockIdPrefix(lockId)} Lock use has been invalidated before. Skip decreasing lock use.", FileLocker.TraceCategory); + return; + } + + var locksInUse = fileLocker.DecreaseLockUse(decreaseToZero, lockId); + + if (0 == locksInUse) + { + this.decreaseLockUseLocker = null; + } + } + } + } +} diff --git a/src/GitVersionCore/FileLocking/FileLockContextExtensions.cs b/src/GitVersionCore/FileLocking/FileLockContextExtensions.cs new file mode 100644 index 0000000000..e2c45bba20 --- /dev/null +++ b/src/GitVersionCore/FileLocking/FileLockContextExtensions.cs @@ -0,0 +1,16 @@ +namespace GitVersion.FileLocking +{ + +#nullable enable + + internal static class FileLockContextExtensions + { + public static bool IsErroneous(this FileLockContext? fileLockContext) + { + if (fileLockContext?.Error is null) + return false; + + return true; + } + } +} diff --git a/src/GitVersionCore/FileLocking/FileLockUse.cs b/src/GitVersionCore/FileLocking/FileLockUse.cs new file mode 100644 index 0000000000..7005311e93 --- /dev/null +++ b/src/GitVersionCore/FileLocking/FileLockUse.cs @@ -0,0 +1,36 @@ +using System; +using System.ComponentModel; +using System.IO; + +namespace GitVersion.FileLocking +{ + +#nullable enable + + public struct FileLockUse : IDisposable + { + public FileStream FileStream => fileLockContext.FileStream!; + + private readonly FileLockContext fileLockContext; + [EditorBrowsable(EditorBrowsableState.Never)] + public readonly string LockId; + + internal FileLockUse(FileLockContext fileLockContext, string LockId) + { + this.fileLockContext = fileLockContext ?? throw new ArgumentNullException(nameof(fileLockContext)); + + if (fileLockContext.FileStream is null) + { + throw new ArgumentException("File stream context has invalid file stream."); + } + + this.LockId = LockId; + } + + public void Dispose() + { + // When stream not closed, we can decrease lock use. + fileLockContext.DecreaseLockUse(false, LockId); + } + } +} diff --git a/src/GitVersionCore/FileLocking/FileLocker.cs b/src/GitVersionCore/FileLocking/FileLocker.cs new file mode 100644 index 0000000000..e566de3a4c --- /dev/null +++ b/src/GitVersionCore/FileLocking/FileLocker.cs @@ -0,0 +1,284 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Threading; + +namespace GitVersion.FileLocking +{ + +#nullable enable + + /// + /// Provides a file locker that is thread-safe and supports nesting. + /// + public sealed class FileLocker : IFileLocker + { +#if TRACE + internal const string TraceCategory = nameof(FileLocker); + private static Random random = new Random(); + + [EditorBrowsable(EditorBrowsableState.Never)] + public static string CurrentThreadWithLockIdPrefix(string lockId) => + $"Thread {Thread.CurrentThread.Name ?? "none"}: Lock {lockId}:"; + + private static string fileStreamHasBeenLockedString(FileStream fileStream) => + "(locked=" + (fileStream != null && (fileStream.CanRead || fileStream.CanWrite)).ToString().ToLower() + ")"; + + private static string unlockSourceString(bool decreaseToZero) => + $"{(decreaseToZero ? "(manual unlock)" : "(dispose unlock)")}"; +#endif + + private static string getLockId() + { +#if TRACE + return random.Next(0, 999).ToString().PadLeft(3, '0'); +#else + return "none"; +#endif + } + + public string FilePath { get; } + + public FileStream? FileStream => + fileLockerState?.FileStream; + + /// + /// If true, the lock attempts are going to throw the exception which occured in the lock before. + /// This happens to all locks until the manual unlock within the lock in which the excpetion initially + /// begun has been processed. + /// + public bool EnableConcurrentRethrow { get; set; } + + public int LocksInUse => locksInUse; + + public FileMode FileMode { get; } + public FileAccess FileAccess { get; } + public FileShare FileShare { get; } + public int TimeoutInMilliseconds { get; } + + /// + /// Zero represents the number where no lock is in place. + /// + private int locksInUse = 0; + private FileLockContext? fileLockerState; + private object decreaseLockUseLocker; + private readonly ILockFileApi lockFileApi; + + public FileLocker(ILockFileApi lockFileApi, string filePath, FileMode fileMode = LockFileApi.DefaultFileMode, FileAccess fileAccess = LockFileApi.DefaultFileAccess, + FileShare fileShare = LockFileApi.DefaultFileShare) + { + decreaseLockUseLocker = new object(); + this.lockFileApi = lockFileApi ?? throw new ArgumentNullException(nameof(lockFileApi)); + FilePath = filePath; + FileMode = fileMode; + FileAccess = fileAccess; + FileShare = fileShare; + TimeoutInMilliseconds = LockFileApi.DefaultTimeoutInMilliseconds; + } + + public FileLocker(ILockFileApi lockFileApi, string filePath, int timeoutInMilliseconds, FileMode fileMode = LockFileApi.DefaultFileMode, FileAccess fileAccess = LockFileApi.DefaultFileAccess, + FileShare fileShare = LockFileApi.DefaultFileShare) + : this(lockFileApi, filePath, fileMode, fileAccess, fileShare) + { + TimeoutInMilliseconds = timeoutInMilliseconds; + } + + public FileLocker(ILockFileApi lockFileApi, string filePath, TimeSpan timeout, FileMode fileMode = LockFileApi.DefaultFileMode, FileAccess fileAccess = LockFileApi.DefaultFileAccess, + FileShare fileShare = LockFileApi.DefaultFileShare) + : this(lockFileApi, filePath, fileMode, fileAccess, fileShare) + { + TimeoutInMilliseconds = Convert.ToInt32(timeout.TotalMilliseconds); + } + + /// + /// Locks the file specified at location . + /// + /// The file lock use that can be revoked by disposing it. + public FileLockUse WaitUntilAcquired() + { + var lockId = getLockId(); + Trace.WriteLine($"{CurrentThreadWithLockIdPrefix(lockId)} Begin locking file {FilePath}.", TraceCategory); + SpinWait spinWait = new SpinWait(); + + while (true) + { + var currentLocksInUse = locksInUse; + var desiredLocksInUse = currentLocksInUse + 1; + var currentFileLockerState = fileLockerState; + + if (currentFileLockerState.IsErroneous()) + { + if (EnableConcurrentRethrow) + { + Trace.WriteLine($"{CurrentThreadWithLockIdPrefix(lockId)} Error from previous lock will be rethrown.", TraceCategory); + throw currentFileLockerState!.Error!; + } + + // Imagine stair steps where each stair step is Lock(): + // Thread #0 Lock #0 -> Incremented to 1 -> Exception occured. + // Thread #1 Lock #1 -> Incremented to 2. Recognozes exception in #0 because #0 not yet entered Unlock(). + // Thread #2 Lock #2 -> Incremented to 3. Recognizes excetion in #1 because #0 not yet entered Unlock(). + // Thread #3 Lock #3 -> Incremented to 1. Lock was successful. + // We want Lock #1 and Lock #2 to retry their Lock(): + // Thread #1 Lock #1 -> Incremented to 2. Lock was successful. + // Thread #2 Lock #2 -> Incremented to 3. Lock was successful. + currentFileLockerState!.ErrorUnlockDone!.WaitOne(); + Trace.WriteLine($"{CurrentThreadWithLockIdPrefix(lockId)} Retry lock due to previously failed lock.", TraceCategory); + continue; + } + // If it is the initial lock, then we expect file stream being null. + // If it is not the initial lock, we expect the stream being not null. + else if ((currentLocksInUse == 0 && currentFileLockerState != null) || + (currentLocksInUse != 0 && currentFileLockerState == null)) + { + spinWait.SpinOnce(); + continue; + } + else + { + if (currentLocksInUse != Interlocked.CompareExchange(ref locksInUse, desiredLocksInUse, currentLocksInUse)) + { + continue; + } + + // The above conditions met, so if it is the initial lock, then we want + // to acquire the lock. + if (desiredLocksInUse == 1) + { + try + { + var fileStream = lockFileApi.WaitUntilAcquired(FilePath, TimeoutInMilliseconds, fileMode: FileMode, + fileAccess: FileAccess, fileShare: FileShare)!; + + currentFileLockerState = new FileLockContext(this, decreaseLockUseLocker, fileStream); + + fileLockerState = currentFileLockerState; + Trace.WriteLine($"{CurrentThreadWithLockIdPrefix(lockId)} File {FilePath} locked by file locker.", TraceCategory); + } + catch (Exception error) + { + var errorUnlockDone = new ManualResetEvent(false); + currentFileLockerState = new FileLockContext(this, decreaseLockUseLocker, error, errorUnlockDone); + fileLockerState = currentFileLockerState; + Unlock(lockId); + // After we processed Unlock(), we can surpass these locks + // who could be dependent on state assigment of this Lock(). + currentFileLockerState.ErrorUnlockDone!.Set(); + throw; + } + } + else + { + Trace.WriteLine($"{CurrentThreadWithLockIdPrefix(lockId)} File {FilePath} locked {desiredLocksInUse} time(s) concurrently by file locker. {fileStreamHasBeenLockedString(currentFileLockerState!.FileStream!)}", TraceCategory); + } + } + + var fileLockContract = new FileLockUse(currentFileLockerState, lockId); + return fileLockContract; + } + } + + /// + /// Decreases the number of locks in use. If becoming zero, file gets unlocked. + /// + internal int DecreaseLockUse(bool decreaseToZero, string? lockId) + { + lockId = lockId ?? "none"; + SpinWait spinWait = new SpinWait(); + int desiredLocksInUse; + + do + { + var currentLocksInUse = locksInUse; + + if (0 >= currentLocksInUse) + { + Trace.WriteLine($"{CurrentThreadWithLockIdPrefix(lockId)} Number of lock remains at 0 because file has been unlocked before. {unlockSourceString(decreaseToZero)}", TraceCategory); + return 0; + } + + if (decreaseToZero) + { + desiredLocksInUse = 0; + } + else + { + desiredLocksInUse = currentLocksInUse - 1; + } + + var actualLocksInUse = Interlocked.CompareExchange(ref locksInUse, desiredLocksInUse, currentLocksInUse); + + if (currentLocksInUse == actualLocksInUse) + { + break; + } + + spinWait.SpinOnce(); + } while (true); + + string decreasedNumberOfLocksInUseMessage() => + $"{CurrentThreadWithLockIdPrefix(lockId)} Number of lock uses is decreased to {desiredLocksInUse}. {unlockSourceString(decreaseToZero)}"; + + // When no locks are registered, we have to .. + if (0 == desiredLocksInUse) + { + // 1. wait for file stream assignment, + FileLockContext? nullState = null; + FileLockContext nonNullState = null!; + + while (true) + { + nullState = Interlocked.CompareExchange(ref fileLockerState, null, nullState); + + /* When class scoped file stream is null local file stream will be null too. + * => If so, spin once and continue loop. + * + * When class scoped file stream is not null the local file stream will become + * not null too. + * => If so, assigned class scoped file streama to to local non null file stream + * and continue loop. + * + * When class scoped file stream is null and local non null file stream is not null + * => If so, break loop. + */ + if (nullState == null && nonNullState is null) + { + spinWait.SpinOnce(); + } + else if (nullState == null && !(nonNullState is null)) + { + break; + } + else + { + nonNullState = nullState!; + } + } + + // 2. invalidate the file stream. + nonNullState.FileStream?.Close(); + nonNullState.FileStream?.Dispose(); + Trace.WriteLine($"{decreasedNumberOfLocksInUseMessage()}{System.Environment.NewLine}{CurrentThreadWithLockIdPrefix(lockId)} File {FilePath} unlocked by file locker. {unlockSourceString(decreaseToZero)}", TraceCategory); + } + else + { + Trace.WriteLine($"{decreasedNumberOfLocksInUseMessage()}"); + } + + return desiredLocksInUse; + } + + /// + /// Unlocks the file specified at location . + /// + /// The lock id is for tracing purposes. + internal void Unlock(string lockId) + { + lock (decreaseLockUseLocker) + { + DecreaseLockUse(true, lockId); + } + } + } +} diff --git a/src/GitVersionCore/FileLocking/FileLockerDefaults.cs b/src/GitVersionCore/FileLocking/FileLockerDefaults.cs new file mode 100644 index 0000000000..1cc014a25a --- /dev/null +++ b/src/GitVersionCore/FileLocking/FileLockerDefaults.cs @@ -0,0 +1,11 @@ + + +namespace GitVersion.FileLocking +{ + public static class FileLockerDefaults + { + // Represents 90 seconds. + public const int LockTimeoutInMilliseconds = 1000 * 90; + public const string LockFileNameWithExtensions = "GitVersion.lock"; + } +} diff --git a/src/GitVersionCore/FileLocking/LockFileApi.cs b/src/GitVersionCore/FileLocking/LockFileApi.cs new file mode 100644 index 0000000000..44e1319957 --- /dev/null +++ b/src/GitVersionCore/FileLocking/LockFileApi.cs @@ -0,0 +1,205 @@ +using System; +using System.IO; +using System.Threading; + +namespace GitVersion.FileLocking +{ + +#nullable enable + + /// + /// This helper class can lock files. + /// + public class LockFileApi : ILockFileApi + { + public const FileMode DefaultFileMode = FileMode.OpenOrCreate; + public const FileAccess DefaultFileAccess = FileAccess.ReadWrite; + public const FileShare DefaultFileShare = FileShare.None; + public const int DefaultTimeoutInMilliseconds = Timeout.Infinite; + + private readonly IFileSystem fileSystem; + + public LockFileApi(IFileSystem fileSystem) + { + this.fileSystem = fileSystem; + } + + /// + /// Try to acquire lock on file but only as long the file stream is opened. + /// + /// The path to file that get locked. + /// The locked file as file stream. + /// The file mode when opening file. + /// The file access when opening file. + /// The file share when opening file + /// If true the lock acquirement was successful. + public bool TryAcquire(string filePath, out FileStream? fileStream, FileMode fileMode = DefaultFileMode, + FileAccess fileAccess = DefaultFileAccess, FileShare fileShare = DefaultFileShare) + { + filePath = filePath ?? throw new ArgumentNullException(nameof(filePath)); + + try + { + fileStream = fileSystem.Open(filePath, fileMode, fileAccess, fileShare); + // Add UNIX support (reference https://github.com/dotnet/coreclr/pull/8233). + fileStream.Lock(0, 0); + return true; + } + // The IOException does specify that the file could not been accessed because + // it was partially locked. All other exception have to be handled by consumer. + // + // See references: + // https://docs.microsoft.com/en-US/dotnet/api/system.io.file.open?view=netcore-3.1 (exceptions) + // https://docs.microsoft.com/en-US/dotnet/api/system.io.filestream.lock?view=netcore-3.1#exceptions + catch (Exception error) when (error.GetType() == typeof(IOException)) + { + fileStream = null; + return false; + } + } + + /// + /// Try to acquire lock on file but only as long the file stream is opened. + /// + /// The path to file that get locked. + /// The file mode when opening file. + /// The file access when opening file. + /// The file share when opening file + /// If not null the lock acquirement was successful. + public FileStream? TryAcquire(string filePath, FileMode fileMode = DefaultFileMode, + FileAccess fileAccess = DefaultFileAccess, FileShare fileShare = DefaultFileShare) + { + TryAcquire(filePath, out var fileStream, fileMode: fileMode, + fileAccess: fileAccess, fileShare: fileShare); + + return fileStream; + } + + private bool waitUntilAcquired(string filePath, out FileStream? fileStream, FileMode fileMode, + FileAccess fileAccess, FileShare fileShare, int timeoutInMilliseconds, bool throwOnTimeout) + { + FileStream? spinningFileStream = null; + + var spinHasBeenFinished = SpinWait.SpinUntil(() => + TryAcquire(filePath, out spinningFileStream, fileMode: fileMode, fileAccess: fileAccess, fileShare: fileShare), timeoutInMilliseconds); + + if (spinHasBeenFinished) + { + fileStream = spinningFileStream ?? throw new ArgumentNullException(nameof(spinningFileStream)); + return true; + } + + if (throwOnTimeout) + { + throw new TimeoutException($"Acquiring file lock failed due to timeout."); + } + + fileStream = null; + return false; + } + + private FileStream? waitUntilAcquired(string filePath, FileMode fileMode, + FileAccess fileAccess, FileShare fileShare, int timeoutInMilliseconds, bool noThrowOnTimeout) + { + waitUntilAcquired(filePath, out var fileStream, fileMode, fileAccess, fileShare, timeoutInMilliseconds, !noThrowOnTimeout); + return fileStream; + } + + /// + /// Wait until file gets acquired lock but only as long the file stream is opened. + /// + /// The path to file that get locked. + /// The locked file as file stream. + /// The file mode when opening file. + /// The file access when opening file. + /// The file share when opening file + /// Enable throw when exception occured due due to timeout. + /// If true the lock acquirement was successful. + public bool WaitUntilAcquired(string filePath, out FileStream? fileStream, FileMode fileMode = DefaultFileMode, + FileAccess fileAccess = DefaultFileAccess, FileShare fileShare = DefaultFileShare, bool throwOnTimeout = false) + { + var timeoutInMilliseconds = DefaultTimeoutInMilliseconds; + return waitUntilAcquired(filePath, out fileStream, fileMode, fileAccess, fileShare, timeoutInMilliseconds, throwOnTimeout); + } + + /// + /// Wait until file gets acquired lock but only as long the file stream is opened. + /// + /// The path to file that get locked. + /// The file mode when opening file. + /// The file access when opening file. + /// The file share when opening file + /// Disable throw when exception occured due due to timeout. + /// If not null the lock acquirement was successful. + public FileStream? WaitUntilAcquired(string filePath, FileMode fileMode = DefaultFileMode, + FileAccess fileAccess = DefaultFileAccess, FileShare fileShare = DefaultFileShare, bool noThrowOnTimeout = false) + { + var timeoutInMilliseconds = DefaultTimeoutInMilliseconds; + return waitUntilAcquired(filePath, fileMode, fileAccess, fileShare, timeoutInMilliseconds, noThrowOnTimeout); + } + + /// + /// Wait until file gets acquired lock but only as long the file stream is opened. + /// + /// The path to file that get locked. + /// The timeout in milliseconds. + /// The locked file as file stream. + /// The file mode when opening file. + /// The file access when opening file. + /// The file share when opening file + /// Enable throw when exception occured due due to timeout. + /// If true the lock acquirement was successful. + public bool WaitUntilAcquired(string filePath, int timeoutInMilliseconds, out FileStream? fileStream, FileMode fileMode = DefaultFileMode, + FileAccess fileAccess = DefaultFileAccess, FileShare fileShare = DefaultFileShare, bool throwOnTimeout = false) => + waitUntilAcquired(filePath, out fileStream, fileMode, fileAccess, fileShare, timeoutInMilliseconds, throwOnTimeout); + + /// + /// Wait until file gets acquired lock but only as long the file stream is opened. + /// + /// The path to file that get locked. + /// The timeout in milliseconds. + /// The file mode when opening file. + /// The file access when opening file. + /// The file share when opening file + /// Disable throw when exception occured due due to timeout. + /// If not null the lock acquirement was successful. + public FileStream? WaitUntilAcquired(string filePath, int timeoutInMilliseconds, FileMode fileMode = DefaultFileMode, + FileAccess fileAccess = DefaultFileAccess, FileShare fileShare = DefaultFileShare, bool noThrowOnTimeout = false) => + waitUntilAcquired(filePath, fileMode, fileAccess, fileShare, timeoutInMilliseconds, noThrowOnTimeout); + + /// + /// Wait until file gets acquired lock but only as long the file stream is opened. + /// + /// The path to file that get locked. + /// The timeout specified as . + /// The locked file as file stream. + /// The file mode when opening file. + /// The file access when opening file. + /// The file share when opening file + /// Enable throw when exception occured due due to timeout. + /// If true the lock acquirement was successful. + public bool WaitUntilAcquired(string filePath, TimeSpan timeout, out FileStream? fileStream, FileMode fileMode = DefaultFileMode, + FileAccess fileAccess = DefaultFileAccess, FileShare fileShare = DefaultFileShare, bool throwOnTimeout = false) + { + var timeoutInMilliseconds = Convert.ToInt32(timeout.TotalMilliseconds); + return waitUntilAcquired(filePath, out fileStream, fileMode, fileAccess, fileShare, timeoutInMilliseconds, throwOnTimeout); + } + + /// + /// Wait until file gets acquired lock but only as long the file stream is opened. + /// + /// The path to file that get locked. + /// The timeout specified as . + /// The file mode when opening file. + /// The file access when opening file. + /// The file share when opening file + /// Disable throw when exception occured due due to timeout. + /// If ont null lock acquirement was successful. + public FileStream? WaitUntilAcquired(string filePath, TimeSpan timeout, FileMode fileMode = DefaultFileMode, + FileAccess fileAccess = DefaultFileAccess, FileShare fileShare = DefaultFileShare, bool noThrowOnTimeout = false) + { + var timeoutInMilliseconds = Convert.ToInt32(timeout.TotalMilliseconds); + return waitUntilAcquired(filePath, fileMode, fileAccess, fileShare, timeoutInMilliseconds, noThrowOnTimeout); + } + } +} diff --git a/src/GitVersionCore/GitVersionCoreModule.cs b/src/GitVersionCore/GitVersionCoreModule.cs index 93279c23b2..d97e3cc7f8 100644 --- a/src/GitVersionCore/GitVersionCoreModule.cs +++ b/src/GitVersionCore/GitVersionCoreModule.cs @@ -1,9 +1,11 @@ using System; +using System.IO; using GitVersion.BuildAgents; using GitVersion.Common; using GitVersion.Configuration; using GitVersion.Configuration.Init; using GitVersion.Extensions; +using GitVersion.FileLocking; using GitVersion.Logging; using GitVersion.VersionCalculation; using GitVersion.VersionCalculation.Cache; @@ -29,6 +31,24 @@ public void RegisterTypes(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(serviceProvider => + { + var lockFileApi = serviceProvider.GetRequiredService(); + var gitVersionCache = serviceProvider.GetRequiredService(); + var cacheDirectory = gitVersionCache.GetCacheDirectory(); + var lockFilePath = Path.Combine(cacheDirectory, FileLockerDefaults.LockFileNameWithExtensions); + return new FileLocker(lockFileApi, lockFilePath); + }); + + services.AddSingleton(serviceProvider => + { + var fileLocker = serviceProvider.GetRequiredService(); + var fileLockUse = fileLocker.WaitUntilAcquired(); + return new FileLock(fileLockUse); + }); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/GitVersionCore/Model/FileLock.cs b/src/GitVersionCore/Model/FileLock.cs new file mode 100644 index 0000000000..443e0e1340 --- /dev/null +++ b/src/GitVersionCore/Model/FileLock.cs @@ -0,0 +1,23 @@ +using GitVersion.FileLocking; +using System; + +namespace GitVersion +{ + public class FileLock : IFileLock + { + public FileLockUse FileLockUse { get; } + + public FileLock(FileLockUse fileLockUse) + { + if (fileLockUse.Equals(default(FileLockUse))) + { + throw new ArgumentNullException(nameof(fileLockUse)); + } + + FileLockUse = fileLockUse; + } + + public void Dispose() => + FileLockUse.Dispose(); + } +} diff --git a/src/GitVersionTask/GitVersionTasks.cs b/src/GitVersionTask/GitVersionTasks.cs index 6c998181da..6f30dc9303 100644 --- a/src/GitVersionTask/GitVersionTasks.cs +++ b/src/GitVersionTask/GitVersionTasks.cs @@ -24,7 +24,7 @@ private static bool ExecuteGitVersionTask(T task, Action(); action(gitVersionTaskExecutor); @@ -58,7 +58,7 @@ private static void Configure(IServiceProvider sp, GitVersionTaskBase task) gitVersionOptions.Settings.NoFetch = gitVersionOptions.Settings.NoFetch || buildAgent != null && buildAgent.PreventFetch(); } - private static IServiceProvider BuildServiceProvider(GitVersionTaskBase task) + private static ServiceProvider BuildServiceProvider(GitVersionTaskBase task) { var services = new ServiceCollection(); @@ -83,7 +83,6 @@ private static IServiceProvider BuildServiceProvider(GitVersionTaskBase task) var sp = services.BuildServiceProvider(); Configure(sp, task); - return sp; } }