diff --git a/DokanNet.Tests/BufferPoolTests.cs b/DokanNet.Tests/BufferPoolTests.cs new file mode 100644 index 00000000..d6bcbda3 --- /dev/null +++ b/DokanNet.Tests/BufferPoolTests.cs @@ -0,0 +1,51 @@ +using System; +using DokanNet.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DokanNet.Tests +{ + /// + /// Tests for . + /// + [TestClass] + public sealed class BufferPoolTests + { + /// + /// Rudimentary test for . + /// + [TestMethod, TestCategory(TestCategories.Success)] + public void BufferPoolBasicTest() + { + BufferPool pool = new BufferPool(); + ILogger logger = new ConsoleLogger(); + + // Verify buffer is pooled. + const int MB = 1024 * 1024; + byte[] buffer = pool.RentBuffer(MB, logger); + pool.ReturnBuffer(buffer, logger); + + byte[] buffer2 = pool.RentBuffer(MB, logger); + Assert.AreSame(buffer, buffer2, "Expected recycling of 1 MB buffer."); + + // Verify buffer that buffer not power of 2 is not pooled. + buffer = pool.RentBuffer(MB - 1, logger); + pool.ReturnBuffer(buffer, logger); + + buffer2 = pool.RentBuffer(MB - 1, logger); + Assert.AreNotSame(buffer, buffer2, "Did not expect recycling of 1 MB - 1 byte buffer."); + + // Run through a bunch of random buffer sizes and make sure we always get a buffer of the right size. + int seed = Environment.TickCount; + Console.WriteLine($"Random seed: {seed}"); + Random random = new Random(seed); + + for (int i = 0; i < 1000; i++) + { + int size = random.Next(0, 2 * MB); + buffer = pool.RentBuffer((uint)size, logger); + Assert.AreEqual(size, buffer.Length, "Wrong buffer size."); + pool.ReturnBuffer(buffer, logger); + } + } + } +} \ No newline at end of file diff --git a/DokanNet.Tests/DirectoryInfoTest.cs b/DokanNet.Tests/DirectoryInfoTest.cs index 5df027aa..d75ccd85 100644 --- a/DokanNet.Tests/DirectoryInfoTest.cs +++ b/DokanNet.Tests/DirectoryInfoTest.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -338,13 +339,12 @@ public void Delete_WhereRecurseIsFalse_CallsApiCorrectly() #endif } - [TestMethod, TestCategory(TestCategories.Success)] - [DeploymentItem("DirectoryInfoTests.Configuration.xml")] - [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML", "|DataDirectory|\\DirectoryInfoTests.Configuration.xml", "ConfigFindFiles", DataAccessMethod.Sequential)] - public void Delete_WhereRecurseIsTrueAndDirectoryIsNonempty_CallsApiCorrectly() - { - var supportsPatternSearch = bool.Parse((string) TestContext.DataRow["SupportsPatternSearch"]); + public static IEnumerable ConfigFindFilesData + => new object[][] { new object[] { true }, new object[] { false } }; + [DataTestMethod, TestCategory(TestCategories.Success), DynamicData(nameof(ConfigFindFilesData))] + public void Delete_WhereRecurseIsTrueAndDirectoryIsNonempty_CallsApiCorrectly(bool supportsPatternSearch) + { var fixture = DokanOperationsFixture.Instance; string path = fixture.DirectoryName.AsRootedPath(), @@ -397,13 +397,9 @@ public void Delete_WhereRecurseIsTrueAndDirectoryIsNonempty_CallsApiCorrectly() #endif } - [TestMethod, TestCategory(TestCategories.Success)] - [DeploymentItem("DirectoryInfoTests.Configuration.xml")] - [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML", "|DataDirectory|\\DirectoryInfoTests.Configuration.xml", "ConfigFindFiles", DataAccessMethod.Sequential)] - public void Delete_WhereRecurseIsTrueAndDirectoryIsEmpty_CallsApiCorrectly() + [DataTestMethod, TestCategory(TestCategories.Success), DynamicData(nameof(ConfigFindFilesData))] + public void Delete_WhereRecurseIsTrueAndDirectoryIsEmpty_CallsApiCorrectly(bool supportsPatternSearch) { - var supportsPatternSearch = bool.Parse((string) TestContext.DataRow["SupportsPatternSearch"]); - var fixture = DokanOperationsFixture.Instance; var path = fixture.DirectoryName.AsRootedPath(); @@ -515,13 +511,9 @@ public void GetDirectories_OnRootDirectory_WithoutPatternSearch_CallsApiCorrectl } [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "SubDirectory")] - [TestMethod, TestCategory(TestCategories.Success)] - [DeploymentItem("DirectoryInfoTests.Configuration.xml")] - [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML", "|DataDirectory|\\DirectoryInfoTests.Configuration.xml", "ConfigFindFiles", DataAccessMethod.Sequential)] - public void GetDirectories_OnSubDirectory_CallsApiCorrectly() + [DataTestMethod, TestCategory(TestCategories.Success), DynamicData(nameof(ConfigFindFilesData))] + public void GetDirectories_OnSubDirectory_CallsApiCorrectly(bool supportsPatternSearch) { - var supportsPatternSearch = bool.Parse((string) TestContext.DataRow["SupportsPatternSearch"]); - var fixture = DokanOperationsFixture.Instance; var path = fixture.DirectoryName.AsRootedPath(); @@ -557,13 +549,9 @@ public void GetDirectories_OnSubDirectory_CallsApiCorrectly() #endif } - [TestMethod, TestCategory(TestCategories.Success)] - [DeploymentItem("DirectoryInfoTests.Configuration.xml")] - [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML", "|DataDirectory|\\DirectoryInfoTests.Configuration.xml", "ConfigFindFiles", DataAccessMethod.Sequential)] - public void GetDirectoriesWithFilter_OnRootDirectory_CallsApiCorrectly() + [DataTestMethod, TestCategory(TestCategories.Success), DynamicData(nameof(ConfigFindFilesData))] + public void GetDirectoriesWithFilter_OnRootDirectory_CallsApiCorrectly(bool supportsPatternSearch) { - var supportsPatternSearch = bool.Parse((string) TestContext.DataRow["SupportsPatternSearch"]); - var fixture = DokanOperationsFixture.Instance; var path = DokanOperationsFixture.RootName; @@ -602,13 +590,9 @@ public void GetDirectoriesWithFilter_OnRootDirectory_CallsApiCorrectly() #endif } - [TestMethod, TestCategory(TestCategories.Success)] - [DeploymentItem("DirectoryInfoTests.Configuration.xml")] - [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML", "|DataDirectory|\\DirectoryInfoTests.Configuration.xml", "ConfigFindFiles", DataAccessMethod.Sequential)] - public void GetFiles_OnRootDirectory_CallsApiCorrectly() + [DataTestMethod, TestCategory(TestCategories.Success), DynamicData(nameof(ConfigFindFilesData))] + public void GetFiles_OnRootDirectory_CallsApiCorrectly(bool supportsPatternSearch) { - var supportsPatternSearch = bool.Parse((string) TestContext.DataRow["SupportsPatternSearch"]); - var fixture = DokanOperationsFixture.Instance; var path = DokanOperationsFixture.RootName; @@ -645,13 +629,9 @@ public void GetFiles_OnRootDirectory_CallsApiCorrectly() } [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "SubDirectory")] - [TestMethod, TestCategory(TestCategories.Success)] - [DeploymentItem("DirectoryInfoTests.Configuration.xml")] - [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML", "|DataDirectory|\\DirectoryInfoTests.Configuration.xml", "ConfigFindFiles", DataAccessMethod.Sequential)] - public void GetFiles_OnSubDirectory_CallsApiCorrectly() + [DataTestMethod, TestCategory(TestCategories.Success), DynamicData(nameof(ConfigFindFilesData))] + public void GetFiles_OnSubDirectory_CallsApiCorrectly(bool supportsPatternSearch) { - var supportsPatternSearch = bool.Parse((string) TestContext.DataRow["SupportsPatternSearch"]); - var fixture = DokanOperationsFixture.Instance; var path = fixture.DirectoryName.AsRootedPath(); @@ -687,13 +667,9 @@ public void GetFiles_OnSubDirectory_CallsApiCorrectly() #endif } - [TestMethod, TestCategory(TestCategories.Success)] - [DeploymentItem("DirectoryInfoTests.Configuration.xml")] - [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML", "|DataDirectory|\\DirectoryInfoTests.Configuration.xml", "ConfigFindFiles", DataAccessMethod.Sequential)] - public void GetFilesWithFilter_OnRootDirectory_CallsApiCorrectly() + [DataTestMethod, TestCategory(TestCategories.Success), DynamicData(nameof(ConfigFindFilesData))] + public void GetFilesWithFilter_OnRootDirectory_CallsApiCorrectly(bool supportsPatternSearch) { - var supportsPatternSearch = bool.Parse((string) TestContext.DataRow["SupportsPatternSearch"]); - var fixture = DokanOperationsFixture.Instance; var path = DokanOperationsFixture.RootName; @@ -732,13 +708,9 @@ public void GetFilesWithFilter_OnRootDirectory_CallsApiCorrectly() } [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "SubDirectory")] - [TestMethod, TestCategory(TestCategories.Success)] - [DeploymentItem("DirectoryInfoTests.Configuration.xml")] - [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML", "|DataDirectory|\\DirectoryInfoTests.Configuration.xml", "ConfigFindFiles", DataAccessMethod.Sequential)] - public void GetFiles_UnknownDates_CallsApiCorrectly() + [DataTestMethod, TestCategory(TestCategories.Success), DynamicData(nameof(ConfigFindFilesData))] + public void GetFiles_UnknownDates_CallsApiCorrectly(bool supportsPatternSearch) { - var supportsPatternSearch = bool.Parse((string) TestContext.DataRow["SupportsPatternSearch"]); - var fixture = DokanOperationsFixture.Instance; var path = fixture.DirectoryName.AsRootedPath(); @@ -795,13 +767,9 @@ public void GetFiles_UnknownDates_CallsApiCorrectly() #endif } - [TestMethod, TestCategory(TestCategories.Success)] - [DeploymentItem("DirectoryInfoTests.Configuration.xml")] - [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML", "|DataDirectory|\\DirectoryInfoTests.Configuration.xml", "ConfigFindFiles", DataAccessMethod.Sequential)] - public void GetFileSystemInfos_OnRootDirectory_CallsApiCorrectly() + [DataTestMethod, TestCategory(TestCategories.Success), DynamicData(nameof(ConfigFindFilesData))] + public void GetFileSystemInfos_OnRootDirectory_CallsApiCorrectly(bool supportsPatternSearch) { - var supportsPatternSearch = bool.Parse((string) TestContext.DataRow["SupportsPatternSearch"]); - var fixture = DokanOperationsFixture.Instance; var path = DokanOperationsFixture.RootName; @@ -836,13 +804,9 @@ public void GetFileSystemInfos_OnRootDirectory_CallsApiCorrectly() } [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "SubDirectory")] - [TestMethod, TestCategory(TestCategories.Success)] - [DeploymentItem("DirectoryInfoTests.Configuration.xml")] - [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML", "|DataDirectory|\\DirectoryInfoTests.Configuration.xml", "ConfigFindFiles", DataAccessMethod.Sequential)] - public void GetFileSystemInfos_OnSubDirectory_CallsApiCorrectly() + [DataTestMethod, TestCategory(TestCategories.Success), DynamicData(nameof(ConfigFindFilesData))] + public void GetFileSystemInfos_OnSubDirectory_CallsApiCorrectly(bool supportsPatternSearch) { - var supportsPatternSearch = bool.Parse((string) TestContext.DataRow["SupportsPatternSearch"]); - var fixture = DokanOperationsFixture.Instance; var path = fixture.DirectoryName.AsRootedPath(); @@ -876,13 +840,9 @@ public void GetFileSystemInfos_OnSubDirectory_CallsApiCorrectly() #endif } - [TestMethod, TestCategory(TestCategories.Success)] - [DeploymentItem("DirectoryInfoTests.Configuration.xml")] - [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML", "|DataDirectory|\\DirectoryInfoTests.Configuration.xml", "ConfigFindFiles", DataAccessMethod.Sequential)] - public void GetFileSystemInfosWithFilter_OnRootDirectory_CallsApiCorrectly() + [DataTestMethod, TestCategory(TestCategories.Success), DynamicData(nameof(ConfigFindFilesData))] + public void GetFileSystemInfosWithFilter_OnRootDirectory_CallsApiCorrectly(bool supportsPatternSearch) { - var supportsPatternSearch = bool.Parse((string) TestContext.DataRow["SupportsPatternSearch"]); - var fixture = DokanOperationsFixture.Instance; var path = DokanOperationsFixture.RootName; @@ -918,13 +878,9 @@ public void GetFileSystemInfosWithFilter_OnRootDirectory_CallsApiCorrectly() #endif } - [TestMethod, TestCategory(TestCategories.Success)] - [DeploymentItem("DirectoryInfoTests.Configuration.xml")] - [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML", "|DataDirectory|\\DirectoryInfoTests.Configuration.xml", "ConfigFindFiles", DataAccessMethod.Sequential)] - public void GetFileSystemInfos_OnRootDirectory_WhereSearchOptionIsAllDirectories_CallsApiCorrectly() + [DataTestMethod, TestCategory(TestCategories.Success), DynamicData(nameof(ConfigFindFilesData))] + public void GetFileSystemInfos_OnRootDirectory_WhereSearchOptionIsAllDirectories_CallsApiCorrectly(bool supportsPatternSearch) { - var supportsPatternSearch = bool.Parse((string) TestContext.DataRow["SupportsPatternSearch"]); - var fixture = DokanOperationsFixture.Instance; var pathsAndItems = new[] @@ -1075,13 +1031,9 @@ public void MoveTo_WhereTargetExists_Throws() #endif } - [TestMethod, TestCategory(TestCategories.Success)] - [DeploymentItem("DirectoryInfoTests.Configuration.xml")] - [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML", "|DataDirectory|\\DirectoryInfoTests.Configuration.xml", "ConfigFindFiles", DataAccessMethod.Sequential)] - public void SetAccessControl_CallsApiCorrectly() + [DataTestMethod, TestCategory(TestCategories.Success), DynamicData(nameof(ConfigFindFilesData))] + public void SetAccessControl_CallsApiCorrectly(bool supportsPatternSearch) { - var supportsPatternSearch = bool.Parse((string) TestContext.DataRow["SupportsPatternSearch"]); - var fixture = DokanOperationsFixture.Instance; var path = fixture.DirectoryName; diff --git a/DokanNet.Tests/DirectoryInfoTests.Configuration.xml b/DokanNet.Tests/DirectoryInfoTests.Configuration.xml deleted file mode 100644 index d9960b60..00000000 --- a/DokanNet.Tests/DirectoryInfoTests.Configuration.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - true - - - false - - \ No newline at end of file diff --git a/DokanNet.Tests/DokanNet.Tests.csproj b/DokanNet.Tests/DokanNet.Tests.csproj index 8f18ac8c..e816c617 100644 --- a/DokanNet.Tests/DokanNet.Tests.csproj +++ b/DokanNet.Tests/DokanNet.Tests.csproj @@ -20,13 +20,12 @@ $(MSBuildProjectName).$(TargetFramework) True + + true + ..\DokanNet\Dokan.snk - - DirectoryInfoTest.cs - PreserveNewest - OverlappedTests.cs PreserveNewest @@ -34,10 +33,10 @@ - + - - + + diff --git a/DokanNet.Tests/DokanOperationsFixture.cs b/DokanNet.Tests/DokanOperationsFixture.cs index d05431b6..b44d9d7a 100644 --- a/DokanNet.Tests/DokanOperationsFixture.cs +++ b/DokanNet.Tests/DokanOperationsFixture.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Security.Principal; using System.Threading; @@ -35,7 +36,7 @@ private class Proxy : IDokanOperations private delegate TResult FuncOut3(T1 arg1, T2 arg2, out T3 arg3, T4 arg4); - private delegate TResult FuncOut3(T1 arg1, T2 arg2, out T3 arg3, T4 arg4, T5 arg5); + protected delegate TResult FuncOut3(T1 arg1, T2 arg2, out T3 arg3, T4 arg4, T5 arg5); [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Explicit Exception handler")] private void TryExecute(string fileName, DokanFileInfo info, Action func, string funcName, bool restrictCallingProcessId = true) @@ -374,30 +375,67 @@ public NtStatus WriteFile(string fileName, byte[] buffer, out int bytesWritten, => TryExecute(fileName, buffer, out bytesWritten, offset, info, (string f, byte[] b, out int w, long o, DokanFileInfo i) => Target.WriteFile(f, b, out w, o, i), nameof(WriteFile)); } - private static string _mount_point; + /// + /// Subclass of that implements by manually marshalling the unmanaged buffers + /// to managed byte[] arrays and subsequently invoking the regular Read/WriteFile(byte[]) overload on the base proxy class. + /// + private class UnsafeProxy : Proxy, IDokanOperationsUnsafe + { + public NtStatus ReadFile(string fileName, IntPtr buffer, uint bufferLength, out int bytesRead, long offset, DokanFileInfo info) + => MarshalUnsafeCall(fileName, buffer, bufferLength, out bytesRead, offset, info, + (string f, byte[] buf, out int r, long o, DokanFileInfo i) => base.ReadFile(f, buf, out r, o, i)); + + public NtStatus WriteFile(string fileName, IntPtr buffer, uint bufferLength, out int bytesWritten, long offset, DokanFileInfo info) + => MarshalUnsafeCall(fileName, buffer, bufferLength, out bytesWritten, offset, info, + (string f, byte[] buf, out int r, long o, DokanFileInfo i) => base.WriteFile(f, buf, out r, o, i)); + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Explicit Exception handler")] + private NtStatus MarshalUnsafeCall(string fileName, IntPtr nativeBuffer, uint bufferLength, out int bytes, long offset, DokanFileInfo info, + FuncOut3 func) + { + byte[] managedBuffer = new byte[bufferLength]; + Marshal.Copy(source: nativeBuffer, destination: managedBuffer, startIndex: 0, length: (int)bufferLength); + NtStatus result = func(fileName, managedBuffer, out bytes, offset, info); + Marshal.Copy(source: managedBuffer, startIndex: 0, destination: nativeBuffer, length: (int)bufferLength); + return result; + } + } + + /// The mount point in use for the implementation. + public static string NormalMountPoint { get; private set; } - public static string MOUNT_POINT + /// The mount point in use for the implementation. + public static string UnsafeMountPoint { get; private set; } + + /// + /// Initializes the mount points by finding the next available drive letters. + /// + private static void InitMountPoints() { - get + var drives = Environment.GetLogicalDrives() + .Select(x => x[0]) + .ToArray(); + + var alphabet = new Stack("ABCDEFGHILMNOPQRSTUVZ"); + + NormalMountPoint = GetMountPoint(); + UnsafeMountPoint = GetMountPoint(); + + string GetMountPoint() { - if (string.IsNullOrWhiteSpace(_mount_point)) + while (alphabet.Any()) { - var drives = Environment.GetLogicalDrives() - .Select(x => x[0]) - .ToArray(); - var alphabet = new Stack("ABCDEFGHILMNOPQRSTUVZ"); - - while (alphabet.Any() && string.IsNullOrWhiteSpace(_mount_point)) - { - var letter = alphabet.Pop(); - if (!drives.Contains(letter)) - _mount_point = $@"{letter}:"; - } + var letter = alphabet.Pop(); + if (!drives.Contains(letter)) + return $"{letter}:"; } - return _mount_point; + + throw new InvalidOperationException("No drive letters available to test with."); } } + public static string MOUNT_POINT { get; private set; } + public const string VOLUME_LABEL = "Dokan Volume"; public const string FILESYSTEM_NAME = "Dokan Test"; @@ -411,6 +449,7 @@ public static string MOUNT_POINT private const FileAttributes EmptyFileAttributes = default(FileAttributes); private static Proxy proxy = new Proxy(); + private static Proxy unsafeProxy = new UnsafeProxy(); [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")] private string currentTestName; @@ -422,10 +461,11 @@ public static string MOUNT_POINT public static bool HasPendingFiles => Instance?.pendingFiles > 0; internal static IDokanOperations Operations => proxy; + internal static IDokanOperations UnsafeOperations => unsafeProxy; internal static DokanOperationsFixture Instance { get; private set; } - internal static string DriveName = MOUNT_POINT; + internal static string DriveName => MOUNT_POINT; internal static string RootName => @"\"; @@ -595,6 +635,7 @@ static DokanOperationsFixture() Instance.PermitMount(); InitSecurity(); + InitMountPoints(); } private static DateTime ToDateTime(string value) => DateTime.Parse(value, CultureInfo.InvariantCulture); @@ -611,11 +652,22 @@ internal static string DriveBasedPath(string fileName) internal static string RootedPath(string fileName) => Path.DirectorySeparatorChar + fileName.TrimStart(Path.DirectorySeparatorChar); - internal static void InitInstance(string currentTestName) + /// + /// Initializes the test fixture for running a test. + /// + /// The name of the test. + /// True to test IDokanOperationsUnsafe, false to test IDokanOperations. + internal static void InitInstance(string currentTestName, bool unsafeOperations = false) { Instance = new DokanOperationsFixture(currentTestName); + proxy.Target = Instance.operations.Object; proxy.HasUnmatchedInvocations = false; + unsafeProxy.Target = Instance.operations.Object; + unsafeProxy.HasUnmatchedInvocations = false; + + // Choose the mount point to operate on based on whether we're testing IDokanOperation of IDokanOperationsUnsafe. + MOUNT_POINT = unsafeOperations ? UnsafeMountPoint : NormalMountPoint; } internal static void ClearInstance(out bool hasUnmatchedInvocations) @@ -623,9 +675,13 @@ internal static void ClearInstance(out bool hasUnmatchedInvocations) // Allow pending calls to process Thread.Sleep(2); - hasUnmatchedInvocations = proxy.HasUnmatchedInvocations; + Proxy proxyInUse = MOUNT_POINT == UnsafeMountPoint ? unsafeProxy : proxy; + hasUnmatchedInvocations = proxyInUse.HasUnmatchedInvocations; + proxy.Target = null; + unsafeProxy.Target = null; Instance = null; + MOUNT_POINT = null; } internal static void Trace(string message) diff --git a/DokanNet.Tests/FileInfoTests.cs b/DokanNet.Tests/FileInfoTests.cs index 10b6b229..8fc4c37d 100644 --- a/DokanNet.Tests/FileInfoTests.cs +++ b/DokanNet.Tests/FileInfoTests.cs @@ -11,7 +11,7 @@ namespace DokanNet.Tests { [TestClass] - public sealed class FileInfoTests + public class FileInfoTests { private const int FILE_BUFFER_SIZE = 262144; @@ -41,13 +41,13 @@ public static void ClassCleanup() } [TestInitialize] - public void Initialize() + public virtual void Initialize() { DokanOperationsFixture.InitInstance(TestContext.TestName); } [TestCleanup] - public void Cleanup() + public virtual void Cleanup() { DokanOperationsFixture.ClearInstance(out bool hasUnmatchedInvocations); Assert.IsFalse(hasUnmatchedInvocations, "Found Mock invocations without corresponding setups"); diff --git a/DokanNet.Tests/FileInfoTestsUnsafe.cs b/DokanNet.Tests/FileInfoTestsUnsafe.cs new file mode 100644 index 00000000..9553bc91 --- /dev/null +++ b/DokanNet.Tests/FileInfoTestsUnsafe.cs @@ -0,0 +1,48 @@ +using System; +using DokanNet.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DokanNet.Tests +{ + /// + /// Tests for . This is leveraging the same set of tests as + /// by deriving from that class, but by calling + /// DokanOperationsFixture.InitInstance(unsafeOperations: true) from setup to send all + /// Read/WriteFile calls through the Read/WriteFile(IntPtr buffer, uint bufferLength) overloads instead + /// of the Read/WriteFile(byte[] buffer) overloads. + /// + [TestClass] + public sealed class FileInfoTestsUnsafe : FileInfoTests + { + [ClassInitialize] + public static new void ClassInitialize(TestContext context) + { + // Just invoke the base class init. + FileInfoTests.ClassInitialize(context); + } + + [ClassCleanup] + public static new void ClassCleanup() + { + // Just invoke the base class cleanup. + FileInfoTests.ClassCleanup(); + } + + [TestInitialize] + public override void Initialize() + { + // Clear the buffer pool (so we can validate in Cleanup()) and init test fixture. + BufferPool.Default.Clear(); + DokanOperationsFixture.InitInstance(TestContext.TestName, unsafeOperations: true); + } + + [TestCleanup] + public override void Cleanup() + { + // Verify no buffers were pooled and then call base class Cleanup(). + Assert.AreEqual(0, BufferPool.Default.ServedBytes, "Expected zero buffer pooling activity when using IDokanOperationsUnsafe."); + BufferPool.Default.Clear(); + base.Cleanup(); + } + } +} diff --git a/DokanNet.Tests/Mounter.cs b/DokanNet.Tests/Mounter.cs index 348423d6..a124eaa8 100644 --- a/DokanNet.Tests/Mounter.cs +++ b/DokanNet.Tests/Mounter.cs @@ -9,6 +9,7 @@ namespace DokanNet.Tests public static class Mounter { private static Thread mounterThread; + private static Thread mounterThread2; [AssemblyInitialize] public static void AssemblyInitialize(TestContext context) @@ -23,9 +24,11 @@ public static void AssemblyInitialize(TestContext context) dokanOptions |= DokanOptions.UserModeLock; #endif - (mounterThread = new Thread(() => DokanOperationsFixture.Operations.Mount(DokanOperationsFixture.MOUNT_POINT, dokanOptions, 5))).Start(); - var drive = new DriveInfo(DokanOperationsFixture.MOUNT_POINT); - while (!drive.IsReady) + (mounterThread = new Thread(() => DokanOperationsFixture.Operations.Mount(DokanOperationsFixture.NormalMountPoint, dokanOptions, 5))).Start(); + (mounterThread2 = new Thread(() => DokanOperationsFixture.UnsafeOperations.Mount(DokanOperationsFixture.UnsafeMountPoint, dokanOptions, 5))).Start(); + var drive = new DriveInfo(DokanOperationsFixture.NormalMountPoint); + var drive2 = new DriveInfo(DokanOperationsFixture.UnsafeMountPoint); + while (!drive.IsReady || !drive2.IsReady) Thread.Sleep(50); while (DokanOperationsFixture.HasPendingFiles) Thread.Sleep(50); @@ -35,8 +38,11 @@ public static void AssemblyInitialize(TestContext context) public static void AssemblyCleanup() { mounterThread.Abort(); - Dokan.Unmount(DokanOperationsFixture.MOUNT_POINT[0]); - Dokan.RemoveMountPoint(DokanOperationsFixture.MOUNT_POINT); + mounterThread2.Abort(); + Dokan.Unmount(DokanOperationsFixture.NormalMountPoint[0]); + Dokan.Unmount(DokanOperationsFixture.UnsafeMountPoint[0]); + Dokan.RemoveMountPoint(DokanOperationsFixture.NormalMountPoint); + Dokan.RemoveMountPoint(DokanOperationsFixture.UnsafeMountPoint); } } } \ No newline at end of file diff --git a/DokanNet/BufferPool.cs b/DokanNet/BufferPool.cs new file mode 100644 index 00000000..eb2eea39 --- /dev/null +++ b/DokanNet/BufferPool.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using DokanNet.Logging; + +namespace DokanNet +{ + /// + /// Simple buffer pool for buffers used by and + /// to avoid excessive Gen2 garbage collections due to large buffer + /// allocation on the large object heap (LOH). + /// + /// This pool is a bit different than say System.Buffers.ArrayPool(T) in that only returns + /// exact size buffers. This is because the Read/WriteFile APIs only take a byte[] array as a parameter, not + /// buffer and length. As such, it would be back compat breaking to return buffers that are bigger than the + /// data length. To limit the amount of memory consumption, we only buffer sizes that are powers of 2 because + /// common buffer sizes are typically that. There isn't anything preventing pooling buffers of any size if + /// we find that there's another common buffer size in use. Only pool buffers 1MB or smaller and only + /// up to 10 buffers of each size for further memory capping. + /// + internal class BufferPool + { + // An empty array does not contain data and can be statically cached. + private static readonly byte[] _emptyArray = new byte[0]; + + private readonly uint _maxBuffersPerPool; // Max buffers to cache per buffer size. + + // The pools for each buffer size. Index is log2(size). + private readonly ConcurrentBag[] _pools; + + // Number of bytes served out over the pool's lifetime. + private long _servedBytes; + + /// + /// Constructs a new buffer pool. + /// + /// The max size (bytes) buffer that will be cached. + /// Maximum number of buffers cached per buffer size. + public BufferPool(uint maxBufferSize = 1024 * 1024, uint maxBuffersPerBufferSize = 10) + { + _maxBuffersPerPool = maxBuffersPerBufferSize; + int log2 = GetPoolIndex(maxBufferSize); + if (log2 == -1) + { + throw new ArgumentOutOfRangeException(nameof(maxBufferSize), maxBufferSize, "Must be a power of 2."); + } + + // Create empty pools for each size. + _pools = new ConcurrentBag[log2 + 1]; + for (int i = 0; i < _pools.Length; i++) + { + _pools[i] = new ConcurrentBag(); + } + } + + /// + /// Default, process-wide buffer pool instance. + /// + public static BufferPool Default { get; } = new BufferPool(); + + /// + /// Clears the buffer pool by releasing all buffers. + /// + public void Clear() + { + _servedBytes = 0; + for (int i = 0; i < _pools.Length; i++) + { + _pools[i] = new ConcurrentBag(); // There's no clear method on ConcurrentBag... + } + } + + /// + /// Number of bytes served over the pool's lifetime. + /// + public long ServedBytes => Interlocked.Read(ref _servedBytes); + + /// + /// Gets a buffer from the buffer pool of the exact specified size. + /// If the size if not a power of 2, a buffer is still returned, but it is not poolable. + /// + /// The size of buffer requested. + /// Logger for debug spew about what the buffer pool did. + /// The byte[] buffer. + public byte[] RentBuffer(uint bufferSize, ILogger logger) + { + if (bufferSize == 0) + { + return _emptyArray; // byte[0] is statically cached. + } + + Interlocked.Add(ref _servedBytes, bufferSize); + + // If the number is not a power of 2, we have nothing to offer. + int poolIndex = GetPoolIndex(bufferSize); + if (poolIndex == -1 || poolIndex >= _pools.Length) + { + logger.Debug($"Buffer size {bufferSize} not power of 2 or too large, returning unpooled buffer."); + return new byte[bufferSize]; + } + + // Try getting a buffer from the pool. If it's empty, make a new buffer. + ConcurrentBag pool = _pools[poolIndex]; + if (pool.TryTake(out byte[] buffer)) + { + logger.Debug($"Using pooled buffer from pool {poolIndex}."); + } + else + { + logger.Debug($"Pool {poolIndex} empty, creating new buffer."); + buffer = new byte[bufferSize]; + } + + return buffer; + } + + /// + /// Returns a previously rented buffer to the buffer pool. + /// If the buffer size is not an exact power of 2, the buffer is ignored. + /// + /// The buffer to return. + /// Logger for debug spew about what the buffer pool did. + public void ReturnBuffer(byte[] buffer, ILogger logger) + { + if (buffer.Length == 0) + { + return; // Do nothing - _emptyArray caches this statically. + } + + // If the buffer is a power of 2 and below max pooled size, return it to the appropriate pool. + int poolIndex = GetPoolIndex((uint)buffer.Length); + if (poolIndex >= 0 && poolIndex < _pools.Length) + { + // Check if the pool is full. This is racy if multiple threads return buffers concurrently, + // but it's close enough - we'd just get a couple extra buffers in the pool at worst. + ConcurrentBag pool = _pools[poolIndex]; + if (pool.Count < _maxBuffersPerPool) + { + Array.Clear(buffer, 0, buffer.Length); + pool.Add(buffer); + logger.Debug($"Returned buffer to pool {poolIndex}."); + } + else + { + logger.Debug($"Pool {poolIndex} is full, discarding buffer."); + } + } + else + { + logger.Debug($"{poolIndex} (size {buffer.Length}) outside pool range, discarding buffer."); + } + } + + /// + /// Computes the pool index given a buffer size. The pool index is log2(size), + /// if size is a power of 2. If size is not a power of 2, -1 is returned (invalid pool index). + /// + /// Buffer size in bytes. + /// The pool index, log2(number), or -1 if bufferSize is not a power of 2. + private static int GetPoolIndex(uint bufferSize) + { + double log2 = Math.Log(bufferSize, 2); + int log2AsInt = (int)log2; + + // If they are not equal, the number is not a power of 2. + // + if (log2 != log2AsInt) + { + return -1; + } + + return log2AsInt; + } + } +} diff --git a/DokanNet/DokanOperationProxy.cs b/DokanNet/DokanOperationProxy.cs index 9b3ecd87..77174b78 100644 --- a/DokanNet/DokanOperationProxy.cs +++ b/DokanNet/DokanOperationProxy.cs @@ -50,7 +50,7 @@ public delegate void CloseFileDelegate( public delegate NtStatus ReadFileDelegate( [MarshalAs(UnmanagedType.LPWStr)] string rawFileName, - [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2), Out] byte[] rawBuffer, + IntPtr rawBuffer, uint rawBufferLength, ref int rawReadLength, long rawOffset, @@ -58,7 +58,7 @@ public delegate NtStatus ReadFileDelegate( public delegate NtStatus WriteFileDelegate( [MarshalAs(UnmanagedType.LPWStr)] string rawFileName, - [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] byte[] rawBuffer, + IntPtr rawBuffer, uint rawNumberOfBytesToWrite, ref int rawNumberOfBytesWritten, long rawOffset, @@ -376,7 +376,7 @@ public void CloseFileProxy(string rawFileName, DokanFileInfo rawFileInfo) public NtStatus ReadFileProxy( string rawFileName, - byte[] rawBuffer, + IntPtr rawBuffer, uint rawBufferLength, ref int rawReadLength, long rawOffset, @@ -389,7 +389,27 @@ public NtStatus ReadFileProxy( logger.Debug("\tOffset\t" + rawOffset); logger.Debug("\tContext\t" + rawFileInfo); - var result = operations.ReadFile(rawFileName, rawBuffer, out rawReadLength, rawOffset, rawFileInfo); + // Check if the file system has implemented the unsafe Dokan interface. + // If so, pass the raw IntPtr through instead of marshalling. + NtStatus result; + if (operations is IDokanOperationsUnsafe unsafeOperations) + { + result = unsafeOperations.ReadFile(rawFileName, rawBuffer, rawBufferLength, out rawReadLength, rawOffset, rawFileInfo); + } + else + { + // Pool the read buffer and return it to the pool when we're done with it. + byte[] buffer = BufferPool.Default.RentBuffer(rawBufferLength, logger); + try + { + result = operations.ReadFile(rawFileName, buffer, out rawReadLength, rawOffset, rawFileInfo); + Marshal.Copy(buffer, 0, rawBuffer, (int)rawBufferLength); + } + finally + { + BufferPool.Default.ReturnBuffer(buffer, logger); + } + } logger.Debug("ReadFileProxy : " + rawFileName + " Return : " + result + " ReadLength : " + rawReadLength); return result; @@ -405,7 +425,7 @@ public NtStatus ReadFileProxy( public NtStatus WriteFileProxy( string rawFileName, - byte[] rawBuffer, + IntPtr rawBuffer, uint rawNumberOfBytesToWrite, ref int rawNumberOfBytesWritten, long rawOffset, @@ -418,12 +438,32 @@ public NtStatus WriteFileProxy( logger.Debug("\tOffset\t{0}", rawOffset); logger.Debug("\tContext\t{0}", rawFileInfo); - var result = operations.WriteFile( - rawFileName, - rawBuffer, - out rawNumberOfBytesWritten, - rawOffset, - rawFileInfo); + // Check if the file system has implemented the unsafe Dokan interface. + // If so, pass the raw IntPtr through instead of marshalling. + NtStatus result; + if (operations is IDokanOperationsUnsafe unsafeOperations) + { + result = unsafeOperations.WriteFile(rawFileName, rawBuffer, rawNumberOfBytesToWrite, out rawNumberOfBytesWritten, rawOffset, rawFileInfo); + } + else + { + // Pool the write buffer and return it to the pool when we're done with it. + byte[] buffer = BufferPool.Default.RentBuffer(rawNumberOfBytesToWrite, logger); + try + { + Marshal.Copy(rawBuffer, buffer, 0, (int)rawNumberOfBytesToWrite); + result = operations.WriteFile( + rawFileName, + buffer, + out rawNumberOfBytesWritten, + rawOffset, + rawFileInfo); + } + finally + { + BufferPool.Default.ReturnBuffer(buffer, logger); + } + } logger.Debug( "WriteFileProxy : {0} Return : {1} NumberOfBytesWritten : {2}", diff --git a/DokanNet/IDokanOperationsUnsafe.cs b/DokanNet/IDokanOperationsUnsafe.cs new file mode 100644 index 00000000..3fd35c5e --- /dev/null +++ b/DokanNet/IDokanOperationsUnsafe.cs @@ -0,0 +1,48 @@ +using System; + +namespace DokanNet +{ + /// + /// This is a sub-interface of that can optionally be implemented + /// to get access to the raw, unmanaged buffers for ReadFile() and WriteFile() for performance optimization. + /// Marshalling the unmanaged buffers to and from byte[] arrays for every call of these APIs incurs an extra copy + /// that can be avoided by reading from or writing directly to the unmanaged buffers. + /// + /// Implementation of this interface is optional. If it is implemented, the overloads of + /// Read/WriteFile(IntPtr, length) will be called instead of Read/WriteFile(byte[]). The caller can fill or read + /// from the unmanaged API with Marshal.Copy, Buffer.MemoryCopy or similar. + /// + public interface IDokanOperationsUnsafe : IDokanOperations + { + /// + /// ReadFile callback on the file previously opened in . + /// It can be called by different thread at the same time, + /// therefore the read has to be thread safe. + /// + /// File path requested by the Kernel on the FileSystem. + /// Read buffer that has to be fill with the read result. + /// The size of 'buffer' in bytes. + /// The buffer size depends of the read size requested by the kernel. + /// Total number of bytes that has been read. + /// Offset from where the read has to be proceed. + /// An with information about the file or directory. + /// or appropriate to the request result. + /// + NtStatus ReadFile(string fileName, IntPtr buffer, uint bufferLength, out int bytesRead, long offset, DokanFileInfo info); + + /// + /// WriteFile callback on the file previously opened in + /// It can be called by different thread at the same time, + /// therefore the write/context has to be thread safe. + /// + /// File path requested by the Kernel on the FileSystem. + /// Data that has to be written. + /// The size of 'buffer' in bytes. + /// Total number of bytes that has been write. + /// Offset from where the write has to be proceed. + /// An with information about the file or directory. + /// or appropriate to the request result. + /// + NtStatus WriteFile(string fileName, IntPtr buffer, uint bufferLength, out int bytesWritten, long offset, DokanFileInfo info); + } +} diff --git a/DokanNet/Properties/AssemblyInfo.cs b/DokanNet/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..0a76f04c --- /dev/null +++ b/DokanNet/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + +// Make internals visible to tests. +[assembly:InternalsVisibleTo("DokanNet.Tests.net4.5, PublicKey=00240000048000009400000006020000002400005253413100040000010001008f83c86f027aaf91b9a5b847ed7a1a3139ec0a899ba7d9cb807c0f019ada751f006179a95a9578dbc18c58c91d1e2f736f418397e3914f77a97c2a08cbadeca4e1a3f1cb90c2adfc44ffb0d2842ed91aa644eea9f8f54148406861288b79d83146a39c7b08af0e533027b4f60f4ea9a3e1508fd8ba0c134b680ec43c734c4cb6")] +[assembly:InternalsVisibleTo("DokanNet.Tests.net4.6, PublicKey=00240000048000009400000006020000002400005253413100040000010001008f83c86f027aaf91b9a5b847ed7a1a3139ec0a899ba7d9cb807c0f019ada751f006179a95a9578dbc18c58c91d1e2f736f418397e3914f77a97c2a08cbadeca4e1a3f1cb90c2adfc44ffb0d2842ed91aa644eea9f8f54148406861288b79d83146a39c7b08af0e533027b4f60f4ea9a3e1508fd8ba0c134b680ec43c734c4cb6")] \ No newline at end of file diff --git a/sample/DokanNetMirror/Mirror.cs b/sample/DokanNetMirror/Mirror.cs index 9c5288b3..7b1366a9 100644 --- a/sample/DokanNetMirror/Mirror.cs +++ b/sample/DokanNetMirror/Mirror.cs @@ -34,12 +34,12 @@ public Mirror(string path) this.path = path; } - private string GetPath(string fileName) + protected string GetPath(string fileName) { return path + fileName; } - private NtStatus Trace(string method, string fileName, DokanFileInfo info, NtStatus result, + protected NtStatus Trace(string method, string fileName, DokanFileInfo info, NtStatus result, params object[] parameters) { #if TRACE diff --git a/sample/DokanNetMirror/Program.cs b/sample/DokanNetMirror/Program.cs index 6916ebb7..6e7be7a0 100644 --- a/sample/DokanNetMirror/Program.cs +++ b/sample/DokanNetMirror/Program.cs @@ -5,11 +5,14 @@ namespace DokanNetMirror { internal class Program { - private static void Main() + private static void Main(string[] args) { try { - var mirror = new Mirror("C:"); + bool unsafeReadWrite = args.Length > 0 && args[0].Equals("-unsafe", StringComparison.OrdinalIgnoreCase); + + Console.WriteLine($"Using unsafe methods: {unsafeReadWrite}"); + var mirror = unsafeReadWrite ? new UnsafeMirror("C:") : new Mirror("C:"); mirror.Mount("n:\\", DokanOptions.DebugMode, 5); Console.WriteLine(@"Success"); diff --git a/sample/DokanNetMirror/UnsafeMirror.cs b/sample/DokanNetMirror/UnsafeMirror.cs new file mode 100644 index 00000000..08d42486 --- /dev/null +++ b/sample/DokanNetMirror/UnsafeMirror.cs @@ -0,0 +1,125 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using DokanNet; +using Microsoft.Win32.SafeHandles; + +namespace DokanNetMirror +{ + /// + /// Implementation of IDokanOperationsUnsafe to demonstrate usage. + /// + internal class UnsafeMirror : Mirror, IDokanOperationsUnsafe + { + /// + /// Constructs a new unsafe mirror for the specified root path. + /// + /// Root path of mirror. + public UnsafeMirror(string path) : base(path) { } + + /// + /// Read from file using unmanaged buffers. + /// + public NtStatus ReadFile(string fileName, IntPtr buffer, uint bufferLength, out int bytesRead, long offset, DokanFileInfo info) + { + if (info.Context == null) // memory mapped read + { + using (var stream = new FileStream(GetPath(fileName), FileMode.Open, System.IO.FileAccess.Read)) + { + DoRead(stream, buffer, bufferLength, out bytesRead, offset); + } + } + else // normal read + { + var stream = info.Context as FileStream; + lock (stream) //Protect from overlapped read + { + DoRead(stream, buffer, bufferLength, out bytesRead, offset); + } + } + + return Trace($"Unsafe{nameof(ReadFile)}", fileName, info, DokanResult.Success, "out " + bytesRead.ToString(), + offset.ToString(CultureInfo.InvariantCulture)); + + void DoRead(FileStream stream, IntPtr innerBuffer, uint innerBufferLength, out int innerBytesRead, long innerOffset) + { + NativeMethods.SetFilePointer(stream.SafeFileHandle, innerOffset); + NativeMethods.ReadFile(stream.SafeFileHandle, innerBuffer, innerBufferLength, out innerBytesRead); + } + } + + /// + /// Write to file using unmanaged buffers. + /// + public NtStatus WriteFile(string fileName, IntPtr buffer, uint bufferLength, out int bytesWritten, long offset, DokanFileInfo info) + { + if (info.Context == null) + { + using (var stream = new FileStream(GetPath(fileName), FileMode.Open, System.IO.FileAccess.Write)) + { + DoWrite(stream, buffer, bufferLength, out bytesWritten, offset); + } + } + else + { + var stream = info.Context as FileStream; + lock (stream) //Protect from overlapped write + { + DoWrite(stream, buffer, bufferLength, out bytesWritten, offset); + } + } + + return Trace($"Unsafe{nameof(WriteFile)}", fileName, info, DokanResult.Success, "out " + bytesWritten.ToString(), + offset.ToString(CultureInfo.InvariantCulture)); + + void DoWrite(FileStream stream, IntPtr innerBuffer, uint innerBufferLength, out int innerBytesWritten, long innerOffset) + { + NativeMethods.SetFilePointer(stream.SafeFileHandle, innerOffset); + NativeMethods.WriteFile(stream.SafeFileHandle, innerBuffer, innerBufferLength, out innerBytesWritten); + } + } + + /// + /// kernel32 file method wrappers. + /// + private class NativeMethods + { + public static void SetFilePointer(SafeFileHandle fileHandle, long offset) + { + if (!SetFilePointerEx(fileHandle, offset, IntPtr.Zero, FILE_BEGIN)) + { + throw new Win32Exception(); + } + } + + public static void ReadFile(SafeFileHandle fileHandle, IntPtr buffer, uint bytesToRead, out int bytesRead) + { + if (!ReadFile(fileHandle, buffer, bytesToRead, out bytesRead, IntPtr.Zero)) + { + throw new Win32Exception(); + } + } + + public static void WriteFile(SafeFileHandle fileHandle, IntPtr buffer, uint bytesToWrite, out int bytesWritten) + { + if (!WriteFile(fileHandle, buffer, bytesToWrite, out bytesWritten, IntPtr.Zero)) + { + throw new Win32Exception(); + } + } + + private const uint FILE_BEGIN = 0; + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool SetFilePointerEx(SafeFileHandle hFile, long liDistanceToMove, IntPtr lpNewFilePointer, uint dwMoveMethod); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool ReadFile(SafeFileHandle hFile, IntPtr lpBuffer, uint nNumberOfBytesToRead, out int lpNumberOfBytesRead, IntPtr lpOverlapped); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool WriteFile(SafeFileHandle hFile, IntPtr lpBuffer, uint nNumberOfBytesToWrite, out int lpNumberOfBytesWritten, IntPtr lpOverlapped); + } + } +}