2015-01-26 18:20:53 -05:00
// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
2015-07-07 10:31:23 -04:00
# include "DirectoryWatcherPrivatePCH.h"
2015-01-26 18:20:53 -05:00
# include "FileCache.h"
2015-01-26 19:12:58 -05:00
# include "DirectoryWatcherModule.h"
2015-07-07 12:28:07 -04:00
# include "ModuleManager.h"
2015-07-07 10:31:23 -04:00
namespace DirectoryWatcher
{
2015-01-26 18:20:53 -05:00
2015-03-20 15:08:50 -04:00
template < typename T >
2015-08-06 10:59:07 -04:00
void ReadWithCustomVersions ( FArchive & Ar , T & Data , ECustomVersionSerializationFormat : : Type CustomVersionFormat )
2015-03-20 15:08:50 -04:00
{
int64 CustomVersionsOffset = 0 ;
Ar < < CustomVersionsOffset ;
const int64 DataStart = Ar . Tell ( ) ;
Ar . Seek ( CustomVersionsOffset ) ;
// Serialize the custom versions
FCustomVersionContainer Vers = Ar . GetCustomVersions ( ) ;
2015-08-06 10:59:07 -04:00
Vers . Serialize ( Ar , CustomVersionFormat ) ;
2015-03-20 15:08:50 -04:00
Ar . SetCustomVersions ( Vers ) ;
Ar . Seek ( DataStart ) ;
Ar < < Data ;
}
template < typename T >
void WriteWithCustomVersions ( FArchive & Ar , T & Data )
{
const int64 CustomVersionsHeader = Ar . Tell ( ) ;
int64 CustomVersionsOffset = CustomVersionsHeader ;
// We'll come back later and fill this in
Ar < < CustomVersionsOffset ;
// Write out the data
Ar < < Data ;
CustomVersionsOffset = Ar . Tell ( ) ;
// Serialize the custom versions
FCustomVersionContainer Vers = Ar . GetCustomVersions ( ) ;
Vers . Serialize ( Ar ) ;
// Write out where the custom versions are in our header
Ar . Seek ( CustomVersionsHeader ) ;
Ar < < CustomVersionsOffset ;
}
2015-03-18 10:20:13 -04:00
/** Convert a FFileChangeData::EFileChangeAction into an EFileAction */
EFileAction ToFileAction ( FFileChangeData : : EFileChangeAction InAction )
{
switch ( InAction )
{
case FFileChangeData : : FCA_Added : return EFileAction : : Added ;
case FFileChangeData : : FCA_Modified : return EFileAction : : Modified ;
case FFileChangeData : : FCA_Removed : return EFileAction : : Removed ;
default : return EFileAction : : Modified ;
}
}
2015-01-26 18:20:53 -05:00
const FGuid FFileCacheCustomVersion : : Key ( 0x8E7DDCB3 , 0x80DA47BB , 0x9FD346A2 , 0x93984DF6 ) ;
FCustomVersionRegistration GRegisterFileCacheVersion ( FFileCacheCustomVersion : : Key , FFileCacheCustomVersion : : Latest , TEXT ( " FileCacheVersion " ) ) ;
2015-08-06 10:59:07 -04:00
static const uint32 CacheFileMagicNumberOldCustomVersionFormat = 0x03DCCB00 ;
static const uint32 CacheFileMagicNumber = 0x03DCCB03 ;
static ECustomVersionSerializationFormat : : Type GetCustomVersionFormatForFileCache ( uint32 MagicNumber )
{
if ( MagicNumber = = CacheFileMagicNumberOldCustomVersionFormat )
{
return ECustomVersionSerializationFormat : : Guids ;
}
else
{
return ECustomVersionSerializationFormat : : Optimized ;
}
}
2015-03-18 10:20:13 -04:00
2015-03-19 18:19:36 -04:00
/** Single runnable thread used to parse file cache directories without blocking the main thread */
struct FAsyncTaskThread : public FRunnable
{
2015-03-30 18:32:46 -04:00
typedef TArray < TWeakPtr < IAsyncFileCacheTask , ESPMode : : ThreadSafe > > FTaskArray ;
2015-03-19 18:19:36 -04:00
FAsyncTaskThread ( ) : Thread ( nullptr ) { }
2015-03-18 10:20:13 -04:00
/** Add a reader to this thread which will get ticked periodically until complete */
2015-03-30 18:32:46 -04:00
void AddTask ( TSharedPtr < IAsyncFileCacheTask , ESPMode : : ThreadSafe > InTask )
2015-03-18 10:20:13 -04:00
{
2015-03-19 18:19:36 -04:00
FScopeLock Lock ( & TaskArrayMutex ) ;
Tasks . Add ( InTask ) ;
2015-03-18 10:20:13 -04:00
if ( ! Thread )
{
static int32 Index = 0 ;
2015-03-19 18:19:36 -04:00
Thread = FRunnableThread : : Create ( this , * FString : : Printf ( TEXT ( " AsyncTaskThread_%d " ) , + + Index ) ) ;
2015-03-18 10:20:13 -04:00
}
}
/** Run this thread */
virtual uint32 Run ( )
{
for ( ; ; )
{
// Copy the array while we tick the readers
2015-03-19 18:19:36 -04:00
FTaskArray Dupl ;
2015-03-18 10:20:13 -04:00
{
2015-03-19 18:19:36 -04:00
FScopeLock Lock ( & TaskArrayMutex ) ;
Dupl = Tasks ;
2015-03-18 10:20:13 -04:00
}
// Tick each one for a second
2015-03-19 18:19:36 -04:00
for ( auto & Task : Dupl )
2015-03-18 10:20:13 -04:00
{
2015-03-19 18:19:36 -04:00
auto PinnedTask = Task . Pin ( ) ;
if ( PinnedTask . IsValid ( ) )
2015-03-18 10:20:13 -04:00
{
2015-03-19 18:19:36 -04:00
PinnedTask - > Tick ( FTimeLimit ( 1 ) ) ;
2015-03-18 10:20:13 -04:00
}
}
2015-03-19 18:19:36 -04:00
// Cleanup dead/finished Tasks
FScopeLock Lock ( & TaskArrayMutex ) ;
for ( int32 Index = 0 ; Index < Tasks . Num ( ) ; )
2015-03-18 10:20:13 -04:00
{
2015-03-19 18:19:36 -04:00
auto Task = Tasks [ Index ] . Pin ( ) ;
if ( ! Task . IsValid ( ) | | Task - > IsComplete ( ) )
2015-03-18 10:20:13 -04:00
{
2015-03-19 18:19:36 -04:00
Tasks . RemoveAt ( Index ) ;
2015-03-18 10:20:13 -04:00
}
else
{
+ + Index ;
}
}
// Shutdown the thread if we've nothing left to do
2015-03-19 18:19:36 -04:00
if ( Tasks . Num ( ) = = 0 )
2015-03-18 10:20:13 -04:00
{
Thread = nullptr ;
break ;
}
}
return 0 ;
}
private :
2015-03-19 18:19:36 -04:00
/** We start our own thread if one doesn't already exist. */
2015-03-18 10:20:13 -04:00
FRunnableThread * Thread ;
2015-03-19 18:19:36 -04:00
/** Array of things that need ticking, and a mutex to protect them */
FCriticalSection TaskArrayMutex ;
FTaskArray Tasks ;
2015-03-18 10:20:13 -04:00
} ;
2015-03-19 18:19:36 -04:00
FAsyncTaskThread AsyncTaskThread ;
2015-03-18 10:20:13 -04:00
2015-05-27 16:16:21 -04:00
/** Threading strategy for FAsyncFileHasher:
2015-03-19 18:19:36 -04:00
* The task is constructed on the main thread with its Data .
* The array ' Data ' * never * changes size . The task thread moves along setting file hashes , while the main thread
* trails behind accessing the completed entries . We should thus never have 2 threads accessing the same memory ,
* except for the atomic ' CurrentIndex '
*/
FAsyncFileHasher : : FAsyncFileHasher ( TArray < FFilenameAndHash > InFilesThatNeedHashing )
: Data ( MoveTemp ( InFilesThatNeedHashing ) ) , NumReturned ( 0 )
2015-01-26 18:57:03 -05:00
{
2015-03-18 15:59:58 -04:00
// Read in files in 1MB chunks
2015-03-18 18:55:29 -04:00
ScratchBuffer . SetNumUninitialized ( 1024 * 1024 ) ;
2015-03-19 18:19:36 -04:00
}
2015-03-18 15:59:58 -04:00
2015-03-19 18:19:36 -04:00
TArray < FFilenameAndHash > FAsyncFileHasher : : GetCompletedData ( )
{
2015-05-27 16:16:21 -04:00
// Don't need to lock here since the thread will never look at the array before CurrentIndex.
2015-03-19 18:19:36 -04:00
TArray < FFilenameAndHash > Local ;
const int32 CompletedIndex = CurrentIndex . GetValue ( ) ;
if ( NumReturned < CompletedIndex )
{
Local . Append ( Data . GetData ( ) + NumReturned , CompletedIndex - NumReturned ) ;
NumReturned = CompletedIndex ;
if ( CompletedIndex = = Data . Num ( ) )
{
Data . Empty ( ) ;
2015-03-20 10:48:13 -04:00
CurrentIndex . Set ( 0 ) ;
2015-03-19 18:19:36 -04:00
}
}
return Local ;
}
bool FAsyncFileHasher : : IsComplete ( ) const
{
return CurrentIndex . GetValue ( ) = = Data . Num ( ) ;
}
2015-03-30 18:32:46 -04:00
IAsyncFileCacheTask : : EProgressResult FAsyncFileHasher : : Tick ( const FTimeLimit & Limit )
2015-03-19 18:19:36 -04:00
{
for ( ; CurrentIndex . GetValue ( ) < Data . Num ( ) ; )
{
const auto Index = CurrentIndex . GetValue ( ) ;
2015-05-27 16:16:21 -04:00
Data [ Index ] . FileHash = FMD5Hash : : HashFile ( * Data [ Index ] . AbsoluteFilename , & ScratchBuffer ) ;
2015-03-19 18:19:36 -04:00
CurrentIndex . Increment ( ) ;
if ( Limit . Exceeded ( ) )
{
return EProgressResult : : Pending ;
}
}
return EProgressResult : : Finished ;
}
2015-05-27 16:16:21 -04:00
/** Threading strategy for FAsyncDirectoryReader:
2015-03-19 18:19:36 -04:00
* The directory reader owns the cached and live state until it has completely finished . Once IsComplete ( ) is true , the main thread can
* have access to both the cached and farmed data .
*/
FAsyncDirectoryReader : : FAsyncDirectoryReader ( const FString & InDirectory , EPathType InPathType )
: RootPath ( InDirectory ) , PathType ( InPathType )
{
2015-01-26 18:57:03 -05:00
PendingDirectories . Add ( InDirectory ) ;
2015-03-18 10:20:13 -04:00
LiveState . Emplace ( ) ;
2015-01-26 18:57:03 -05:00
}
2015-03-18 10:20:13 -04:00
TOptional < FDirectoryState > FAsyncDirectoryReader : : GetLiveState ( )
2015-01-26 18:57:03 -05:00
{
TOptional < FDirectoryState > OldState ;
2015-03-19 18:19:36 -04:00
2015-07-10 05:31:32 -04:00
if ( ensureMsgf ( IsComplete ( ) , TEXT ( " Invalid property access from thread before task completion " ) ) )
2015-03-19 18:19:36 -04:00
{
Swap ( OldState , LiveState ) ;
}
2015-03-18 10:20:13 -04:00
return OldState ;
}
TOptional < FDirectoryState > FAsyncDirectoryReader : : GetCachedState ( )
{
TOptional < FDirectoryState > OldState ;
2015-03-19 18:19:36 -04:00
2015-07-10 05:31:32 -04:00
if ( ensureMsgf ( IsComplete ( ) , TEXT ( " Invalid property access from thread before task completion " ) ) )
2015-03-19 18:19:36 -04:00
{
Swap ( OldState , CachedState ) ;
}
2015-01-26 18:57:03 -05:00
return OldState ;
}
bool FAsyncDirectoryReader : : IsComplete ( ) const
{
2015-03-18 10:20:13 -04:00
return bIsComplete ;
2015-01-26 18:57:03 -05:00
}
2015-01-26 19:11:28 -05:00
FAsyncDirectoryReader : : EProgressResult FAsyncDirectoryReader : : Tick ( const FTimeLimit & TimeLimit )
2015-01-26 18:57:03 -05:00
{
2015-03-18 10:20:13 -04:00
if ( IsComplete ( ) )
{
return EProgressResult : : Finished ;
}
2015-01-26 18:57:03 -05:00
auto & FileManager = IFileManager : : Get ( ) ;
2015-01-26 19:01:45 -05:00
const int32 RootPathLen = RootPath . Len ( ) ;
2015-01-26 18:57:03 -05:00
2015-03-18 10:20:13 -04:00
// Discover files
2015-01-26 18:57:03 -05:00
for ( int32 Index = 0 ; Index < PendingDirectories . Num ( ) ; + + Index )
{
ScanDirectory ( PendingDirectories [ Index ] ) ;
2015-01-26 19:11:28 -05:00
if ( TimeLimit . Exceeded ( ) )
2015-01-26 18:57:03 -05:00
{
// We've spent too long, bail
2015-02-24 05:54:48 -05:00
PendingDirectories . RemoveAt ( 0 , Index + 1 , false ) ;
2015-01-26 18:57:03 -05:00
return EProgressResult : : Pending ;
}
}
PendingDirectories . Empty ( ) ;
2015-03-18 10:20:13 -04:00
// Process files
2015-01-26 18:57:03 -05:00
for ( int32 Index = 0 ; Index < PendingFiles . Num ( ) ; + + Index )
{
const auto & File = PendingFiles [ Index ] ;
2015-01-26 19:01:45 -05:00
// Store the file relative or absolute
FString Filename = ( PathType = = EPathType : : Relative ? * File + RootPathLen : * File ) ;
2015-01-26 18:57:03 -05:00
2015-03-18 10:20:13 -04:00
const auto Timestamp = FileManager . GetTimeStamp ( * File ) ;
FMD5Hash MD5 ;
if ( CachedState . IsSet ( ) )
{
const FFileData * CachedData = CachedState - > Files . Find ( Filename ) ;
if ( CachedData & & CachedData - > Timestamp = = Timestamp & & CachedData - > FileHash . IsValid ( ) )
{
// Use the cached MD5 to avoid opening the file
MD5 = CachedData - > FileHash ;
}
}
if ( ! MD5 . IsValid ( ) )
{
2015-03-19 18:19:36 -04:00
FilesThatNeedHashing . Emplace ( File ) ;
2015-03-18 10:20:13 -04:00
}
LiveState - > Files . Emplace ( MoveTemp ( Filename ) , FFileData ( Timestamp , MD5 ) ) ;
if ( TimeLimit . Exceeded ( ) )
2015-01-26 18:57:03 -05:00
{
// We've spent too long, bail
2015-02-24 05:54:48 -05:00
PendingFiles . RemoveAt ( 0 , Index + 1 , false ) ;
2015-01-26 18:57:03 -05:00
return EProgressResult : : Pending ;
}
}
PendingFiles . Empty ( ) ;
2015-03-18 10:20:13 -04:00
bIsComplete = true ;
2015-07-07 10:31:23 -04:00
UE_LOG ( LogFileCache , Log , TEXT ( " Scanning file cache for directory '%s' took %.2fs " ) , * RootPath , GetAge ( ) ) ;
2015-01-26 18:57:03 -05:00
return EProgressResult : : Finished ;
}
void FAsyncDirectoryReader : : ScanDirectory ( const FString & InDirectory )
{
struct FVisitor : public IPlatformFile : : FDirectoryVisitor
{
TArray < FString > * PendingFiles ;
TArray < FString > * PendingDirectories ;
2015-03-18 10:20:13 -04:00
FMatchRules * Rules ;
int32 RootPathLength ;
2015-01-26 18:57:03 -05:00
virtual bool Visit ( const TCHAR * FilenameOrDirectory , bool bIsDirectory )
{
2015-03-18 10:20:13 -04:00
FString FileStr ( FilenameOrDirectory ) ;
2015-01-26 18:57:03 -05:00
if ( bIsDirectory )
{
2015-03-18 10:20:13 -04:00
PendingDirectories - > Add ( MoveTemp ( FileStr ) ) ;
2015-01-26 18:57:03 -05:00
}
2015-03-18 10:20:13 -04:00
else if ( Rules - > IsFileApplicable ( FilenameOrDirectory + RootPathLength ) )
2015-01-26 18:57:03 -05:00
{
2015-03-18 10:20:13 -04:00
PendingFiles - > Add ( MoveTemp ( FileStr ) ) ;
2015-01-26 18:57:03 -05:00
}
return true ;
}
} ;
FVisitor Visitor ;
Visitor . PendingFiles = & PendingFiles ;
Visitor . PendingDirectories = & PendingDirectories ;
2015-03-18 10:20:13 -04:00
Visitor . Rules = & LiveState - > Rules ;
Visitor . RootPathLength = RootPath . Len ( ) ;
2015-01-26 18:57:03 -05:00
IFileManager : : Get ( ) . IterateDirectory ( * InDirectory , Visitor ) ;
}
2015-01-26 18:20:53 -05:00
FFileCache : : FFileCache ( const FFileCacheConfig & InConfig )
: Config ( InConfig )
, bSavedCacheDirty ( false )
2015-03-19 18:19:36 -04:00
, LastFileHashGetTime ( 0 )
2015-01-26 18:20:53 -05:00
{
2015-03-18 10:20:13 -04:00
// Ensure the directory has a trailing /
Config . Directory / = TEXT ( " " ) ;
2015-05-27 16:16:21 -04:00
// bDetectMoves implies bRequireFileHashes
Config . bRequireFileHashes = Config . bRequireFileHashes | | Config . bDetectMoves ;
2015-03-18 10:20:13 -04:00
DirectoryReader = MakeShareable ( new FAsyncDirectoryReader ( Config . Directory , Config . PathType ) ) ;
DirectoryReader - > SetMatchRules ( Config . Rules ) ;
2015-01-26 18:20:53 -05:00
// Attempt to load an existing cache file
auto ExistingCache = ReadCache ( ) ;
if ( ExistingCache . IsSet ( ) )
{
2015-03-18 10:20:13 -04:00
DirectoryReader - > UseCachedState ( MoveTemp ( ExistingCache . GetValue ( ) ) ) ;
2015-01-26 18:20:53 -05:00
}
2015-03-19 18:19:36 -04:00
AsyncTaskThread . AddTask ( DirectoryReader ) ;
2015-03-18 10:20:13 -04:00
2015-01-26 18:20:53 -05:00
FDirectoryWatcherModule & Module = FModuleManager : : LoadModuleChecked < FDirectoryWatcherModule > ( TEXT ( " DirectoryWatcher " ) ) ;
if ( IDirectoryWatcher * DirectoryWatcher = Module . Get ( ) )
{
auto Callback = IDirectoryWatcher : : FDirectoryChanged : : CreateRaw ( this , & FFileCache : : OnDirectoryChanged ) ;
DirectoryWatcher - > RegisterDirectoryChangedCallback_Handle ( Config . Directory , Callback , WatcherDelegate ) ;
}
}
FFileCache : : ~ FFileCache ( )
{
UnbindWatcher ( ) ;
WriteCache ( ) ;
}
void FFileCache : : Destroy ( )
{
// Delete the cache file, and clear out everything
bSavedCacheDirty = false ;
2015-03-18 10:20:13 -04:00
if ( ! Config . CacheFile . IsEmpty ( ) )
{
IFileManager : : Get ( ) . Delete ( * Config . CacheFile , false , true , true ) ;
}
2015-01-26 18:20:53 -05:00
2015-03-18 10:20:13 -04:00
DirectoryReader = nullptr ;
2015-03-19 18:19:36 -04:00
AsyncFileHasher = nullptr ;
2015-05-27 16:16:21 -04:00
DirtyFileHasher = nullptr ;
2015-03-19 18:19:36 -04:00
2015-03-18 10:20:13 -04:00
DirtyFiles . Empty ( ) ;
2015-01-26 18:20:53 -05:00
CachedDirectoryState = FDirectoryState ( ) ;
UnbindWatcher ( ) ;
}
2015-03-20 10:48:13 -04:00
bool FFileCache : : HasStartedUp ( ) const
{
return ! DirectoryReader . IsValid ( ) | | DirectoryReader - > IsComplete ( ) ;
}
bool FFileCache : : MoveDetectionInitialized ( ) const
{
if ( ! HasStartedUp ( ) )
{
return false ;
}
else if ( ! Config . bDetectMoves )
{
return true ;
}
else
{
// We don't check AsyncFileHasher->IsComplete() here because that doesn't necessarily mean we've harvested the results off the thread
return ! AsyncFileHasher . IsValid ( ) ;
}
}
2015-03-18 10:20:13 -04:00
const FFileData * FFileCache : : FindFileData ( FImmutableString InFilename ) const
{
if ( ! ensure ( HasStartedUp ( ) ) )
{
// It's invalid to call this while the cached state is still being updated on a thread.
return nullptr ;
}
return CachedDirectoryState . Files . Find ( InFilename ) ;
}
2015-01-26 18:20:53 -05:00
void FFileCache : : UnbindWatcher ( )
{
2015-02-24 12:57:35 -05:00
if ( ! WatcherDelegate . IsValid ( ) )
2015-01-26 18:20:53 -05:00
{
return ;
}
if ( FDirectoryWatcherModule * Module = FModuleManager : : GetModulePtr < FDirectoryWatcherModule > ( TEXT ( " DirectoryWatcher " ) ) )
{
if ( IDirectoryWatcher * DirectoryWatcher = Module - > Get ( ) )
{
DirectoryWatcher - > UnregisterDirectoryChangedCallback_Handle ( Config . Directory , WatcherDelegate ) ;
}
}
2015-02-24 12:57:35 -05:00
WatcherDelegate . Reset ( ) ;
2015-01-26 18:20:53 -05:00
}
TOptional < FDirectoryState > FFileCache : : ReadCache ( ) const
{
TOptional < FDirectoryState > Optional ;
2015-03-18 10:20:13 -04:00
if ( ! Config . CacheFile . IsEmpty ( ) )
{
FArchive * Ar = IFileManager : : Get ( ) . CreateFileReader ( * Config . CacheFile ) ;
if ( Ar )
{
2015-03-20 15:08:50 -04:00
// Serialize the magic number - the first iteration omitted version information, so we have a magic number to ignore this data
uint32 MagicNumber = 0 ;
* Ar < < MagicNumber ;
2015-08-06 10:59:07 -04:00
if ( MagicNumber = = CacheFileMagicNumber | | MagicNumber = = CacheFileMagicNumberOldCustomVersionFormat )
2015-03-20 15:08:50 -04:00
{
FDirectoryState Result ;
2015-08-06 10:59:07 -04:00
ReadWithCustomVersions ( * Ar , Result , GetCustomVersionFormatForFileCache ( MagicNumber ) ) ;
2015-03-20 15:08:50 -04:00
Optional . Emplace ( MoveTemp ( Result ) ) ;
}
2015-01-26 18:20:53 -05:00
2015-03-18 10:20:13 -04:00
Ar - > Close ( ) ;
delete Ar ;
}
2015-01-26 18:20:53 -05:00
}
return Optional ;
}
void FFileCache : : WriteCache ( )
{
2015-03-18 10:20:13 -04:00
if ( bSavedCacheDirty & & ! Config . CacheFile . IsEmpty ( ) )
2015-01-26 18:20:53 -05:00
{
2015-03-06 10:03:57 -05:00
const FString ParentFolder = FPaths : : GetPath ( Config . CacheFile ) ;
if ( ! IFileManager : : Get ( ) . DirectoryExists ( * ParentFolder ) )
{
IFileManager : : Get ( ) . MakeDirectory ( * ParentFolder , true ) ;
}
2015-03-20 13:26:30 -04:00
// Write to a temp file to avoid corruption
FString TempFile = Config . CacheFile + TEXT ( " .tmp " ) ;
2015-03-06 10:03:57 -05:00
2015-03-20 13:26:30 -04:00
FArchive * Ar = IFileManager : : Get ( ) . CreateFileWriter ( * TempFile ) ;
2015-03-06 10:03:57 -05:00
if ( ensureMsgf ( Ar , TEXT ( " Unable to write file-cache for '%s' to '%s'. " ) , * Config . Directory , * Config . CacheFile ) )
{
2015-03-20 15:08:50 -04:00
// Serialize the magic number
uint32 MagicNumber = CacheFileMagicNumber ;
* Ar < < MagicNumber ;
WriteWithCustomVersions ( * Ar , CachedDirectoryState ) ;
2015-01-26 18:20:53 -05:00
2015-03-06 10:03:57 -05:00
Ar - > Close ( ) ;
delete Ar ;
2015-01-26 18:20:53 -05:00
2015-03-06 10:03:57 -05:00
CachedDirectoryState . Files . Shrink ( ) ;
2015-01-26 18:20:53 -05:00
2015-03-06 10:03:57 -05:00
bSavedCacheDirty = false ;
2015-03-20 13:26:30 -04:00
const bool bMoved = IFileManager : : Get ( ) . Move ( * Config . CacheFile , * TempFile , true , true ) ;
ensureMsgf ( bMoved , TEXT ( " Unable to move file-cache for '%s' from '%s' to '%s'. " ) , * Config . Directory , * TempFile , * Config . CacheFile ) ;
2015-03-06 10:03:57 -05:00
}
2015-01-26 18:20:53 -05:00
}
}
2015-03-18 10:20:13 -04:00
FString FFileCache : : GetAbsolutePath ( const FString & InTransactionPath ) const
{
if ( Config . PathType = = EPathType : : Relative )
{
return Config . Directory / InTransactionPath ;
}
else
{
return InTransactionPath ;
}
}
TOptional < FString > FFileCache : : GetTransactionPath ( const FString & InAbsolutePath ) const
{
FString Temp = FPaths : : ConvertRelativePathToFull ( InAbsolutePath ) ;
FString RelativePath ( * Temp + Config . Directory . Len ( ) ) ;
// If it's a directory or is not applicable, ignore it
if ( ! Temp . StartsWith ( Config . Directory ) | | IFileManager : : Get ( ) . DirectoryExists ( * Temp ) | | ! Config . Rules . IsFileApplicable ( * RelativePath ) )
{
return TOptional < FString > ( ) ;
}
if ( Config . PathType = = EPathType : : Relative )
{
return MoveTemp ( RelativePath ) ;
}
else
{
return MoveTemp ( Temp ) ;
}
}
2015-05-27 16:16:21 -04:00
void FFileCache : : DiffDirtyFiles ( TMap < FImmutableString , FFileData > & InDirtyFiles , TArray < FUpdateCacheTransaction > & OutTransactions , const FDirectoryState * InFileSystemState ) const
2015-03-18 10:20:13 -04:00
{
2015-03-19 18:19:36 -04:00
TArray < uint8 > ScratchBuffer ;
ScratchBuffer . SetNumUninitialized ( 1024 * 1024 ) ;
2015-03-18 10:20:13 -04:00
TMap < FImmutableString , FFileData > AddedFiles , ModifiedFiles ;
2015-04-22 08:28:21 -04:00
TSet < FImmutableString > RemovedFiles , InvalidDirtyFiles ;
2015-03-18 10:20:13 -04:00
auto & FileManager = IFileManager : : Get ( ) ;
auto & PlatformFile = FPlatformFileManager : : Get ( ) . GetPlatformFile ( ) ;
2015-04-22 08:28:21 -04:00
for ( const auto & Pair : InDirtyFiles )
2015-03-18 10:20:13 -04:00
{
2015-04-22 08:28:21 -04:00
const auto & File = Pair . Key ;
2015-03-18 10:20:13 -04:00
FString AbsoluteFilename = GetAbsolutePath ( File . Get ( ) ) ;
const auto * CachedState = CachedDirectoryState . Files . Find ( File ) ;
const bool bFileExists = InFileSystemState ? InFileSystemState - > Files . Find ( File ) ! = nullptr : PlatformFile . FileExists ( * AbsoluteFilename ) ;
if ( bFileExists )
{
2015-05-27 16:16:21 -04:00
FFileData FileData ;
if ( const auto * FoundData = InFileSystemState ? InFileSystemState - > Files . Find ( File ) : nullptr )
{
FileData = * FoundData ;
}
else
{
// The dirty file timestamp is the time that the file was dirtied, not necessarily its modification time
FileData = FFileData ( FileManager . GetTimeStamp ( * AbsoluteFilename ) , Pair . Value . FileHash ) ;
}
if ( Config . bRequireFileHashes & & ! FileData . FileHash . IsValid ( ) )
{
// We don't have this file's hash yet. Temporarily ignore it.
continue ;
}
2015-03-18 10:20:13 -04:00
// Do we think it exists in the cache?
if ( CachedState )
{
2015-05-27 16:16:21 -04:00
// A file has changed if its hash is now different
if ( Config . bRequireFileHashes & &
Config . ChangeDetectionBits [ FFileCacheConfig : : FileHash ] & &
CachedState - > FileHash ! = FileData . FileHash
)
{
ModifiedFiles . Add ( File , FileData ) ;
}
// or the timestamp has changed
else if ( Config . ChangeDetectionBits [ FFileCacheConfig : : Timestamp ] & &
CachedState - > Timestamp ! = FileData . Timestamp )
2015-03-18 10:20:13 -04:00
{
ModifiedFiles . Add ( File , FileData ) ;
}
2015-04-22 08:28:21 -04:00
else
{
// File hasn't changed
InvalidDirtyFiles . Add ( File ) ;
}
2015-03-18 10:20:13 -04:00
}
else
{
AddedFiles . Add ( File , FileData ) ;
}
}
// We only report it as removed if it exists in the cache
else if ( CachedState )
{
RemovedFiles . Add ( File ) ;
}
2015-04-22 08:28:21 -04:00
else
{
// File doesn't exist, and isn't in the cache
InvalidDirtyFiles . Add ( File ) ;
}
}
// Remove any dirty files that aren't dirty
for ( auto & Filename : InvalidDirtyFiles )
{
InDirtyFiles . Remove ( Filename ) ;
2015-03-18 10:20:13 -04:00
}
// Rename / move detection
2015-03-19 18:19:36 -04:00
if ( Config . bDetectMoves )
2015-03-18 10:20:13 -04:00
{
2015-05-27 16:16:21 -04:00
bool bHavePendingHashes = false ;
// Remove any additions that don't have their hash generated yet
for ( auto AdIt = AddedFiles . CreateIterator ( ) ; AdIt ; + + AdIt )
{
if ( ! AdIt . Value ( ) . FileHash . IsValid ( ) )
{
bHavePendingHashes = true ;
AdIt . RemoveCurrent ( ) ;
}
}
// We can only detect renames or moves for files that have had their file hash harvested.
// If we can't find a valid move destination for this file, and we have pending hashes, ignore the removal until we can be sure it's not a move
2015-03-19 18:19:36 -04:00
for ( auto RemoveIt = RemovedFiles . CreateIterator ( ) ; RemoveIt ; + + RemoveIt )
2015-03-18 10:20:13 -04:00
{
2015-03-19 18:19:36 -04:00
const auto * CachedState = CachedDirectoryState . Files . Find ( * RemoveIt ) ;
2015-03-20 13:27:59 -04:00
if ( CachedState & & CachedState - > FileHash . IsValid ( ) )
2015-03-19 18:19:36 -04:00
{
for ( auto AdIt = AddedFiles . CreateIterator ( ) ; AdIt ; + + AdIt )
2015-05-27 16:16:21 -04:00
{
2015-03-20 13:27:59 -04:00
if ( AdIt . Value ( ) . FileHash = = CachedState - > FileHash )
2015-03-19 18:19:36 -04:00
{
// Found a move destination!
2015-04-22 08:28:21 -04:00
OutTransactions . Add ( FUpdateCacheTransaction ( * RemoveIt , AdIt . Key ( ) , AdIt . Value ( ) ) ) ;
2015-03-18 10:20:13 -04:00
2015-03-19 18:19:36 -04:00
AdIt . RemoveCurrent ( ) ;
RemoveIt . RemoveCurrent ( ) ;
2015-05-27 16:16:21 -04:00
goto next ;
2015-03-19 18:19:36 -04:00
}
2015-03-18 10:20:13 -04:00
}
2015-05-27 16:16:21 -04:00
// We can't be sure this isn't a move (yet) so temporarily ignore this
if ( bHavePendingHashes )
{
RemoveIt . RemoveCurrent ( ) ;
}
2015-03-18 10:20:13 -04:00
}
2015-05-27 16:16:21 -04:00
next :
continue ;
2015-03-18 10:20:13 -04:00
}
}
for ( auto & RemovedFile : RemovedFiles )
{
2015-04-22 08:28:21 -04:00
OutTransactions . Add ( FUpdateCacheTransaction ( MoveTemp ( RemovedFile ) , EFileAction : : Removed ) ) ;
2015-03-18 10:20:13 -04:00
}
// RemovedFiles is now bogus
for ( auto & Pair : AddedFiles )
{
2015-04-22 08:28:21 -04:00
OutTransactions . Add ( FUpdateCacheTransaction ( MoveTemp ( Pair . Key ) , EFileAction : : Added , Pair . Value ) ) ;
2015-03-18 10:20:13 -04:00
}
// AddedFiles is now bogus
for ( auto & Pair : ModifiedFiles )
{
2015-04-22 08:28:21 -04:00
OutTransactions . Add ( FUpdateCacheTransaction ( MoveTemp ( Pair . Key ) , EFileAction : : Modified , Pair . Value ) ) ;
2015-03-18 10:20:13 -04:00
}
// ModifiedFiles is now bogus
}
2015-01-26 18:20:53 -05:00
TArray < FUpdateCacheTransaction > FFileCache : : GetOutstandingChanges ( )
{
2015-05-27 16:16:21 -04:00
HarvestDirtyFileHashes ( ) ;
2015-04-22 08:28:21 -04:00
TArray < FUpdateCacheTransaction > PendingTransactions ;
2015-03-18 10:20:13 -04:00
DiffDirtyFiles ( DirtyFiles , PendingTransactions ) ;
DirtyFiles . Empty ( ) ;
2015-05-27 16:16:21 -04:00
return PendingTransactions ;
2015-04-22 08:28:21 -04:00
}
2015-03-18 10:20:13 -04:00
2015-04-22 08:28:21 -04:00
TArray < FUpdateCacheTransaction > FFileCache : : FilterOutstandingChanges ( const TFunctionRef < bool ( const FUpdateCacheTransaction & , const FDateTime & ) > & InPredicate )
{
2015-05-27 16:16:21 -04:00
HarvestDirtyFileHashes ( ) ;
2015-04-22 08:28:21 -04:00
TArray < FUpdateCacheTransaction > AllTransactions ;
2015-05-27 16:16:21 -04:00
DiffDirtyFiles ( DirtyFiles , AllTransactions , nullptr ) ;
2015-04-22 08:28:21 -04:00
2015-05-27 16:16:21 -04:00
// Filter the transactions based on the predicate
2015-04-22 08:28:21 -04:00
TArray < FUpdateCacheTransaction > FilteredTransactions ;
for ( auto & Transaction : AllTransactions )
{
2015-05-27 16:16:21 -04:00
FFileData FileData = DirtyFiles . FindRef ( Transaction . Filename ) ;
// Timestamp is the time the file was dirtied, not necessarily the timestamp of the file
if ( InPredicate ( Transaction , FileData . Timestamp ) )
2015-04-22 08:28:21 -04:00
{
DirtyFiles . Remove ( Transaction . Filename ) ;
FilteredTransactions . Add ( MoveTemp ( Transaction ) ) ;
}
}
// Anything left in AllTransactions is discarded
return FilteredTransactions ;
2015-01-26 18:20:53 -05:00
}
2015-03-18 10:20:13 -04:00
void FFileCache : : IgnoreNewFile ( const FString & Filename )
2015-02-17 10:10:10 -05:00
{
2015-03-18 10:20:13 -04:00
auto TransactionPath = GetTransactionPath ( Filename ) ;
if ( TransactionPath . IsSet ( ) )
2015-02-17 10:10:10 -05:00
{
2015-03-18 10:20:13 -04:00
DirtyFiles . Remove ( TransactionPath . GetValue ( ) ) ;
2015-05-27 16:16:21 -04:00
const FFileData FileData ( IFileManager : : Get ( ) . GetTimeStamp ( * Filename ) , FMD5Hash : : HashFile ( * Filename ) ) ;
2015-03-18 10:20:13 -04:00
CompleteTransaction ( FUpdateCacheTransaction ( MoveTemp ( TransactionPath . GetValue ( ) ) , EFileAction : : Added , FileData ) ) ;
}
}
void FFileCache : : IgnoreFileModification ( const FString & Filename )
{
auto TransactionPath = GetTransactionPath ( Filename ) ;
if ( TransactionPath . IsSet ( ) )
{
DirtyFiles . Remove ( TransactionPath . GetValue ( ) ) ;
2015-05-27 16:16:21 -04:00
const FFileData FileData ( IFileManager : : Get ( ) . GetTimeStamp ( * Filename ) , FMD5Hash : : HashFile ( * Filename ) ) ;
2015-03-18 10:20:13 -04:00
CompleteTransaction ( FUpdateCacheTransaction ( MoveTemp ( TransactionPath . GetValue ( ) ) , EFileAction : : Modified , FileData ) ) ;
}
}
void FFileCache : : IgnoreMovedFile ( const FString & SrcFilename , const FString & DstFilename )
{
auto SrcTransactionPath = GetTransactionPath ( SrcFilename ) ;
auto DstTransactionPath = GetTransactionPath ( DstFilename ) ;
if ( SrcTransactionPath . IsSet ( ) & & DstTransactionPath . IsSet ( ) )
{
DirtyFiles . Remove ( SrcTransactionPath . GetValue ( ) ) ;
DirtyFiles . Remove ( DstTransactionPath . GetValue ( ) ) ;
2015-05-27 16:16:21 -04:00
const FFileData FileData ( IFileManager : : Get ( ) . GetTimeStamp ( * DstFilename ) , FMD5Hash : : HashFile ( * DstFilename ) ) ;
2015-03-18 10:20:13 -04:00
CompleteTransaction ( FUpdateCacheTransaction ( MoveTemp ( SrcTransactionPath . GetValue ( ) ) , MoveTemp ( DstTransactionPath . GetValue ( ) ) , FileData ) ) ;
}
}
void FFileCache : : IgnoreDeletedFile ( const FString & Filename )
{
auto TransactionPath = GetTransactionPath ( Filename ) ;
if ( TransactionPath . IsSet ( ) )
{
DirtyFiles . Remove ( TransactionPath . GetValue ( ) ) ;
CompleteTransaction ( FUpdateCacheTransaction ( MoveTemp ( TransactionPath . GetValue ( ) ) , EFileAction : : Removed ) ) ;
2015-02-17 10:10:10 -05:00
}
}
2015-01-26 18:20:53 -05:00
void FFileCache : : CompleteTransaction ( FUpdateCacheTransaction & & Transaction )
{
auto * CachedData = CachedDirectoryState . Files . Find ( Transaction . Filename ) ;
switch ( Transaction . Action )
{
2015-03-18 10:20:13 -04:00
case EFileAction : : Moved :
2015-01-26 18:20:53 -05:00
{
2015-03-18 10:20:13 -04:00
CachedDirectoryState . Files . Remove ( Transaction . MovedFromFilename ) ;
if ( ! CachedData )
{
CachedDirectoryState . Files . Add ( Transaction . Filename , Transaction . FileData ) ;
}
else
{
* CachedData = Transaction . FileData ;
}
2015-01-26 18:20:53 -05:00
bSavedCacheDirty = true ;
}
break ;
2015-03-18 10:20:13 -04:00
case EFileAction : : Modified :
if ( CachedData )
{
// Update the timestamp
* CachedData = Transaction . FileData ;
bSavedCacheDirty = true ;
}
break ;
case EFileAction : : Added :
2015-01-26 18:20:53 -05:00
if ( ! CachedData )
{
// Add the file information to the cache
2015-03-18 10:20:13 -04:00
CachedDirectoryState . Files . Emplace ( Transaction . Filename , Transaction . FileData ) ;
2015-01-26 18:20:53 -05:00
bSavedCacheDirty = true ;
}
break ;
2015-03-18 10:20:13 -04:00
case EFileAction : : Removed :
2015-01-26 18:20:53 -05:00
if ( CachedData )
{
// Remove the file information to the cache
CachedDirectoryState . Files . Remove ( Transaction . Filename ) ;
bSavedCacheDirty = true ;
}
break ;
default :
checkf ( false , TEXT ( " Invalid file cached transaction " ) ) ;
break ;
}
}
2015-03-18 10:20:13 -04:00
void FFileCache : : Tick ( )
2015-01-26 18:20:53 -05:00
{
2015-03-19 18:19:36 -04:00
/** Stage one: wait for the asynchronous directory reader to finish harvesting timestamps for the directory */
if ( DirectoryReader . IsValid ( ) )
2015-01-26 18:57:03 -05:00
{
2015-03-19 18:19:36 -04:00
if ( ! DirectoryReader - > IsComplete ( ) )
{
return ;
}
else
{
ReadStateFromAsyncReader ( ) ;
2015-01-26 18:20:53 -05:00
2015-05-27 16:16:21 -04:00
if ( Config . bRequireFileHashes )
2015-03-19 18:19:36 -04:00
{
auto FilesThatNeedHashing = DirectoryReader - > GetFilesThatNeedHashing ( ) ;
if ( FilesThatNeedHashing . Num ( ) > 0 )
{
AsyncFileHasher = MakeShareable ( new FAsyncFileHasher ( MoveTemp ( FilesThatNeedHashing ) ) ) ;
AsyncTaskThread . AddTask ( AsyncFileHasher ) ;
}
}
// Null out our pointer to the directory reader to indicate that we've finished
DirectoryReader = nullptr ;
}
}
/** The file cache is now running, and will report changes. */
/** Keep harvesting file hashes from the file hashing task until complete. These are much slower to gather, and only required for rename/move detection. */
else if ( AsyncFileHasher . IsValid ( ) )
{
double Now = FPlatformTime : : Seconds ( ) ;
if ( Now - LastFileHashGetTime > 5.f )
{
LastFileHashGetTime = Now ;
auto Hashes = AsyncFileHasher - > GetCompletedData ( ) ;
if ( Hashes . Num ( ) > 0 )
{
bSavedCacheDirty = true ;
for ( const auto & Data : Hashes )
{
FImmutableString CachePath = ( Config . PathType = = EPathType : : Relative ) ? * Data . AbsoluteFilename + Config . Directory . Len ( ) : * Data . AbsoluteFilename ;
auto * FileData = CachedDirectoryState . Files . Find ( CachePath ) ;
if ( FileData & & ! FileData - > FileHash . IsValid ( ) )
{
FileData - > FileHash = Data . FileHash ;
}
}
}
if ( AsyncFileHasher - > IsComplete ( ) )
{
2015-07-07 10:31:23 -04:00
UE_LOG ( LogFileCache , Log , TEXT ( " Retrieving MD5 hashes for directory '%s' took %.2fs " ) , * Config . Directory , AsyncFileHasher - > GetAge ( ) ) ;
2015-03-19 18:19:36 -04:00
AsyncFileHasher = nullptr ;
}
}
}
}
void FFileCache : : ReadStateFromAsyncReader ( )
{
2015-01-26 18:57:03 -05:00
// We should only ever get here once. The directory reader has finished scanning, and we can now diff the results with what we had saved in the cache file.
2015-03-18 10:20:13 -04:00
check ( DirectoryReader - > IsComplete ( ) ) ;
2015-01-26 18:57:03 -05:00
2015-03-18 10:20:13 -04:00
TOptional < FDirectoryState > LiveState = DirectoryReader - > GetLiveState ( ) ;
TOptional < FDirectoryState > CachedState = DirectoryReader - > GetCachedState ( ) ;
if ( ! CachedState . IsSet ( ) | | ! Config . bDetectChangesSinceLastRun )
2015-01-26 18:20:53 -05:00
{
// If we don't have any cached data yet, just use the file data we just harvested
2015-03-18 10:20:13 -04:00
CachedDirectoryState = MoveTemp ( LiveState . GetValue ( ) ) ;
2015-01-26 18:20:53 -05:00
bSavedCacheDirty = true ;
return ;
}
2015-03-18 10:20:13 -04:00
else
{
// Use the cache that we gave to the directory reader
CachedDirectoryState = MoveTemp ( CachedState . GetValue ( ) ) ;
}
2015-05-27 16:16:21 -04:00
const FDateTime Now = FDateTime : : UtcNow ( ) ;
2015-01-26 18:20:53 -05:00
// We already have cached data so we need to compare it with the harvested data
// to detect additions, modifications, and removals
2015-03-18 10:20:13 -04:00
for ( const auto & FilenameAndData : LiveState - > Files )
2015-01-26 18:20:53 -05:00
{
const FString & Filename = FilenameAndData . Key . Get ( ) ;
2015-05-27 16:16:21 -04:00
// If the file we've discovered was not applicable to the old cache, we can't report a change for it as we don't know if it's new or not, just add it straight to the cache.
if ( ! CachedDirectoryState . Rules . IsFileApplicable ( * Filename ) )
2015-01-26 18:20:53 -05:00
{
2015-05-27 16:16:21 -04:00
CachedDirectoryState . Files . Add ( FilenameAndData . Key , FilenameAndData . Value ) ;
bSavedCacheDirty = true ;
}
else
{
const auto * CachedData = CachedDirectoryState . Files . Find ( Filename ) ;
if ( ! CachedData | | CachedData - > Timestamp ! = FilenameAndData . Value . Timestamp )
{
DirtyFiles . Add ( FilenameAndData . Key , FFileData ( Now , FMD5Hash ( ) ) ) ;
}
2015-01-26 18:20:53 -05:00
}
}
2015-03-18 10:20:13 -04:00
// Check for anything that doesn't exist on disk anymore
2015-01-26 18:20:53 -05:00
for ( auto It = CachedDirectoryState . Files . CreateIterator ( ) ; It ; + + It )
{
const FImmutableString & Filename = It . Key ( ) ;
2015-05-27 16:16:21 -04:00
if ( LiveState - > Rules . IsFileApplicable ( * Filename . Get ( ) ) & & ! LiveState - > Files . Contains ( Filename ) )
2015-01-26 18:20:53 -05:00
{
2015-05-27 16:16:21 -04:00
DirtyFiles . Add ( Filename , FFileData ( Now , FMD5Hash ( ) ) ) ;
2015-01-26 18:20:53 -05:00
}
}
2015-03-18 10:20:13 -04:00
2015-05-27 16:16:21 -04:00
RescanForDirtyFileHashes ( ) ;
2015-03-18 10:20:13 -04:00
// Update the applicable extensions now that we've updated the cache
CachedDirectoryState . Rules = LiveState - > Rules ;
2015-01-26 18:20:53 -05:00
}
2015-05-27 16:16:21 -04:00
void FFileCache : : HarvestDirtyFileHashes ( )
{
if ( ! DirtyFileHasher . IsValid ( ) )
{
return ;
}
else for ( FFilenameAndHash & Data : DirtyFileHasher - > GetCompletedData ( ) )
{
FImmutableString CachePath = ( Config . PathType = = EPathType : : Relative ) ? * Data . AbsoluteFilename + Config . Directory . Len ( ) : * Data . AbsoluteFilename ;
if ( auto * FileData = DirtyFiles . Find ( CachePath ) )
{
FileData - > FileHash = Data . FileHash ;
}
}
if ( DirtyFileHasher - > IsComplete ( ) )
{
DirtyFileHasher = nullptr ;
}
}
void FFileCache : : RescanForDirtyFileHashes ( )
{
TArray < FFilenameAndHash > FilesThatNeedHashing ;
for ( const auto & Pair : DirtyFiles )
{
if ( ! Pair . Value . FileHash . IsValid ( ) )
{
FilesThatNeedHashing . Emplace ( GetAbsolutePath ( Pair . Key . Get ( ) ) ) ;
}
}
if ( FilesThatNeedHashing . Num ( ) > 0 )
{
// Re-create the dirty file hasher with the new data that needs hashing. The old task will clean itself up if it already exists.
DirtyFileHasher = MakeShareable ( new FAsyncFileHasher ( MoveTemp ( FilesThatNeedHashing ) ) ) ;
AsyncTaskThread . AddTask ( DirtyFileHasher ) ;
}
}
2015-01-26 18:20:53 -05:00
void FFileCache : : OnDirectoryChanged ( const TArray < FFileChangeData > & FileChanges )
{
2015-05-27 16:16:21 -04:00
// Harvest any completed data from the file hasher before we discard it
HarvestDirtyFileHashes ( ) ;
const FDateTime Now = FDateTime : : UtcNow ( ) ;
2015-01-26 18:20:53 -05:00
for ( const auto & ThisEntry : FileChanges )
{
2015-03-18 10:20:13 -04:00
auto TransactionPath = GetTransactionPath ( ThisEntry . Filename ) ;
if ( TransactionPath . IsSet ( ) )
2015-01-26 18:20:53 -05:00
{
2015-05-27 16:16:21 -04:00
// Add the file that changed to the dirty files map, potentially invalidating the MD5 hash (we'll need to calculate it again)
DirtyFiles . Add ( MoveTemp ( TransactionPath . GetValue ( ) ) , FFileData ( Now , FMD5Hash ( ) ) ) ;
2015-01-26 18:20:53 -05:00
}
}
2015-05-27 16:16:21 -04:00
RescanForDirtyFileHashes ( ) ;
2015-07-07 10:31:23 -04:00
}
} // namespace DirectoryWatcher