Windows filewatcher: switch to ReadDirectoryChangesW.

Based on danimo's #2454 fix for #2455 and related to #2297.
This commit is contained in:
Christian Kamm 2014-11-06 00:36:04 +01:00
Родитель 9dc57359b9
Коммит d4e0941c27
3 изменённых файлов: 174 добавлений и 56 удалений

Просмотреть файл

@ -13,6 +13,7 @@
#include <QThread>
#include <QDebug>
#include <QDir>
#include "mirall/folderwatcher.h"
#include "mirall/folderwatcher_win.h"
@ -23,52 +24,123 @@
namespace Mirall {
void WatcherThread::run()
void WatcherThread::watchChanges(size_t fileNotifyBufferSize,
bool* increaseBufferSize)
{
_handle = FindFirstChangeNotification((wchar_t*)_path.utf16(),
true, // recursive watch
FILE_NOTIFY_CHANGE_FILE_NAME |
FILE_NOTIFY_CHANGE_DIR_NAME |
FILE_NOTIFY_CHANGE_LAST_WRITE);
*increaseBufferSize = false;
_handle = CreateFileW(
(wchar_t*)_path.utf16(),
FILE_LIST_DIRECTORY,
FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
NULL
);
if (_handle == INVALID_HANDLE_VALUE)
{
qDebug() << Q_FUNC_INFO << "FindFirstChangeNotification function failed, stopping watcher!";
FindCloseChangeNotification(_handle);
DWORD errorCode = GetLastError();
qDebug() << Q_FUNC_INFO << "Failed to create handle for" << _path << ", error:" << errorCode;
_handle = 0;
return;
}
if (_handle == NULL)
{
qDebug() << Q_FUNC_INFO << "FindFirstChangeNotification returned null, stopping watcher!";
FindCloseChangeNotification(_handle);
_handle = 0;
return;
}
// QVarLengthArray ensures the stack-buffer is aligned like double and qint64.
QVarLengthArray<char, 4096*10> fileNotifyBuffer;
fileNotifyBuffer.resize(fileNotifyBufferSize);
while(true) {
switch(WaitForSingleObject(_handle, /*wait*/ INFINITE)) {
case WAIT_OBJECT_0:
if (FindNextChangeNotification(_handle) == false) {
qDebug() << Q_FUNC_INFO << "FindFirstChangeNotification returned FALSE, stopping watcher!";
FindCloseChangeNotification(_handle);
_handle = 0;
return;
const size_t fileNameBufferSize = 4096;
TCHAR fileNameBuffer[fileNameBufferSize];
forever {
FILE_NOTIFY_INFORMATION *pFileNotifyBuffer =
(FILE_NOTIFY_INFORMATION*)fileNotifyBuffer.data();
DWORD dwBytesReturned = 0;
SecureZeroMemory(pFileNotifyBuffer, fileNotifyBufferSize);
if(ReadDirectoryChangesW( _handle, (LPVOID)pFileNotifyBuffer,
fileNotifyBufferSize, true,
FILE_NOTIFY_CHANGE_FILE_NAME |
FILE_NOTIFY_CHANGE_DIR_NAME |
FILE_NOTIFY_CHANGE_LAST_WRITE,
&dwBytesReturned, NULL, NULL))
{
FILE_NOTIFY_INFORMATION *curEntry = pFileNotifyBuffer;
forever {
size_t len = curEntry->FileNameLength / 2;
QString file = _path + "\\" + QString::fromWCharArray(curEntry->FileName, len);
// Unless the file was removed or renamed, get its full long name
// TODO: We could still try expanding the path in the tricky cases...
QString longfile = file;
if (curEntry->Action != FILE_ACTION_REMOVED
&& curEntry->Action != FILE_ACTION_RENAMED_OLD_NAME) {
size_t longNameSize = GetLongPathNameW(reinterpret_cast<LPCWSTR>(file.utf16()), fileNameBuffer, fileNameBufferSize);
if (longNameSize > 0) {
longfile = QString::fromUtf16(reinterpret_cast<const ushort *>(fileNameBuffer), longNameSize);
} else {
qDebug() << Q_FUNC_INFO << "Error converting file name to full length, keeping original name.";
}
}
longfile = QDir::cleanPath(longfile);
qDebug() << Q_FUNC_INFO << "Found change in" << longfile << "action:" << curEntry->Action;
emit changed(longfile);
if (curEntry->NextEntryOffset == 0) {
break;
}
curEntry = (FILE_NOTIFY_INFORMATION*)(
(char*)curEntry + curEntry->NextEntryOffset);
}
// qDebug() << Q_FUNC_INFO << "Change detected in" << _path << "from" << QThread::currentThread ();
emit changed(_path);
break;
default:
qDebug() << Q_FUNC_INFO << "Error while watching";
} else {
DWORD errorCode = GetLastError();
switch(errorCode) {
case ERROR_NOTIFY_ENUM_DIR:
qDebug() << Q_FUNC_INFO << "The buffer for changes overflowed! Triggering a generic change and resizing";
emit changed(_path);
*increaseBufferSize = true;
break;
default:
qDebug() << Q_FUNC_INFO << "General error" << errorCode << "while watching. Exiting.";
break;
}
CloseHandle(_handle);
_handle = NULL;
return;
}
}
}
void WatcherThread::run()
{
// If this buffer fills up before we've extracted its data we will lose
// change information. Therefore start big.
size_t bufferSize = 4096*10;
size_t maxBuffer = 64*1024;
forever {
bool increaseBufferSize = false;
watchChanges(bufferSize, &increaseBufferSize);
if (increaseBufferSize) {
bufferSize = qMin(bufferSize*2, maxBuffer);
} else {
// Other errors shouldn't actually happen,
// so sleep a bit to avoid running into the same error case in a
// tight loop.
sleep(2);
}
}
}
WatcherThread::~WatcherThread()
{
if (_handle)
FindCloseChangeNotification(_handle);
if (_handle) {
CloseHandle(_handle);
_handle = NULL;
}
}
FolderWatcherPrivate::FolderWatcherPrivate(FolderWatcher *p, const QString& path)

Просмотреть файл

@ -33,6 +33,8 @@ public:
protected:
void run();
void watchChanges(size_t fileNotifyBufferSize,
bool* increaseBufferSize);
signals:
void changed(const QString &path);

Просмотреть файл

@ -15,15 +15,23 @@
using namespace Mirall;
class FriendlyThread : public QThread
{
friend class TestFolderWatcher;
};
class TestFolderWatcher : public QObject
{
Q_OBJECT
public slots:
void slotFolderChanged( const QString& path ) {
qDebug() << "COMPARE: " << path << _checkMark;
QVERIFY(_checkMark == path);
_checkMark.clear();
if (_skipNotifications.contains(path)) {
return;
}
if (_requiredNotifications.contains(path)) {
_receivedNotifications.insert(path);
}
}
void slotEnd() { // in case something goes wrong...
@ -36,7 +44,16 @@ private:
FolderWatcher *_watcher;
QEventLoop _loop;
QTimer _timer;
QString _checkMark;
QSet<QString> _requiredNotifications;
QSet<QString> _receivedNotifications;
QSet<QString> _skipNotifications;
void processAndWait()
{
_loop.processEvents();
FriendlyThread::msleep(200);
_loop.processEvents();
}
private slots:
void initTestCase() {
@ -52,68 +69,95 @@ private slots:
rootDir.mkpath(_root + "/a2/b3/c3");
Utility::writeRandomFile( _root+"/a1/random.bin");
Utility::writeRandomFile( _root+"/a1/b2/todelete.bin");
Utility::writeRandomFile( _root+"/a2/movefile");
Utility::writeRandomFile( _root+"/a2/renamefile");
Utility::writeRandomFile( _root+"/a1/movefile");
_watcher = new FolderWatcher(_root);
QObject::connect(_watcher, SIGNAL(folderChanged(QString)), this, SLOT(slotFolderChanged(QString)));
_timer.singleShot(3000, this, SLOT(slotEnd()));
_timer.singleShot(5000, this, SLOT(slotEnd()));
}
void init()
{
_receivedNotifications.clear();
_requiredNotifications.clear();
_skipNotifications.clear();
}
void checkNotifications()
{
processAndWait();
QCOMPARE(_receivedNotifications, _requiredNotifications);
}
void testACreate() { // create a new file
QString cmd;
_checkMark = _root;
_requiredNotifications.insert(_root);
cmd = QString("echo \"xyz\" > %1/foo.txt").arg(_root);
qDebug() << "Command: " << cmd;
system(cmd.toLocal8Bit());
_loop.processEvents();
QVERIFY(_checkMark.isEmpty()); // the slot clears the checkmark.
checkNotifications();
}
void testATouch() { // touch an existing file.
_requiredNotifications.insert(_root+"/a1");
#ifdef Q_OS_WIN
Utility::writeRandomFile(QString("%1/a1/random.bin").arg(_root));
#else
QString cmd;
cmd = QString("/usr/bin/touch %1/a1/random.bin").arg(_root);
_checkMark = _root+"/a1";
qDebug() << "Command: " << cmd;
system(cmd.toLocal8Bit());
#endif
_loop.processEvents();
QVERIFY(_checkMark.isEmpty()); // the slot clears the checkmark.
checkNotifications();
}
void testCreateADir() {
_checkMark = _root+"/a1/b1";
_requiredNotifications.insert(_root+"/a1/b1");
_skipNotifications.insert(_root + "/a1/b1/new_dir");
QDir dir;
dir.mkdir( _root + "/a1/b1/new_dir");
QVERIFY(QFile::exists(_root + "/a1/b1/new_dir"));
_loop.processEvents();
QVERIFY(_checkMark.isEmpty()); // the slot clears the checkmark.
checkNotifications();
}
void testRemoveADir() {
_checkMark = _root+"/a1/b3";
_requiredNotifications.insert(_root+"/a1/b3");
QDir dir;
QVERIFY(dir.rmdir(_root+"/a1/b3/c3"));
_loop.processEvents();
QVERIFY(_checkMark.isEmpty()); // the slot clears the checkmark.
checkNotifications();
}
void testRemoveAFile() {
_checkMark = _root+"/a1/b2";
_requiredNotifications.insert(_root+"/a1/b2");
QVERIFY(QFile::exists(_root+"/a1/b2/todelete.bin"));
QFile::remove(_root+"/a1/b2/todelete.bin");
QVERIFY(!QFile::exists(_root+"/a1/b2/todelete.bin"));
_loop.processEvents();
QVERIFY(_checkMark.isEmpty()); // the slot clears the checkmark.
checkNotifications();
}
void testRenameAFile() {
_requiredNotifications.insert(_root+"/a2");
QVERIFY(QFile::exists(_root+"/a2/renamefile"));
QFile::rename(_root+"/a2/renamefile", _root+"/a2/renamefile.renamed" );
QVERIFY(QFile::exists(_root+"/a2/renamefile.renamed"));
checkNotifications();
}
void testMoveAFile() {
_checkMark = _root+"/a2";
QVERIFY(QFile::exists(_root+"/a2/movefile"));
QFile::rename(_root+"/a2/movefile", _root+"/a2/movefile.renamed" );
_requiredNotifications.insert(_root+"/a1");
_requiredNotifications.insert(_root+"/a2");
QVERIFY(QFile::exists(_root+"/a1/movefile"));
QFile::rename(_root+"/a1/movefile", _root+"/a2/movefile.renamed" );
QVERIFY(QFile::exists(_root+"/a2/movefile.renamed"));
_loop.processEvents();
QVERIFY(_checkMark.isEmpty()); // the slot clears the checkmark.
checkNotifications();
}
void cleanupTestCase() {