Skip to content

Instantly share code, notes, and snippets.

@dwilliamson
Created January 22, 2015 21:39
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save dwilliamson/caa8a3874238967fc427 to your computer and use it in GitHub Desktop.
Save dwilliamson/caa8a3874238967fc427 to your computer and use it in GitHub Desktop.
Single-threaded file watcher with overlapped I/O and filtering of multiple notifications
class FileWatcher
{
public:
FileWatcher(const Path& path)
: m_Path(path)
, m_DirHandle(INVALID_HANDLE_VALUE)
, m_BufferSize(CORE_KB(100))
, m_Buffer(nullptr)
, m_ChangedFiles(1024)
{
memset(&m_Overlapped, 0, sizeof(m_Overlapped));
m_Buffer = new u8[m_BufferSize];
}
~FileWatcher()
{
// Abort any pending watches
if (m_DirHandle != INVALID_HANDLE_VALUE)
{
CancelIo(m_DirHandle);
DWORD nb_bytes;
GetOverlappedResult(m_DirHandle, &m_Overlapped, &nb_bytes, TRUE);
}
if (m_DirHandle != INVALID_HANDLE_VALUE)
CloseHandle(m_DirHandle);
delete [] m_Buffer;
}
bool Init()
{
// Get a handle to the directory being watched
m_DirHandle = CreateFileA(
m_Path.c_str(),
FILE_LIST_DIRECTORY,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
0);
if (m_DirHandle == INVALID_HANDLE_VALUE)
return false;
// Create an event for async waiting
// The even handle is unused when using completion routines
// Use it to store the this pointer
m_Overlapped.hEvent = this;
// Kick-off the initial watch
if (!Watch())
return false;
return true;
}
void Update(core::Vector<Path>& changed_files)
{
rmt_ScopedCPUSample(DirectoryWatcherUpdate);
core::Milliseconds cur_time = core::GetLowResTimer();
// Walk all changed files in the hash table
core::Vector<u32> expired_changes;
changed_files.clear_resize(0);
for (core::HashTableIterator i(m_ChangedFiles); i.IsValid(); i.MoveNext())
{
ChangedFile& changed_file = *(ChangedFile*)i.GetPtr();
// Pass filename back to the caller if not done before
if (!changed_file.processed)
{
changed_files.push_back(changed_file.path);
changed_file.processed = true;
}
// Track which changes are safe to discard
if (cur_time - changed_file.first_change_time > core::Milliseconds(500))
expired_changes.push_back(i.GetHash());
}
// Remove all expired file changes
for (u32 i = 0; i < expired_changes.size(); i++)
m_ChangedFiles.remove(expired_changes[i]);
// Make the thread alertable so that IO events are processed
MsgWaitForMultipleObjectsEx(0, NULL, 0, QS_ALLINPUT, MWMO_ALTERTABLE);
}
private:
void AddFile(const file::Path& path)
{
// Don't add if it's already been added
ChangedFile change(path);
if (m_ChangedFiles.find(change.hash))
return;
core::String512 text;
text.setv("FILE WATCHER: Changed file %s", path.c_str());
rmt_LogText(text.c_str());
// Hash table owns the new entry
m_ChangedFiles.insert(change.hash, new ChangedFile(change));
}
static void CALLBACK WatchCallback(DWORD dwErrorCode, DWORD dwNumberOfBytesTransferred, LPOVERLAPPED lpOverlapped)
{
if (dwNumberOfBytesTransferred == 0)
return;
if (dwErrorCode != 0)
return;
// Alias event in overlapped structure to get this pointer
FileWatcher* watcher = (FileWatcher*)lpOverlapped->hEvent;
if (watcher == nullptr)
return;
// Walk all notifications
DWORD offset = 0;
while (true)
{
FILE_NOTIFY_INFORMATION* notify = (FILE_NOTIFY_INFORMATION*)(watcher->m_Buffer + offset);
// Hacky way to convert wchar path to ascii
file::Path path;
for (DWORD i = 0; i < notify->FileNameLength; i += 2)
path.append(((char*)notify->FileName)[i]);
watcher->AddFile(path);
if (notify->NextEntryOffset == 0)
break;
offset += notify->NextEntryOffset;
}
// Kick-off the next watch
watcher->Watch();
}
bool Watch()
{
return ReadDirectoryChangesW(
m_DirHandle,
m_Buffer,
m_BufferSize,
TRUE,
FILE_NOTIFY_CHANGE_LAST_WRITE,
NULL,
&m_Overlapped,
&WatchCallback) != 0;
}
OVERLAPPED m_Overlapped;
Path m_Path;
HANDLE m_DirHandle;
const u32 m_BufferSize;
u8* m_Buffer;
// Hash table of recent changed files for filtering out double-notifications
// from the file system
core::HashTable m_ChangedFiles;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment