1
0
forked from 0ad/0ad
0ad/source/lib/sysdep/os/win/wdir_watch.cpp
2009-04-18 17:51:05 +00:00

408 lines
11 KiB
C++

/* Copyright (C) 2009 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Win32 directory change notification
*/
#include "precompiled.h"
#include "lib/sysdep/dir_watch.h"
#include "lib/path_util.h" // path_is_subpath
#include "win.h"
#include "winit.h"
#include "wutil.h"
WINIT_REGISTER_MAIN_INIT(wdir_watch_Init);
WINIT_REGISTER_MAIN_SHUTDOWN(wdir_watch_Shutdown);
//-----------------------------------------------------------------------------
// DirWatchRequest
class DirWatchRequest
{
public:
DirWatchRequest()
: m_data(new char[dataSize])
{
m_undefined = 0;
memset(&m_ovl, 0, sizeof(m_ovl));
}
/**
* @return the buffer containing one or more FILE_NOTIFY_INFORMATION
**/
char* Results() const
{
debug_assert(HasOverlappedIoCompleted(&m_ovl));
return m_data.get();
}
void Issue(HANDLE hDir)
{
memset(&m_ovl, 0, sizeof(m_ovl));
const DWORD filter = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME |
FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE |
FILE_NOTIFY_CHANGE_CREATION;
// (this is much faster than watching every directory separately.)
const BOOL watchSubtree = TRUE;
const BOOL ok = ReadDirectoryChangesW(hDir, m_data.get(), dataSize, watchSubtree, filter, &m_undefined, &m_ovl, 0);
WARN_IF_FALSE(ok);
}
private:
// rationale:
// - if too small, notifications may be lost! (the CSD-poll application
// may be confronted with hundreds of new files in a short timeframe)
// - requests larger than 64 KiB fail on SMB due to packet restrictions.
static const size_t dataSize = 64*KiB;
// note: each instance needs their own buffer. (we can't share a central
// copy because the watches are independent and may be triggered
// 'simultaneously' before the next poll.)
shared_ptr<char> m_data;
// (passing this instead of a null pointer avoids a BoundsChecker warning
// but has no other value since its contents are undefined.)
DWORD m_undefined;
// (ReadDirectoryChangesW's asynchronous mode is triggered by passing
// a valid OVERLAPPED parameter; we don't use its fields because
// notification proceeds via completion ports.)
OVERLAPPED m_ovl;
};
//-----------------------------------------------------------------------------
// CompletionPort
// this appears to be the best solution for IO notification.
// there are three alternatives:
// - multiple threads with blocking I/O. this is rather inefficient when
// many directories (e.g. mods) are being watched.
// - normal overlapped I/O: build a contiguous array of the hEvents
// in all OVERLAPPED structures, and WaitForMultipleObjects.
// it would be cumbersome to update this array when adding/removing watches.
// - callback notification: a notification function is called when the thread
// that initiated the I/O (ReadDirectoryChangesW) enters an alertable
// wait state. it is desirable for notifications to arrive at a single
// known point - see dir_watch_Poll. unfortunately there doesn't appear to
// be a reliable and non-blocking means of entering AWS - SleepEx(1) may
// wait for 10..15 ms if the system timer granularity is low. even worse,
// it was noted in a previous project that APCs are sometimes delivered from
// within APIs without having used SleepEx (it seems threads sometimes enter
// an "AWS" when calling the kernel).
class CompletionPort
{
public:
CompletionPort()
{
m_hIOCP = 0; // CreateIoCompletionPort requires 0, not INVALID_HANDLE_VALUE
}
~CompletionPort()
{
CloseHandle(m_hIOCP);
m_hIOCP = INVALID_HANDLE_VALUE;
}
void Attach(HANDLE hFile, uintptr_t key)
{
WinScopedPreserveLastError s; // CreateIoCompletionPort
// (when called for the first time, ends up creating m_hIOCP)
m_hIOCP = CreateIoCompletionPort(hFile, m_hIOCP, (ULONG_PTR)key, 0);
debug_assert(wutil_IsValidHandle(m_hIOCP));
}
LibError Poll(size_t& bytesTransferred, uintptr_t& key, OVERLAPPED*& ovl)
{
DWORD dwBytesTransferred = 0;
ULONG_PTR ulKey = 0;
ovl = 0;
{
const DWORD timeout = 0;
const BOOL gotPacket = GetQueuedCompletionStatus(m_hIOCP, &dwBytesTransferred, &ulKey, &ovl, timeout);
bytesTransferred = size_t(bytesTransferred);
key = uintptr_t(ulKey);
if(gotPacket)
return INFO::OK;
}
if(GetLastError() == WAIT_TIMEOUT)
return ERR::AGAIN; // NOWARN
// else: there was actually an error
return LibError_from_GLE();
}
private:
HANDLE m_hIOCP;
};
//-----------------------------------------------------------------------------
class DirWatch
{
public:
DirWatch(const fs::wpath& path, CompletionPort& completionPort)
: m_path(path)
{
m_path /= L"\\"; // must end in slash
// open handle to directory
{
WinScopedPreserveLastError s; // CreateFile
const DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
const DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED;
const std::wstring dirPath = m_path.external_directory_string();
m_hDir = CreateFileW(dirPath.c_str(), FILE_LIST_DIRECTORY, share, 0, OPEN_EXISTING, flags, 0);
if(m_hDir == INVALID_HANDLE_VALUE)
throw std::runtime_error("");
}
completionPort.Attach(m_hDir, (uintptr_t)this);
}
~DirWatch()
{
// contrary to MSDN, the RDC IOs do not issue a completion notification.
// no packet was received on the IOCP while or after canceling in a test.
// if cancel fails and future packets are received, we'd need weak pointers..
BOOL ok = CancelIo(m_hDir);
debug_assert(ok);
CloseHandle(m_hDir);
m_hDir = INVALID_HANDLE_VALUE;
}
void Issue()
{
m_request.Issue(m_hDir);
}
bool IncludesDirectory(const fs::wpath& path) const
{
return path_is_subpathw(path.string().c_str(), m_path.string().c_str());
}
const fs::wpath& Path() const
{
return m_path;
}
const char* Results() const
{
return m_request.Results();
}
private:
fs::wpath m_path;
HANDLE m_hDir;
DirWatchRequest m_request;
};
//-----------------------------------------------------------------------------
// DirWatchNotificationQueue
// DirWatchRequest needs to reissue its IO immediately, else subsequent
// changes may be lost. using a second buffer is more complicated and
// also not perfectly safe. instead it is preferable to copy the data to
// a queue, thus completely decoupling the application and IO logic.
class DirWatchNotificationQueue
{
public:
/**
* extract all notifications from a buffer and add them to the queue.
*
* @param path of the watched directory; necessary since
* ReadDirectoryChangesW only returns filenames.
* @param packets points to at least one (variable-length)
* FILE_NOTIFY_INFORMATION.
**/
void Enqueue(const fs::wpath& path, const char* const packets)
{
const char* pos = packets;
for(;;)
{
const FILE_NOTIFY_INFORMATION* fni = (const FILE_NOTIFY_INFORMATION*)pos;
const DirWatchNotification::Event type = TypeFromAction(fni->Action);
// convert from BSTR (non-zero-terminated)
debug_assert(sizeof(wchar_t) == sizeof(WCHAR));
const size_t nameChars = fni->FileNameLength / sizeof(WCHAR);
const std::wstring name(fni->FileName, nameChars);
const fs::wpath pathname(path/name);
m_queue.push(DirWatchNotification(pathname, type));
const DWORD ofs = fni->NextEntryOffset;
if(!ofs) // this was the last entry.
break;
pos += ofs;
}
}
/**
* retrieve a pending notification from the queue.
*
* @param notification is only valid if INFO::OK was returned.
*
* @return ERR::AGAIN if no notifications are pending, otherwise INFO::OK.
**/
LibError Dequeue(DirWatchNotification& notification)
{
if(m_queue.empty())
return ERR::AGAIN; // NOWARN
notification = m_queue.front();
m_queue.pop();
return INFO::OK;
}
private:
static DirWatchNotification::Event TypeFromAction(const DWORD action)
{
switch(action)
{
case FILE_ACTION_ADDED:
case FILE_ACTION_RENAMED_NEW_NAME:
return DirWatchNotification::Created;
case FILE_ACTION_REMOVED:
case FILE_ACTION_RENAMED_OLD_NAME:
return DirWatchNotification::Deleted;
case FILE_ACTION_MODIFIED:
return DirWatchNotification::Changed;
default:
throw std::logic_error("invalid action");
}
}
std::queue<DirWatchNotification> m_queue;
};
//-----------------------------------------------------------------------------
// list of active watches (required for detecting duplicates)
class DirWatchManager
{
public:
LibError Add(const fs::wpath& path, PDirWatch& dirWatch)
{
// check if this is a subdirectory of a tree that's already being
// watched (this is much faster than issuing a new watch; it also
// prevents accidentally watching the same directory twice).
for(Watches::const_iterator it = m_watches.begin(); it != m_watches.end(); ++it)
{
const PDirWatch& existingDirWatch = *it;
if(existingDirWatch->IncludesDirectory(path))
{
dirWatch = existingDirWatch;
return INFO::OK;
}
}
try
{
dirWatch.reset(new DirWatch(path, m_completionPort));
dirWatch->Issue();
}
catch(std::runtime_error e)
{
return ERR::FAIL;
}
m_watches.push_back(dirWatch);
return INFO::OK;
}
void Remove(PDirWatch& dirWatch)
{
m_watches.remove(dirWatch);
}
LibError Poll(DirWatchNotification& notification)
{
size_t bytesTransferred; uintptr_t key; OVERLAPPED* ovl;
if(m_completionPort.Poll(bytesTransferred, key, ovl) == INFO::OK)
{
DirWatch& dirWatch = *(DirWatch*)key;
const fs::wpath& path = dirWatch.Path();
const char* const packets = dirWatch.Results();
m_queue.Enqueue(path, packets);
dirWatch.Issue(); // re-issue
}
return m_queue.Dequeue(notification);
}
private:
typedef std::list<PDirWatch> Watches;
Watches m_watches;
CompletionPort m_completionPort;
DirWatchNotificationQueue m_queue;
};
static DirWatchManager* s_dirWatchManager;
//-----------------------------------------------------------------------------
LibError dir_watch_Add(const fs::wpath& path, PDirWatch& dirWatch)
{
WinScopedLock lock(WDIR_WATCH_CS);
return s_dirWatchManager->Add(path, dirWatch);
}
void dir_watch_Remove(PDirWatch& dirWatch)
{
WinScopedLock lock(WDIR_WATCH_CS);
return s_dirWatchManager->Remove(dirWatch);
}
LibError dir_watch_Poll(DirWatchNotification& notification)
{
WinScopedLock lock(WDIR_WATCH_CS);
return s_dirWatchManager->Poll(notification);
}
//-----------------------------------------------------------------------------
static LibError wdir_watch_Init()
{
s_dirWatchManager = new DirWatchManager;
return INFO::OK;
}
static LibError wdir_watch_Shutdown()
{
SAFE_DELETE(s_dirWatchManager);
return INFO::OK;
}