/*** Copyright (C) 2022 J Reece Wilson (a/k/a "Reece"). All rights reserved. File: Watcher.NT.cpp Date: 2022-4-1 Author: Reece ***/ #include #include "FS.hpp" #include #include #include "winioctl.h" namespace Aurora::IO::FS { struct NTWatchObject; struct NTWatcher; struct NTWatchhandle { NTWatcher *object; }; struct NTEvent : Loop::LSEvent { NTEvent(AuSPtr parent) : LSEvent(false, true /*[1]*/, true), parent_(parent) { } ~NTEvent() { } bool IsSignaled() override; Loop::ELoopSource GetType() override; //[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 void OnPresleep() override; bool OnTrigger(AuUInt handle) override; private: AuWPtr parent_; }; bool NTEvent::IsSignaled() { return LSEvent::IsSignaled(); } Loop::ELoopSource NTEvent::GetType() { return Loop::ELoopSource::eSourceFileWatcher; } 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 userData; AuSPtr watcher; HANDLE hFile; FILETIME lastFileTime {}; bool CheckRun() { FILETIME curFileTime {}; if (!GetFileTime(this->hFile, NULL, NULL, &lastFileTime)) { return true; } bool ret = !((this->lastFileTime.dwLowDateTime == curFileTime.dwLowDateTime) && (this->lastFileTime.dwHighDateTime == curFileTime.dwHighDateTime)); this->lastFileTime = curFileTime; return ret; } }; struct NTWatcher : IWatcher { virtual bool AddWatch(const WatchedFile &file) override; virtual bool RemoveByName(const AuString &path) override; virtual bool RemoveByPrivateContext(const AuSPtr &file) override; virtual AuSPtr AsLoopSource() override; virtual AuList QueryUpdates() override; bool Init(); bool GoBrr(); private: // we don't expect to yield to another thread to hit our vector // our apis are generally expected to be called from a single thread // so, it's not like we expect to sync against a worker or anything, // this is pre auroxtl object object, so, fuck it, this is just a dword or so AuThreadPrimitives::SpinLock spinlock_; AuBST> daObjects_; public: AuList triggered_; AuSPtr ntEvent_; AuList paths_; private: AuSPtr watchHandler_; }; void NTEvent::OnPresleep() { if (auto watcher = parent_.lock()) { if (watcher->object->triggered_.size()) { Set(); } } } bool NTEvent::OnTrigger(AuUInt handle) { bool ret {}; if (auto watcher = parent_.lock()) { ret = watcher->object->GoBrr(); } else { ret = true; } return ret; } bool NTWatcher::GoBrr() { AU_LOCK_GUARD(this->spinlock_); // TODO: purge if lock fails for (auto &[a, watcher] : this->daObjects_) { if (auto test = watcher.lock()) { if (!test->CheckBroken()) { this->ntEvent_->Set(); } } } return this->triggered_.size(); } bool NTWatchObject::Init(const AuString &usrStr) { AuCtorCode_t code; this->strBaseDir = AuTryConstruct(code, usrStr); if (!code) { return false; } auto c = this->strBaseDir[this->strBaseDir.size() - 1]; if ((c == '\\') || (c == '/')) { this->strBaseDir.pop_back(); } bool isDir = true; 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, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED | (isDir ? FILE_FLAG_BACKUP_SEMANTICS : 0), NULL); if (this->hFileHandle == INVALID_HANDLE_VALUE) { return false; } AuMemset(&this->ntOverlapped, 0, sizeof(this->ntOverlapped)); return ScheduleOnce(); } bool NTWatchObject::ScheduleOnce() { bool firstTime = !this->ntOverlapped.hEvent; this->ntOverlapped.hEvent = (HANDLE)this->parent->ntEvent_->GetHandle(); // REQUEST_OPLOCK_INPUT_FLAG_ACK 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)) { // doesn't count... this->CheckBroken(); } else { if (GetLastError() != ERROR_IO_PENDING) { SysPushErrorIO(); return false; } } return true; } bool NTWatchObject::CheckBroken() { DWORD bytesTransferred; if (this->bReschedule) { return ScheduleOnce(); } if (this->bBroken || GetOverlappedResult(this->hFileHandle, &this->ntOverlapped, &bytesTransferred, false)) { 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(code, filesWatched.userData, filesWatched.strTheCakeIsALie); if (!code) { return false; } if (!AuTryInsert(this->parent->triggered_, AuMove(watchedFile))) { return false; } bAnyTriggered = true; } bool success {true}; // if (bAnyTriggered) { success = ScheduleOnce(); } this->bBroken = false; return success; } return true; } void NTWatchObject::Cancel() { CancelIoEx(this->hFileHandle, &this->ntOverlapped); AuWin32CloseHandle(this->hFileHandle); } bool NTWatcher::AddWatch(const WatchedFile &file) { AuCtorCode_t code; AU_LOCK_GUARD(this->spinlock_); AuSPtr watcher; AuString translated; if (!AuIOFS::NormalizePath(translated, file.path)) { translated = file.path; } if (AuIOFS::FileExists(translated)) { AuIOFS::GetDirectoryFromPath(translated, translated); } for (const auto &[base, wptr] : this->daObjects_) { if (AuStartsWith(translated, base)) { watcher = wptr.lock(); break; } } NTCachedPath cached; cached.strNormalizedPath = AuTryConstruct(code, translated); if (!code) { return false; } cached.strTheCakeIsALie = AuTryConstruct(code, file.path); if (!code) { return false; } cached.userData = file.userData; cached.hFile = CreateFileW(AuLocale::ConvertFromUTF8(cached.strNormalizedPath).c_str(), GENERIC_READ, FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED | (AuIOFS::DirExists(cached.strNormalizedPath) ? FILE_FLAG_BACKUP_SEMANTICS : 0), NULL); if (cached.hFile == INVALID_HANDLE_VALUE) { return false; } // update the last edited timestamp cached.CheckRun(); if (!watcher) { auto item = AuMakeShared(); if (!item) { SysPushErrorMem(); return false; } item->parent = this; if (!item->Init(translated)) { SysPushErrorGen(); return false; } watcher = item; if (!AuTryInsert(this->daObjects_, AuMove(translated), AuMove(item))) { return false; } } cached.watcher = watcher; if (!AuTryInsert(this->paths_, AuMove(cached))) { return false; } return true; } bool NTWatcher::RemoveByName(const AuString &path) { AU_LOCK_GUARD(this->spinlock_); AuString strNormalized; AuIOFS::NormalizePath(strNormalized, path); 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 &file) { AU_LOCK_GUARD(this->spinlock_); return AuRemoveIf(this->paths_, [&](const NTCachedPath &object) -> bool { if (file == object.userData) { return true; } return false; }); } bool NTWatcher::Init() { this->watchHandler_ = AuMakeShared(); if (!this->watchHandler_) { return false; } this->watchHandler_->object = this; this->ntEvent_ = AuMakeShared(this->watchHandler_); if (!this->ntEvent_) { return false; } if (!this->ntEvent_->HasValidHandle()) { return false; } return true; } AuSPtr NTWatcher::AsLoopSource() { return AuStaticCast(this->ntEvent_); } AuList 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(watcher); } }