diff --git a/GVFS/GVFS.Common/FileSystem/IPlatformFileSystem.cs b/GVFS/GVFS.Common/FileSystem/IPlatformFileSystem.cs index fd7f5ec37..93c8cb2e9 100644 --- a/GVFS/GVFS.Common/FileSystem/IPlatformFileSystem.cs +++ b/GVFS/GVFS.Common/FileSystem/IPlatformFileSystem.cs @@ -5,7 +5,7 @@ public interface IPlatformFileSystem bool SupportsFileMode { get; } void FlushFileBuffers(string path); void MoveAndOverwriteFile(string sourceFileName, string destinationFilename); - void CreateHardLink(string newFileName, string existingFileName); + void CreateHardLink(string newLinkFileName, string existingFileName); bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage); void ChangeMode(string path, int mode); } diff --git a/GVFS/GVFS.Common/NativeMethods.cs b/GVFS/GVFS.Common/NativeMethods.cs index c4564e44d..4f19f1e25 100644 --- a/GVFS/GVFS.Common/NativeMethods.cs +++ b/GVFS/GVFS.Common/NativeMethods.cs @@ -98,11 +98,11 @@ public static void MoveFile(string existingFileName, string newFileName, MoveFil } } - public static void CreateHardLink(string newFileName, string existingFileName) + public static void CreateHardLink(string newLinkFileName, string existingFileName) { - if (!CreateHardLink(newFileName, existingFileName, IntPtr.Zero)) + if (!CreateHardLink(newLinkFileName, existingFileName, IntPtr.Zero)) { - ThrowLastWin32Exception($"Failed to create hard link from '{newFileName}' to '{existingFileName}'"); + ThrowLastWin32Exception($"Failed to create hard link from '{newLinkFileName}' to '{existingFileName}'"); } } @@ -172,7 +172,7 @@ private static extern bool MoveFileEx( [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool CreateHardLink( - string newFileName, + string newLinkFileName, string existingFileName, IntPtr securityAttributes); diff --git a/GVFS/GVFS.FunctionalTests/FileSystemRunners/BashRunner.cs b/GVFS/GVFS.FunctionalTests/FileSystemRunners/BashRunner.cs index 6b2310a70..40f2fbc12 100644 --- a/GVFS/GVFS.FunctionalTests/FileSystemRunners/BashRunner.cs +++ b/GVFS/GVFS.FunctionalTests/FileSystemRunners/BashRunner.cs @@ -47,6 +47,11 @@ public BashRunner() } } + public override bool SupportsHardlinkCreation + { + get { return true; } + } + protected override string FileName { get @@ -158,6 +163,14 @@ public override void CreateEmptyFile(string path) this.RunProcess(string.Format("-c \"touch {0}\"", bashPath)); } + public override void CreateHardLink(string newLinkFilePath, string existingFilePath) + { + string existingFileBashPath = this.ConvertWinPathToBashPath(existingFilePath); + string newLinkBashPath = this.ConvertWinPathToBashPath(newLinkFilePath); + + this.RunProcess(string.Format("-c \"ln {0} {1}\"", existingFileBashPath, newLinkBashPath)); + } + public override void WriteAllText(string path, string contents) { string bashPath = this.ConvertWinPathToBashPath(path); diff --git a/GVFS/GVFS.FunctionalTests/FileSystemRunners/CmdRunner.cs b/GVFS/GVFS.FunctionalTests/FileSystemRunners/CmdRunner.cs index cb14a0bec..cb5184c06 100644 --- a/GVFS/GVFS.FunctionalTests/FileSystemRunners/CmdRunner.cs +++ b/GVFS/GVFS.FunctionalTests/FileSystemRunners/CmdRunner.cs @@ -27,6 +27,11 @@ public class CmdRunner : ShellRunner "The process cannot access the file because it is being used by another process" }; + public override bool SupportsHardlinkCreation + { + get { return true; } + } + protected override string FileName { get @@ -108,6 +113,11 @@ public override void CreateEmptyFile(string path) this.RunProcess(string.Format("/C type NUL > \"{0}\"", path)); } + public override void CreateHardLink(string newLinkFilePath, string existingFilePath) + { + this.RunProcess(string.Format("/C mklink /H \"{0}\" \"{1}\"", newLinkFilePath, existingFilePath)); + } + public override void AppendAllText(string path, string contents) { // Use echo|set /p with "" to avoid adding any trailing whitespace or newline diff --git a/GVFS/GVFS.FunctionalTests/FileSystemRunners/FileSystemRunner.cs b/GVFS/GVFS.FunctionalTests/FileSystemRunners/FileSystemRunner.cs index 7679f4fe7..b8a2f9cc6 100644 --- a/GVFS/GVFS.FunctionalTests/FileSystemRunners/FileSystemRunner.cs +++ b/GVFS/GVFS.FunctionalTests/FileSystemRunners/FileSystemRunner.cs @@ -1,4 +1,5 @@ -using System; +using NUnit.Framework; +using System; namespace GVFS.FunctionalTests.FileSystemRunners { @@ -46,6 +47,11 @@ public static FileSystemRunner DefaultRunner get { return defaultRunner; } } + public virtual bool SupportsHardlinkCreation + { + get { return false; } + } + // File methods public abstract bool FileExists(string path); public abstract string MoveFile(string sourcePath, string targetPath); @@ -69,6 +75,11 @@ public static FileSystemRunner DefaultRunner public abstract void CreateEmptyFile(string path); + public virtual void CreateHardLink(string newLinkFilePath, string existingFilePath) + { + Assert.Fail($"This runner does not support {nameof(this.CreateHardLink)}"); + } + /// /// Write the specified contents to the specified file. By calling this method the caller is /// indicating that they expect the write to succeed. However, the caller is responsible for verifying that diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitFilesTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitFilesTests.cs index c2fac6926..5ec2ee3b3 100644 --- a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitFilesTests.cs +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitFilesTests.cs @@ -3,7 +3,6 @@ using GVFS.FunctionalTests.Tools; using GVFS.Tests.Should; using NUnit.Framework; -using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -12,7 +11,7 @@ namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture { - [TestFixtureSource(typeof(GitFilesTestsRunners), GitFilesTestsRunners.TestRunners)] + [TestFixtureSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] [Category(Categories.Mac.M2)] public class GitFilesTests : TestsWithEnlistmentPerFixture { @@ -38,10 +37,35 @@ public void CreateFileTest() this.fileSystem.CreateEmptyFile(this.Enlistment.GetVirtualPathTo(emptyFileName)); this.Enlistment.WaitForBackgroundOperations().ShouldEqual(true, "Background operations failed to complete."); GVFSHelpers.ModifiedPathsShouldContain(this.fileSystem, this.Enlistment.DotGVFSRoot, emptyFileName); - this.Enlistment.GetVirtualPathTo(fileName).ShouldBeAFile(this.fileSystem); + this.Enlistment.GetVirtualPathTo(emptyFileName).ShouldBeAFile(this.fileSystem); } [TestCase, Order(2)] + public void CreateHardLinkTest() + { + if (!this.fileSystem.SupportsHardlinkCreation) + { + return; + } + + string existingFileName = "fileToLinkTo.txt"; + string existingFilePath = this.Enlistment.GetVirtualPathTo(existingFileName); + GVFSHelpers.ModifiedPathsShouldNotContain(this.fileSystem, this.Enlistment.DotGVFSRoot, existingFileName); + this.fileSystem.WriteAllText(existingFilePath, "Some content here"); + this.Enlistment.WaitForBackgroundOperations().ShouldEqual(true, "Background operations failed to complete."); + GVFSHelpers.ModifiedPathsShouldContain(this.fileSystem, this.Enlistment.DotGVFSRoot, existingFileName); + existingFilePath.ShouldBeAFile(this.fileSystem).WithContents("Some content here"); + + string newLinkFileName = "newHardLink.txt"; + string newLinkFilePath = this.Enlistment.GetVirtualPathTo(newLinkFileName); + GVFSHelpers.ModifiedPathsShouldNotContain(this.fileSystem, this.Enlistment.DotGVFSRoot, newLinkFileName); + this.fileSystem.CreateHardLink(newLinkFilePath, existingFilePath); + this.Enlistment.WaitForBackgroundOperations().ShouldEqual(true, "Background operations failed to complete."); + GVFSHelpers.ModifiedPathsShouldContain(this.fileSystem, this.Enlistment.DotGVFSRoot, newLinkFileName); + newLinkFilePath.ShouldBeAFile(this.fileSystem).WithContents("Some content here"); + } + + [TestCase, Order(3)] [Category(Categories.Mac.M2TODO)] public void CreateFileInFolderTest() { @@ -62,7 +86,7 @@ public void CreateFileInFolderTest() GVFSHelpers.ModifiedPathsShouldContain(this.fileSystem, this.Enlistment.DotGVFSRoot, folderName + "/" + fileName); } - [TestCase, Order(3)] + [TestCase, Order(4)] [Category(Categories.Mac.M2TODO)] public void RenameEmptyFolderTest() { @@ -83,7 +107,7 @@ public void RenameEmptyFolderTest() GVFSHelpers.ModifiedPathsShouldContain(this.fileSystem, this.Enlistment.DotGVFSRoot, expectedModifiedEntries); } - [TestCase, Order(4)] + [TestCase, Order(5)] [Category(Categories.Mac.M2TODO)] public void RenameFolderTest() { @@ -116,7 +140,7 @@ public void RenameFolderTest() GVFSHelpers.ModifiedPathsShouldContain(this.fileSystem, this.Enlistment.DotGVFSRoot, expectedModifiedEntries); } - [TestCase, Order(5)] + [TestCase, Order(6)] [Category(Categories.Mac.M2TODO)] public void CaseOnlyRenameOfNewFolderKeepsExcludeEntries() { @@ -138,7 +162,7 @@ public void CaseOnlyRenameOfNewFolderKeepsExcludeEntries() GVFSHelpers.ModifiedPathsShouldContain(this.fileSystem, this.Enlistment.DotGVFSRoot, expectedModifiedPathsEntries); } - [TestCase, Order(6)] + [TestCase, Order(7)] public void ReadingFileDoesNotUpdateIndexOrSparseCheckout() { string gitFileToCheck = "GVFS/GVFS.FunctionalTests/Category/CategoryConstants.cs"; @@ -166,7 +190,7 @@ public void ReadingFileDoesNotUpdateIndexOrSparseCheckout() } // TODO(Mac): Enable this test once the LockHolder is converted to .NET Core - [TestCase, Order(7)] + [TestCase, Order(8)] [Category(Categories.Mac.M2TODO)] public void ModifiedFileWillGetAddedToModifiedPathsFile() { @@ -186,19 +210,16 @@ public void ModifiedFileWillGetAddedToModifiedPathsFile() this.VerifyWorktreeBit(gitFileToTest, LsFilesStatus.Cached); } - [TestCase, Order(8)] - [Category(Categories.Mac.M2TODO)] - public void RenamedFileAddedToSparseCheckoutAndSkipWorktreeBitCleared() + [TestCase, Order(9)] + public void RenamedFileAddedToModifiedPathsFile() { string fileToRenameEntry = "Test_EPF_MoveRenameFileTests/ChangeUnhydratedFileName/Program.cs"; string fileToRenameTargetEntry = "Test_EPF_MoveRenameFileTests/ChangeUnhydratedFileName/Program2.cs"; - string fileToRenameRelativePath = "Test_EPF_MoveRenameFileTests\\ChangeUnhydratedFileName\\Program.cs"; - string fileToRenameTargetRelativePath = "Test_EPF_MoveRenameFileTests\\ChangeUnhydratedFileName\\Program2.cs"; this.VerifyWorktreeBit(fileToRenameEntry, LsFilesStatus.SkipWorktree); this.fileSystem.MoveFile( - this.Enlistment.GetVirtualPathTo(fileToRenameRelativePath), - this.Enlistment.GetVirtualPathTo(fileToRenameTargetRelativePath)); + this.Enlistment.GetVirtualPathTo(fileToRenameEntry), + this.Enlistment.GetVirtualPathTo(fileToRenameTargetEntry)); this.Enlistment.WaitForBackgroundOperations().ShouldEqual(true, "Background operations failed to complete."); GVFSHelpers.ModifiedPathsShouldContain(this.fileSystem, this.Enlistment.DotGVFSRoot, fileToRenameEntry); @@ -208,20 +229,17 @@ public void RenamedFileAddedToSparseCheckoutAndSkipWorktreeBitCleared() this.VerifyWorktreeBit(fileToRenameEntry, LsFilesStatus.Cached); } - [TestCase, Order(9)] - [Category(Categories.Mac.M2TODO)] - public void RenamedFileAndOverwrittenTargetAddedToSparseCheckoutAndSkipWorktreeBitCleared() + [TestCase, Order(10)] + public void RenamedFileAndOverwrittenTargetAddedToModifiedPathsFile() { string fileToRenameEntry = "Test_EPF_MoveRenameFileTests_2/MoveUnhydratedFileToOverwriteUnhydratedFileAndWrite/RunUnitTests.bat"; string fileToRenameTargetEntry = "Test_EPF_MoveRenameFileTests_2/MoveUnhydratedFileToOverwriteUnhydratedFileAndWrite/RunFunctionalTests.bat"; - string fileToRenameRelativePath = "Test_EPF_MoveRenameFileTests_2\\MoveUnhydratedFileToOverwriteUnhydratedFileAndWrite\\RunUnitTests.bat"; - string fileToRenameTargetRelativePath = "Test_EPF_MoveRenameFileTests_2\\MoveUnhydratedFileToOverwriteUnhydratedFileAndWrite\\RunFunctionalTests.bat"; this.VerifyWorktreeBit(fileToRenameEntry, LsFilesStatus.SkipWorktree); this.VerifyWorktreeBit(fileToRenameTargetEntry, LsFilesStatus.SkipWorktree); this.fileSystem.ReplaceFile( - this.Enlistment.GetVirtualPathTo(fileToRenameRelativePath), - this.Enlistment.GetVirtualPathTo(fileToRenameTargetRelativePath)); + this.Enlistment.GetVirtualPathTo(fileToRenameEntry), + this.Enlistment.GetVirtualPathTo(fileToRenameTargetEntry)); this.Enlistment.WaitForBackgroundOperations().ShouldEqual(true, "Background operations failed to complete."); GVFSHelpers.ModifiedPathsShouldContain(this.fileSystem, this.Enlistment.DotGVFSRoot, fileToRenameEntry); @@ -232,7 +250,7 @@ public void RenamedFileAndOverwrittenTargetAddedToSparseCheckoutAndSkipWorktreeB this.VerifyWorktreeBit(fileToRenameTargetEntry, LsFilesStatus.Cached); } - [TestCase, Order(10)] + [TestCase, Order(11)] public void DeletedFileAddedToModifiedPathsFile() { string fileToDeleteEntry = "GVFlt_DeleteFileTest/GVFlt_DeleteFullFileWithoutFileContext_DeleteOnClose/a.txt"; @@ -247,7 +265,7 @@ public void DeletedFileAddedToModifiedPathsFile() this.VerifyWorktreeBit(fileToDeleteEntry, LsFilesStatus.Cached); } - [TestCase, Order(11)] + [TestCase, Order(12)] public void DeletedFolderAndChildrenAddedToToModifiedPathsFile() { string folderToDelete = "Scripts"; @@ -279,7 +297,7 @@ public void DeletedFolderAndChildrenAddedToToModifiedPathsFile() } } - [TestCase, Order(12)] + [TestCase, Order(13)] public void FileRenamedOutOfRepoAddedToModifiedPathsFile() { string fileToRenameEntry = "GVFlt_MoveFileTest/PartialToOutside/from/lessInFrom.txt"; @@ -298,7 +316,7 @@ public void FileRenamedOutOfRepoAddedToModifiedPathsFile() this.VerifyWorktreeBit(fileToRenameEntry, LsFilesStatus.Cached); } - [TestCase, Order(13)] + [TestCase, Order(14)] public void OverwrittenFileAddedToSparseCheckoutAndSkipWorktreeBitCleared() { string fileToOverwriteEntry = "Test_EPF_WorkingDirectoryTests/1/2/3/4/ReadDeepProjectedFile.cpp"; @@ -318,7 +336,7 @@ public void OverwrittenFileAddedToSparseCheckoutAndSkipWorktreeBitCleared() this.VerifyWorktreeBit(fileToOverwriteEntry, LsFilesStatus.Cached); } - [TestCase, Order(14)] + [TestCase, Order(15)] [Category(Categories.Mac.M2TODO)] public void SupersededFileAddedToSparseCheckoutAndSkipWorktreeBitCleared() { @@ -357,29 +375,5 @@ private static class LsFilesStatus public const char Cached = 'H'; public const char SkipWorktree = 'S'; } - - private class GitFilesTestsRunners - { - public const string TestRunners = "Runners"; - - public static object[] Runners - { - get - { - // Don't use the BashRunner for GitFilesTests as the BashRunner always strips off the last trailing newline (\n) - // and we expect there to be a trailing new line - List runners = new List(); - foreach (object[] runner in FileSystemRunner.Runners.ToList()) - { - if (!(runner.ToList().First() is BashRunner)) - { - runners.Add(new object[] { runner.ToList().First() }); - } - } - - return runners.ToArray(); - } - } - } } } diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitMoveRenameTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitMoveRenameTests.cs index d0b1a09e4..fa0fbe74f 100644 --- a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitMoveRenameTests.cs +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/GitMoveRenameTests.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Runtime.InteropServices; namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture { @@ -37,6 +38,7 @@ public void GitStatusAfterNewFile() string filename = "new.cs"; string filePath = this.Enlistment.GetVirtualPathTo(filename); + filePath.ShouldNotExistOnDisk(this.fileSystem); this.fileSystem.WriteAllText(filePath, this.testFileContents); filePath.ShouldBeAFile(this.fileSystem).WithContents(this.testFileContents); @@ -52,14 +54,14 @@ public void GitStatusAfterNewFile() } [TestCase, Order(3)] - [Category(Categories.Mac.M2TODO)] public void GitStatusAfterFileNameCaseChange() { string oldFilename = "new.cs"; this.EnsureTestFileExists(oldFilename); string newFilename = "New.cs"; - this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldFilename), this.Enlistment.GetVirtualPathTo(newFilename)); + string newFilePath = this.Enlistment.GetVirtualPathTo(newFilename); + this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldFilename), newFilePath); GitHelpers.CheckGitCommandAgainstGVFSRepo( this.Enlistment.RepoRoot, @@ -67,17 +69,19 @@ public void GitStatusAfterFileNameCaseChange() "On branch " + Properties.Settings.Default.Commitish, "Untracked files:", newFilename); + + this.fileSystem.DeleteFile(newFilePath); } [TestCase, Order(4)] - [Category(Categories.Mac.M2TODO)] public void GitStatusAfterFileRename() { string oldFilename = "New.cs"; - this.Enlistment.GetVirtualPathTo(oldFilename).ShouldBeAFile(this.fileSystem); + this.EnsureTestFileExists(oldFilename); string newFilename = "test.cs"; - this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldFilename), this.Enlistment.GetVirtualPathTo(newFilename)); + string newFilePath = this.Enlistment.GetVirtualPathTo(newFilename); + this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldFilename), newFilePath); GitHelpers.CheckGitCommandAgainstGVFSRepo( this.Enlistment.RepoRoot, @@ -178,7 +182,6 @@ public void GitWithEnvironmentVariables() } [TestCase, Order(9)] - [Category(Categories.Mac.M2TODO)] public void GitStatusAfterRenameFileIntoRepo() { string filename = "GitStatusAfterRenameFileIntoRepo.cs"; @@ -190,7 +193,7 @@ public void GitStatusAfterRenameFileIntoRepo() this.fileSystem.WriteAllText(filePath, this.testFileContents); filePath.ShouldBeAFile(this.fileSystem).WithContents(this.testFileContents); - string renamedFileName = "GVFlt_MoveFileTest\\GitStatusAfterRenameFileIntoRepo.cs"; + string renamedFileName = Path.Combine("GVFlt_MoveFileTest", "GitStatusAfterRenameFileIntoRepo.cs"); this.fileSystem.MoveFile(filePath, this.Enlistment.GetVirtualPathTo(renamedFileName)); this.Enlistment.GetVirtualPathTo(filePath).ShouldNotExistOnDisk(this.fileSystem); @@ -219,7 +222,6 @@ public void GitStatusAfterRenameFileOutOfRepo() } [TestCase, Order(11)] - [Category(Categories.Mac.M2TODO)] public void GitStatusAfterRenameFolderIntoRepo() { string folderName = "GitStatusAfterRenameFolderIntoRepo"; diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFileTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFileTests.cs index 5ea5b7ab0..43af93dc6 100644 --- a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFileTests.cs +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFileTests.cs @@ -10,7 +10,7 @@ namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture // TODO 452590 - Combine all of the MoveRenameTests into a single fixture, and have each use different // well known files [TestFixtureSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] - [Category(Categories.Mac.M2TODO)] + [Category(Categories.Mac.M2)] public class MoveRenameFileTests : TestsWithEnlistmentPerFixture { public const string TestFileContents = @@ -53,8 +53,8 @@ public MoveRenameFileTests(FileSystemRunner fileSystem) [TestCase] public void ChangeUnhydratedFileName() { - string oldFilename = "Test_EPF_MoveRenameFileTests\\ChangeUnhydratedFileName\\Program.cs"; - string newFilename = "Test_EPF_MoveRenameFileTests\\ChangeUnhydratedFileName\\renamed_Program.cs"; + string oldFilename = Path.Combine("Test_EPF_MoveRenameFileTests", "ChangeUnhydratedFileName", "Program.cs"); + string newFilename = Path.Combine("Test_EPF_MoveRenameFileTests", "ChangeUnhydratedFileName", "renamed_Program.cs"); // Don't read oldFilename or check for its existence before calling MoveFile, because doing so // can cause the file to hydrate @@ -86,7 +86,7 @@ public void ChangeNestedUnhydratedFileNameCase() { string oldName = "Program.cs"; string newName = "program.cs"; - string folderName = "Test_EPF_MoveRenameFileTests\\ChangeNestedUnhydratedFileNameCase\\"; + string folderName = Path.Combine("Test_EPF_MoveRenameFileTests", "ChangeNestedUnhydratedFileNameCase"); string oldVirtualPath = this.Enlistment.GetVirtualPathTo(Path.Combine(folderName, oldName)); string newVirtualPath = this.Enlistment.GetVirtualPathTo(Path.Combine(folderName, newName)); @@ -101,8 +101,8 @@ public void MoveUnhydratedFileToDotGitFolder() this.Enlistment.GetVirtualPathTo(targetFolderName).ShouldBeADirectory(this.fileSystem); string testFileName = "Program.cs"; - string testFileFolder = "Test_EPF_MoveRenameFileTests\\MoveUnhydratedFileToDotGitFolder"; - string testFilePathSubPath = testFileFolder + "\\" + testFileName; + string testFileFolder = Path.Combine("Test_EPF_MoveRenameFileTests", "MoveUnhydratedFileToDotGitFolder"); + string testFilePathSubPath = Path.Combine(testFileFolder, testFileName); string newTestFileVirtualPath = Path.Combine(this.Enlistment.GetVirtualPathTo(targetFolderName), testFileName); diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFileTests_2.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFileTests_2.cs index 01c2fe908..746f22561 100644 --- a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFileTests_2.cs +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/MoveRenameFileTests_2.cs @@ -8,7 +8,7 @@ namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture // TODO 452590 - Combine all of the MoveRenameTests into a single fixture, and have each use different // well known files [TestFixtureSource(typeof(FileSystemRunner), FileSystemRunner.TestRunners)] - [Category(Categories.Mac.M2TODO)] + [Category(Categories.Mac.M2)] public class MoveRenameFileTests_2 : TestsWithEnlistmentPerFixture { private const string TestFileFolder = "Test_EPF_MoveRenameFileTests_2"; @@ -44,8 +44,8 @@ public void MoveUnhydratedFileToUnhydratedFolderAndWrite() // Assume there will always be a GVFS folder when running tests string testFolderName = "GVFS"; - string oldTestFileVirtualPath = this.Enlistment.GetVirtualPathTo(TestFileFolder + "\\" + testFileName); - string newTestFileVirtualPath = this.Enlistment.GetVirtualPathTo(testFolderName + "\\" + testFileName); + string oldTestFileVirtualPath = this.Enlistment.GetVirtualPathTo(TestFileFolder, testFileName); + string newTestFileVirtualPath = this.Enlistment.GetVirtualPathTo(testFolderName, testFileName); this.fileSystem.MoveFile(oldTestFileVirtualPath, newTestFileVirtualPath); oldTestFileVirtualPath.ShouldNotExistOnDisk(this.fileSystem); @@ -72,8 +72,8 @@ public void MoveUnhydratedFileToNewFolderAndWrite() string newTestFileVirtualPath = Path.Combine(this.Enlistment.GetVirtualPathTo(testFolderName), testFolderName); - this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(TestFileFolder + "\\" + testFileName), newTestFileVirtualPath); - this.Enlistment.GetVirtualPathTo(TestFileFolder + "\\" + testFileName).ShouldNotExistOnDisk(this.fileSystem); + this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(TestFileFolder, testFileName), newTestFileVirtualPath); + this.Enlistment.GetVirtualPathTo(TestFileFolder, testFileName).ShouldNotExistOnDisk(this.fileSystem); newTestFileVirtualPath.ShouldBeAFile(this.fileSystem).WithContents(testFileContents); // Writing after the move should succeed @@ -92,8 +92,8 @@ public void MoveUnhydratedFileToNewFolderAndWrite() [TestCase, Order(3)] public void MoveUnhydratedFileToOverwriteUnhydratedFileAndWrite() { - string targetFilename = TestFileFolder + "\\MoveUnhydratedFileToOverwriteUnhydratedFileAndWrite\\RunFunctionalTests.bat"; - string sourceFilename = TestFileFolder + "\\MoveUnhydratedFileToOverwriteUnhydratedFileAndWrite\\RunUnitTests.bat"; + string targetFilename = Path.Combine(TestFileFolder, "MoveUnhydratedFileToOverwriteUnhydratedFileAndWrite", "RunFunctionalTests.bat"); + string sourceFilename = Path.Combine(TestFileFolder, "MoveUnhydratedFileToOverwriteUnhydratedFileAndWrite", "RunUnitTests.bat"); string sourceFileContents = RunUnitTestsContents; // Overwriting one unhydrated file with another should create a file at the target @@ -130,7 +130,11 @@ public void MoveUnhydratedFileToOverwriteFullFileAndWrite() string targetFilename = "TargetFile.txt"; string targetFileContents = "The Target"; - string sourceFilename = TestFileFolder + "\\MoveUnhydratedFileToOverwriteFullFileAndWrite\\MoveUnhydratedFileToOverwriteFullFileAndWrite.txt"; + string sourceFilename = Path.Combine( + TestFileFolder, + "MoveUnhydratedFileToOverwriteFullFileAndWrite", + "MoveUnhydratedFileToOverwriteFullFileAndWrite.txt"); + string sourceFileContents = @" diff --git a/GVFS/GVFS.FunctionalTests/Tests/GitCommands/AddStageTests.cs b/GVFS/GVFS.FunctionalTests/Tests/GitCommands/AddStageTests.cs index f079abe29..ac38b5e19 100644 --- a/GVFS/GVFS.FunctionalTests/Tests/GitCommands/AddStageTests.cs +++ b/GVFS/GVFS.FunctionalTests/Tests/GitCommands/AddStageTests.cs @@ -32,12 +32,30 @@ public void StageBasicTest() } [TestCase, Order(3)] + [Category(Categories.Mac.M2)] + public void AddAndStageHardLinksTest() + { + if (!this.FileSystem.SupportsHardlinkCreation) + { + return; + } + + this.CreateHardLink("ReadmeLink.md", "Readme.md"); + this.ValidateGitCommand("add ReadmeLink.md"); + this.RunGitCommand("commit -m \"Created ReadmeLink.md\""); + + this.CreateHardLink("AuthoringTestsLink.md", "AuthoringTests.md"); + this.ValidateGitCommand("stage AuthoringTestsLink.md"); + this.RunGitCommand("commit -m \"Created AuthoringTestsLink.md\""); + } + + [TestCase, Order(4)] public void AddAllowsPlaceholderCreation() { this.CommandAllowsPlaceholderCreation("add", @"GVFS\GVFS\Program.cs"); } - [TestCase, Order(4)] + [TestCase, Order(5)] public void StageAllowsPlaceholderCreation() { this.CommandAllowsPlaceholderCreation("stage", @"GVFS\GVFS\App.config"); diff --git a/GVFS/GVFS.FunctionalTests/Tests/GitCommands/GitRepoTests.cs b/GVFS/GVFS.FunctionalTests/Tests/GitCommands/GitRepoTests.cs index 6dd862117..89fe43289 100644 --- a/GVFS/GVFS.FunctionalTests/Tests/GitCommands/GitRepoTests.cs +++ b/GVFS/GVFS.FunctionalTests/Tests/GitCommands/GitRepoTests.cs @@ -215,6 +215,23 @@ protected void EditFile(string filePath, string content) this.FileSystem.AppendAllText(controlFile, content); } + protected void CreateHardLink(string newLinkFileName, string existingFileName) + { + string virtualExistingFile = Path.Combine(this.Enlistment.RepoRoot, existingFileName); + string controlExistingFile = Path.Combine(this.ControlGitRepo.RootPath, existingFileName); + string virtualNewLinkFile = Path.Combine(this.Enlistment.RepoRoot, newLinkFileName); + string controlNewLinkFile = Path.Combine(this.ControlGitRepo.RootPath, newLinkFileName); + + // GitRepoTests are only run with SystemIORunner (which does not support hardlink + // creation) so use a BashRunner instead. + this.FileSystem.SupportsHardlinkCreation.ShouldBeFalse( + "If this.FileSystem.SupportsHardlinkCreation is true, CreateHardLink no longer needs to create a BashRunner"); + FileSystemRunner runner = new BashRunner(); + + runner.CreateHardLink(virtualNewLinkFile, virtualExistingFile); + runner.CreateHardLink(controlNewLinkFile, controlExistingFile); + } + protected void SetFileAsReadOnly(string filePath) { string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); diff --git a/GVFS/GVFS.Platform.Mac/MacFileSystemVirtualizer.cs b/GVFS/GVFS.Platform.Mac/MacFileSystemVirtualizer.cs index 66a7a83c9..f358f07bb 100644 --- a/GVFS/GVFS.Platform.Mac/MacFileSystemVirtualizer.cs +++ b/GVFS/GVFS.Platform.Mac/MacFileSystemVirtualizer.cs @@ -102,6 +102,8 @@ protected override bool TryStart(out string error) this.virtualizationInstance.OnFileModified = this.OnFileModified; this.virtualizationInstance.OnPreDelete = this.OnPreDelete; this.virtualizationInstance.OnNewFileCreated = this.OnNewFileCreated; + this.virtualizationInstance.OnFileRenamed = this.OnFileRenamed; + this.virtualizationInstance.OnHardLinkCreated = this.OnHardLinkCreated; uint threadCount = (uint)Environment.ProcessorCount * 2; @@ -118,8 +120,8 @@ protected override bool TryStart(out string error) this.Context.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.TryStart)}_StartedVirtualization", metadata: null); return true; - } - + } + private static byte[] ToVersionIdByteArray(byte[] version) { byte[] bytes = new byte[VirtualizationInstance.PlaceholderIdLength]; @@ -189,8 +191,8 @@ private Result OnGetFileStream( (stream, blobLength) => { // TODO(Mac): Find a better solution than reading from the stream one byte at at time - byte[] buffer = new byte[4096]; - uint bufferIndex = 0; + byte[] buffer = new byte[4096]; + uint bufferIndex = 0; int nextByte = stream.ReadByte(); while (nextByte != -1) { @@ -347,6 +349,24 @@ private void OnNewFileCreated(string relativePath, bool isDirectory) this.LogUnhandledExceptionAndExit(nameof(this.OnNewFileCreated), metadata); } } + + private void OnFileRenamed(string relativeDestinationPath, bool isDirectory) + { + // ProjFS for Mac *could* be updated to provide us with relativeSourcePath as well, + // but because VFSForGit doesn't need the source path on Mac for correct behavior + // the relativeSourcePath is left out of the notification to keep the kext simple + this.OnFileRenamed( + relativeSourcePath: string.Empty, + relativeDestinationPath: relativeDestinationPath, + isDirectory: isDirectory); + } + + private void OnHardLinkCreated(string relativeNewLinkPath) + { + this.OnHardLinkCreated( + relativeExistingFilePath: string.Empty, + relativeNewLinkPath: relativeNewLinkPath); + } private Result OnEnumerateDirectory( ulong commandId, diff --git a/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs b/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs index 02167bd3e..6503194c0 100644 --- a/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs +++ b/GVFS/GVFS.Platform.Windows/WindowsFileSystemVirtualizer.cs @@ -159,7 +159,7 @@ protected override bool TryStart(out string error) this.virtualizationInstance.OnNotifyPreRename = this.NotifyPreRenameHandler; this.virtualizationInstance.OnNotifyPreSetHardlink = null; this.virtualizationInstance.OnNotifyFileRenamed = this.NotifyFileRenamedHandler; - this.virtualizationInstance.OnNotifyHardlinkCreated = null; + this.virtualizationInstance.OnNotifyHardlinkCreated = this.NotifyHardlinkCreated; this.virtualizationInstance.OnNotifyFileHandleClosedNoModification = null; this.virtualizationInstance.OnNotifyFileHandleClosedFileModifiedOrDeleted = this.NotifyFileHandleClosedFileModifiedOrDeletedHandler; this.virtualizationInstance.OnNotifyFilePreConvertToFull = this.NotifyFilePreConvertToFullHandler; @@ -1223,35 +1223,14 @@ private void NotifyFileRenamedHandler( bool isDirectory, ref NotificationType notificationMask) { - try - { - bool srcPathInDotGit = FileSystemCallbacks.IsPathInsideDotGit(virtualPath); - bool dstPathInDotGit = FileSystemCallbacks.IsPathInsideDotGit(destinationPath); - - if (dstPathInDotGit) - { - this.OnDotGitFileOrFolderChanged(destinationPath); - } + this.OnFileRenamed(virtualPath, destinationPath, isDirectory); + } - if (!(srcPathInDotGit && dstPathInDotGit)) - { - if (isDirectory) - { - this.FileSystemCallbacks.OnFolderRenamed(virtualPath, destinationPath); - } - else - { - this.FileSystemCallbacks.OnFileRenamed(virtualPath, destinationPath); - } - } - } - catch (Exception e) - { - EventMetadata metadata = this.CreateEventMetadata(virtualPath, e); - metadata.Add("destinationPath", destinationPath); - metadata.Add("isDirectory", isDirectory); - this.LogUnhandledExceptionAndExit(nameof(this.NotifyFileRenamedHandler), metadata); - } + private void NotifyHardlinkCreated( + string relativeExistingFilePath, + string relativeNewLinkPath) + { + this.OnHardLinkCreated(relativeExistingFilePath, relativeNewLinkPath); } private void NotifyFileHandleClosedFileModifiedOrDeletedHandler( @@ -1386,19 +1365,23 @@ private class Notifications NotificationType.PreRename | NotificationType.PreDelete | NotificationType.FileRenamed | + NotificationType.HardlinkCreated | NotificationType.FileHandleClosedFileModified; public const NotificationType LogsHeadFile = - NotificationType.FileRenamed | + NotificationType.FileRenamed | + NotificationType.HardlinkCreated | NotificationType.FileHandleClosedFileModified; public const NotificationType ExcludeAndHeadFile = NotificationType.FileRenamed | + NotificationType.HardlinkCreated | NotificationType.FileHandleClosedFileDeleted | NotificationType.FileHandleClosedFileModified; public const NotificationType FilesAndFoldersInRefsHeads = NotificationType.FileRenamed | + NotificationType.HardlinkCreated | NotificationType.FileHandleClosedFileDeleted | NotificationType.FileHandleClosedFileModified; @@ -1406,6 +1389,7 @@ private class Notifications NotificationType.NewFileCreated | NotificationType.FileSupersededOrOverwritten | NotificationType.FileRenamed | + NotificationType.HardlinkCreated | NotificationType.FileHandleClosedFileDeleted | NotificationType.FilePreConvertToFull | NotificationType.FileHandleClosedFileModified; diff --git a/GVFS/GVFS.UnitTests/Virtualization/FileSystemCallbacksTests.cs b/GVFS/GVFS.UnitTests/Virtualization/FileSystemCallbacksTests.cs index 55c1ad296..7585b6179 100644 --- a/GVFS/GVFS.UnitTests/Virtualization/FileSystemCallbacksTests.cs +++ b/GVFS/GVFS.UnitTests/Virtualization/FileSystemCallbacksTests.cs @@ -275,6 +275,12 @@ public void FileAndFolderCallbacksScheduleBackgroundTasks() "OnFileRenamed2.txt", FileSystemTask.OperationType.OnFileRenamed); + this.CallbackSchedulesBackgroundTask( + backgroundTaskRunner, + (path) => fileSystemCallbacks.OnFileHardLinkCreated(path), + "OnFileHardLinkCreated.txt", + FileSystemTask.OperationType.OnFileHardLinkCreated); + this.CallbackSchedulesBackgroundTask( backgroundTaskRunner, (path) => fileSystemCallbacks.OnFileSuperseded(path), diff --git a/GVFS/GVFS.Virtualization/Background/FileSystemTask.cs b/GVFS/GVFS.Virtualization/Background/FileSystemTask.cs index a88d18e6a..b5644bf86 100644 --- a/GVFS/GVFS.Virtualization/Background/FileSystemTask.cs +++ b/GVFS/GVFS.Virtualization/Background/FileSystemTask.cs @@ -28,7 +28,8 @@ public enum OperationType OnFolderDeleted, OnFolderFirstWrite, OnIndexWriteWithoutProjectionChange, - OnPlaceholderCreationsBlockedForGit + OnPlaceholderCreationsBlockedForGit, + OnFileHardLinkCreated } public OperationType Operation { get; } @@ -46,6 +47,11 @@ public static FileSystemTask OnFileRenamed(string oldVirtualPath, string newVirt return new FileSystemTask(OperationType.OnFileRenamed, newVirtualPath, oldVirtualPath); } + public static FileSystemTask OnFileHardLinkCreated(string newLinkRelativePath) + { + return new FileSystemTask(OperationType.OnFileHardLinkCreated, newLinkRelativePath, oldVirtualPath: null); + } + public static FileSystemTask OnFileDeleted(string virtualPath) { return new FileSystemTask(OperationType.OnFileDeleted, virtualPath, oldVirtualPath: null); diff --git a/GVFS/GVFS.Virtualization/FileSystem/FileSystemVirtualizer.cs b/GVFS/GVFS.Virtualization/FileSystem/FileSystemVirtualizer.cs index 33c90c988..4357d85a4 100644 --- a/GVFS/GVFS.Virtualization/FileSystem/FileSystemVirtualizer.cs +++ b/GVFS/GVFS.Virtualization/FileSystem/FileSystemVirtualizer.cs @@ -208,6 +208,62 @@ protected void OnWorkingDirectoryFileOrFolderDeleted(string relativePath, bool i this.FileSystemCallbacks.InvalidateGitStatusCache(); } + + protected void OnFileRenamed(string relativeSourcePath, string relativeDestinationPath, bool isDirectory) + { + try + { + bool srcPathInDotGit = FileSystemCallbacks.IsPathInsideDotGit(relativeSourcePath); + bool dstPathInDotGit = FileSystemCallbacks.IsPathInsideDotGit(relativeDestinationPath); + + if (dstPathInDotGit) + { + this.OnDotGitFileOrFolderChanged(relativeDestinationPath); + } + + if (!(srcPathInDotGit && dstPathInDotGit)) + { + if (isDirectory) + { + this.FileSystemCallbacks.OnFolderRenamed(relativeSourcePath, relativeDestinationPath); + } + else + { + this.FileSystemCallbacks.OnFileRenamed(relativeSourcePath, relativeDestinationPath); + } + } + } + catch (Exception e) + { + EventMetadata metadata = this.CreateEventMetadata(relativeSourcePath, e); + metadata.Add("destinationPath", relativeDestinationPath); + metadata.Add("isDirectory", isDirectory); + this.LogUnhandledExceptionAndExit(nameof(this.OnFileRenamed), metadata); + } + } + + protected void OnHardLinkCreated(string relativeExistingFilePath, string relativeNewLinkPath) + { + try + { + bool pathInDotGit = FileSystemCallbacks.IsPathInsideDotGit(relativeNewLinkPath); + + if (pathInDotGit) + { + this.OnDotGitFileOrFolderChanged(relativeNewLinkPath); + } + else + { + this.FileSystemCallbacks.OnFileHardLinkCreated(relativeNewLinkPath); + } + } + catch (Exception e) + { + EventMetadata metadata = this.CreateEventMetadata(relativeNewLinkPath, e); + metadata.Add(nameof(relativeExistingFilePath), relativeExistingFilePath); + this.LogUnhandledExceptionAndExit(nameof(this.OnHardLinkCreated), metadata); + } + } protected EventMetadata CreateEventMetadata( Guid enumerationId, diff --git a/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs b/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs index 906bad018..980ab2b87 100644 --- a/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs +++ b/GVFS/GVFS.Virtualization/FileSystemCallbacks.cs @@ -423,6 +423,11 @@ public virtual void OnFileRenamed(string oldRelativePath, string newRelativePath this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileRenamed(oldRelativePath, newRelativePath)); } + public virtual void OnFileHardLinkCreated(string newLinkRelativePath) + { + this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileHardLinkCreated(newLinkRelativePath)); + } + public void OnFileDeleted(string relativePath) { this.backgroundFileSystemTaskRunner.Enqueue(FileSystemTask.OnFileDeleted(relativePath)); @@ -606,6 +611,7 @@ private FileSystemTaskResult ExecuteBackgroundOperation(FileSystemTask gitUpdate { case FileSystemTask.OperationType.OnFileCreated: case FileSystemTask.OperationType.OnFailedPlaceholderDelete: + case FileSystemTask.OperationType.OnFileHardLinkCreated: metadata.Add("virtualPath", gitUpdate.VirtualPath); result = this.AddModifiedPathAndRemoveFromPlaceholderList(gitUpdate.VirtualPath); break; diff --git a/MirrorProvider/MirrorProvider.Mac/MacFileSystemVirtualizer.cs b/MirrorProvider/MirrorProvider.Mac/MacFileSystemVirtualizer.cs index 5f20521bb..3d9396c82 100644 --- a/MirrorProvider/MirrorProvider.Mac/MacFileSystemVirtualizer.cs +++ b/MirrorProvider/MirrorProvider.Mac/MacFileSystemVirtualizer.cs @@ -22,7 +22,9 @@ public override bool TryStartVirtualizationInstance(Enlistment enlistment, out s this.virtualizationInstance.OnGetFileStream = this.OnGetFileStream; this.virtualizationInstance.OnFileModified = this.OnFileModified; this.virtualizationInstance.OnPreDelete = this.OnPreDelete; - this.virtualizationInstance.OnNewFileCreated = this.OnNewFileCreated; + this.virtualizationInstance.OnNewFileCreated = this.OnNewFileCreated; + this.virtualizationInstance.OnFileRenamed = this.OnFileRenamed; + this.virtualizationInstance.OnHardLinkCreated = this.OnHardLinkCreated; Result result = this.virtualizationInstance.StartVirtualizationInstance( enlistment.SrcRoot, @@ -164,6 +166,16 @@ private void OnNewFileCreated(string relativePath, bool isDirectory) { Console.WriteLine($"OnNewFileCreated (isDirectory: {isDirectory}): {relativePath}"); } + + private void OnFileRenamed(string relativeDestinationPath, bool isDirectory) + { + Console.WriteLine($"OnFileRenamed (isDirectory: {isDirectory}) destination: {relativeDestinationPath}"); + } + + private void OnHardLinkCreated(string relativeNewLinkPath) + { + Console.WriteLine($"OnHardLinkCreated: {relativeNewLinkPath}"); + } private static byte[] ToVersionIdByteArray(byte version) { diff --git a/MirrorProvider/MirrorProvider.Windows/WindowsFileSystemVirtualizer.cs b/MirrorProvider/MirrorProvider.Windows/WindowsFileSystemVirtualizer.cs index cecea9dce..5100884cf 100644 --- a/MirrorProvider/MirrorProvider.Windows/WindowsFileSystemVirtualizer.cs +++ b/MirrorProvider/MirrorProvider.Windows/WindowsFileSystemVirtualizer.cs @@ -37,6 +37,8 @@ public override bool TryStartVirtualizationInstance(Enlistment enlistment, out s this.virtualizationInstance.OnNotifyPreDelete = this.OnPreDelete; this.virtualizationInstance.OnNotifyNewFileCreated = this.OnNewFileCreated; this.virtualizationInstance.OnNotifyFileHandleClosedFileModifiedOrDeleted = this.OnFileModifiedOrDeleted; + this.virtualizationInstance.OnNotifyFileRenamed = this.OnFileRenamed; + this.virtualizationInstance.OnNotifyHardlinkCreated = this.OnHardlinkCreated; uint threadCount = (uint)Environment.ProcessorCount * 2; @@ -44,7 +46,9 @@ public override bool TryStartVirtualizationInstance(Enlistment enlistment, out s { new NotificationMapping( NotificationType.NewFileCreated | - NotificationType.PreDelete | + NotificationType.PreDelete | + NotificationType.FileRenamed | + NotificationType.HardlinkCreated | NotificationType.FileHandleClosedFileModified, string.Empty), }; @@ -316,6 +320,22 @@ private void OnFileModifiedOrDeleted(string relativePath, bool isDirectory, bool Console.WriteLine($"OnFileModifiedOrDeleted: `{relativePath}`, isDirectory: {isDirectory}, isModfied: {isFileDeleted}, isDeleted: {isFileDeleted}"); } + private void OnFileRenamed( + string relativeSourcePath, + string relativeDestinationPath, + bool isDirectory, + ref NotificationType notificationMask) + { + Console.WriteLine($"OnFileRenamed (isDirectory: {isDirectory}), relativeSourcePath: {relativeSourcePath}, relativeDestinationPath: {relativeDestinationPath}"); + } + + private void OnHardlinkCreated( + string relativeExistingFilePath, + string relativeNewLinkFilePath) + { + Console.WriteLine($"OnHardlinkCreated, relativeExistingFilePath: {relativeExistingFilePath}, relativeNewLinkFilePath: {relativeNewLinkFilePath}"); + } + // TODO: Add this to the ProjFS API private static HResult HResultFromWin32(int win32error) { diff --git a/ProjFS.Mac/PrjFSKext/PrjFSKext/KauthHandler.cpp b/ProjFS.Mac/PrjFSKext/PrjFSKext/KauthHandler.cpp index 7dc73f78f..c5d749782 100644 --- a/ProjFS.Mac/PrjFSKext/PrjFSKext/KauthHandler.cpp +++ b/ProjFS.Mac/PrjFSKext/PrjFSKext/KauthHandler.cpp @@ -226,6 +226,9 @@ void KauthHandler_HandleKernelMessageResponse(uint64_t messageId, MessageType re case MessageType_KtoU_NotifyFilePreDelete: case MessageType_KtoU_NotifyDirectoryPreDelete: case MessageType_KtoU_NotifyFileCreated: + case MessageType_KtoU_NotifyFileRenamed: + case MessageType_KtoU_NotifyDirectoryRenamed: + case MessageType_KtoU_NotifyFileHardLinkCreated: KextLog_Error("KauthHandler_HandleKernelMessageResponse: Unexpected responseType: %d", responseType); break; } @@ -338,7 +341,8 @@ static int HandleVnodeOperation( KAUTH_VNODE_WRITE_EXTATTRIBUTES | KAUTH_VNODE_READ_DATA | KAUTH_VNODE_WRITE_DATA | - KAUTH_VNODE_EXECUTE)) + KAUTH_VNODE_EXECUTE | + KAUTH_VNODE_DELETE)) // Hydrate on delete to ensure files are hydrated before rename operations { if (FileFlagsBitIsSet(currentVnodeFileFlags, FileFlags_IsEmpty)) { @@ -376,18 +380,66 @@ static int HandleFileOpOperation( atomic_fetch_add(&s_numActiveKauthEvents, 1); vfs_context_t context = vfs_context_create(NULL); + vnode_t currentVnodeFromPath = NULLVP; - if (KAUTH_FILEOP_CLOSE == action) + if (KAUTH_FILEOP_RENAME == action || + KAUTH_FILEOP_LINK == action) { - vnode_t currentVnode = reinterpret_cast(arg0); - // arg1 is the (const char *) path - int closeFlags = static_cast(arg2); + // arg0 is the (const char *) fromPath (or the file being linked to) + const char* newPath = (const char*)arg1; + + // TODO(Mac): We need to handle failures to lookup the vnode. If we fail to lookup the vnode + // it's possible that we'll miss notifications + errno_t toErr = vnode_lookup(newPath, 0 /* flags */, ¤tVnodeFromPath, context); + if (0 != toErr) + { + goto CleanupAndReturn; + } + + VirtualizationRoot* root = nullptr; + int pid; + if (!ShouldHandleFileOpEvent( + context, + currentVnodeFromPath, + action, + &root, + &pid)) + { + goto CleanupAndReturn; + } - vtype vnodeType = vnode_vtype(currentVnode); - if (ShouldIgnoreVnodeType(vnodeType, currentVnode)) + char procname[MAXCOMLEN + 1]; + proc_name(pid, procname, MAXCOMLEN + 1); + + MessageType messageType; + if (KAUTH_FILEOP_RENAME == action) + { + messageType = vnode_isdir(currentVnodeFromPath) ? MessageType_KtoU_NotifyDirectoryRenamed : MessageType_KtoU_NotifyFileRenamed; + } + else + { + messageType = MessageType_KtoU_NotifyFileHardLinkCreated; + } + + int kauthResult; + int kauthError; + if (!TrySendRequestAndWaitForResponse( + root, + messageType, + currentVnodeFromPath, + pid, + procname, + &kauthResult, + &kauthError)) { goto CleanupAndReturn; } + } + else if (KAUTH_FILEOP_CLOSE == action) + { + vnode_t currentVnode = reinterpret_cast(arg0); + // arg1 is the (const char *) path + int closeFlags = static_cast(arg2); if (vnode_isdir(currentVnode)) { @@ -449,7 +501,12 @@ static int HandleFileOpOperation( } } -CleanupAndReturn: +CleanupAndReturn: + if (NULLVP != currentVnodeFromPath) + { + vnode_put(currentVnodeFromPath); + } + vfs_context_rele(context); atomic_fetch_sub(&s_numActiveKauthEvents, 1); @@ -472,6 +529,7 @@ static bool ShouldHandleVnodeOpEvent( char procname[MAXCOMLEN + 1], int* kauthResult) { + *root = nullptr; *kauthResult = KAUTH_RESULT_DEFER; if (!VirtualizationRoot_VnodeIsOnAllowedFilesystem(vnode)) @@ -557,6 +615,12 @@ static bool ShouldHandleFileOpEvent( VirtualizationRoot** root, int* pid) { + vtype vnodeType = vnode_vtype(vnode); + if (ShouldIgnoreVnodeType(vnodeType, vnode)) + { + return false; + } + *root = VirtualizationRoots_FindForVnode(vnode); if (nullptr == *root) { diff --git a/ProjFS.Mac/PrjFSKext/PrjFSKext/VirtualizationRoots.hpp b/ProjFS.Mac/PrjFSKext/PrjFSKext/VirtualizationRoots.hpp index a74d33e74..49389c10c 100644 --- a/ProjFS.Mac/PrjFSKext/PrjFSKext/VirtualizationRoots.hpp +++ b/ProjFS.Mac/PrjFSKext/PrjFSKext/VirtualizationRoots.hpp @@ -18,7 +18,7 @@ struct VirtualizationRoot fsid_t rootFsid; uint64_t rootInode; - // TODO: this should eventually be entirely diagnostic and not used for decisions + // TODO(Mac): this should eventually be entirely diagnostic and not used for decisions char path[PrjFSMaxPath]; int32_t index; diff --git a/ProjFS.Mac/PrjFSKext/public/Message.h b/ProjFS.Mac/PrjFSKext/public/Message.h index 723303327..a4a200fb8 100644 --- a/ProjFS.Mac/PrjFSKext/public/Message.h +++ b/ProjFS.Mac/PrjFSKext/public/Message.h @@ -20,6 +20,9 @@ typedef enum MessageType_KtoU_NotifyFilePreDelete, MessageType_KtoU_NotifyDirectoryPreDelete, MessageType_KtoU_NotifyFileCreated, + MessageType_KtoU_NotifyFileRenamed, + MessageType_KtoU_NotifyDirectoryRenamed, + MessageType_KtoU_NotifyFileHardLinkCreated, // Responses MessageType_Response_Success, diff --git a/ProjFS.Mac/PrjFSLib.Mac.Managed/CallbackDelegates.cs b/ProjFS.Mac/PrjFSLib.Mac.Managed/CallbackDelegates.cs index bb70f658e..769a9743e 100644 --- a/ProjFS.Mac/PrjFSLib.Mac.Managed/CallbackDelegates.cs +++ b/ProjFS.Mac/PrjFSLib.Mac.Managed/CallbackDelegates.cs @@ -31,8 +31,7 @@ public delegate Result NotifyOperationCallback( int triggeringProcessId, string triggeringProcessName, bool isDirectory, - NotificationType notificationType, - string destinationRelativePath); + NotificationType notificationType); // Pre-event notifications public delegate Result NotifyPreDeleteEvent( @@ -51,10 +50,12 @@ public delegate void NotifyNewFileCreatedEvent( bool isDirectory); public delegate void NotifyFileRenamedEvent( - string relativeSourcePath, string relativeDestinationPath, bool isDirectory); + public delegate void NotifyHardLinkCreatedEvent( + string relativeNewLinkPath); + public delegate void NotifyFileModified( string relativePath); diff --git a/ProjFS.Mac/PrjFSLib.Mac.Managed/NotificationType.cs b/ProjFS.Mac/PrjFSLib.Mac.Managed/NotificationType.cs index 7e3c6b084..08b780d72 100644 --- a/ProjFS.Mac/PrjFSLib.Mac.Managed/NotificationType.cs +++ b/ProjFS.Mac/PrjFSLib.Mac.Managed/NotificationType.cs @@ -11,6 +11,7 @@ public enum NotificationType NewFileCreated = 0x00000004, PreDelete = 0x00000010, FileRenamed = 0x00000080, + HardLinkCreated = 0x00000100, PreConvertToFull = 0x00001000, PreModify = 0x10000001, diff --git a/ProjFS.Mac/PrjFSLib.Mac.Managed/VirtualizationInstance.cs b/ProjFS.Mac/PrjFSLib.Mac.Managed/VirtualizationInstance.cs index 40638e54b..b2ddce178 100644 --- a/ProjFS.Mac/PrjFSLib.Mac.Managed/VirtualizationInstance.cs +++ b/ProjFS.Mac/PrjFSLib.Mac.Managed/VirtualizationInstance.cs @@ -16,8 +16,10 @@ public class VirtualizationInstance public virtual NotifyFileModified OnFileModified { get; set; } public virtual NotifyPreDeleteEvent OnPreDelete { get; set; } - public virtual NotifyNewFileCreatedEvent OnNewFileCreated { get; set; } - + public virtual NotifyNewFileCreatedEvent OnNewFileCreated { get; set; } + public virtual NotifyFileRenamedEvent OnFileRenamed { get; set; } + public virtual NotifyHardLinkCreatedEvent OnHardLinkCreated { get; set; } + public static Result ConvertDirectoryToVirtualizationRoot(string fullPath) { return Interop.PrjFSLib.ConvertDirectoryToVirtualizationRoot(fullPath); @@ -131,8 +133,7 @@ private Result OnNotifyOperation( int triggeringProcessId, string triggeringProcessName, bool isDirectory, - NotificationType notificationType, - string destinationRelativePath) + NotificationType notificationType) { switch (notificationType) { @@ -146,6 +147,14 @@ private Result OnNotifyOperation( case NotificationType.NewFileCreated: this.OnNewFileCreated(relativePath, isDirectory); return Result.Success; + + case NotificationType.FileRenamed: + this.OnFileRenamed(relativePath, isDirectory); + return Result.Success; + + case NotificationType.HardLinkCreated: + this.OnHardLinkCreated(relativePath); + return Result.Success; } return Result.ENotYetImplemented; diff --git a/ProjFS.Mac/PrjFSLib/PrjFSLib.cpp b/ProjFS.Mac/PrjFSLib/PrjFSLib.cpp index 5e18ffec9..dd8ab7cec 100644 --- a/ProjFS.Mac/PrjFSLib/PrjFSLib.cpp +++ b/ProjFS.Mac/PrjFSLib/PrjFSLib.cpp @@ -44,6 +44,8 @@ template static bool InitializeEmptyPlaceholder(const cha static bool AddXAttr(const char* path, const char* name, const void* value, size_t size); static bool GetXAttr(const char* path, const char* name, size_t size, _Out_ void* value); +static inline PrjFS_NotificationType KUMessageTypeToNotificationType(MessageType kuNotificationType); + static bool IsVirtualizationRoot(const char* path); static void CombinePaths(const char* root, const char* relative, char (&combined)[PrjFSMaxPath]); @@ -422,48 +424,34 @@ static void HandleKernelRequest(Message request, void* messageMemory) } case MessageType_KtoU_NotifyFileModified: - { - result = HandleFileNotification( - requestHeader, - request.path, - false, // isDirectory - PrjFS_NotificationType_FileModified); - break; - } - case MessageType_KtoU_NotifyFilePreDelete: - { - result = HandleFileNotification( - requestHeader, - request.path, - false, // isDirectory - PrjFS_NotificationType_PreDelete); - break; - } - case MessageType_KtoU_NotifyDirectoryPreDelete: { result = HandleFileNotification( requestHeader, request.path, - true, // isDirectory - PrjFS_NotificationType_PreDelete); + requestHeader->messageType == MessageType_KtoU_NotifyDirectoryPreDelete, // isDirectory + KUMessageTypeToNotificationType(static_cast(requestHeader->messageType))); break; } case MessageType_KtoU_NotifyFileCreated: + case MessageType_KtoU_NotifyFileRenamed: + case MessageType_KtoU_NotifyDirectoryRenamed: + case MessageType_KtoU_NotifyFileHardLinkCreated: { char fullPath[PrjFSMaxPath]; CombinePaths(s_virtualizationRootFullPath.c_str(), request.path, fullPath); // TODO(Mac): Handle SetBitInFileFlags failures SetBitInFileFlags(fullPath, FileFlags_IsInVirtualizationRoot, true); - + + bool isDirectory = requestHeader->messageType == MessageType_KtoU_NotifyDirectoryRenamed; result = HandleFileNotification( requestHeader, request.path, - false, // isDirectory - PrjFS_NotificationType_NewFileCreated); + isDirectory, + KUMessageTypeToNotificationType(static_cast(requestHeader->messageType))); break; } } @@ -726,6 +714,39 @@ static bool GetXAttr(const char* path, const char* name, size_t size, _Out_ void return false; } +static inline PrjFS_NotificationType KUMessageTypeToNotificationType(MessageType kuNotificationType) +{ + switch(kuNotificationType) + { + case MessageType_KtoU_NotifyFileModified: + return PrjFS_NotificationType_FileModified; + + case MessageType_KtoU_NotifyFilePreDelete: + case MessageType_KtoU_NotifyDirectoryPreDelete: + return PrjFS_NotificationType_PreDelete; + + case MessageType_KtoU_NotifyFileCreated: + return PrjFS_NotificationType_NewFileCreated; + + case MessageType_KtoU_NotifyFileRenamed: + case MessageType_KtoU_NotifyDirectoryRenamed: + return PrjFS_NotificationType_FileRenamed; + + case MessageType_KtoU_NotifyFileHardLinkCreated: + return PrjFS_NotificationType_HardLinkCreated; + + // Non-notification types + case MessageType_Invalid: + case MessageType_UtoK_StartVirtualizationInstance: + case MessageType_UtoK_StopVirtualizationInstance: + case MessageType_KtoU_EnumerateDirectory: + case MessageType_KtoU_HydrateFile: + case MessageType_Response_Success: + case MessageType_Response_Fail: + return PrjFS_NotificationType_Invalid; + } +} + static errno_t SendKernelMessageResponse(uint64_t messageId, MessageType responseType) { const uint64_t inputs[] = { messageId, responseType }; @@ -777,6 +798,8 @@ static const char* NotificationTypeToString(PrjFS_NotificationType notificationT return STRINGIFY(PrjFS_NotificationType_PreDelete); case PrjFS_NotificationType_FileRenamed: return STRINGIFY(PrjFS_NotificationType_FileRenamed); + case PrjFS_NotificationType_HardLinkCreated: + return STRINGIFY(PrjFS_NotificationType_HardLinkCreated); case PrjFS_NotificationType_PreConvertToFull: return STRINGIFY(PrjFS_NotificationType_PreConvertToFull); diff --git a/ProjFS.Mac/PrjFSLib/PrjFSLib.h b/ProjFS.Mac/PrjFSLib/PrjFSLib.h index 98b512dc0..ceddbb578 100644 --- a/ProjFS.Mac/PrjFSLib/PrjFSLib.h +++ b/ProjFS.Mac/PrjFSLib/PrjFSLib.h @@ -46,6 +46,7 @@ typedef enum PrjFS_NotificationType_NewFileCreated = 0x00000004, PrjFS_NotificationType_PreDelete = 0x00000010, PrjFS_NotificationType_FileRenamed = 0x00000080, + PrjFS_NotificationType_HardLinkCreated = 0x00000100, PrjFS_NotificationType_PreConvertToFull = 0x00001000, PrjFS_NotificationType_PreModify = 0x10000001,