diff --git a/src/Orchard.Tests/FileSystems/LockFile/LockFileManagerTests.cs b/src/Orchard.Tests/FileSystems/LockFile/LockFileManagerTests.cs new file mode 100644 index 000000000..e1493bdd4 --- /dev/null +++ b/src/Orchard.Tests/FileSystems/LockFile/LockFileManagerTests.cs @@ -0,0 +1,116 @@ +using System.IO; +using System.Linq; +using NUnit.Framework; +using Orchard.FileSystems.AppData; +using Orchard.FileSystems.LockFile; +using Orchard.Tests.Stubs; + +namespace Orchard.Tests.FileSystems.LockFile { + [TestFixture] + public class LockFileManagerTests { + private string _tempFolder; + private IAppDataFolder _appDataFolder; + private ILockFileManager _lockFileManager; + private StubClock _clock; + + public class StubAppDataFolderRoot : IAppDataFolderRoot { + public string RootPath { get; set; } + public string RootFolder { get; set; } + } + + public static IAppDataFolder CreateAppDataFolder(string tempFolder) { + var folderRoot = new StubAppDataFolderRoot {RootPath = "~/App_Data", RootFolder = tempFolder}; + var monitor = new StubVirtualPathMonitor(); + return new AppDataFolder(folderRoot, monitor); + } + + [SetUp] + public void Init() { + _tempFolder = Path.GetTempFileName(); + File.Delete(_tempFolder); + _appDataFolder = CreateAppDataFolder(_tempFolder); + + _clock = new StubClock(); + _lockFileManager = new DefaultLockFileManager(_appDataFolder, _clock); + } + + [TearDown] + public void Term() { + Directory.Delete(_tempFolder, true); + } + + [Test] + public void LockShouldBeGrantedWhenDoesNotExist() { + ILockFile lockFile = null; + var granted = _lockFileManager.TryAcquireLock("foo.txt.lock", ref lockFile); + + Assert.That(granted, Is.True); + Assert.That(lockFile, Is.Not.Null); + Assert.That(_lockFileManager.IsLocked("foo.txt.lock"), Is.True); + Assert.That(_appDataFolder.ListFiles("").Count(), Is.EqualTo(1)); + } + + [Test] + public void ExistingLockFileShouldPreventGrants() { + ILockFile lockFile = null; + _lockFileManager.TryAcquireLock("foo.txt.lock", ref lockFile); + + Assert.That(_lockFileManager.TryAcquireLock("foo.txt.lock", ref lockFile), Is.False); + Assert.That(_lockFileManager.IsLocked("foo.txt.lock"), Is.True); + Assert.That(_appDataFolder.ListFiles("").Count(), Is.EqualTo(1)); + } + + [Test] + public void ReleasingALockShouldAllowGranting() { + ILockFile lockFile = null; + _lockFileManager.TryAcquireLock("foo.txt.lock", ref lockFile); + + using (lockFile) { + Assert.That(_lockFileManager.IsLocked("foo.txt.lock"), Is.True); + Assert.That(_appDataFolder.ListFiles("").Count(), Is.EqualTo(1)); + } + + Assert.That(_lockFileManager.IsLocked("foo.txt.lock"), Is.False); + Assert.That(_appDataFolder.ListFiles("").Count(), Is.EqualTo(0)); + } + + [Test] + public void ReleasingAReleasedLockShouldWork() { + ILockFile lockFile = null; + _lockFileManager.TryAcquireLock("foo.txt.lock", ref lockFile); + + Assert.That(_lockFileManager.IsLocked("foo.txt.lock"), Is.True); + Assert.That(_appDataFolder.ListFiles("").Count(), Is.EqualTo(1)); + + lockFile.Release(); + Assert.That(_lockFileManager.IsLocked("foo.txt.lock"), Is.False); + Assert.That(_appDataFolder.ListFiles("").Count(), Is.EqualTo(0)); + + lockFile.Release(); + Assert.That(_lockFileManager.IsLocked("foo.txt.lock"), Is.False); + Assert.That(_appDataFolder.ListFiles("").Count(), Is.EqualTo(0)); + } + + [Test] + public void ExpiredLockShouldBeAvailable() { + ILockFile lockFile = null; + _lockFileManager.TryAcquireLock("foo.txt.lock", ref lockFile); + + _clock.Advance(DefaultLockFileManager.Expiration); + Assert.That(_lockFileManager.IsLocked("foo.txt.lock"), Is.False); + Assert.That(_appDataFolder.ListFiles("").Count(), Is.EqualTo(1)); + } + + [Test] + public void ShouldGrantExpiredLock() { + ILockFile lockFile = null; + _lockFileManager.TryAcquireLock("foo.txt.lock", ref lockFile); + + _clock.Advance(DefaultLockFileManager.Expiration); + var granted = _lockFileManager.TryAcquireLock("foo.txt.lock", ref lockFile); + + Assert.That(granted, Is.True); + Assert.That(_appDataFolder.ListFiles("").Count(), Is.EqualTo(1)); + } + } +} diff --git a/src/Orchard.Tests/Orchard.Framework.Tests.csproj b/src/Orchard.Tests/Orchard.Framework.Tests.csproj index 7c7b052db..4064da875 100644 --- a/src/Orchard.Tests/Orchard.Framework.Tests.csproj +++ b/src/Orchard.Tests/Orchard.Framework.Tests.csproj @@ -245,6 +245,7 @@ + diff --git a/src/Orchard/Environment/OrchardStarter.cs b/src/Orchard/Environment/OrchardStarter.cs index 30988def7..5f4b1e1c9 100644 --- a/src/Orchard/Environment/OrchardStarter.cs +++ b/src/Orchard/Environment/OrchardStarter.cs @@ -18,6 +18,7 @@ using Orchard.Environment.Descriptor; using Orchard.Events; using Orchard.FileSystems.AppData; using Orchard.FileSystems.Dependencies; +using Orchard.FileSystems.LockFile; using Orchard.FileSystems.VirtualPath; using Orchard.FileSystems.WebSite; using Orchard.Logging; @@ -55,6 +56,7 @@ namespace Orchard.Environment { RegisterVolatileProvider(builder); RegisterVolatileProvider(builder); + RegisterVolatileProvider(builder); RegisterVolatileProvider(builder); RegisterVolatileProvider(builder); RegisterVolatileProvider(builder); diff --git a/src/Orchard/FileSystems/LockFile/DefaultLockFileManager.cs b/src/Orchard/FileSystems/LockFile/DefaultLockFileManager.cs new file mode 100644 index 000000000..28ab27b71 --- /dev/null +++ b/src/Orchard/FileSystems/LockFile/DefaultLockFileManager.cs @@ -0,0 +1,55 @@ +using System; +using Orchard.FileSystems.AppData; +using Orchard.Services; + +namespace Orchard.FileSystems.LockFile { + public class DefaultLockFileManager : ILockFileManager { + private readonly IAppDataFolder _appDataFolder; + private readonly IClock _clock; + + public static TimeSpan Expiration { get; private set; } + + public DefaultLockFileManager(IAppDataFolder appDataFolder, IClock clock) { + _appDataFolder = appDataFolder; + _clock = clock; + Expiration = TimeSpan.FromMinutes(10); + } + + public bool TryAcquireLock(string path, ref ILockFile lockFile) { + try { + if(IsLocked(path)) { + return false; + } + + lockFile = new LockFile(_appDataFolder, path, _clock.UtcNow.ToString()); + return true; + } + catch { + // an error occured while reading/creating the lock file + return false; + } + } + + public bool IsLocked(string path) { + try { + if (_appDataFolder.FileExists(path)) { + var content = _appDataFolder.ReadFile(path); + + DateTime creationUtc; + if (DateTime.TryParse(content, out creationUtc)) { + // if expired the file is not removed + // it should be automatically as there is a finalizer in LockFile + // or the next taker can do it, unless it also fails, again + return creationUtc.Add(Expiration) > _clock.UtcNow; + } + } + } + catch { + // an error occured while reading the file + return true; + } + + return false; + } + } +} diff --git a/src/Orchard/FileSystems/LockFile/ILockFile.cs b/src/Orchard/FileSystems/LockFile/ILockFile.cs new file mode 100644 index 000000000..69e5ee5cc --- /dev/null +++ b/src/Orchard/FileSystems/LockFile/ILockFile.cs @@ -0,0 +1,8 @@ +using System; + +namespace Orchard.FileSystems.LockFile +{ + public interface ILockFile : IDisposable { + void Release(); + } +} diff --git a/src/Orchard/FileSystems/LockFile/ILockFileManager.cs b/src/Orchard/FileSystems/LockFile/ILockFileManager.cs new file mode 100644 index 000000000..b1f242861 --- /dev/null +++ b/src/Orchard/FileSystems/LockFile/ILockFileManager.cs @@ -0,0 +1,26 @@ +using Orchard.Caching; + +namespace Orchard.FileSystems.LockFile { + /// + /// Abstraction for lock files creation. + /// + /// + /// All virtual paths passed in or returned are relative to "~/App_Data". + /// + public interface ILockFileManager : IVolatileProvider { + /// + /// Attempts to acquire an exclusive lock file. + /// + /// The filename of the lock file to create. + /// A reference to the lock file object if the lock is granted. + /// true if the lock is granted; otherwise, false. + bool TryAcquireLock(string path, ref ILockFile lockFile); + + /// + /// Wether a lock file is already existing. + /// + /// The filename of the lock file to test. + /// true if the lock file exists; otherwise, false. + bool IsLocked(string path); + } +} diff --git a/src/Orchard/FileSystems/LockFile/LockFile.cs b/src/Orchard/FileSystems/LockFile/LockFile.cs new file mode 100644 index 000000000..1ecb594e3 --- /dev/null +++ b/src/Orchard/FileSystems/LockFile/LockFile.cs @@ -0,0 +1,60 @@ +using System; +using Orchard.FileSystems.AppData; + +namespace Orchard.FileSystems.LockFile { + /// + /// Represents a Lock File acquire on the file system + /// + public class LockFile : ILockFile { + private readonly IAppDataFolder _appDataFolder; + private readonly string _path; + private readonly string _content; + private bool _released; + + public LockFile(IAppDataFolder appDataFolder, string path, string content) { + _appDataFolder = appDataFolder; + _path = path; + _content = content; + + // create the physical lock file + _appDataFolder.CreateFile(path, content); + } + + public void Dispose() { + // dispose both managed and unmanaged resources + Dispose(true); + + // don't call the finalizer if dispose is called + GC.SuppressFinalize(this); + } + + public void Release() { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) { + if(disposing) { + // release managed code here + // nothing right now, just a placeholder to preserve the pattern + } + + if (_released || !_appDataFolder.FileExists(_path)) { + // nothing to do, night happen if re-granted, and already released + return; + } + + _released = true; + + // check it has not been granted in the meantime + var current = _appDataFolder.ReadFile(_path); + if (current == _content) { + _appDataFolder.DeleteFile(_path); + } + } + + ~LockFile() { + // dispose unmanaged resources (file) + Dispose(false); + } + } +} diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index 32067969b..92bb5bf98 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -183,6 +183,10 @@ + + + +