diff options
author | ckamm <mail@ckamm.de> | 2016-04-29 17:14:18 +0300 |
---|---|---|
committer | Klaas Freitag <freitag@owncloud.com> | 2016-04-29 17:14:18 +0300 |
commit | e6b937f508a78d6eae3e9d187d70271535d23d86 (patch) | |
tree | 3b5db20231d7a8bfd4ab947baec318685a075528 | |
parent | 31c13f74fbc2149fd9e58a5796dc17bb209440fb (diff) |
LockWatcher: Keep an eye on Windows file locks (#4758)
When a conflict-rename or a temporary-rename fails, notify the
LockWatcher. It'll regularly check whether the file has become
accesible again. When it has, another sync is triggered.
owncloud/enterprise#1288
-rw-r--r-- | src/gui/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/gui/folder.cpp | 1 | ||||
-rw-r--r-- | src/gui/folderman.cpp | 17 | ||||
-rw-r--r-- | src/gui/folderman.h | 15 | ||||
-rw-r--r-- | src/gui/lockwatcher.cpp | 51 | ||||
-rw-r--r-- | src/gui/lockwatcher.h | 66 | ||||
-rw-r--r-- | src/libsync/filesystem.cpp | 35 | ||||
-rw-r--r-- | src/libsync/filesystem.h | 5 | ||||
-rw-r--r-- | src/libsync/owncloudpropagator.h | 3 | ||||
-rw-r--r-- | src/libsync/propagatedownload.cpp | 18 | ||||
-rw-r--r-- | src/libsync/syncengine.cpp | 1 | ||||
-rw-r--r-- | src/libsync/syncengine.h | 6 | ||||
-rw-r--r-- | test/CMakeLists.txt | 1 |
13 files changed, 218 insertions, 2 deletions
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 93ebd2083..1bb50b864 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -54,6 +54,7 @@ set(client_SRCS folderwizard.cpp generalsettings.cpp ignorelisteditor.cpp + lockwatcher.cpp logbrowser.cpp networksettings.cpp ocsjob.cpp diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index 6f4407d3f..4984ca872 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -115,6 +115,7 @@ Folder::Folder(const FolderDefinition& definition, connect(_engine.data(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &)), this, SLOT(slotItemCompleted(const SyncFileItem &, const PropagatorJob &))); connect(_engine.data(), SIGNAL(newBigFolder(QString)), this, SLOT(slotNewBigFolderDiscovered(QString))); + connect(_engine.data(), SIGNAL(seenLockedFile(QString)), FolderMan::instance(), SLOT(slotSyncOnceFileUnlocks(QString))); } Folder::~Folder() diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp index 3bda86756..fc4268053 100644 --- a/src/gui/folderman.cpp +++ b/src/gui/folderman.cpp @@ -22,6 +22,7 @@ #include "accountstate.h" #include "accountmanager.h" #include "filesystem.h" +#include "lockwatcher.h" #include <syncengine.h> #ifdef Q_OS_MAC @@ -44,6 +45,7 @@ FolderMan::FolderMan(QObject *parent) : QObject(parent), _currentSyncFolder(0), _syncEnabled( true ), + _lockWatcher(new LockWatcher), _appRestartRequired(false) { Q_ASSERT(!_instance); @@ -64,6 +66,9 @@ FolderMan::FolderMan(QObject *parent) : connect(AccountManager::instance(), SIGNAL(accountRemoved(AccountState*)), SLOT(slotRemoveFoldersForAccount(AccountState*))); + + connect(_lockWatcher.data(), SIGNAL(fileUnlocked(QString)), + SLOT(slotScheduleFolderOwningFile(QString))); } FolderMan *FolderMan::instance() @@ -460,6 +465,11 @@ void FolderMan::slotScheduleAppRestart() qDebug() << "## Application restart requested!"; } +void FolderMan::slotSyncOnceFileUnlocks(const QString& path) +{ + _lockWatcher->addFile(path); +} + /* * if a folder wants to be synced, it calls this slot and is added * to the queue. The slot to actually start a sync is called afterwards. @@ -744,6 +754,13 @@ void FolderMan::slotServerVersionChanged(Account *account) } } +void FolderMan::slotScheduleFolderOwningFile(const QString& path) +{ + if (Folder* f = folderForPath(path)) { + slotScheduleSync(f); + } +} + void FolderMan::slotFolderSyncStarted( ) { qDebug() << ">===================================== sync started for " << _currentSyncFolder->remoteUrl().toString(); diff --git a/src/gui/folderman.h b/src/gui/folderman.h index e597e52d4..62205d050 100644 --- a/src/gui/folderman.h +++ b/src/gui/folderman.h @@ -32,6 +32,7 @@ namespace OCC { class Application; class SyncResult; class SocketApi; +class LockWatcher; /** * @brief The FolderMan class @@ -184,6 +185,13 @@ public slots: */ void slotScheduleAppRestart(); + /** + * Triggers a sync run once the lock on the given file is removed. + * + * Automatically detemines the folder that's responsible for the file. + */ + void slotSyncOnceFileUnlocks(const QString& path); + private slots: // slot to take the next folder from queue and start syncing. void slotStartScheduledFolderSync(); @@ -197,6 +205,12 @@ private slots: void slotServerVersionChanged(Account* account); + /** + * Schedules the folder for synchronization that contains + * the file with the given path. + */ + void slotScheduleFolderOwningFile(const QString& path); + private: /** Adds a new folder, does not add it to the account settings and * does not set an account on the new folder. @@ -227,6 +241,7 @@ private: QPointer<RequestEtagJob> _currentEtagJob; // alias of Folder running the current RequestEtagJob QMap<QString, FolderWatcher*> _folderWatchers; + QScopedPointer<LockWatcher> _lockWatcher; QScopedPointer<SocketApi> _socketApi; /** The aliases of folders that shall be synced. */ diff --git a/src/gui/lockwatcher.cpp b/src/gui/lockwatcher.cpp new file mode 100644 index 000000000..7b1247990 --- /dev/null +++ b/src/gui/lockwatcher.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (C) by Christian Kamm <mail@ckamm.de> + * + * This program 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; version 2 of the License. + * + * This program 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. + */ + +#include "lockwatcher.h" +#include "filesystem.h" + +#include <QTimer> + +using namespace OCC; + +static const int check_frequency = 20 * 1000; // ms + +LockWatcher::LockWatcher(QObject* parent) + : QObject(parent) +{ + connect(&_timer, SIGNAL(timeout()), + SLOT(checkFiles())); + _timer.start(check_frequency); +} + +void LockWatcher::addFile(const QString& path) +{ + _watchedPaths.insert(path); +} + +void LockWatcher::checkFiles() +{ + QSet<QString> unlocked; + + foreach (const QString& path, _watchedPaths) { + if (!FileSystem::isFileLocked(path)) { + emit fileUnlocked(path); + unlocked.insert(path); + } + } + + // Doing it this way instead of with a QMutableSetIterator + // ensures that calling back into addFile from connected + // slots isn't a problem. + _watchedPaths.subtract(unlocked); +} diff --git a/src/gui/lockwatcher.h b/src/gui/lockwatcher.h new file mode 100644 index 000000000..d5a7a05fa --- /dev/null +++ b/src/gui/lockwatcher.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) by Christian Kamm <mail@ckamm.de> + * + * This program 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; version 2 of the License. + * + * This program 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. + */ + +#pragma once + +#include "config.h" + +#include <QList> +#include <QObject> +#include <QString> +#include <QSet> +#include <QTimer> + +namespace OCC { + +/** + * @brief Monitors files that are locked, signaling when they become unlocked + * + * Only relevant on Windows. Some high-profile applications like Microsoft + * Word lock the document that is currently being edited. The synchronization + * client will be unable to update them while they are locked. + * + * In this situation we do want to start a sync run as soon as the file + * becomes available again. To do that, we need to regularly check whether + * the file is still being locked. + * + * @ingroup gui + */ + +class LockWatcher : public QObject +{ + Q_OBJECT +public: + explicit LockWatcher(QObject* parent = 0); + + /** Start watching a file. + * + * If the file is not locked later on, the fileUnlocked signal will be + * emitted once. + */ + void addFile(const QString& path); + +signals: + /** Emitted when one of the watched files is no longer + * being locked. */ + void fileUnlocked(const QString& path); + +private slots: + void checkFiles(); + +private: + QSet<QString> _watchedPaths; + QTimer _timer; +}; + +} diff --git a/src/libsync/filesystem.cpp b/src/libsync/filesystem.cpp index 09fd16826..f5f5e7da4 100644 --- a/src/libsync/filesystem.cpp +++ b/src/libsync/filesystem.cpp @@ -43,6 +43,7 @@ extern "C" { #include "csync.h" #include "vio/csync_vio_local.h" #include "std/c_path.h" +#include "std/c_string.h" } namespace OCC { @@ -589,4 +590,38 @@ bool FileSystem::remove(const QString &fileName, QString *errorString) return true; } +bool FileSystem::isFileLocked(const QString& fileName) +{ +#ifdef Q_OS_WIN + mbchar_t *wuri = c_utf8_path_to_locale(fileName.toUtf8()); + + // Check if file exists + DWORD attr = GetFileAttributesW(wuri); + if (attr != INVALID_FILE_ATTRIBUTES) { + // Try to open the file with as much access as possible.. + HANDLE win_h = CreateFileW( + wuri, + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, + NULL); + + c_free_locale_string(wuri); + if (win_h == INVALID_HANDLE_VALUE) { + /* could not be opened, so locked? */ + /* 32 == ERROR_SHARING_VIOLATION */ + return true; + } else { + CloseHandle(win_h); + } + } else { + c_free_locale_string(wuri); + } +#else + Q_UNUSED(fileName); +#endif + return false; +} + } // namespace OCC diff --git a/src/libsync/filesystem.h b/src/libsync/filesystem.h index addab46c5..ebee6b949 100644 --- a/src/libsync/filesystem.h +++ b/src/libsync/filesystem.h @@ -182,6 +182,11 @@ QByteArray OWNCLOUDSYNC_EXPORT calcAdler32( const QString& fileName ); */ QString OWNCLOUDSYNC_EXPORT makeConflictFileName(const QString &fn, const QDateTime &dt); +/** + * Returns true when a file is locked. (Windows only) + */ +bool OWNCLOUDSYNC_EXPORT isFileLocked(const QString& fileName); + } /** @} */ diff --git a/src/libsync/owncloudpropagator.h b/src/libsync/owncloudpropagator.h index 3718efff3..db8a86b41 100644 --- a/src/libsync/owncloudpropagator.h +++ b/src/libsync/owncloudpropagator.h @@ -369,6 +369,9 @@ signals: void progress(const SyncFileItem&, quint64 bytes); void finished(); + /** Emitted when propagation has problems with a locked file. */ + void seenLockedFile(const QString &fileName); + private: AccountPtr _account; diff --git a/src/libsync/propagatedownload.cpp b/src/libsync/propagatedownload.cpp index 779fe5c36..17b2421db 100644 --- a/src/libsync/propagatedownload.cpp +++ b/src/libsync/propagatedownload.cpp @@ -704,7 +704,14 @@ void PropagateDownloadFileQNAM::downloadFinished() QString renameError; QString conflictFileName = FileSystem::makeConflictFileName(fn, Utility::qDateTimeFromTime_t(_item->_modtime)); if (!FileSystem::rename(fn, conflictFileName, &renameError)) { - //If the rename fails, don't replace it. + // If the rename fails, don't replace it. + + // If the file is locked, we want to retry this sync when it + // becomes available again. + if (FileSystem::isFileLocked(fn)) { + emit _propagator->seenLockedFile(fn); + } + done(SyncFileItem::SoftError, renameError); return; } @@ -764,7 +771,14 @@ void PropagateDownloadFileQNAM::downloadFinished() _propagator->_journal->commit("download finished"); } - _propagator->_anotherSyncNeeded = true; + // If the file is locked, we want to retry this sync when it + // becomes available again, otherwise try again directly + if (FileSystem::isFileLocked(fn)) { + emit _propagator->seenLockedFile(fn); + } else { + _propagator->_anotherSyncNeeded = true; + } + done(SyncFileItem::SoftError, error); return; } diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index a826d7840..fdb9de483 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -933,6 +933,7 @@ void SyncEngine::slotDiscoveryJobFinished(int discoveryResult) connect(_propagator.data(), SIGNAL(progress(const SyncFileItem &,quint64)), this, SLOT(slotProgress(const SyncFileItem &,quint64))); connect(_propagator.data(), SIGNAL(finished()), this, SLOT(slotFinished()), Qt::QueuedConnection); + connect(_propagator.data(), SIGNAL(seenLockedFile(QString)), SIGNAL(seenLockedFile(QString))); // apply the network limits to the propagator setNetworkLimits(_uploadLimit, _downloadLimit); diff --git a/src/libsync/syncengine.h b/src/libsync/syncengine.h index 934e0728c..084ad2387 100644 --- a/src/libsync/syncengine.h +++ b/src/libsync/syncengine.h @@ -143,6 +143,12 @@ signals: // A new folder was discovered and was not synced because of the confirmation feature void newBigFolder(const QString &folder); + /** Emitted when propagation has problems with a locked file. + * + * Forwarded from OwncloudPropagator::seenLockedFile. + */ + void seenLockedFile(const QString &fileName); + private slots: void slotRootEtagReceived(const QString &); void slotItemCompleted(const SyncFileItem& item, const PropagatorJob & job); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index cf3f0a78c..f701700aa 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -48,6 +48,7 @@ list(APPEND FolderMan_SRC ../src/gui/folder.cpp ) list(APPEND FolderMan_SRC ../src/gui/socketapi.cpp ) list(APPEND FolderMan_SRC ../src/gui/accountstate.cpp ) list(APPEND FolderMan_SRC ../src/gui/syncrunfilelog.cpp ) +list(APPEND FolderMan_SRC ../src/gui/lockwatcher.cpp ) list(APPEND FolderMan_SRC ${FolderWatcher_SRC}) list(APPEND FolderMan_SRC stub.cpp ) #include_directories(${QTKEYCHAIN_INCLUDE_DIR}) |