2022-04-03 01:52:25 +00:00
/***
Copyright ( C ) 2022 J Reece Wilson ( a / k / a " Reece " ) . All rights reserved .
File : Watcher . NT . cpp
Date : 2022 - 4 - 1
Author : Reece
* * */
# include <Source/RuntimeInternal.hpp>
# include "FS.hpp"
# include <Source/Loop/Loop.hpp>
# include <Source/Loop/LSEvent.hpp>
# include "winioctl.h"
namespace Aurora : : IO : : FS
{
struct NTWatchObject ;
struct NTWatcher ;
struct NTWatchhandle
{
NTWatcher * object ;
} ;
struct NTEvent : Loop : : LSEvent
{
2022-04-03 04:46:52 +00:00
NTEvent ( AuSPtr < NTWatchhandle > parent ) ;
2022-04-03 01:52:25 +00:00
bool IsSignaled ( ) override ;
Loop : : ELoopSource GetType ( ) override ;
void OnPresleep ( ) override ;
bool OnTrigger ( AuUInt handle ) override ;
private :
AuWPtr < NTWatchhandle > parent_ ;
} ;
struct NTWatchObject
{
OVERLAPPED ntOverlapped { } ;
bool bBroken { } ;
bool bReschedule { } ;
NTWatcher * parent ;
AuString strBaseDir ;
HANDLE hFileHandle { INVALID_HANDLE_VALUE } ;
AuUInt32 dwReferences { } ;
~ NTWatchObject ( )
{
Cancel ( ) ;
}
bool Init ( const AuString & usrStr ) ;
bool ScheduleOnce ( ) ;
bool CheckBroken ( ) ;
void Cancel ( ) ;
private :
REQUEST_OPLOCK_OUTPUT_BUFFER whoAsked_ ;
} ;
struct NTCachedPath
{
AuString strNormalizedPath ;
AuString strTheCakeIsALie ;
AuSPtr < UserWatchData > userData ;
AuSPtr < NTWatchObject > watcher ;
FILETIME lastFileTime { } ;
2022-04-03 02:26:23 +00:00
2022-04-03 04:46:52 +00:00
bool CheckRun ( ) ;
2022-04-03 01:52:25 +00:00
} ;
struct NTWatcher : IWatcher
{
virtual bool AddWatch ( const WatchedFile & file ) override ;
virtual bool RemoveByName ( const AuString & path ) override ;
virtual bool RemoveByPrivateContext ( const AuSPtr < UserWatchData > & file ) override ;
virtual AuSPtr < Loop : : ILoopSource > AsLoopSource ( ) override ;
virtual AuList < WatchedFile > QueryUpdates ( ) override ;
bool Init ( ) ;
bool GoBrr ( ) ;
private :
// we don't expect to yield to another thread to hit our vector
2022-04-03 04:46:52 +00:00
// these apis are generally expected to be called from a single thread
2022-04-03 01:52:25 +00:00
AuThreadPrimitives : : SpinLock spinlock_ ;
2022-04-03 04:46:52 +00:00
AuBST < AuString , AuWPtr < NTWatchObject > > cachedWatchers_ ;
2022-04-03 01:52:25 +00:00
public :
2022-04-03 04:46:52 +00:00
AuList < WatchedFile > triggered_ ;
AuSPtr < NTEvent > ntEvent_ ;
2022-04-03 01:52:25 +00:00
AuList < NTCachedPath > paths_ ;
private :
AuSPtr < NTWatchhandle > watchHandler_ ;
} ;
2022-04-03 04:46:52 +00:00
// NT Event class
NTEvent : : NTEvent ( AuSPtr < NTWatchhandle > parent ) : LSEvent ( false , true /*[1]*/ , true ) , parent_ ( parent )
{
// [1] Functions such as GetOverlappedResult and the synchronization wait functions reset auto-reset events to the nonsignaled state. Therefore, you should use a manual reset event; if you use an auto-reset event, your application can stop responding if you wait for the operation to complete and then call GetOverlappedResult with the bWait parameter set to TRUE.
// - https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-overlapped
}
bool NTEvent : : IsSignaled ( )
{
return LSEvent : : IsSignaled ( ) ;
}
Loop : : ELoopSource NTEvent : : GetType ( )
{
return Loop : : ELoopSource : : eSourceFileWatcher ;
}
// Event type is latching, not a type of binary semaphore
// Resignal if work is still available
2022-04-03 01:52:25 +00:00
void NTEvent : : OnPresleep ( )
{
if ( auto watcher = parent_ . lock ( ) )
{
if ( watcher - > object - > triggered_ . size ( ) )
{
Set ( ) ;
}
}
}
2022-04-03 04:46:52 +00:00
// Filter the latching events signal state once
// based on the availability of work
// ...
/// also, this is kind of like a tick
2022-04-03 01:52:25 +00:00
bool NTEvent : : OnTrigger ( AuUInt handle )
{
bool ret { } ;
if ( auto watcher = parent_ . lock ( ) )
{
ret = watcher - > object - > GoBrr ( ) ;
}
else
{
ret = true ;
}
return ret ;
}
2022-04-03 04:46:52 +00:00
// NT Cached Path / File Watch Item class
bool NTCachedPath : : CheckRun ( )
2022-04-03 01:52:25 +00:00
{
2022-04-03 04:46:52 +00:00
FILETIME curFileTime { } ;
HANDLE hFile ;
hFile = CreateFileW ( AuLocale : : ConvertFromUTF8 ( this - > strNormalizedPath ) . c_str ( ) ,
GENERIC_READ ,
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE ,
NULL ,
OPEN_EXISTING ,
FILE_ATTRIBUTE_NORMAL | ( AuIOFS : : DirExists ( this - > strNormalizedPath ) ? FILE_FLAG_BACKUP_SEMANTICS : 0 ) ,
NULL ) ;
if ( hFile = = INVALID_HANDLE_VALUE )
{
return false ;
}
2022-04-03 01:52:25 +00:00
2022-04-03 04:46:52 +00:00
if ( ! GetFileTime ( hFile , NULL , NULL , & curFileTime ) )
2022-04-03 01:52:25 +00:00
{
2022-04-03 04:46:52 +00:00
CloseHandle ( hFile ) ;
return true ;
2022-04-03 01:52:25 +00:00
}
2022-04-03 04:46:52 +00:00
bool ret = ! ( ( this - > lastFileTime . dwLowDateTime = = curFileTime . dwLowDateTime ) & &
( this - > lastFileTime . dwHighDateTime = = curFileTime . dwHighDateTime ) ) ;
this - > lastFileTime = curFileTime ;
CloseHandle ( hFile ) ;
return ret ;
2022-04-03 01:52:25 +00:00
}
2022-04-03 04:46:52 +00:00
// Directory Watcher Object
2022-04-03 01:52:25 +00:00
bool NTWatchObject : : Init ( const AuString & usrStr )
{
AuCtorCode_t code ;
this - > strBaseDir = AuTryConstruct < AuString > ( code , usrStr ) ;
if ( ! code )
{
return false ;
}
auto c = this - > strBaseDir [ this - > strBaseDir . size ( ) - 1 ] ;
if ( ( c = = ' \\ ' ) | |
( c = = ' / ' ) )
{
this - > strBaseDir . pop_back ( ) ;
}
this - > hFileHandle = CreateFileW ( AuLocale : : ConvertFromUTF8 ( this - > strBaseDir . c_str ( ) ) . c_str ( ) ,
GENERIC_READ ,
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE ,
NULL ,
OPEN_EXISTING ,
2022-04-03 04:46:52 +00:00
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED | FILE_FLAG_BACKUP_SEMANTICS ,
2022-04-03 01:52:25 +00:00
NULL ) ;
if ( this - > hFileHandle = = INVALID_HANDLE_VALUE )
{
return false ;
}
AuMemset ( & this - > ntOverlapped , 0 , sizeof ( this - > ntOverlapped ) ) ;
return ScheduleOnce ( ) ;
}
2022-04-03 04:46:52 +00:00
// Request OP lock
2022-04-03 01:52:25 +00:00
bool NTWatchObject : : ScheduleOnce ( )
{
bool firstTime = ! this - > ntOverlapped . hEvent ;
this - > ntOverlapped . hEvent = ( HANDLE ) this - > parent - > ntEvent_ - > GetHandle ( ) ;
REQUEST_OPLOCK_INPUT_BUFFER input
{
REQUEST_OPLOCK_CURRENT_VERSION ,
sizeof ( REQUEST_OPLOCK_INPUT_BUFFER ) ,
OPLOCK_LEVEL_CACHE_READ | OPLOCK_LEVEL_CACHE_HANDLE ,
firstTime ? REQUEST_OPLOCK_INPUT_FLAG_REQUEST : REQUEST_OPLOCK_INPUT_FLAG_ACK ,
} ;
DWORD bytesReturned ;
if ( DeviceIoControl ( this - > hFileHandle , FSCTL_REQUEST_OPLOCK , & input , sizeof ( input ) , & whoAsked_ , sizeof ( whoAsked_ ) , & bytesReturned , & this - > ntOverlapped ) )
{
this - > CheckBroken ( ) ;
}
else
{
if ( GetLastError ( ) ! = ERROR_IO_PENDING )
{
SysPushErrorIO ( ) ;
return false ;
}
}
return true ;
}
bool NTWatchObject : : CheckBroken ( )
{
DWORD bytesTransferred ;
if ( this - > bBroken | | GetOverlappedResult ( this - > hFileHandle , & this - > ntOverlapped , & bytesTransferred , false ) )
{
2022-04-03 04:46:52 +00:00
bool bSuccess { true } ;
2022-04-03 01:52:25 +00:00
this - > bBroken = true ;
bool bAnyTriggered { } ;
for ( auto & filesWatched : this - > parent - > paths_ )
{
if ( ! AuStartsWith ( filesWatched . strNormalizedPath , this - > strBaseDir ) )
{
continue ;
}
if ( ! filesWatched . CheckRun ( ) )
{
continue ;
}
bAnyTriggered = true ;
AuCtorCode_t code ;
auto watchedFile = AuTryConstruct < WatchedFile > ( code , filesWatched . userData , filesWatched . strTheCakeIsALie ) ;
if ( ! code )
{
return false ;
}
if ( ! AuTryInsert ( this - > parent - > triggered_ , AuMove ( watchedFile ) ) )
{
return false ;
}
bAnyTriggered = true ;
}
2022-04-03 02:44:59 +00:00
if ( this - > whoAsked_ . Flags & REQUEST_OPLOCK_OUTPUT_FLAG_ACK_REQUIRED )
2022-04-03 01:52:25 +00:00
{
2022-04-03 04:46:52 +00:00
this - > whoAsked_ . Flags = 0 ;
bSuccess = ScheduleOnce ( ) ;
2022-04-03 01:52:25 +00:00
}
this - > bBroken = false ;
2022-04-03 04:46:52 +00:00
return bSuccess ;
2022-04-03 01:52:25 +00:00
}
return true ;
}
void NTWatchObject : : Cancel ( )
{
CancelIoEx ( this - > hFileHandle , & this - > ntOverlapped ) ;
AuWin32CloseHandle ( this - > hFileHandle ) ;
}
2022-04-03 04:46:52 +00:00
// NT Watcher - primary interface implementation
2022-04-03 01:52:25 +00:00
bool NTWatcher : : AddWatch ( const WatchedFile & file )
{
AuCtorCode_t code ;
2022-04-03 04:46:52 +00:00
AuSPtr < NTWatchObject > watcher ;
NTCachedPath cached ;
2022-04-03 01:52:25 +00:00
AU_LOCK_GUARD ( this - > spinlock_ ) ;
AuString translated ;
if ( ! AuIOFS : : NormalizePath ( translated , file . path ) )
{
translated = file . path ;
}
2022-04-03 02:26:23 +00:00
// Create the NT path in the midst of path normalization
2022-04-03 01:52:25 +00:00
cached . strNormalizedPath = AuTryConstruct < AuString > ( code , translated ) ;
if ( ! code )
{
return false ;
}
cached . strTheCakeIsALie = AuTryConstruct < AuString > ( code , file . path ) ;
if ( ! code )
{
return false ;
}
cached . userData = file . userData ;
2022-04-03 04:46:52 +00:00
// Update the last edited timestamp as a frame of reference for fast compare of
// directory entries upon lock breakage
2022-04-03 01:52:25 +00:00
cached . CheckRun ( ) ;
2022-04-03 02:26:23 +00:00
// Continue normalizing the parent path
if ( AuIOFS : : FileExists ( translated ) )
{
AuIOFS : : GetDirectoryFromPath ( translated , translated ) ;
}
2022-04-03 04:46:52 +00:00
// Attempt to locate a watcher for the directoy
2022-04-03 02:26:23 +00:00
2022-04-03 04:46:52 +00:00
for ( const auto & [ base , wptr ] : this - > cachedWatchers_ )
2022-04-03 02:26:23 +00:00
{
if ( AuStartsWith ( translated , base ) )
{
watcher = wptr . lock ( ) ;
break ;
}
}
2022-04-03 01:52:25 +00:00
2022-04-03 04:46:52 +00:00
// Create one, if missing
2022-04-03 01:52:25 +00:00
if ( ! watcher )
{
auto item = AuMakeShared < NTWatchObject > ( ) ;
if ( ! item )
{
SysPushErrorMem ( ) ;
return false ;
}
item - > parent = this ;
if ( ! item - > Init ( translated ) )
{
SysPushErrorGen ( ) ;
return false ;
}
watcher = item ;
2022-04-03 04:46:52 +00:00
if ( ! AuTryInsert ( this - > cachedWatchers_ , AuMove ( translated ) , AuMove ( item ) ) )
2022-04-03 01:52:25 +00:00
{
return false ;
}
}
cached . watcher = watcher ;
2022-04-03 04:46:52 +00:00
// Append path / user private pair alongside the watcher for book keeping
2022-04-03 01:52:25 +00:00
if ( ! AuTryInsert ( this - > paths_ , AuMove ( cached ) ) )
{
return false ;
}
2022-04-03 04:46:52 +00:00
// Done
2022-04-03 01:52:25 +00:00
return true ;
}
2022-04-03 04:46:52 +00:00
// Post-event trigger processor and filterer
bool NTWatcher : : GoBrr ( )
2022-04-03 01:52:25 +00:00
{
AU_LOCK_GUARD ( this - > spinlock_ ) ;
2022-04-03 04:46:52 +00:00
for ( auto & [ dir , weakWatcher ] : this - > cachedWatchers_ )
{
if ( auto watcher = weakWatcher . lock ( ) )
{
if ( ! watcher - > CheckBroken ( ) )
{
this - > ntEvent_ - > Set ( ) ;
}
}
}
return this - > triggered_ . size ( ) ;
}
bool NTWatcher : : RemoveByName ( const AuString & path )
{
AU_LOCK_GUARD ( this - > spinlock_ ) ;
2022-04-03 01:52:25 +00:00
AuString strNormalized ;
2022-04-03 04:46:52 +00:00
if ( ! AuIOFS : : NormalizePath ( strNormalized , path ) )
{
return false ;
}
2022-04-03 01:52:25 +00:00
return AuRemoveIf ( this - > paths_ , [ & ] ( const NTCachedPath & object ) - > bool
{
if ( ( strNormalized = = object . strNormalizedPath ) | |
( strNormalized . empty ( ) & & object . strNormalizedPath = = path ) )
{
return true ;
}
return false ;
} ) ;
}
bool NTWatcher : : RemoveByPrivateContext ( const AuSPtr < UserWatchData > & file )
{
AU_LOCK_GUARD ( this - > spinlock_ ) ;
return AuRemoveIf ( this - > paths_ , [ & ] ( const NTCachedPath & object ) - > bool
{
2022-04-03 04:46:52 +00:00
return file = = object . userData ;
2022-04-03 01:52:25 +00:00
} ) ;
}
bool NTWatcher : : Init ( )
{
this - > watchHandler_ = AuMakeShared < NTWatchhandle > ( ) ;
if ( ! this - > watchHandler_ )
{
return false ;
}
this - > watchHandler_ - > object = this ;
this - > ntEvent_ = AuMakeShared < NTEvent > ( this - > watchHandler_ ) ;
if ( ! this - > ntEvent_ )
{
return false ;
}
if ( ! this - > ntEvent_ - > HasValidHandle ( ) )
{
return false ;
}
return true ;
}
AuSPtr < Loop : : ILoopSource > NTWatcher : : AsLoopSource ( )
{
return AuStaticCast < Loop : : ILSEvent > ( this - > ntEvent_ ) ;
}
AuList < WatchedFile > NTWatcher : : NTWatcher : : QueryUpdates ( )
{
AU_LOCK_GUARD ( this - > spinlock_ ) ;
return AuExchange ( this - > triggered_ , { } ) ;
}
AUKN_SYM IWatcher * NewWatcherNew ( )
{
auto watcher = _new NTWatcher ( ) ;
if ( ! watcher )
{
return { } ;
}
if ( ! watcher - > Init ( ) )
{
delete watcher ;
return { } ;
}
return watcher ;
}
AUKN_SYM void NewWatcherRelease ( IWatcher * watcher )
{
AuSafeDelete < NTWatcher * > ( watcher ) ;
}
}