2015-01-26 18:20:53 -05:00
// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
# pragma once
# include "Serialization/CustomVersion.h"
2015-07-07 10:31:23 -04:00
# include "FileCacheUtilities.h"
# include "SecureHash.h"
2015-01-26 18:20:53 -05:00
# include "IDirectoryWatcher.h"
2015-07-07 10:31:23 -04:00
namespace DirectoryWatcher
{
2015-01-26 18:20:53 -05:00
/** Custom serialization version for FFileCache */
2015-07-07 10:31:23 -04:00
struct DIRECTORYWATCHER_API FFileCacheCustomVersion
2015-01-26 18:20:53 -05:00
{
static const FGuid Key ;
2015-03-18 10:20:13 -04:00
enum Type { Initial , IncludeFileHash , Latest = IncludeFileHash } ;
2015-01-26 18:20:53 -05:00
} ;
/** Structure representing specific information about a particular file */
struct FFileData
{
/** Constructiors */
2015-03-18 10:20:13 -04:00
FFileData ( ) : Timestamp ( 0 ) { }
FFileData ( const FDateTime & InTimestamp , const FMD5Hash & InFileHash ) : Timestamp ( InTimestamp ) , FileHash ( InFileHash ) { }
friend bool operator = = ( const FFileData & LHS , const FFileData & RHS )
{
return LHS . Timestamp = = RHS . Timestamp & & LHS . FileHash = = RHS . FileHash ;
}
friend bool operator ! = ( const FFileData & LHS , const FFileData & RHS )
{
return LHS . Timestamp ! = RHS . Timestamp | | LHS . FileHash ! = RHS . FileHash ;
}
2015-01-26 18:20:53 -05:00
/** Serializer for this type */
friend FArchive & operator < < ( FArchive & Ar , FFileData & Data )
{
if ( Ar . CustomVer ( FFileCacheCustomVersion : : Key ) > = FFileCacheCustomVersion : : Initial )
{
Ar < < Data . Timestamp ;
}
2015-03-18 10:20:13 -04:00
if ( Ar . CustomVer ( FFileCacheCustomVersion : : Key ) > = FFileCacheCustomVersion : : IncludeFileHash )
{
Ar < < Data . FileHash ;
}
2015-01-26 18:20:53 -05:00
return Ar ;
}
2015-03-18 10:20:13 -04:00
/** The cached timestamp of the file on disk */
2015-01-26 18:20:53 -05:00
FDateTime Timestamp ;
2015-03-18 10:20:13 -04:00
/** The cached MD5 hash of the file on disk */
FMD5Hash FileHash ;
2015-01-26 18:20:53 -05:00
} ;
/** Structure representing the file data for a number of files in a directory */
struct FDirectoryState
{
/** Default construction */
FDirectoryState ( ) { }
/** Move construction */
2015-03-18 10:20:13 -04:00
FDirectoryState ( FDirectoryState & & In ) : Rules ( MoveTemp ( In . Rules ) ) , Files ( MoveTemp ( In . Files ) ) { }
FDirectoryState & operator = ( FDirectoryState & & In ) { Swap ( Rules , In . Rules ) ; Swap ( Files , In . Files ) ; return * this ; }
/** The rules that define what this state applies to */
FMatchRules Rules ;
2015-01-26 18:20:53 -05:00
/** Filename -> data map */
TMap < FImmutableString , FFileData > Files ;
/** Serializer for this type */
friend FArchive & operator < < ( FArchive & Ar , FDirectoryState & State )
{
Ar . UsingCustomVersion ( FFileCacheCustomVersion : : Key ) ;
2015-03-18 10:20:13 -04:00
// Ignore any old versions to data to ensure that we generate a new cache
if ( Ar . CustomVer ( FFileCacheCustomVersion : : Key ) > = FFileCacheCustomVersion : : IncludeFileHash )
2015-01-26 18:20:53 -05:00
{
2015-03-18 10:20:13 -04:00
Ar < < State . Rules ;
2015-01-26 18:20:53 -05:00
// Number of files
int32 Num = State . Files . Num ( ) ;
Ar < < Num ;
if ( Ar . IsLoading ( ) )
{
State . Files . Reserve ( Num ) ;
}
Ar < < State . Files ;
}
return Ar ;
}
} ;
2015-03-18 10:20:13 -04:00
enum class EFileAction : uint8
{
Added , Modified , Removed , Moved
} ;
2015-01-26 18:20:53 -05:00
/** A transaction issued by FFileCache to describe a change to the cache. The change is only committed once the transaction is returned to the cache (see FFileCache::CompleteTransaction). */
struct FUpdateCacheTransaction
{
/** The path of the file to which this transaction relates */
FImmutableString Filename ;
2015-03-18 10:20:13 -04:00
/** In the case of a moved file, this represents the path the file was moved from */
FImmutableString MovedFromFilename ;
/** File data pertaining to this change at the time of dispatch */
FFileData FileData ;
2015-01-26 18:20:53 -05:00
/** The type of action that prompted this transaction */
2015-03-18 10:20:13 -04:00
EFileAction Action ;
2015-01-26 18:20:53 -05:00
/** Publically moveable */
2015-03-18 10:20:13 -04:00
FUpdateCacheTransaction ( FUpdateCacheTransaction & & In ) : Filename ( MoveTemp ( In . Filename ) ) , MovedFromFilename ( MoveTemp ( In . MovedFromFilename ) ) , FileData ( MoveTemp ( In . FileData ) ) , Action ( MoveTemp ( In . Action ) ) { }
FUpdateCacheTransaction & operator = ( FUpdateCacheTransaction & & In ) { Swap ( Filename , In . Filename ) ; Swap ( MovedFromFilename , In . MovedFromFilename ) ; Swap ( FileData , In . FileData ) ; Swap ( Action , In . Action ) ; return * this ; }
2015-01-26 18:20:53 -05:00
private :
friend class FFileCache ;
2015-02-17 10:10:10 -05:00
2015-01-26 18:20:53 -05:00
/** Construction responsibility is held by FFileCache */
2015-03-18 10:20:13 -04:00
FUpdateCacheTransaction ( FImmutableString InFilename , EFileAction InAction , const FFileData & InFileData = FFileData ( ) )
: Filename ( MoveTemp ( InFilename ) ) , FileData ( InFileData ) , Action ( InAction )
{ }
/** Construction responsibility is held by FFileCache */
FUpdateCacheTransaction ( FImmutableString InMovedFromFilename , FImmutableString InMovedToFilename , const FFileData & InFileData )
: Filename ( MoveTemp ( InMovedToFilename ) ) , MovedFromFilename ( MoveTemp ( InMovedFromFilename ) ) , FileData ( InFileData ) , Action ( EFileAction : : Moved )
2015-01-26 18:20:53 -05:00
{ }
2015-02-17 10:10:10 -05:00
/** Not Copyable */
FUpdateCacheTransaction ( const FUpdateCacheTransaction & ) = delete ;
FUpdateCacheTransaction & operator = ( const FUpdateCacheTransaction & ) = delete ;
2015-01-26 18:20:53 -05:00
} ;
2015-01-26 19:01:45 -05:00
/** Enum specifying whether a path should be relative or absolute */
enum EPathType
2015-01-26 18:57:03 -05:00
{
2015-01-26 19:01:45 -05:00
/** Paths should be cached relative to the root cache directory */
Relative ,
/** Paths should be cached as absolute file system paths */
Absolute
2015-01-26 18:57:03 -05:00
} ;
2015-03-30 18:32:46 -04:00
struct IAsyncFileCacheTask : public TSharedFromThis < IAsyncFileCacheTask , ESPMode : : ThreadSafe >
2015-03-19 18:19:36 -04:00
{
enum class EProgressResult
{
Finished , Pending
} ;
2015-03-30 18:32:46 -04:00
IAsyncFileCacheTask ( ) : StartTime ( FPlatformTime : : Seconds ( ) ) { }
virtual ~ IAsyncFileCacheTask ( ) { }
2015-03-19 18:59:08 -04:00
2015-03-19 18:19:36 -04:00
/** Tick this task. Only to be called on the task thread. */
virtual EProgressResult Tick ( const FTimeLimit & TimeLimit ) = 0 ;
/** Check whether this task is complete. Must be implemented in a thread-safe manner. */
virtual bool IsComplete ( ) const = 0 ;
/** Get the age of this task in seconds */
double GetAge ( ) const { return FPlatformTime : : Seconds ( ) - StartTime ; }
protected :
/** The time this task started */
double StartTime ;
} ;
/** Simple struct that encapsulates a filename and its associated MD5 hash */
struct FFilenameAndHash
{
FString AbsoluteFilename ;
FMD5Hash FileHash ;
FFilenameAndHash ( ) { }
FFilenameAndHash ( const FString & File ) : AbsoluteFilename ( File ) { }
} ;
/** Async task responsible for MD5 hashing a number of files, reporting completed hashes to the client when done */
2015-03-30 18:32:46 -04:00
struct FAsyncFileHasher : public IAsyncFileCacheTask
2015-03-19 18:19:36 -04:00
{
/** Constructor */
FAsyncFileHasher ( TArray < FFilenameAndHash > InFilesThatNeedHashing ) ;
/** Return any completed filenames and their corresponding hashes */
TArray < FFilenameAndHash > GetCompletedData ( ) ;
2015-05-27 16:16:21 -04:00
/** Returns true when this task has finished hashing all its files */
2015-03-19 18:19:36 -04:00
virtual bool IsComplete ( ) const override ;
protected :
2015-05-27 16:16:21 -04:00
/** Tick this reader (hashes as many files as possible in the time allowed). Returns progress state. */
2015-03-19 18:19:36 -04:00
virtual EProgressResult Tick ( const FTimeLimit & Limit ) override ;
/** The array of data that we will process */
TArray < FFilenameAndHash > Data ;
/** The number of items we have returned to the client. Only accessed from the main thread. */
int32 NumReturned ;
/** The number of files that we have hashed on the task thread. Atomic - safe to access from any thread. */
FThreadSafeCounter CurrentIndex ;
/** Scratch buffer used for reading in files */
TArray < uint8 > ScratchBuffer ;
} ;
2015-01-26 18:57:03 -05:00
/**
2015-03-19 18:19:36 -04:00
* Class responsible for ' asynchronously ' scanning a folder for files and timestamps .
2015-01-26 18:57:03 -05:00
* Example usage :
2015-01-26 19:01:45 -05:00
* FAsyncDirectoryReader Reader ( TEXT ( " C: \\ Path " ) , EPathType : : Relative ) ;
2015-01-26 18:57:03 -05:00
*
* while ( ! Reader . IsComplete ( ) )
* {
* FPlatformProcess : : Sleep ( 1 ) ;
2015-01-26 19:11:28 -05:00
* Reader . Tick ( FTimedSignal ( 1 ) ) ; // Do 1 second of work
2015-01-26 18:57:03 -05:00
* }
* TOptional < FDirectoryState > State = Reader . GetFinalState ( ) ;
*/
2015-03-30 18:32:46 -04:00
struct FAsyncDirectoryReader : public IAsyncFileCacheTask
2015-01-26 18:57:03 -05:00
{
/** Constructor that sets up the directory reader to the specified directory */
2015-01-26 19:01:45 -05:00
FAsyncDirectoryReader ( const FString & InDirectory , EPathType InPathType ) ;
2015-01-26 18:57:03 -05:00
2015-03-18 10:20:13 -04:00
/** Set what files are relevant to this reader. Calling this once the reader starts results in undefined behaviour. */
void SetMatchRules ( const FMatchRules & InRules )
{
if ( LiveState . IsSet ( ) )
{
LiveState - > Rules = InRules ;
}
}
2015-01-26 18:57:03 -05:00
/**
* Get the state of the directory once finished . Relinquishes the currently stored directory state to the client .
2015-03-18 10:20:13 -04:00
* Returns nothing if incomplete , or if GetLiveState ( ) has already been called .
2015-01-26 18:57:03 -05:00
*/
2015-03-18 10:20:13 -04:00
TOptional < FDirectoryState > GetLiveState ( ) ;
/** Retrieve the cached state supplied to this class through UseCachedState(). */
TOptional < FDirectoryState > GetCachedState ( ) ;
2015-03-19 18:19:36 -04:00
/** Retrieve the cached state supplied to this class through UseCachedState(). */
TArray < FFilenameAndHash > GetFilesThatNeedHashing ( )
{
TArray < FFilenameAndHash > Swapped ;
Swap ( FilesThatNeedHashing , Swapped ) ;
return Swapped ;
}
2015-03-18 10:20:13 -04:00
/** Instruct the directory reader to use the specified cached state to lookup file hashes, where timestamps haven't changed */
void UseCachedState ( FDirectoryState InCachedState )
{
CachedState = MoveTemp ( InCachedState ) ;
}
2015-01-26 18:57:03 -05:00
/** Returns true when this directory reader has finished scanning the directory */
2015-03-19 18:19:36 -04:00
virtual bool IsComplete ( ) const override ;
2015-01-26 18:57:03 -05:00
/** Tick this reader (discover new directories / files). Returns progress state. */
2015-03-19 18:19:36 -04:00
virtual EProgressResult Tick ( const FTimeLimit & Limit ) override ;
2015-01-26 18:57:03 -05:00
private :
/** Non-recursively scan a single directory for its contents. Adds results to Pending arrays. */
void ScanDirectory ( const FString & InDirectory ) ;
2015-01-26 19:01:45 -05:00
/** Path to the root directory we want to scan */
FString RootPath ;
/** Whether we should return relative or absolute paths */
EPathType PathType ;
2015-03-18 10:20:13 -04:00
/** The currently discovered state of the directory - reset once relinquished to the client through GetLiveState */
TOptional < FDirectoryState > LiveState ;
/** The previously cached state of the directory, optional */
TOptional < FDirectoryState > CachedState ;
2015-01-26 18:57:03 -05:00
2015-03-19 18:19:36 -04:00
/** An array of files that need hashing */
TArray < FFilenameAndHash > FilesThatNeedHashing ;
2015-01-26 18:57:03 -05:00
/** A list of directories we have recursively found on our travels */
TArray < FString > PendingDirectories ;
/** A list of files we have recursively found on our travels */
TArray < FString > PendingFiles ;
2015-03-18 10:20:13 -04:00
/** Thread safe flag to signify when this class has finished reading */
FThreadSafeBool bIsComplete ;
2015-01-26 18:57:03 -05:00
} ;
2015-01-26 18:20:53 -05:00
/** Configuration structure required to construct a FFileCache */
struct FFileCacheConfig
{
2015-05-27 16:16:21 -04:00
/** Enum that specifies what changes are required for a change to be reported. When combined, any valid change is reported. */
enum EChangeDetection
{
/** Report modifications when the timestamp of a file changes */
Timestamp ,
/** Report modifications when the contents of a file changes */
FileHash ,
} ;
2015-01-26 18:57:03 -05:00
FFileCacheConfig ( FString InDirectory , FString InCacheFile )
2015-05-27 16:16:21 -04:00
: Directory ( InDirectory ) , CacheFile ( InCacheFile ) , PathType ( EPathType : : Relative ) , bDetectChangesSinceLastRun ( false )
, ChangeDetectionBits ( false , 2 )
{
DetectMoves ( true ) ;
ChangeDetectionBits [ EChangeDetection : : Timestamp ] = true ;
}
2015-01-26 18:57:03 -05:00
2015-01-26 18:20:53 -05:00
/** String specifying the directory on disk that the cache should reflect */
FString Directory ;
2015-03-18 10:20:13 -04:00
/** String specifying the file that the cache should be saved to. When empty, no cache file will be maintained (thus only an in-memory cache is used) */
2015-01-26 18:20:53 -05:00
FString CacheFile ;
2015-03-18 10:20:13 -04:00
/** List of rules which define what we will be watching */
FMatchRules Rules ;
2015-01-26 19:01:45 -05:00
/** Path type to return, relative to the directory or absolute. */
EPathType PathType ;
2015-03-18 10:20:13 -04:00
/** When true, changes to the directory since the cache shutdown will be detected and reported. When false, said changes will silently be applied to the serialized cache. */
bool bDetectChangesSinceLastRun ;
2015-03-19 18:19:36 -04:00
2015-05-27 16:16:21 -04:00
/** Set up this cache to detect moves */
FFileCacheConfig & DetectMoves ( bool bInDetectMoves )
{
bDetectMoves = bInDetectMoves ;
if ( bDetectMoves )
{
bRequireFileHashes = true ;
}
return * this ;
}
/** Set up this cache to generate MD5 hashes for its constituent files */
FFileCacheConfig & RequireFileHashes ( bool bInRequireFileHashes )
{
2015-07-10 05:31:32 -04:00
if ( ensureMsgf ( bInRequireFileHashes | | ! bDetectMoves , TEXT ( " Unable to disable file hashing when move detection is enabled " ) ) )
2015-05-27 16:16:21 -04:00
{
bRequireFileHashes = bInRequireFileHashes ;
}
return * this ;
}
/** Instruct the cache to report the specified changes to files */
FFileCacheConfig & DetectChangesFor ( EChangeDetection ChangeType , bool Value )
{
ChangeDetectionBits [ ChangeType ] = Value ;
return * this ;
}
private :
/** True to detect moves and renames (based on file hash), false otherwise. Implies bRequireFileHashes. */
2015-03-19 18:19:36 -04:00
bool bDetectMoves ;
2015-05-27 16:16:21 -04:00
/** When true, the cache will also calculate MD5 hashes for files. When true, an additional thread will be launched on startup to harvest unknown MD5 hashes for the directory. */
bool bRequireFileHashes ;
/** Bitfied specifying how we will be detecting changes. See EChangeDetection. */
TBitArray < > ChangeDetectionBits ;
friend class FFileCache ;
2015-01-26 18:20:53 -05:00
} ;
/**
* A class responsible for scanning a directory , and maintaining a cache of its state ( files and timestamps ) .
* Changes in the cache can be retrieved through GetOutstandingChanges ( ) . Changes will be reported for any
* change in the cached state even between runs of the process .
*/
2015-07-07 10:31:23 -04:00
class DIRECTORYWATCHER_API FFileCache
2015-01-26 18:20:53 -05:00
{
public :
/** Construction from a config */
FFileCache ( const FFileCacheConfig & InConfig ) ;
FFileCache ( const FFileCache & ) = delete ;
FFileCache & operator = ( const FFileCache & ) = delete ;
/** Destructor */
~ FFileCache ( ) ;
/** Destroy this cache. Cleans out the in-memory state and deletes the cache file, if present. */
void Destroy ( ) ;
/** Get the absolute path of the directory this cache reflects */
const FString & GetDirectory ( ) const { return Config . Directory ; }
2015-03-20 10:48:13 -04:00
/** Check whether this file cache has finished starting up yet. Does not imply rename/move detection is fully initialized. (see MoveDetectionInitialized()) */
bool HasStartedUp ( ) const ;
/** Check whether this move/rename detection has been initiated or not. This can take much longer than startup, so can be checked separately */
bool MoveDetectionInitialized ( ) const ;
2015-03-18 10:20:13 -04:00
/** Attempt to locate file data pertaining to the specified filename.
* @ param InFilename The filename to find data for - either relative to the directory , or absolute , depending on Config . PathType .
* @ return The current cached file data pertaining to the specified filename , or nullptr . May not be completely up to date if there are outstanding transactions .
*/
const FFileData * FindFileData ( FImmutableString InFilename ) const ;
/** Tick this FileCache */
void Tick ( ) ;
2015-01-26 18:20:53 -05:00
/** Write out the cached file, if we have any changes to write */
void WriteCache ( ) ;
/**
* Return a transaction to the cache for completion . Will update the cached state with the change
* described in the transaction , and mark the cache as needing to be saved .
*/
void CompleteTransaction ( FUpdateCacheTransaction & & Transaction ) ;
2015-02-17 10:10:10 -05:00
/** Report an external change to the manager, such that a subsequent equal change reported by the os be ignored */
2015-03-18 10:20:13 -04:00
void IgnoreNewFile ( const FString & Filename ) ;
void IgnoreFileModification ( const FString & Filename ) ;
void IgnoreMovedFile ( const FString & SrcFilename , const FString & DstFilename ) ;
void IgnoreDeletedFile ( const FString & Filename ) ;
2015-02-17 10:10:10 -05:00
2015-01-26 19:11:28 -05:00
/** Get the number of pending changes to the cache. */
2015-04-22 08:28:21 -04:00
int32 GetNumOutstandingChanges ( ) const { return DirtyFiles . Num ( ) ; }
2015-01-26 19:11:28 -05:00
2015-05-27 16:16:21 -04:00
/** Get pending changes to the cache. Transactions must be returned to CompleteTransaction to update the cache.
* Filter predicate recieves a transaction and the time the change was reported . */
2015-04-22 08:28:21 -04:00
TArray < FUpdateCacheTransaction > FilterOutstandingChanges ( const TFunctionRef < bool ( const FUpdateCacheTransaction & , const FDateTime & ) > & InPredicate ) ;
2015-01-26 18:20:53 -05:00
TArray < FUpdateCacheTransaction > GetOutstandingChanges ( ) ;
private :
/** Called when the directory we are monitoring has been changed in some way */
void OnDirectoryChanged ( const TArray < FFileChangeData > & FileChanges ) ;
2015-03-18 10:20:13 -04:00
/** Diff the specified set of dirty files (absolute paths, or relative to the monitor directory), adding transactions to the specified array if necessary.
* Optionally takes a directory state from which we can retrieve current file system state , without having to ask the FS directly .
2015-04-22 08:28:21 -04:00
* Optionally exclude files that have changed since the specified threshold , to ensure that related events get grouped together correctly .
2015-01-26 18:20:53 -05:00
*/
2015-05-27 16:16:21 -04:00
void DiffDirtyFiles ( TMap < FImmutableString , FFileData > & InDirtyFiles , TArray < FUpdateCacheTransaction > & OutTransactions , const FDirectoryState * InFileSystemState = nullptr ) const ;
/** Detect a rename for the specified file */
void DetectRename ( ) ;
2015-03-18 10:20:13 -04:00
/** Get the absolute path from a transaction filename */
FString GetAbsolutePath ( const FString & InTransactionPath ) const ;
/** Get the transaction path from an absolute filename. Returns nothing if the absolute path is invalid (not under the correct folder, or not applicable) */
TOptional < FString > GetTransactionPath ( const FString & InAbsolutePath ) const ;
2015-01-26 18:20:53 -05:00
/** Unbind the watcher from the directory. Called on destruction. */
void UnbindWatcher ( ) ;
/** Read the cache file data and return the contents */
TOptional < FDirectoryState > ReadCache ( ) const ;
2015-03-19 18:19:36 -04:00
/** Called when the initial async reader has finished harvesting file system timestamps */
void ReadStateFromAsyncReader ( ) ;
2015-05-27 16:16:21 -04:00
/** Called to harvest any file hashes that have been generated by the DirtyFileHasher thread task */
void HarvestDirtyFileHashes ( ) ;
void RescanForDirtyFileHashes ( ) ;
2015-01-26 18:20:53 -05:00
private :
2015-01-26 18:57:03 -05:00
/** Configuration settings applied on construction */
FFileCacheConfig Config ;
2015-05-27 16:16:21 -04:00
/** Asynchronous directory reader responsible for gathering all file/timestamp information recursively from our cache directory */
2015-03-18 10:20:13 -04:00
TSharedPtr < FAsyncDirectoryReader , ESPMode : : ThreadSafe > DirectoryReader ;
2015-05-27 16:16:21 -04:00
/** Asynchronous task used to harvest the MD5 hashes of a set of filenames */
2015-03-19 18:19:36 -04:00
TSharedPtr < FAsyncFileHasher , ESPMode : : ThreadSafe > AsyncFileHasher ;
2015-05-27 16:16:21 -04:00
/** Asynchronous task used to harvest the MD5 hashes of a set of recently changed filenames */
TSharedPtr < FAsyncFileHasher , ESPMode : : ThreadSafe > DirtyFileHasher ;
2015-01-26 18:57:03 -05:00
2015-05-27 16:16:21 -04:00
/** A map of dirty files that we will use to report changes to the user. Timestamp value of FFileData here pertains to the *time of the change* not the timestamp of the file. */
TMap < FImmutableString , FFileData > DirtyFiles ;
2015-01-26 18:20:53 -05:00
/** Our in-memory view of the cached directory state. */
FDirectoryState CachedDirectoryState ;
/** Handle to the directory watcher delegate so we can delete it properly */
FDelegateHandle WatcherDelegate ;
/** True when the cached state we have in memory is more up to date than the serialized file. Enables WriteCache() when true. */
bool bSavedCacheDirty ;
2015-03-19 18:19:36 -04:00
/** The time we last retrieved file hashes from the thread */
double LastFileHashGetTime ;
2015-04-22 08:28:21 -04:00
2015-01-26 18:20:53 -05:00
} ;
2015-07-07 10:31:23 -04:00
} // namespace DirectoryWatcher