From 2c294e6196fc11be7c1b2d10410ded0459dfcf6f Mon Sep 17 00:00:00 2001 From: Iman Narasamdya Date: Wed, 16 Mar 2022 15:02:03 -0700 Subject: [PATCH 1/3] Fix directory enumeration callback in Simple provider --- simpleProviderManaged/Program.cs | 42 +++++++++++++++++------- simpleProviderManaged/ProviderOptions.cs | 3 ++ simpleProviderManaged/SimpleProvider.cs | 17 ++++++---- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/simpleProviderManaged/Program.cs b/simpleProviderManaged/Program.cs index 82c62d9..36b3294 100644 --- a/simpleProviderManaged/Program.cs +++ b/simpleProviderManaged/Program.cs @@ -3,8 +3,8 @@ using CommandLine; using Serilog; -using System; - +using System; + namespace SimpleProviderManaged { public class Program @@ -20,17 +20,35 @@ public static int Main(string[] args) { try { - // We want verbose logging so we can see all our callback invocations. - Log.Logger = new LoggerConfiguration() - .WriteTo.Console() - .WriteTo.File("SimpleProviderManaged-.log", rollingInterval: RollingInterval.Day) - .CreateLogger(); - - Log.Information("Start"); - - var parserResult = Parser.Default + var parser = new Parser(with => + { + with.AutoHelp = true; + with.AutoVersion = true; + with.EnableDashDash = true; + with.CaseSensitive = false; + with.CaseInsensitiveEnumValues = true; + with.HelpWriter = Console.Out; + }); + + var parserResult = parser .ParseArguments(args) - .WithParsed((ProviderOptions options) => Run(options)); + .WithParsed((ProviderOptions options) => + { + // We want verbose logging so we can see all our callback invocations. + var logConfig = new LoggerConfiguration() + .WriteTo.Console() + .WriteTo.File("SimpleProviderManaged-.log", rollingInterval: RollingInterval.Day); + + if (options.Verbose) + { + logConfig = logConfig.MinimumLevel.Verbose(); + } + + Log.Logger = logConfig.CreateLogger(); + + Log.Information("Start"); + Run(options); + }); Log.Information("Exit successfully"); return (int) ReturnCode.Success; diff --git a/simpleProviderManaged/ProviderOptions.cs b/simpleProviderManaged/ProviderOptions.cs index 40bbd25..ca048a5 100644 --- a/simpleProviderManaged/ProviderOptions.cs +++ b/simpleProviderManaged/ProviderOptions.cs @@ -21,6 +21,9 @@ public class ProviderOptions [Option('n', "notifications", HelpText = "Enable file system operation notifications.")] public bool EnableNotifications { get; set; } + [Option('v', "verbose", HelpText = "Use verbose log level.")] + public bool Verbose { get; set; } + [Option('d', "denyDeletes", HelpText = "Deny deletes.", Hidden = true)] public bool DenyDeletes { get; set; } diff --git a/simpleProviderManaged/SimpleProvider.cs b/simpleProviderManaged/SimpleProvider.cs index 87313a7..727072f 100644 --- a/simpleProviderManaged/SimpleProvider.cs +++ b/simpleProviderManaged/SimpleProvider.cs @@ -9,7 +9,6 @@ using System.IO; using System.Threading; using Microsoft.Windows.ProjFS; -using System.Runtime.InteropServices; namespace SimpleProviderManaged { @@ -390,6 +389,7 @@ internal HResult GetDirectoryEnumerationCallback( enumeration.TrySaveFilterString(filterFileName); } + int numEntriesAdded = 0; HResult hr = HResult.Ok; while (enumeration.IsCurrentValid) @@ -409,28 +409,33 @@ internal HResult GetDirectoryEnumerationCallback( // remembers the entry it couldn't add simply by not advancing its ActiveEnumeration. if (AddFileInfoToEnum(enumResult, fileInfo, targetPath)) { + Log.Verbose("----> GetDirectoryEnumerationCallback Added {Entry} {Kind} {Target}", fileInfo.Name, fileInfo.IsDirectory, targetPath); + + ++numEntriesAdded; enumeration.MoveNext(); } else { - // If we could not add the very first entry in the enumeration, a provider must - // return InsufficientBuffer. - if (enumeration.IsCurrentFirst) + Log.Verbose("----> GetDirectoryEnumerationCallback NOT added {Entry} {Kind} {Target}", fileInfo.Name, fileInfo.IsDirectory, targetPath); + + if (numEntriesAdded == 0) { hr = HResult.InsufficientBuffer; } + break; } } if (hr == HResult.Ok) { - Log.Information("<---- GetDirectoryEnumerationCallback {Result}", hr); + Log.Information("<---- GetDirectoryEnumerationCallback {Result} [Added entries: {EntryCount}]", hr, numEntriesAdded); } else { - Log.Error("<---- GetDirectoryEnumerationCallback {Result}", hr); + Log.Error("<---- GetDirectoryEnumerationCallback {Result} [Added entries: {EntryCount}]", hr, numEntriesAdded); } + return hr; } From 88ffdc273421c7be403b7b8838241e4343a25a60 Mon Sep 17 00:00:00 2001 From: Iman Narasamdya Date: Wed, 16 Mar 2022 15:08:13 -0700 Subject: [PATCH 2/3] Removed unnecessary code --- simpleProviderManaged/ActiveEnumeration.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/simpleProviderManaged/ActiveEnumeration.cs b/simpleProviderManaged/ActiveEnumeration.cs index 872c948..f1cf4fe 100644 --- a/simpleProviderManaged/ActiveEnumeration.cs +++ b/simpleProviderManaged/ActiveEnumeration.cs @@ -19,11 +19,6 @@ public ActiveEnumeration(List fileInfos) this.MoveNext(); } - /// - /// Indicates whether the current item is the first one in the enumeration. - /// - public bool IsCurrentFirst { get; private set; } - /// /// true if Current refers to an element in the enumeration, false if Current is past the end of the collection /// @@ -59,8 +54,7 @@ public void RestartEnumeration( public bool MoveNext() { this.IsCurrentValid = this.fileInfoEnumerator.MoveNext(); - this.IsCurrentFirst = false; - + while (this.IsCurrentValid && this.IsCurrentHidden()) { this.IsCurrentValid = this.fileInfoEnumerator.MoveNext(); @@ -146,7 +140,6 @@ private bool IsCurrentHidden() private void ResetEnumerator() { this.fileInfoEnumerator = this.fileInfos.GetEnumerator(); - this.IsCurrentFirst = true; } } } From 4975c35682f8425347a61cadcb46ea517fde8677 Mon Sep 17 00:00:00 2001 From: Iman Narasamdya Date: Wed, 16 Mar 2022 15:52:17 -0700 Subject: [PATCH 3/3] Run symlink tests only for NET core app --- ProjectedFSLib.Managed.Test/BasicTests.cs | 67 +++++++++++++---------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/ProjectedFSLib.Managed.Test/BasicTests.cs b/ProjectedFSLib.Managed.Test/BasicTests.cs index d64c784..159ba3b 100644 --- a/ProjectedFSLib.Managed.Test/BasicTests.cs +++ b/ProjectedFSLib.Managed.Test/BasicTests.cs @@ -4,12 +4,10 @@ using NUnit.Framework; using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; -using System.Threading; namespace ProjectedFSLib.Managed.Test { @@ -126,6 +124,14 @@ public void TestCanReadThroughVirtualizationRoot(string destinationFile) Assert.That("RandomNonsense", Is.Not.EqualTo(line)); } +#if NETCOREAPP3_1_OR_GREATER + // Running this test in NET framework causes CI failures in the Win 2022 version. + // They fail because the .NET Framework 4.8 version of the fixed Simple provider trips over the platform bug. + // The .NET Core 3.1 one works fine. Evidently Framework and Core enumerate differently, with Framework using + // a buffer that is small enough to hit the platform bug. + // + // The CI Win 2019 version doesn't run the symlink tests at all, since symlink support isn't in that version of ProjFS. + // We start the virtualization instance in each test case, so that exercises the following // methods in Microsoft.Windows.ProjFS: // VirtualizationInstance.VirtualizationInstance() @@ -197,37 +203,30 @@ public void TestCanReadSymlinksThroughVirtualizationRoot(string destinationFile, // IRequiredCallbacks.StartDirectoryEnumeration() // IRequiredCallbacks.GetDirectoryEnumeration() // IRequiredCallbacks.EndDirectoryEnumeration() - [TestCase("dir1\\dir2\\dir3\\sourcebar.txt", "dir4\\dir5\\dir6\\symbar.txt", "..\\..\\..\\dir1\\dir2\\dir3\\sourcebar.txt", Category = SymlinkTestCategory)] - public void TestCanReadSymlinksWithRelativePathTargetsThroughVirtualizationRoot(string destinationFile, string symlinkFile, string symlinkTarget) + [TestCase("dir1\\dir2\\dir3\\", "file.txt", "dir4\\dir5\\sdir6", Category = SymlinkTestCategory)] + public void TestCanReadSymlinkDirsThroughVirtualizationRoot(string destinationDir, string destinationFileName, string symlinkDir) { helpers.StartTestProvider(out string sourceRoot, out string virtRoot); + // Some contents to write to the file in the source and read out through the virtualization. - string fileContent = nameof(TestCanReadSymlinksThroughVirtualizationRoot); + string fileContent = nameof(TestCanReadSymlinkDirsThroughVirtualizationRoot); - // Create a file and a symlink to it. + string destinationFile = Path.Combine(destinationDir, destinationFileName); helpers.CreateVirtualFile(destinationFile, fileContent); - helpers.CreateVirtualSymlink(symlinkFile, symlinkTarget, false); - - // Open the file through the virtualization and read its contents. - string line = helpers.ReadFileInVirtRoot(destinationFile); - Assert.That(fileContent, Is.EqualTo(line)); + helpers.CreateVirtualSymlinkDirectory(symlinkDir, destinationDir, true); // Enumerate and ensure the symlink is present. - var pathToEnumerate = Path.Combine(virtRoot, Path.GetDirectoryName(symlinkFile)); + var pathToEnumerate = Path.Combine(virtRoot, Path.GetDirectoryName(symlinkDir)); DirectoryInfo virtDirInfo = new DirectoryInfo(pathToEnumerate); List virtList = new List(virtDirInfo.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)); - string fullPath = Path.Combine(virtRoot, symlinkFile); - FileSystemInfo symlink = virtList.Where(x => x.FullName == fullPath).First(); - Assert.That((symlink.Attributes & FileAttributes.ReparsePoint) != 0); - - // Get the symlink target and check that it points to the correct file. - string reparsePointTarget = helpers.ReadReparsePointTargetInVirtualRoot(symlinkFile); - Assert.That(reparsePointTarget, Is.EqualTo(symlinkTarget)); + string fullPath = Path.Combine(virtRoot, symlinkDir); - // Check if we have the same content if accessing the file through a symlink. + // Ensure we can access the file through directory symlink. + string symlinkFile = Path.Combine(virtRoot, symlinkDir, destinationFileName); string lineAccessedThroughSymlink = helpers.ReadFileInVirtRoot(symlinkFile); Assert.That(fileContent, Is.EqualTo(lineAccessedThroughSymlink)); } +#endif // We start the virtualization instance in each test case, so that exercises the following // methods in Microsoft.Windows.ProjFS: @@ -247,26 +246,34 @@ public void TestCanReadSymlinksWithRelativePathTargetsThroughVirtualizationRoot( // IRequiredCallbacks.StartDirectoryEnumeration() // IRequiredCallbacks.GetDirectoryEnumeration() // IRequiredCallbacks.EndDirectoryEnumeration() - [TestCase("dir1\\dir2\\dir3\\", "file.txt", "dir4\\dir5\\sdir6", Category = SymlinkTestCategory)] - public void TestCanReadSymlinkDirsThroughVirtualizationRoot(string destinationDir, string destinationFileName, string symlinkDir) + [TestCase("dir1\\dir2\\dir3\\sourcebar.txt", "dir4\\dir5\\dir6\\symbar.txt", "..\\..\\..\\dir1\\dir2\\dir3\\sourcebar.txt", Category = SymlinkTestCategory)] + public void TestCanReadSymlinksWithRelativePathTargetsThroughVirtualizationRoot(string destinationFile, string symlinkFile, string symlinkTarget) { helpers.StartTestProvider(out string sourceRoot, out string virtRoot); - // Some contents to write to the file in the source and read out through the virtualization. - string fileContent = nameof(TestCanReadSymlinkDirsThroughVirtualizationRoot); + string fileContent = nameof(TestCanReadSymlinksWithRelativePathTargetsThroughVirtualizationRoot); - string destinationFile = Path.Combine(destinationDir, destinationFileName); + // Create a file and a symlink to it. helpers.CreateVirtualFile(destinationFile, fileContent); - helpers.CreateVirtualSymlinkDirectory(symlinkDir, destinationDir, true); + helpers.CreateVirtualSymlink(symlinkFile, symlinkTarget, false); + + // Open the file through the virtualization and read its contents. + string line = helpers.ReadFileInVirtRoot(destinationFile); + Assert.That(fileContent, Is.EqualTo(line)); // Enumerate and ensure the symlink is present. - var pathToEnumerate = Path.Combine(virtRoot, Path.GetDirectoryName(symlinkDir)); + var pathToEnumerate = Path.Combine(virtRoot, Path.GetDirectoryName(symlinkFile)); DirectoryInfo virtDirInfo = new DirectoryInfo(pathToEnumerate); List virtList = new List(virtDirInfo.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)); - string fullPath = Path.Combine(virtRoot, symlinkDir); + string fullPath = Path.Combine(virtRoot, symlinkFile); + FileSystemInfo symlink = virtList.Where(x => x.FullName == fullPath).First(); + Assert.That((symlink.Attributes & FileAttributes.ReparsePoint) != 0); - // Ensure we can access the file through directory symlink. - string symlinkFile = Path.Combine(virtRoot, symlinkDir, destinationFileName); + // Get the symlink target and check that it points to the correct file. + string reparsePointTarget = helpers.ReadReparsePointTargetInVirtualRoot(symlinkFile); + Assert.That(reparsePointTarget, Is.EqualTo(symlinkTarget)); + + // Check if we have the same content if accessing the file through a symlink. string lineAccessedThroughSymlink = helpers.ReadFileInVirtRoot(symlinkFile); Assert.That(fileContent, Is.EqualTo(lineAccessedThroughSymlink)); }