diff --git a/GVFS/GVFS.Common/FileSystem/IKernelDriver.cs b/GVFS/GVFS.Common/FileSystem/IKernelDriver.cs index a31dc81d63..8e8b6431c0 100644 --- a/GVFS/GVFS.Common/FileSystem/IKernelDriver.cs +++ b/GVFS/GVFS.Common/FileSystem/IKernelDriver.cs @@ -19,5 +19,6 @@ public interface IKernelDriver bool TryPrepareFolderForCallbacks(string folderPath, out string error, out Exception exception); bool IsReady(JsonTracer tracer, string enlistmentRoot, TextWriter output, out string error); bool IsGVFSUpgradeSupported(); + bool RegisterForOfflineIO(); } } diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/RepairTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/RepairTests.cs index e089d20e98..678eb561da 100644 --- a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/RepairTests.cs +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerTestCase/RepairTests.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text; namespace GVFS.FunctionalTests.Tests.EnlistmentPerTestCase @@ -12,6 +13,26 @@ namespace GVFS.FunctionalTests.Tests.EnlistmentPerTestCase [Category(Categories.ExtraCoverage)] public class RepairTests : TestsWithEnlistmentPerTestCase { + private const string PrjFSLibPath = "libPrjFSLib.dylib"; + + [OneTimeSetUp] + public void TurnOfflineIOOn() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + RegisterForOfflineIO(); + } + } + + [OneTimeTearDown] + public void TurnOfflineIOOff() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + UnregisterForOfflineIO(); + } + } + [TestCase] public void NoFixesNeeded() { @@ -154,6 +175,12 @@ public void FixesCorruptGitConfig() this.Enlistment.MountGVFS(); } + [DllImport(PrjFSLibPath, EntryPoint = "PrjFS_RegisterForOfflineIO")] + private static extern uint RegisterForOfflineIO(); + + [DllImport(PrjFSLibPath, EntryPoint = "PrjFS_UnregisterForOfflineIO")] + private static extern uint UnregisterForOfflineIO(); + private void CreateCorruptIndexAndRename(string indexPath, Action corruptionAction) { string tempIndexPath = indexPath + ".lock"; diff --git a/GVFS/GVFS.Platform.Mac/ProjFSKext.cs b/GVFS/GVFS.Platform.Mac/ProjFSKext.cs index ba6ad98f70..e882ab5d95 100644 --- a/GVFS/GVFS.Platform.Mac/ProjFSKext.cs +++ b/GVFS/GVFS.Platform.Mac/ProjFSKext.cs @@ -84,6 +84,11 @@ public bool TryPrepareFolderForCallbacks(string folderPath, out string error, ou } return true; + } + + public bool RegisterForOfflineIO() + { + return PrjFSLib.Mac.Managed.OfflineIO.RegisterForOfflineIO(); } private bool TryLoad(ITracer tracer, TextWriter output, out string errorMessage) diff --git a/GVFS/GVFS.Platform.Windows/ProjFSFilter.cs b/GVFS/GVFS.Platform.Windows/ProjFSFilter.cs index d67dddd1a4..d255f4de64 100644 --- a/GVFS/GVFS.Platform.Windows/ProjFSFilter.cs +++ b/GVFS/GVFS.Platform.Windows/ProjFSFilter.cs @@ -466,6 +466,11 @@ public bool IsReady(JsonTracer tracer, string enlistmentRoot, TextWriter output, TryAttach(enlistmentRoot, out error); } + public bool RegisterForOfflineIO() + { + return true; + } + private static bool IsInboxAndEnabled() { ProcessResult getOptionalFeatureResult = GetProjFSOptionalFeatureStatus(); diff --git a/GVFS/GVFS/Program.cs b/GVFS/GVFS/Program.cs index 2e87b0131f..d2605841a9 100644 --- a/GVFS/GVFS/Program.cs +++ b/GVFS/GVFS/Program.cs @@ -13,6 +13,7 @@ public class Program public static void Main(string[] args) { GVFSPlatformLoader.Initialize(); + GVFSPlatform.Instance.KernelDriver.RegisterForOfflineIO(); Type[] verbTypes = new Type[] { diff --git a/ProjFS.Mac/PrjFS.xcodeproj/project.pbxproj b/ProjFS.Mac/PrjFS.xcodeproj/project.pbxproj index 3da96f6990..3f03d82ed9 100644 --- a/ProjFS.Mac/PrjFS.xcodeproj/project.pbxproj +++ b/ProjFS.Mac/PrjFS.xcodeproj/project.pbxproj @@ -89,6 +89,8 @@ 4A558DBA22357AB000AFDE07 /* ProviderMessaging.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A558DB822357AB000AFDE07 /* ProviderMessaging.cpp */; }; 4A558DBC22357AB000AFDE07 /* ProviderMessaging.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 4A558DB922357AB000AFDE07 /* ProviderMessaging.hpp */; }; 4A558DBD22357AB000AFDE07 /* ProviderMessaging.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 4A558DB922357AB000AFDE07 /* ProviderMessaging.hpp */; }; + 4A5EC302229D5F12005E8D8F /* PrjFSOfflineIOUserClient.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A5EC300229D5F12005E8D8F /* PrjFSOfflineIOUserClient.cpp */; }; + 4A5EC303229D5F12005E8D8F /* PrjFSOfflineIOUserClient.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 4A5EC301229D5F12005E8D8F /* PrjFSOfflineIOUserClient.hpp */; }; 4A781DA52220946000DB7733 /* VirtualizationRootsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4A781DA42220946000DB7733 /* VirtualizationRootsTests.mm */; }; 4A781DA72220971E00DB7733 /* VirtualizationRoots.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4391F8A721E42AC50008103C /* VirtualizationRoots.cpp */; }; 4A781DAB222330F700DB7733 /* KextMockUtilities.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A781DAA222330F700DB7733 /* KextMockUtilities.cpp */; }; @@ -290,6 +292,8 @@ 4A2A69A12297375A00ACAAAF /* KextAssertIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KextAssertIntegration.h; sourceTree = ""; }; 4A558DB822357AB000AFDE07 /* ProviderMessaging.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ProviderMessaging.cpp; sourceTree = ""; }; 4A558DB922357AB000AFDE07 /* ProviderMessaging.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ProviderMessaging.hpp; sourceTree = ""; }; + 4A5EC300229D5F12005E8D8F /* PrjFSOfflineIOUserClient.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = PrjFSOfflineIOUserClient.cpp; sourceTree = ""; }; + 4A5EC301229D5F12005E8D8F /* PrjFSOfflineIOUserClient.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = PrjFSOfflineIOUserClient.hpp; sourceTree = ""; }; 4A781DA42220946000DB7733 /* VirtualizationRootsTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = VirtualizationRootsTests.mm; sourceTree = ""; }; 4A781DA6222094C300DB7733 /* VirtualizationRootsPrivate.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = VirtualizationRootsPrivate.hpp; sourceTree = ""; }; 4A781DA82222C6EA00DB7733 /* PrjFSProviderUserClient.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = PrjFSProviderUserClient.hpp; sourceTree = ""; }; @@ -465,6 +469,8 @@ 4391F89C21E42AC40008103C /* PrjFSKext.cpp */, 4391F8A021E42AC50008103C /* PrjFSLogUserClient.cpp */, 4391F89E21E42AC50008103C /* PrjFSLogUserClient.hpp */, + 4A5EC300229D5F12005E8D8F /* PrjFSOfflineIOUserClient.cpp */, + 4A5EC301229D5F12005E8D8F /* PrjFSOfflineIOUserClient.hpp */, 4391F89521E42AC40008103C /* PrjFSProviderUserClient.cpp */, 4391F89F21E42AC50008103C /* PrjFSProviderUserClientPrivate.hpp */, 4A781DA82222C6EA00DB7733 /* PrjFSProviderUserClient.hpp */, @@ -561,6 +567,7 @@ files = ( 4391F8A821E42AC50008103C /* Locks.hpp in Headers */, 4391F8B821E42AC50008103C /* PrjFSLogUserClient.hpp in Headers */, + 4A5EC303229D5F12005E8D8F /* PrjFSOfflineIOUserClient.hpp in Headers */, 4391F8B221E42AC50008103C /* Memory.hpp in Headers */, 4391F8AE21E42AC50008103C /* VirtualizationRoots.hpp in Headers */, 4391F8BE21E42AC50008103C /* KauthHandler.hpp in Headers */, @@ -868,6 +875,7 @@ 4391F8C121E42AC50008103C /* VirtualizationRoots.cpp in Sources */, 4391F8C021E42AC50008103C /* Locks.cpp in Sources */, 4391F8AD21E42AC50008103C /* KauthHandler.cpp in Sources */, + 4A5EC302229D5F12005E8D8F /* PrjFSOfflineIOUserClient.cpp in Sources */, 4391F8AF21E42AC50008103C /* PrjFSProviderUserClient.cpp in Sources */, 4391F8BA21E42AC50008103C /* PrjFSLogUserClient.cpp in Sources */, 4A558DBA22357AB000AFDE07 /* ProviderMessaging.cpp in Sources */, diff --git a/ProjFS.Mac/PrjFSKext/ArrayUtilities.hpp b/ProjFS.Mac/PrjFSKext/ArrayUtilities.hpp index af873d4677..2df2696117 100644 --- a/ProjFS.Mac/PrjFSKext/ArrayUtilities.hpp +++ b/ProjFS.Mac/PrjFSKext/ArrayUtilities.hpp @@ -22,3 +22,8 @@ template void Array_DefaultInit(T* array, size_t count) } } +template + auto clamp(const T& value, const MIN_T& min, const MAX_T& max) +{ + return value < min ? min : (value > max ? max : value); +} diff --git a/ProjFS.Mac/PrjFSKext/KauthHandler.cpp b/ProjFS.Mac/PrjFSKext/KauthHandler.cpp index ba5a0780b4..425e189a11 100644 --- a/ProjFS.Mac/PrjFSKext/KauthHandler.cpp +++ b/ProjFS.Mac/PrjFSKext/KauthHandler.cpp @@ -28,18 +28,19 @@ #include "KauthHandlerTestable.hpp" #endif -template - auto clamp(const T& value, const MIN_T& min, const MAX_T& max) -{ - return value < min ? min : (value > max ? max : value); -} - enum ProviderCallbackPolicy { CallbackPolicy_AllowAny, CallbackPolicy_UserInitiatedOnly, }; +enum ProviderStatus +{ + Provider_StatusUnknown, + Provider_IsOffline, + Provider_IsOnline, +}; + struct PendingRenameOperation { vnode_t vnode; @@ -97,12 +98,14 @@ static bool TryGetVirtualizationRoot( const vnode_t vnode, pid_t pidMakingRequest, ProviderCallbackPolicy callbackPolicy, - + bool denyIfOffline, + // Out params: VirtualizationRootHandle* root, FsidInode* vnodeFsidInode, int* kauthResult, - int* kauthError); + int* kauthError, + ProviderStatus* _Nullable providerStatus = nullptr); KEXT_STATIC bool ShouldHandleFileOpEvent( // In params: @@ -130,6 +133,7 @@ static SpinLock s_renameLock; static PendingRenameOperation* s_pendingRenames = nullptr; KEXT_STATIC uint32_t s_pendingRenameCount = 0; KEXT_STATIC uint32_t s_maxPendingRenames = 0; +static bool s_osSupportsRenameDetection = false; // Public functions kern_return_t KauthHandler_Init() @@ -249,7 +253,9 @@ KEXT_STATIC void UseMainForkIfNamedStream( KEXT_STATIC bool InitPendingRenames() { - if (version_major >= PrjFSDarwinMajorVersion::MacOS10_14_Mojave) // Only need to track renames on Mojave and newer + // Only need to/can track renames on Mojave and newer + s_osSupportsRenameDetection = (version_major >= PrjFSDarwinMajorVersion::MacOS10_14_Mojave); + if (s_osSupportsRenameDetection) { s_renameLock = SpinLock_Alloc(); s_maxPendingRenames = 8; // Arbitrary choice, should be maximum number of expected concurrent threads performing renames, but array will resize on demand @@ -268,7 +274,7 @@ KEXT_STATIC bool InitPendingRenames() KEXT_STATIC void CleanupPendingRenames() { - if (version_major >= PrjFSDarwinMajorVersion::MacOS10_14_Mojave) + if (s_osSupportsRenameDetection) { if (SpinLock_IsValid(s_renameLock)) { @@ -322,7 +328,7 @@ KEXT_STATIC void ResizePendingRenames(uint32_t newMaxPendingRenames) KEXT_STATIC void RecordPendingRenameOperation(vnode_t vnode) { - assertf(version_major >= PrjFSDarwinMajorVersion::MacOS10_14_Mojave, "This function should only be called from the KAUTH_FILEOP_WILL_RENAME handler, which is only supported by Darwin 18 (macOS 10.14 Mojave) and newer (version_major = %u)", version_major); + assertf(s_osSupportsRenameDetection, "This function should only be called from the KAUTH_FILEOP_WILL_RENAME handler, which is only supported by Darwin 18 (macOS 10.14 Mojave) and newer (version_major = %u)", version_major); thread_t myThread = current_thread(); bool resizeTable; @@ -367,7 +373,7 @@ KEXT_STATIC void RecordPendingRenameOperation(vnode_t vnode) KEXT_STATIC bool DeleteOpIsForRename(vnode_t vnode) { - if (version_major < PrjFSDarwinMajorVersion::MacOS10_14_Mojave) + if (!s_osSupportsRenameDetection) { // High Sierra and earlier do not support WILL_RENAME notification, so we have to assume any delete may be caused by a rename assert(s_pendingRenameCount == 0); @@ -483,8 +489,21 @@ KEXT_STATIC int HandleVnodeOperation( // Recursively expand directory on rename as user will expect the moved directory to have the same contents as in its original location if (isRename) { - // Prevent system services from expanding directories as part of enumeration as this tends to cause deadlocks with the kauth listeners for Antivirus software - if (!TryGetVirtualizationRoot(&perfTracer, context, currentVnode, pid, CallbackPolicy_UserInitiatedOnly, &root, &vnodeFsidInode, &kauthResult, kauthError)) + if (!TryGetVirtualizationRoot( + &perfTracer, + context, + currentVnode, + pid, + // Prevent system services from expanding directories as part of enumeration as this tends to cause deadlocks with the kauth listeners for Antivirus software + CallbackPolicy_UserInitiatedOnly, + // We want to block directory renames when provider is offline, but we can only do this on + // newer OS versions as any delete operation could be a rename on High Sierra and older, + // so on those versions, isRename is true for all delete events. + s_osSupportsRenameDetection, + &root, + &vnodeFsidInode, + &kauthResult, + kauthError)) { goto CleanupAndReturn; } @@ -508,8 +527,18 @@ KEXT_STATIC int HandleVnodeOperation( } else if (FileFlagsBitIsSet(currentVnodeFileFlags, FileFlags_IsEmpty)) { - // Prevent system services from expanding directories as part of enumeration as this tends to cause deadlocks with the kauth listeners for Antivirus software - if (!TryGetVirtualizationRoot(&perfTracer, context, currentVnode, pid, CallbackPolicy_UserInitiatedOnly, &root, &vnodeFsidInode, &kauthResult, kauthError)) + if (!TryGetVirtualizationRoot( + &perfTracer, + context, + currentVnode, + pid, + // Prevent system services from expanding directories as part of enumeration as this tends to cause deadlocks with the kauth listeners for Antivirus software + CallbackPolicy_UserInitiatedOnly, + false, // allow reading offline directories even if not expanded + &root, + &vnodeFsidInode, + &kauthResult, + kauthError)) { goto CleanupAndReturn; } @@ -549,8 +578,25 @@ KEXT_STATIC int HandleVnodeOperation( { if (FileFlagsBitIsSet(currentVnodeFileFlags, FileFlags_IsEmpty)) { - // Prevent system services from hydrating files as this tends to cause deadlocks with the kauth listeners for Antivirus software - if (!TryGetVirtualizationRoot(&perfTracer, context, currentVnode, pid, CallbackPolicy_UserInitiatedOnly, &root, &vnodeFsidInode, &kauthResult, kauthError)) + bool isWriteOperation = ActionBitIsSet( + action, + KAUTH_VNODE_WRITE_ATTRIBUTES | + KAUTH_VNODE_WRITE_EXTATTRIBUTES | + KAUTH_VNODE_WRITE_DATA | + KAUTH_VNODE_APPEND_DATA); + if (!TryGetVirtualizationRoot( + &perfTracer, + context, + currentVnode, + pid, + // Prevent system services from hydrating files as this tends to cause deadlocks with the kauth listeners for Antivirus software + CallbackPolicy_UserInitiatedOnly, + // Prevent write access to empty files in offline roots. For now allow reads, and always allow the user to delete files. + isWriteOperation || (isRename && s_osSupportsRenameDetection), + &root, + &vnodeFsidInode, + &kauthResult, + kauthError)) { goto CleanupAndReturn; } @@ -575,19 +621,68 @@ KEXT_STATIC int HandleVnodeOperation( if (ActionBitIsSet(action, KAUTH_VNODE_WRITE_DATA | KAUTH_VNODE_APPEND_DATA)) { - if (!TryGetVirtualizationRoot(&perfTracer, context, currentVnode, pid, CallbackPolicy_UserInitiatedOnly, &root, &vnodeFsidInode, &kauthResult, kauthError)) + /* At this stage we know the file is NOT empty, but it could be + * in either placeholder (hydrated) or full (modified/added to + * watchlist) state. + * + * If it's a placeholder, we need to notify the provider before + * allowing modifications so that it is tracked as a full file + * (and the placeholder xattr is removed). + * If the provider is offline in this situation, we want to + * prevent writes (by most processes) to the file so that the + * modifications don't go undetected. + * + * If it's already a full file on the other hand, nothing needs + * to be done, so we can allow writes even when the provider is + * offline. + */ + ProviderStatus providerStatus; + bool shouldMessageProvider = TryGetVirtualizationRoot( + &perfTracer, + context, + currentVnode, + pid, + CallbackPolicy_UserInitiatedOnly, + // At this stage, we don't yet know if the file is still a placeholder, so don't deny yet even if offline + false, // denyIfOffline + &root, + &vnodeFsidInode, + &kauthResult, + kauthError, + &providerStatus); + + if (!shouldMessageProvider) { - goto CleanupAndReturn; + // If we don't need to message the provider, that's normally + // the end of it, but in the case where the provider is + // offline we need to do further checks. + if (providerStatus != Provider_IsOffline) + { + goto CleanupAndReturn; + } } PrjFSFileXAttrData rootXattr = {}; SizeOrError xattrResult = Vnode_ReadXattr(currentVnode, PrjFSFileXAttrName, &rootXattr, sizeof(rootXattr)); if (xattrResult.error == ENOATTR) { - // Only notify if the file is still a placeholder + // If the file does not have the attribute, this means it's + // already in the "full" state, so no need to send a provider + // message or block the I/O if the provider is offline goto CleanupAndReturn; } + if (providerStatus == Provider_IsOffline) + { + assert(!shouldMessageProvider); + // Deny write access to placeholders in an offline root + kauthResult = KAUTH_RESULT_DENY; + goto CleanupAndReturn; + } + + assert(shouldMessageProvider); + assert(providerStatus == Provider_IsOnline); + PerfSample preConvertToFullSample(&perfTracer, PrjFSPerfCounter_VnodeOp_PreConvertToFull); if (!ProviderMessaging_TrySendRequestAndWaitForResponse( @@ -610,8 +705,20 @@ KEXT_STATIC int HandleVnodeOperation( if (isDeleteAction) { - // Allow any user to delete individual files, as this generally doesn't cause nested kauth callbacks. - if (!TryGetVirtualizationRoot(&perfTracer, context, currentVnode, pid, CallbackPolicy_AllowAny, &root, &vnodeFsidInode, &kauthResult, kauthError)) + if (!TryGetVirtualizationRoot( + &perfTracer, + context, + currentVnode, + pid, + // Allow any user to delete individual files, as this generally doesn't cause nested kauth callbacks. + CallbackPolicy_AllowAny, + // Allow deletes even if provider offline, except renames. Allow all deletes + // on OS versions where we can't distinguish renames & other deletes. + isRename && s_osSupportsRenameDetection, + &root, + &vnodeFsidInode, + &kauthResult, + kauthError)) { goto CleanupAndReturn; } @@ -1089,15 +1196,22 @@ static bool TryGetVirtualizationRoot( const vnode_t vnode, pid_t pidMakingRequest, ProviderCallbackPolicy callbackPolicy, + bool denyIfOffline, // Out params: VirtualizationRootHandle* root, FsidInode* vnodeFsidInode, int* kauthResult, - int* kauthError) + int* kauthError, + ProviderStatus* _Nullable providerStatus) { PerfSample findRootSample(perfTracer, PrjFSPerfCounter_VnodeOp_GetVirtualizationRoot); + if (providerStatus != nullptr) + { + *providerStatus = Provider_StatusUnknown; + } + *root = VnodeCache_FindRootForVnode( perfTracer, PrjFSPerfCounter_VnodeOp_Vnode_Cache_Hit, @@ -1124,14 +1238,21 @@ static bool TryGetVirtualizationRoot( } ActiveProviderProperties provider = VirtualizationRoot_GetActiveProvider(*root); + if (providerStatus != nullptr) + { + *providerStatus = provider.isOnline ? Provider_IsOnline : Provider_IsOffline; + } + if (!provider.isOnline) { - // TODO(#182): Protect files in the worktree from modification (and prevent - // the creation of new files) when the provider is offline - perfTracer->IncrementCount(PrjFSPerfCounter_VnodeOp_GetVirtualizationRoot_ProviderOffline); *kauthResult = KAUTH_RESULT_DEFER; + if (denyIfOffline && !VirtualizationRoots_ProcessMayAccessOfflineRoots(pidMakingRequest)) + { + *kauthResult = KAUTH_RESULT_DENY; + } + return false; } diff --git a/ProjFS.Mac/PrjFSKext/KextLog.hpp b/ProjFS.Mac/PrjFSKext/KextLog.hpp index fdb7dc524d..00cef0de0f 100644 --- a/ProjFS.Mac/PrjFSKext/KextLog.hpp +++ b/ProjFS.Mac/PrjFSKext/KextLog.hpp @@ -110,7 +110,7 @@ template #define KextLog_VnodeOp(vnode, vnodeType, procname, action, message) \ do { \ - if (VDIR == vnodeType) \ + if (VDIR == (vnodeType)) \ { \ KextLog_File( \ vnode, \ diff --git a/ProjFS.Mac/PrjFSKext/PrjFSClasses.hpp b/ProjFS.Mac/PrjFSKext/PrjFSClasses.hpp index 5a2a33d193..1693f79a91 100644 --- a/ProjFS.Mac/PrjFSKext/PrjFSClasses.hpp +++ b/ProjFS.Mac/PrjFSKext/PrjFSClasses.hpp @@ -1,9 +1,11 @@ #pragma once // IOKit class naming convention is to use reverse DNS notation -#define PrjFSService org_vfsforgit_PrjFS +#define PrjFSService org_vfsforgit_PrjFS class PrjFSService; -#define PrjFSProviderUserClient org_vfsforgit_PrjFSProviderUserClient +#define PrjFSProviderUserClient org_vfsforgit_PrjFSProviderUserClient class PrjFSProviderUserClient; -#define PrjFSLogUserClient org_vfsforgit_PrjFSLogUserClient +#define PrjFSLogUserClient org_vfsforgit_PrjFSLogUserClient class PrjFSLogUserClient; +#define PrjFSOfflineIOUserClient org_vfsforgit_PrjFSOfflineIOUserClient +class PrjFSOfflineIOUserClient; diff --git a/ProjFS.Mac/PrjFSKext/PrjFSOfflineIOUserClient.cpp b/ProjFS.Mac/PrjFSKext/PrjFSOfflineIOUserClient.cpp new file mode 100644 index 0000000000..137b35f8b9 --- /dev/null +++ b/ProjFS.Mac/PrjFSKext/PrjFSOfflineIOUserClient.cpp @@ -0,0 +1,41 @@ +#include "PrjFSOfflineIOUserClient.hpp" +#include "VirtualizationRoots.hpp" +#include + +OSDefineMetaClassAndStructors(PrjFSOfflineIOUserClient, IOUserClient); + +bool PrjFSOfflineIOUserClient::initWithTask( + task_t owningTask, void* securityToken, UInt32 type, OSDictionary* properties) +{ + if (!this->super::initWithTask(owningTask, securityToken, type, properties)) + { + return false; + } + + this->pid = proc_selfpid(); + bool success = VirtualizationRoots_AddOfflineIOProcess(this->pid); + if (!success) + { + this->pid = 0; + } + + return success; +} + +IOReturn PrjFSOfflineIOUserClient::clientClose() +{ + this->terminate(0); + return kIOReturnSuccess; +} + +void PrjFSOfflineIOUserClient::stop(IOService* provider) +{ + if (this->pid != 0) + { + VirtualizationRoots_RemoveOfflineIOProcess(this->pid); + this->pid = 0; + } + + this->super::stop(provider); +} + diff --git a/ProjFS.Mac/PrjFSKext/PrjFSOfflineIOUserClient.hpp b/ProjFS.Mac/PrjFSKext/PrjFSOfflineIOUserClient.hpp new file mode 100644 index 0000000000..6832929209 --- /dev/null +++ b/ProjFS.Mac/PrjFSKext/PrjFSOfflineIOUserClient.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "PrjFSClasses.hpp" +#include + +class PrjFSOfflineIOUserClient : public IOUserClient +{ + OSDeclareDefaultStructors(PrjFSOfflineIOUserClient); +private: + typedef IOUserClient super; + pid_t pid; + +public: + // IOUserClient methods: + virtual bool initWithTask(task_t owningTask, void* securityToken, UInt32 type, OSDictionary* properties) override; + virtual IOReturn clientClose() override; + + // IOService methods: + virtual void stop(IOService* provider) override; +}; + diff --git a/ProjFS.Mac/PrjFSKext/PrjFSService.cpp b/ProjFS.Mac/PrjFSKext/PrjFSService.cpp index 5a542bc2b5..7e3eddc356 100644 --- a/ProjFS.Mac/PrjFSKext/PrjFSService.cpp +++ b/ProjFS.Mac/PrjFSKext/PrjFSService.cpp @@ -3,6 +3,7 @@ #include "PrjFSService.hpp" #include "PrjFSProviderUserClientPrivate.hpp" #include "PrjFSLogUserClient.hpp" +#include "PrjFSOfflineIOUserClient.hpp" #include "KextLog.hpp" #include "VirtualizationRoots.hpp" @@ -124,6 +125,16 @@ IOReturn PrjFSService::newUserClient( } } break; + case UserClientType_OfflineIO: + { + PrjFSOfflineIOUserClient* offline_io_client = new PrjFSOfflineIOUserClient(); + if (InitAttachAndStartUserClient(this, offline_io_client, owningTask, securityID, type, properties)) + { + *handler = offline_io_client; + result = kIOReturnSuccess; + } + } + break; } return result; diff --git a/ProjFS.Mac/PrjFSKext/ProviderMessaging.cpp b/ProjFS.Mac/PrjFSKext/ProviderMessaging.cpp index da037761b8..6e1402cd5a 100644 --- a/ProjFS.Mac/PrjFSKext/ProviderMessaging.cpp +++ b/ProjFS.Mac/PrjFSKext/ProviderMessaging.cpp @@ -179,14 +179,15 @@ bool ProviderMessaging_TrySendRequestAndWaitForResponse( { if (0 != sendError) { - // TODO: appropriately handle unresponsive providers - *kauthResult = KAUTH_RESULT_DEFER; + // If message delivery fails, block the operation to avoid missed messages in the provider. + *kauthResult = KAUTH_RESULT_DENY; } else { while (!message.receivedResult && !s_isShuttingDown) { + // TODO: appropriately handle unresponsive providers Mutex_Sleep(5, &message, &s_outstandingMessagesMutex); } diff --git a/ProjFS.Mac/PrjFSKext/VirtualizationRoots.cpp b/ProjFS.Mac/PrjFSKext/VirtualizationRoots.cpp index 7dc7d6a7ba..3688803802 100644 --- a/ProjFS.Mac/PrjFSKext/VirtualizationRoots.cpp +++ b/ProjFS.Mac/PrjFSKext/VirtualizationRoots.cpp @@ -28,6 +28,11 @@ KEXT_STATIC uint16_t s_maxVirtualizationRoots = 0; static const uint16_t MaxVirtualizationRootsLimit = INT16_MAX + 1u; KEXT_STATIC VirtualizationRoot* s_virtualizationRoots = nullptr; +static constexpr uint32_t MaxOfflineIOPIDs = 128; +// Also protected by the lock +static uint32_t s_offlineIOPIDCount = 0; +static pid_t s_offlineIOPIDs[MaxOfflineIOPIDs] = {}; + // Looks up the vnode/vid and fsid/inode pairs among the known roots static VirtualizationRootHandle FindRootAtVnode_Locked(vnode_t vnode, uint32_t vid, FsidInode fileId); @@ -120,6 +125,8 @@ kern_return_t VirtualizationRoots_Cleanup() s_maxVirtualizationRoots = 0; } + assert(s_offlineIOPIDCount == 0); + if (RWLock_IsValid(s_virtualizationRootsLock)) { RWLock_FreeMemory(&s_virtualizationRootsLock); @@ -650,3 +657,70 @@ VirtualizationRootHandle ActiveProvider_FindForPath(const char* _Nonnull path) return matchingHandle; } + +bool VirtualizationRoots_ProcessMayAccessOfflineRoots(pid_t pid) +{ + bool result = false; + + RWLock_AcquireShared(s_virtualizationRootsLock); + { + for (uint32_t i = 0; i < s_offlineIOPIDCount; ++i) + { + if (s_offlineIOPIDs[i] == pid) + { + result = true; + break; + } + } + } + RWLock_ReleaseShared(s_virtualizationRootsLock); + + return result; +} + +bool VirtualizationRoots_AddOfflineIOProcess(pid_t pid) +{ + bool success = false; + + RWLock_AcquireExclusive(s_virtualizationRootsLock); + { + if (s_offlineIOPIDCount < MaxOfflineIOPIDs) + { + s_offlineIOPIDs[s_offlineIOPIDCount] = pid; + ++s_offlineIOPIDCount; + success = true; + } + } + RWLock_ReleaseExclusive(s_virtualizationRootsLock); + + return success; +} + +void VirtualizationRoots_RemoveOfflineIOProcess(pid_t pid) +{ + bool removed = false; + + RWLock_AcquireExclusive(s_virtualizationRootsLock); + { + assert(s_offlineIOPIDCount > 0); + + for (uint32_t i = 0; i < s_offlineIOPIDCount; ++i) + { + if (s_offlineIOPIDs[i] == pid) + { + --s_offlineIOPIDCount; + if (i != s_offlineIOPIDCount) + { + // move last element to fill vacated slot + s_offlineIOPIDs[i] = s_offlineIOPIDs[s_offlineIOPIDCount]; + } + + removed = true; + break; + } + } + } + RWLock_ReleaseExclusive(s_virtualizationRootsLock); + + assert(removed); +} diff --git a/ProjFS.Mac/PrjFSKext/VirtualizationRoots.hpp b/ProjFS.Mac/PrjFSKext/VirtualizationRoots.hpp index b0b97f6533..7c27c23ada 100644 --- a/ProjFS.Mac/PrjFSKext/VirtualizationRoots.hpp +++ b/ProjFS.Mac/PrjFSKext/VirtualizationRoots.hpp @@ -52,3 +52,6 @@ errno_t ActiveProvider_SendMessage(VirtualizationRootHandle rootHandle, const Me bool VirtualizationRoot_VnodeIsOnAllowedFilesystem(vnode_t _Nonnull vnode); bool VirtualizationRoot_IsValidRootHandle(VirtualizationRootHandle rootHandle); ActiveProviderProperties VirtualizationRoot_GetActiveProvider(VirtualizationRootHandle rootHandle); +bool VirtualizationRoots_ProcessMayAccessOfflineRoots(pid_t pid); +bool VirtualizationRoots_AddOfflineIOProcess(pid_t pid); +void VirtualizationRoots_RemoveOfflineIOProcess(pid_t pid); diff --git a/ProjFS.Mac/PrjFSKext/public/PrjFSCommon.h b/ProjFS.Mac/PrjFSKext/public/PrjFSCommon.h index 54965a41d8..6357e90246 100644 --- a/ProjFS.Mac/PrjFSKext/public/PrjFSCommon.h +++ b/ProjFS.Mac/PrjFSKext/public/PrjFSCommon.h @@ -32,6 +32,7 @@ enum PrjFSServiceUserClientType UserClientType_Provider, UserClientType_Log, + UserClientType_OfflineIO, }; // When building the kext in user space for unit testing, we want some functions diff --git a/ProjFS.Mac/PrjFSKextTests/HandleOperationTests.mm b/ProjFS.Mac/PrjFSKextTests/HandleOperationTests.mm index d94128af9d..ffd3554150 100644 --- a/ProjFS.Mac/PrjFSKextTests/HandleOperationTests.mm +++ b/ProjFS.Mac/PrjFSKextTests/HandleOperationTests.mm @@ -22,11 +22,14 @@ using std::shared_ptr; using std::vector; using std::string; +using std::extent; using KextMock::_; // Darwin version of running kernel int version_major = PrjFSDarwinMajorVersion::MacOS10_14_Mojave; +static const pid_t RunningMockProcessPID = 501; + class PrjFSProviderUserClient { }; @@ -99,9 +102,9 @@ - (void) setUp XCTAssertEqual(result.error, 0); self->dummyRepoHandle = result.root; - MockProcess_AddContext(context, 501 /*pid*/); - MockProcess_SetSelfPid(501); - MockProcess_AddProcess(501 /*pid*/, 1 /*credentialId*/, 1 /*ppid*/, "test" /*name*/); + MockProcess_AddContext(context, RunningMockProcessPID /*pid*/); + MockProcess_SetSelfPid(RunningMockProcessPID); + MockProcess_AddProcess(RunningMockProcessPID, 1 /*credentialId*/, 1 /*ppid*/, "test" /*name*/); ProvidermessageMock_ResetResultCount(); ProviderMessageMock_SetDefaultRequestResult(true); @@ -447,6 +450,7 @@ - (void) testDeleteFileNonRenamed { testFileVnode->attrValues.va_flags = FileFlags_IsEmpty | FileFlags_IsInVirtualizationRoot; + InitPendingRenames(); XCTAssertTrue(HandleVnodeOperation( nullptr, nullptr, @@ -481,10 +485,12 @@ - (void) testDeleteFileNonRenamed _, _, nullptr)); + CleanupPendingRenames(); } - (void) testConcurrentRenameOperationRecording { + InitPendingRenames(); self->testFileVnode->attrValues.va_flags = FileFlags_IsEmpty | FileFlags_IsInVirtualizationRoot; string otherFilePath = repoPath + "/otherFile"; @@ -585,6 +591,7 @@ - (void) testConcurrentRenameOperationRecording nullptr)) ); XCTAssertTrue(MockCalls::CallCount(ProviderMessaging_TrySendRequestAndWaitForResponse) == 4); + CleanupPendingRenames(); } @@ -645,6 +652,7 @@ - (void) testDeleteDirNonRenamed testDirVnode->attrValues.va_flags = FileFlags_IsInVirtualizationRoot; TestForAllSupportedDarwinVersions(^{ + InitPendingRenames(); XCTAssertTrue(HandleVnodeOperation( nullptr, nullptr, @@ -689,6 +697,7 @@ - (void) testDeleteDirNonRenamed } MockCalls::Clear(); + CleanupPendingRenames(); }); } @@ -881,6 +890,67 @@ -(void) testWriteFileHydrated { XCTAssertTrue(MockCalls::CallCount(ProviderMessaging_TrySendRequestAndWaitForResponse) == 1); } +-(void) testWriteFileHydratedOfflineRoot +{ + testFileVnode->attrValues.va_flags = FileFlags_IsInVirtualizationRoot; + SetPrjFSFileXattrData(testFileVnode); + ActiveProvider_Disconnect(self->dummyRepoHandle, &self->dummyClient); + + XCTAssertEqual( + KAUTH_RESULT_DENY, + HandleVnodeOperation( + nullptr, + nullptr, + KAUTH_VNODE_WRITE_DATA, + reinterpret_cast(context), + reinterpret_cast(testFileVnode.get()), + 0, + 0)); + XCTAssertFalse( + MockCalls::DidCallFunction( + ProviderMessaging_TrySendRequestAndWaitForResponse, + _, + _, + testFileVnode.get(), + _, + _, + _, + _, + _, + _, + nullptr)); +} + +-(void) testWriteFlaggedNonRepoFile +{ + string testFilePath = "/Users/test/code/otherproject/file"; + shared_ptr testFile = testMount->CreateVnodeTree(testFilePath); + testFile->attrValues.va_flags = FileFlags_IsInVirtualizationRoot; + XCTAssertEqual( + KAUTH_RESULT_DEFER, + HandleVnodeOperation( + nullptr, + nullptr, + KAUTH_VNODE_WRITE_DATA, + reinterpret_cast(context), + reinterpret_cast(testFile.get()), + 0, + 0)); + XCTAssertFalse( + MockCalls::DidCallFunction( + ProviderMessaging_TrySendRequestAndWaitForResponse, + _, + _, + testFileVnode.get(), + _, + _, + _, + _, + _, + _, + nullptr)); +} + -(void) testWriteFileFull { testFileVnode->attrValues.va_flags = FileFlags_IsInVirtualizationRoot; XCTAssertTrue(HandleVnodeOperation( @@ -1125,20 +1195,32 @@ - (void) testReadAttributesWhenRequestFails { } - (void) testWriteWithDisappearingVirtualizationRoot { + // Tests provider disappearing between hydration and attempting to convert to full. + + // Start with empty file… + testFileVnode->attrValues.va_flags = FileFlags_IsEmpty | FileFlags_IsInVirtualizationRoot; + SetPrjFSFileXattrData(self->testFileVnode); + + // First message to provider marks the file as hydrated and also disconnects the providers ProviderMessageMock_SetRequestSideEffect( [&]() { ActiveProvider_Disconnect(self->dummyRepoHandle, &self->dummyClient); + // mark as hydrated + testFileVnode->attrValues.va_flags &= ~FileFlags_IsEmpty; }); - testFileVnode->attrValues.va_flags = FileFlags_IsEmpty | FileFlags_IsInVirtualizationRoot; - XCTAssertTrue(HandleVnodeOperation( - nullptr, - nullptr, - KAUTH_VNODE_WRITE_DATA, - reinterpret_cast(context), - reinterpret_cast(testFileVnode.get()), - 0, - 0) == KAUTH_RESULT_DEFER); + + // File should become hydrated but write access should be denied due to failure to convert to full. + XCTAssertEqual( + KAUTH_RESULT_DENY, + HandleVnodeOperation( + nullptr, + nullptr, + KAUTH_VNODE_WRITE_DATA, + reinterpret_cast(context), + reinterpret_cast(testFileVnode.get()), + 0, + 0)); XCTAssertTrue(MockCalls::CallCount(ProviderMessaging_TrySendRequestAndWaitForResponse) == 1); } @@ -1196,4 +1278,181 @@ - (void) testIneligibleFilesystemType XCTAssertFalse(MockCalls::DidCallFunction(ProviderMessaging_TrySendRequestAndWaitForResponse)); } +- (void) testOfflineRootDeniesAccessToEmptyFile +{ + testFileVnode->attrValues.va_flags = FileFlags_IsEmpty | FileFlags_IsInVirtualizationRoot; + SetPrjFSFileXattrData(testFileVnode); + + ActiveProvider_Disconnect(self->dummyRepoHandle, &self->dummyClient); + + kauth_action_t actions[] = + { + // TODO(Mac): Also deny read access to empty files. (#182) +// KAUTH_VNODE_READ_ATTRIBUTES, + KAUTH_VNODE_WRITE_ATTRIBUTES, +// KAUTH_VNODE_READ_EXTATTRIBUTES, + KAUTH_VNODE_WRITE_EXTATTRIBUTES, +// KAUTH_VNODE_READ_DATA, + KAUTH_VNODE_WRITE_DATA, +// KAUTH_VNODE_EXECUTE, + KAUTH_VNODE_APPEND_DATA, + }; + const size_t actionCount = extent::value; + + for (size_t i = 0; i < actionCount; i++) + { + XCTAssertEqual( + KAUTH_RESULT_DENY, + HandleVnodeOperation( + nullptr, + nullptr, + actions[i], + reinterpret_cast(context), + reinterpret_cast(testFileVnode.get()), + 0, + 0)); + XCTAssertFalse( + MockCalls::DidCallFunction( + ProviderMessaging_TrySendRequestAndWaitForResponse, + _, + MessageType_KtoU_HydrateFile, + testFileVnode.get(), + _, + _, + _, + _, + _, + _, + nullptr)); + MockCalls::Clear(); + } +} + +- (void) testOfflineRootDeniesRename +{ + // Where we can detect renames (Mojave and newer), file/directory renames + // should be prevented when the provider is offline. On High Sierra we have + // to let them happen as we can't distinguish them from file deletions + // before it's already happened. + + ActiveProvider_Disconnect(self->dummyRepoHandle, &self->dummyClient); + + // Check the behaviour works for both empty and full files, and empty files are not hydrated. + vector vnode_flags { FileFlags_IsInVirtualizationRoot, FileFlags_IsInVirtualizationRoot | FileFlags_IsEmpty }; + + std::for_each(vnode_flags.begin(), vnode_flags.end(), + [self](uint32_t flags) + { + testFileVnode->attrValues.va_flags = flags; + + TestForAllSupportedDarwinVersions(^{ + InitPendingRenames(); + + if (version_major >= PrjFSDarwinMajorVersion::MacOS10_14_Mojave) + { + string renamedFilePath = self->filePath + "_renamed"; + HandleFileOpOperation( + nullptr, // credential + nullptr, /* idata, unused */ + KAUTH_FILEOP_WILL_RENAME, + reinterpret_cast(self->testFileVnode.get()), + reinterpret_cast(self->filePath.c_str()), + reinterpret_cast(renamedFilePath.c_str()), + 0); // unused + } + + int deleteAuthResult = + HandleVnodeOperation( + nullptr, + nullptr, + KAUTH_VNODE_DELETE, + reinterpret_cast(self->context), + reinterpret_cast(self->testFileVnode.get()), + 0, + 0); + + if (version_major <= PrjFSDarwinMajorVersion::MacOS10_13_HighSierra) + { + XCTAssertEqual(deleteAuthResult, KAUTH_RESULT_DEFER); + } + else + { + // On Mojave+, renames should be blocked: + XCTAssertEqual(deleteAuthResult, KAUTH_RESULT_DENY, "flags = 0x%x", flags); + } + + XCTAssertFalse( + MockCalls::DidCallFunction( + ProviderMessaging_TrySendRequestAndWaitForResponse, + _, + MessageType_KtoU_HydrateFile, + self->testFileVnode.get(), + _, + _, + _, + _, + _, + _, + nullptr)); + + MockCalls::Clear(); + CleanupPendingRenames(); + }); + }); +} + +- (void) testOfflineRootAllowsRegisteredProcessAccessToEmptyFile +{ + testFileVnode->attrValues.va_flags = FileFlags_IsEmpty | FileFlags_IsInVirtualizationRoot; + SetPrjFSFileXattrData(testFileVnode); + + ActiveProvider_Disconnect(self->dummyRepoHandle, &self->dummyClient); + + XCTAssertTrue(VirtualizationRoots_AddOfflineIOProcess(RunningMockProcessPID)); + + kauth_action_t actions[] = + { + KAUTH_VNODE_READ_ATTRIBUTES, + KAUTH_VNODE_WRITE_ATTRIBUTES, + KAUTH_VNODE_READ_EXTATTRIBUTES, + KAUTH_VNODE_WRITE_EXTATTRIBUTES, + KAUTH_VNODE_READ_DATA, + KAUTH_VNODE_WRITE_DATA, + KAUTH_VNODE_EXECUTE, + KAUTH_VNODE_DELETE, + KAUTH_VNODE_APPEND_DATA, + }; + const size_t actionCount = extent::value; + + for (size_t i = 0; i < actionCount; i++) + { + XCTAssertEqual( + KAUTH_RESULT_DEFER, + HandleVnodeOperation( + nullptr, + nullptr, + actions[i], + reinterpret_cast(context), + reinterpret_cast(testFileVnode.get()), + 0, + 0)); + XCTAssertFalse( + MockCalls::DidCallFunction( + ProviderMessaging_TrySendRequestAndWaitForResponse, + _, + MessageType_KtoU_HydrateFile, + testFileVnode.get(), + _, + _, + _, + _, + _, + _, + nullptr)); + MockCalls::Clear(); + } + + VirtualizationRoots_RemoveOfflineIOProcess(RunningMockProcessPID); +} + @end diff --git a/ProjFS.Mac/PrjFSKextTests/VirtualizationRootsTests.mm b/ProjFS.Mac/PrjFSKextTests/VirtualizationRootsTests.mm index 60f557268c..58caf63b96 100644 --- a/ProjFS.Mac/PrjFSKextTests/VirtualizationRootsTests.mm +++ b/ProjFS.Mac/PrjFSKextTests/VirtualizationRootsTests.mm @@ -530,5 +530,66 @@ - (void) testPathInsideDirectory XCTAssertFalse(PathInsideDirectory("/some/directory/with/sub/directories", "/some/directory")); } +- (void) testOfflineIOProcessArrayOperations +{ + XCTAssertTrue(VirtualizationRoots_AddOfflineIOProcess(10)); + // Processes can technically register more than once + XCTAssertTrue(VirtualizationRoots_AddOfflineIOProcess(10)); + // Adding so many items should resize the array a few times + XCTAssertTrue(VirtualizationRoots_AddOfflineIOProcess(11)); + XCTAssertTrue(VirtualizationRoots_AddOfflineIOProcess(12)); + XCTAssertTrue(VirtualizationRoots_AddOfflineIOProcess(13)); + + for (pid_t testPid = 1; testPid < 20; ++testPid) + { + bool allowed = VirtualizationRoots_ProcessMayAccessOfflineRoots(testPid); + if (testPid < 10 || testPid > 13) + { + XCTAssertFalse(allowed); + } + else + { + XCTAssertTrue(allowed); + } + } + + // remove from the middle of the array + VirtualizationRoots_RemoveOfflineIOProcess(11); + + for (pid_t testPid = 1; testPid < 20; ++testPid) + { + bool allowed = VirtualizationRoots_ProcessMayAccessOfflineRoots(testPid); + if (testPid < 10 || testPid > 13 || testPid == 11) + { + XCTAssertFalse(allowed); + } + else + { + XCTAssertTrue(allowed); + } + } + + XCTAssertTrue(VirtualizationRoots_AddOfflineIOProcess(14)); + + for (pid_t testPid = 1; testPid < 20; ++testPid) + { + bool allowed = VirtualizationRoots_ProcessMayAccessOfflineRoots(testPid); + if (testPid < 10 || testPid > 14 || testPid == 11) + { + XCTAssertFalse(allowed); + } + else + { + XCTAssertTrue(allowed); + } + } + + // These must balance out + VirtualizationRoots_RemoveOfflineIOProcess(10); + VirtualizationRoots_RemoveOfflineIOProcess(10); + VirtualizationRoots_RemoveOfflineIOProcess(14); + VirtualizationRoots_RemoveOfflineIOProcess(12); + VirtualizationRoots_RemoveOfflineIOProcess(13); +} @end diff --git a/ProjFS.Mac/PrjFSLib.Mac.Managed/Interop/PrjFSLib.cs b/ProjFS.Mac/PrjFSLib.Mac.Managed/Interop/PrjFSLib.cs index e7206f4284..09cf81bf38 100644 --- a/ProjFS.Mac/PrjFSLib.Mac.Managed/Interop/PrjFSLib.cs +++ b/ProjFS.Mac/PrjFSLib.Mac.Managed/Interop/PrjFSLib.cs @@ -65,5 +65,11 @@ public static extern Result WriteFileContents( IntPtr fileHandle, IntPtr bytes, uint byteCount); + + [DllImport(PrjFSLibPath, EntryPoint = "PrjFS_RegisterForOfflineIO")] + public static extern Result RegisterForOfflineIO(); + + [DllImport(PrjFSLibPath, EntryPoint = "PrjFS_UnregisterForOfflineIO")] + public static extern Result UnregisterForOfflineIO(); } } diff --git a/ProjFS.Mac/PrjFSLib.Mac.Managed/OfflineIO.cs b/ProjFS.Mac/PrjFSLib.Mac.Managed/OfflineIO.cs new file mode 100644 index 0000000000..a33bf82613 --- /dev/null +++ b/ProjFS.Mac/PrjFSLib.Mac.Managed/OfflineIO.cs @@ -0,0 +1,16 @@ +using System; +namespace PrjFSLib.Mac.Managed +{ + public class OfflineIO + { + public static bool RegisterForOfflineIO() + { + return Result.Success == Interop.PrjFSLib.RegisterForOfflineIO(); + } + + public static bool UnregisterForOfflineIO() + { + return Result.Success == Interop.PrjFSLib.UnregisterForOfflineIO(); + } + } +} diff --git a/ProjFS.Mac/PrjFSLib/PrjFSLib.cpp b/ProjFS.Mac/PrjFSLib/PrjFSLib.cpp index 58eaf209b4..ce4a2cf333 100644 --- a/ProjFS.Mac/PrjFSLib/PrjFSLib.cpp +++ b/ProjFS.Mac/PrjFSLib/PrjFSLib.cpp @@ -147,6 +147,8 @@ static string s_virtualizationRootFullPath; static PrjFS_Callbacks s_callbacks; static dispatch_queue_t s_messageQueueDispatchQueue; static dispatch_queue_t s_kernelRequestHandlingConcurrentQueue; +static io_connect_t s_kernelServiceOfflineWriterConnection = IO_OBJECT_NULL; + // Map of FsidInode -> MutexAndUseCount for that FsidInode, plus mutex to protect the map itself. static FileMutexMap s_fileLocks; @@ -158,6 +160,26 @@ static mutex s_fileLocksMutex; // Public functions +PrjFS_Result PrjFS_RegisterForOfflineIO() +{ + s_kernelServiceOfflineWriterConnection = PrjFSService_ConnectToDriver(UserClientType_OfflineIO); + if (s_kernelServiceOfflineWriterConnection == IO_OBJECT_NULL) + { + return PrjFS_Result_EDriverNotLoaded; + } + + return PrjFS_Result_Success; +} + +PrjFS_Result PrjFS_UnregisterForOfflineIO() +{ + assert(s_kernelServiceOfflineWriterConnection != IO_OBJECT_NULL); + + IOServiceClose(s_kernelServiceOfflineWriterConnection); + s_kernelServiceOfflineWriterConnection = IO_OBJECT_NULL; + return PrjFS_Result_Success; +} + PrjFS_Result PrjFS_StartVirtualizationInstance( _In_ const char* virtualizationRootFullPath, _In_ PrjFS_Callbacks callbacks, diff --git a/ProjFS.Mac/PrjFSLib/PrjFSLib.h b/ProjFS.Mac/PrjFSLib/PrjFSLib.h index 126d3adce9..bb14e3b099 100644 --- a/ProjFS.Mac/PrjFSLib/PrjFSLib.h +++ b/ProjFS.Mac/PrjFSLib/PrjFSLib.h @@ -91,6 +91,9 @@ extern "C" PrjFS_Result PrjFS_WriteSymLink( _In_ const char* relativePath, _In_ const char* symLinkTarget); +extern "C" PrjFS_Result PrjFS_RegisterForOfflineIO(); +extern "C" PrjFS_Result PrjFS_UnregisterForOfflineIO(); + typedef enum { PrjFS_UpdateType_Invalid = 0x00000000, diff --git a/ProjFS.Mac/Scripts/Build.sh b/ProjFS.Mac/Scripts/Build.sh index a2343e1118..18dae23ad1 100755 --- a/ProjFS.Mac/Scripts/Build.sh +++ b/ProjFS.Mac/Scripts/Build.sh @@ -83,7 +83,8 @@ while read line; do [[ $line != *"ActiveProvider_"* ]] && [[ $line != *"GetRelativePath"* ]] && [[ $line != *"VirtualizationRoot_GetRootRelativePath"* ]] && - [[ $line != *"MockCalls"* ]] && + [[ $line != *"VirtualizationRoots_AddOfflineIOProcess"* ]] && # The only branch not covered is one for dealing with a race. We can't yet provoke this in tests. + [[ $line != *"MockCalls"* ]] && [[ $line != *"VnodeCacheEntriesWrapper"* ]] && [[ $line != *"PerfTracing_"* ]] && [[ $line != *"proc_"* ]] &&