diff options
Diffstat (limited to 'FreeFileSync/Source/RealTimeSync/monitor.cpp')
-rwxr-xr-x[-rw-r--r--] | FreeFileSync/Source/RealTimeSync/monitor.cpp | 568 |
1 files changed, 278 insertions, 290 deletions
diff --git a/FreeFileSync/Source/RealTimeSync/monitor.cpp b/FreeFileSync/Source/RealTimeSync/monitor.cpp index 87c7e5e1..6d80e689 100644..100755 --- a/FreeFileSync/Source/RealTimeSync/monitor.cpp +++ b/FreeFileSync/Source/RealTimeSync/monitor.cpp @@ -1,290 +1,278 @@ -// ***************************************************************************** -// * This file is part of the FreeFileSync project. It is distributed under * -// * GNU General Public License: http://www.gnu.org/licenses/gpl-3.0 * -// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * -// ***************************************************************************** - -#include "monitor.h" -#include <ctime> -#include <set> -#include <zen/file_access.h> -#include <zen/dir_watcher.h> -#include <zen/thread.h> -//#include <zen/tick_count.h> -#include <zen/basic_math.h> -#include <wx/utils.h> -#include "../lib/resolve_path.h" -//#include "../library/db_file.h" //SYNC_DB_FILE_ENDING -> complete file too much of a dependency; file ending too little to decouple into single header -//#include "../library/lock_holder.h" //LOCK_FILE_ENDING -//TEMP_FILE_ENDING - -using namespace zen; - - -namespace -{ -const int FOLDER_EXISTENCE_CHECK_INTERVAL_SEC = 1; //unit: [s] - - -std::vector<Zstring> getFormattedDirs(const std::vector<Zstring>& folderPathPhrases) //throw FileError -{ - std::set<Zstring, LessFilePath> folderPaths; //make unique - for (const Zstring& phrase : std::set<Zstring, LessFilePath>(folderPathPhrases.begin(), folderPathPhrases.end())) - { - if (pathStartsWith(trimCpy(phrase), Zstr("ftp:")) || - pathStartsWith(trimCpy(phrase), Zstr("sftp:")) || - pathStartsWith(trimCpy(phrase), Zstr("mtp:"))) - throw FileError(_("The following path does not support directory monitoring:") + L"\n\n" + fmtPath(phrase)); - - //make unique: no need to resolve duplicate phrases more than once! (consider "[volume name]" syntax) -> shouldn't this be already buffered by OS? - folderPaths.insert(getResolvedFilePath(phrase)); - } - - return { folderPaths.begin(), folderPaths.end() }; -} - - -//wait until changes are detected or if a directory is not available (anymore) -struct WaitResult -{ - enum ChangeType - { - CHANGE_DETECTED, - CHANGE_DIR_UNAVAILABLE //not existing or can't access - }; - - WaitResult(const zen::DirWatcher::Entry& changedItem) : type(CHANGE_DETECTED), changedItem_(changedItem) {} - WaitResult(const Zstring& folderPath) : type(CHANGE_DIR_UNAVAILABLE), folderPath_(folderPath) {} - - ChangeType type; - zen::DirWatcher::Entry changedItem_; //for type == CHANGE_DETECTED: file or directory - Zstring folderPath_; //for type == CHANGE_DIR_UNAVAILABLE -}; - - -WaitResult waitForChanges(const std::vector<Zstring>& folderPathPhrases, //throw FileError - const std::function<void(bool readyForSync)>& onRefreshGui) -{ - const std::vector<Zstring> folderPaths = getFormattedDirs(folderPathPhrases); //throw FileError - if (folderPaths.empty()) //pathological case, but we have to check else this function will wait endlessly - throw FileError(_("A folder input field is empty.")); //should have been checked by caller! - - //detect when volumes are removed/are not available anymore - std::vector<std::pair<Zstring, std::shared_ptr<DirWatcher>>> watches; - - for (const Zstring& folderPath : folderPaths) - { - try - { - //a non-existent network path may block, so check existence asynchronously! - auto ftDirAvailable = runAsync([=] { return dirAvailable(folderPath); }); - - while (ftDirAvailable.wait_for(std::chrono::milliseconds(rts::UI_UPDATE_INTERVAL_MS / 2)) != std::future_status::ready) - onRefreshGui(false /*readyForSync*/); //may throw! - - if (!ftDirAvailable.get()) //folder not existing or can't access - return WaitResult(folderPath); - - watches.emplace_back(folderPath, std::make_shared<DirWatcher>(folderPath)); //throw FileError - } - catch (FileError&) - { - if (!dirAvailable(folderPath)) //folder not existing or can't access - return WaitResult(folderPath); - throw; - } - } - - auto lastCheckTime = std::chrono::steady_clock::now(); - for (;;) - { - const bool checkDirNow = [&]() -> bool //checking once per sec should suffice - { - const auto now = std::chrono::steady_clock::now(); - - if (numeric::dist(now, lastCheckTime) > std::chrono::seconds(FOLDER_EXISTENCE_CHECK_INTERVAL_SEC)) //handle potential chrono wrap-around! - { - lastCheckTime = now; - return true; - } - return false; - }(); - - - for (auto it = watches.begin(); it != watches.end(); ++it) - { - const Zstring& folderPath = it->first; - DirWatcher& watcher = *(it->second); - - //IMPORTANT CHECK: dirwatcher has problems detecting removal of top watched directories! - if (checkDirNow) - if (!dirAvailable(folderPath)) //catch errors related to directory removal, e.g. ERROR_NETNAME_DELETED - return WaitResult(folderPath); - try - { - std::vector<DirWatcher::Entry> changedItems = watcher.getChanges([&] { onRefreshGui(false /*readyForSync*/); /*may throw!*/ }); //throw FileError - - //remove to be ignored changes - erase_if(changedItems, [](const DirWatcher::Entry& e) - { - return -#ifdef ZEN_MAC - pathEndsWith(e.filepath_, Zstr("/.DS_Store")) || -#endif - //pathEndsWith(e.filepath_, Zstr(".ffs_tmp")) || - pathEndsWith(e.filepath_, Zstr(".ffs_lock")) || //sync.ffs_lock, sync.Del.ffs_lock - pathEndsWith(e.filepath_, Zstr(".ffs_db")); //sync.ffs_db, .sync.tmp.ffs_db - //no need to ignore temporal recycle bin directory: this must be caused by a file deletion anyway - }); - - if (!changedItems.empty()) - return WaitResult(changedItems[0]); //directory change detected - } - catch (FileError&) - { - if (!dirAvailable(folderPath)) //a benign(?) race condition with FileError - return WaitResult(folderPath); - throw; - } - } - - std::this_thread::sleep_for(std::chrono::milliseconds(rts::UI_UPDATE_INTERVAL_MS / 2)); - onRefreshGui(true /*readyForSync*/); //throw ?: may start sync at this presumably idle time - } -} - - -//wait until all directories become available (again) + logs in network share -void waitForMissingDirs(const std::vector<Zstring>& folderPathPhrases, //throw FileError - const std::function<void(const Zstring& folderPath)>& onRefreshGui) -{ - for (;;) - { - bool allAvailable = true; - //support specifying volume by name => call getResolvedFilePath() repeatedly - for (const Zstring& folderPath : getFormattedDirs(folderPathPhrases)) //throw FileError - { - auto ftDirAvailable = runAsync([=] - { -#ifdef ZEN_WIN - //1. login to network share, if necessary -> we probably do NOT want multiple concurrent runs: GUI!? - try - { - connectNetworkShare(folderPath, false /*allowUserInteraction*/); //throw FileError - //is this really RTS's job? - } - catch (FileError&) {} -#endif - //2. check dir availability - return dirAvailable(folderPath); - }); - while (ftDirAvailable.wait_for(std::chrono::milliseconds(rts::UI_UPDATE_INTERVAL_MS / 2)) != std::future_status::ready) - onRefreshGui(folderPath); //may throw! - - if (!ftDirAvailable.get()) - { - allAvailable = false; - //wait some time... - const int refreshInterval = rts::UI_UPDATE_INTERVAL_MS / 2; - static_assert(FOLDER_EXISTENCE_CHECK_INTERVAL_SEC * 1000 % refreshInterval == 0, ""); - for (int i = 0; i < FOLDER_EXISTENCE_CHECK_INTERVAL_SEC * 1000 / refreshInterval; ++i) - { - onRefreshGui(folderPath); //may throw! - std::this_thread::sleep_for(std::chrono::milliseconds(refreshInterval)); - } - break; - } - } - if (allAvailable) - return; - } -} - - -inline -wxString toString(DirWatcher::ActionType type) -{ - switch (type) - { - case DirWatcher::ACTION_CREATE: - return L"CREATE"; - case DirWatcher::ACTION_UPDATE: - return L"UPDATE"; - case DirWatcher::ACTION_DELETE: - return L"DELETE"; - } - return L"ERROR"; -} - -struct ExecCommandNowException {}; -} - - -void rts::monitorDirectories(const std::vector<Zstring>& folderPathPhrases, unsigned int delay, rts::MonitorCallback& callback) -{ - if (folderPathPhrases.empty()) - { - assert(false); - return; - } - - auto execMonitoring = [&] //throw FileError - { - callback.setPhase(MonitorCallback::MONITOR_PHASE_WAITING); - waitForMissingDirs(folderPathPhrases, [&](const Zstring& folderPath) { callback.requestUiRefresh(); }); //throw FileError - callback.setPhase(MonitorCallback::MONITOR_PHASE_ACTIVE); - - //schedule initial execution (*after* all directories have arrived, which could take some time which we don't want to include) - time_t nextExecDate = std::time(nullptr) + delay; - - for (;;) //loop over command invocations - { - DirWatcher::Entry lastChangeDetected; - try - { - for (;;) //loop over detected changes - { - //wait for changes (and for all directories to become available) - WaitResult res = waitForChanges(folderPathPhrases, [&](bool readyForSync) //throw FileError, ExecCommandNowException - { - if (readyForSync) - if (nextExecDate <= std::time(nullptr)) - throw ExecCommandNowException(); //abort wait and start sync - callback.requestUiRefresh(); - }); - switch (res.type) - { - case WaitResult::CHANGE_DIR_UNAVAILABLE: //don't execute the command before all directories are available! - callback.setPhase(MonitorCallback::MONITOR_PHASE_WAITING); - waitForMissingDirs(folderPathPhrases, [&](const Zstring& folderPath) { callback.requestUiRefresh(); }); //throw FileError - callback.setPhase(MonitorCallback::MONITOR_PHASE_ACTIVE); - break; - - case WaitResult::CHANGE_DETECTED: - lastChangeDetected = res.changedItem_; - break; - } - nextExecDate = std::time(nullptr) + delay; - } - } - catch (ExecCommandNowException&) {} - - ::wxSetEnv(L"change_path", utfCvrtTo<wxString>(lastChangeDetected.filepath_)); //some way to output what file changed to the user - ::wxSetEnv(L"change_action", toString(lastChangeDetected.action_)); // - - //execute command - callback.executeExternalCommand(); - nextExecDate = std::numeric_limits<time_t>::max(); - } - }; - - for (;;) - try - { - execMonitoring(); //throw FileError - } - catch (const FileError& e) - { - callback.reportError(e.toString()); - } -} +// *****************************************************************************
+// * This file is part of the FreeFileSync project. It is distributed under *
+// * GNU General Public License: http://www.gnu.org/licenses/gpl-3.0 *
+// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
+// *****************************************************************************
+
+#include "monitor.h"
+#include <ctime>
+#include <set>
+#include <zen/file_access.h>
+#include <zen/dir_watcher.h>
+#include <zen/thread.h>
+//#include <zen/tick_count.h>
+#include <zen/basic_math.h>
+#include <wx/utils.h>
+#include "../lib/resolve_path.h"
+//#include "../library/db_file.h" //SYNC_DB_FILE_ENDING -> complete file too much of a dependency; file ending too little to decouple into single header
+//#include "../library/lock_holder.h" //LOCK_FILE_ENDING
+//TEMP_FILE_ENDING
+
+using namespace zen;
+
+
+namespace
+{
+const int FOLDER_EXISTENCE_CHECK_INTERVAL_SEC = 1; //unit: [s]
+
+
+std::vector<Zstring> getFormattedDirs(const std::vector<Zstring>& folderPathPhrases) //throw FileError
+{
+ std::set<Zstring, LessFilePath> folderPaths; //make unique
+ for (const Zstring& phrase : std::set<Zstring, LessFilePath>(folderPathPhrases.begin(), folderPathPhrases.end()))
+ {
+ if (ciStartsWith(trimCpy(phrase), Zstr("ftp:")) ||
+ ciStartsWith(trimCpy(phrase), Zstr("sftp:")) ||
+ ciStartsWith(trimCpy(phrase), Zstr("mtp:")))
+ throw FileError(_("The following path does not support directory monitoring:") + L"\n\n" + fmtPath(phrase));
+
+ //make unique: no need to resolve duplicate phrases more than once! (consider "[volume name]" syntax) -> shouldn't this be already buffered by OS?
+ folderPaths.insert(getResolvedFilePath(phrase));
+ }
+
+ return { folderPaths.begin(), folderPaths.end() };
+}
+
+
+//wait until changes are detected or if a directory is not available (anymore)
+struct WaitResult
+{
+ enum ChangeType
+ {
+ CHANGE_DETECTED,
+ CHANGE_DIR_UNAVAILABLE //not existing or can't access
+ };
+
+ WaitResult(const zen::DirWatcher::Entry& changedItem) : type(CHANGE_DETECTED), changedItem_(changedItem) {}
+ WaitResult(const Zstring& folderPath) : type(CHANGE_DIR_UNAVAILABLE), folderPath_(folderPath) {}
+
+ ChangeType type;
+ zen::DirWatcher::Entry changedItem_; //for type == CHANGE_DETECTED: file or directory
+ Zstring folderPath_; //for type == CHANGE_DIR_UNAVAILABLE
+};
+
+
+WaitResult waitForChanges(const std::vector<Zstring>& folderPathPhrases, //throw FileError
+ const std::function<void(bool readyForSync)>& onRefreshGui)
+{
+ const std::vector<Zstring> folderPaths = getFormattedDirs(folderPathPhrases); //throw FileError
+ if (folderPaths.empty()) //pathological case, but we have to check else this function will wait endlessly
+ throw FileError(_("A folder input field is empty.")); //should have been checked by caller!
+
+ //detect when volumes are removed/are not available anymore
+ std::vector<std::pair<Zstring, std::shared_ptr<DirWatcher>>> watches;
+
+ for (const Zstring& folderPath : folderPaths)
+ {
+ try
+ {
+ //a non-existent network path may block, so check existence asynchronously!
+ auto ftDirAvailable = runAsync([=] { return dirAvailable(folderPath); });
+
+ while (ftDirAvailable.wait_for(std::chrono::milliseconds(rts::UI_UPDATE_INTERVAL_MS / 2)) != std::future_status::ready)
+ onRefreshGui(false /*readyForSync*/); //may throw!
+
+ if (!ftDirAvailable.get()) //folder not existing or can't access
+ return WaitResult(folderPath);
+
+ watches.emplace_back(folderPath, std::make_shared<DirWatcher>(folderPath)); //throw FileError
+ }
+ catch (FileError&)
+ {
+ if (!dirAvailable(folderPath)) //folder not existing or can't access
+ return WaitResult(folderPath);
+ throw;
+ }
+ }
+
+ auto lastCheckTime = std::chrono::steady_clock::now();
+ for (;;)
+ {
+ const bool checkDirNow = [&]() -> bool //checking once per sec should suffice
+ {
+ const auto now = std::chrono::steady_clock::now();
+
+ if (numeric::dist(now, lastCheckTime) > std::chrono::seconds(FOLDER_EXISTENCE_CHECK_INTERVAL_SEC)) //handle potential chrono wrap-around!
+ {
+ lastCheckTime = now;
+ return true;
+ }
+ return false;
+ }();
+
+
+ for (auto it = watches.begin(); it != watches.end(); ++it)
+ {
+ const Zstring& folderPath = it->first;
+ DirWatcher& watcher = *(it->second);
+
+ //IMPORTANT CHECK: dirwatcher has problems detecting removal of top watched directories!
+ if (checkDirNow)
+ if (!dirAvailable(folderPath)) //catch errors related to directory removal, e.g. ERROR_NETNAME_DELETED
+ return WaitResult(folderPath);
+ try
+ {
+ std::vector<DirWatcher::Entry> changedItems = watcher.getChanges([&] { onRefreshGui(false /*readyForSync*/); /*may throw!*/ }); //throw FileError
+
+ //remove to be ignored changes
+ erase_if(changedItems, [](const DirWatcher::Entry& e)
+ {
+ return
+ //endsWith(e.filepath_, Zstr(".ffs_tmp")) ||
+ endsWith(e.filepath_, Zstr(".ffs_lock")) || //sync.ffs_lock, sync.Del.ffs_lock
+ endsWith(e.filepath_, Zstr(".ffs_db")); //sync.ffs_db, .sync.tmp.ffs_db
+ //no need to ignore temporary recycle bin directory: this must be caused by a file deletion anyway
+ });
+
+ if (!changedItems.empty())
+ return WaitResult(changedItems[0]); //directory change detected
+ }
+ catch (FileError&)
+ {
+ if (!dirAvailable(folderPath)) //a benign(?) race condition with FileError
+ return WaitResult(folderPath);
+ throw;
+ }
+ }
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(rts::UI_UPDATE_INTERVAL_MS / 2));
+ onRefreshGui(true /*readyForSync*/); //throw ?: may start sync at this presumably idle time
+ }
+}
+
+
+//wait until all directories become available (again) + logs in network share
+void waitForMissingDirs(const std::vector<Zstring>& folderPathPhrases, //throw FileError
+ const std::function<void(const Zstring& folderPath)>& onRefreshGui)
+{
+ for (;;)
+ {
+ bool allAvailable = true;
+ //support specifying volume by name => call getResolvedFilePath() repeatedly
+ for (const Zstring& folderPath : getFormattedDirs(folderPathPhrases)) //throw FileError
+ {
+ auto ftDirAvailable = runAsync([=]
+ {
+ //2. check dir availability
+ return dirAvailable(folderPath);
+ });
+ while (ftDirAvailable.wait_for(std::chrono::milliseconds(rts::UI_UPDATE_INTERVAL_MS / 2)) != std::future_status::ready)
+ onRefreshGui(folderPath); //may throw!
+
+ if (!ftDirAvailable.get())
+ {
+ allAvailable = false;
+ //wait some time...
+ const int refreshInterval = rts::UI_UPDATE_INTERVAL_MS / 2;
+ static_assert(FOLDER_EXISTENCE_CHECK_INTERVAL_SEC * 1000 % refreshInterval == 0, "");
+ for (int i = 0; i < FOLDER_EXISTENCE_CHECK_INTERVAL_SEC * 1000 / refreshInterval; ++i)
+ {
+ onRefreshGui(folderPath); //may throw!
+ std::this_thread::sleep_for(std::chrono::milliseconds(refreshInterval));
+ }
+ break;
+ }
+ }
+ if (allAvailable)
+ return;
+ }
+}
+
+
+inline
+wxString toString(DirWatcher::ActionType type)
+{
+ switch (type)
+ {
+ case DirWatcher::ACTION_CREATE:
+ return L"CREATE";
+ case DirWatcher::ACTION_UPDATE:
+ return L"UPDATE";
+ case DirWatcher::ACTION_DELETE:
+ return L"DELETE";
+ }
+ return L"ERROR";
+}
+
+struct ExecCommandNowException {};
+}
+
+
+void rts::monitorDirectories(const std::vector<Zstring>& folderPathPhrases, unsigned int delay, rts::MonitorCallback& callback)
+{
+ if (folderPathPhrases.empty())
+ {
+ assert(false);
+ return;
+ }
+
+ auto execMonitoring = [&] //throw FileError
+ {
+ callback.setPhase(MonitorCallback::MONITOR_PHASE_WAITING);
+ waitForMissingDirs(folderPathPhrases, [&](const Zstring& folderPath) { callback.requestUiRefresh(); }); //throw FileError
+ callback.setPhase(MonitorCallback::MONITOR_PHASE_ACTIVE);
+
+ //schedule initial execution (*after* all directories have arrived, which could take some time which we don't want to include)
+ time_t nextExecDate = std::time(nullptr) + delay;
+
+ for (;;) //loop over command invocations
+ {
+ DirWatcher::Entry lastChangeDetected;
+ try
+ {
+ for (;;) //loop over detected changes
+ {
+ //wait for changes (and for all directories to become available)
+ WaitResult res = waitForChanges(folderPathPhrases, [&](bool readyForSync) //throw FileError, ExecCommandNowException
+ {
+ if (readyForSync)
+ if (nextExecDate <= std::time(nullptr))
+ throw ExecCommandNowException(); //abort wait and start sync
+ callback.requestUiRefresh();
+ });
+ switch (res.type)
+ {
+ case WaitResult::CHANGE_DIR_UNAVAILABLE: //don't execute the command before all directories are available!
+ callback.setPhase(MonitorCallback::MONITOR_PHASE_WAITING);
+ waitForMissingDirs(folderPathPhrases, [&](const Zstring& folderPath) { callback.requestUiRefresh(); }); //throw FileError
+ callback.setPhase(MonitorCallback::MONITOR_PHASE_ACTIVE);
+ break;
+
+ case WaitResult::CHANGE_DETECTED:
+ lastChangeDetected = res.changedItem_;
+ break;
+ }
+ nextExecDate = std::time(nullptr) + delay;
+ }
+ }
+ catch (ExecCommandNowException&) {}
+
+ ::wxSetEnv(L"change_path", utfCvrtTo<wxString>(lastChangeDetected.filepath_)); //some way to output what file changed to the user
+ ::wxSetEnv(L"change_action", toString(lastChangeDetected.action_)); //
+
+ //execute command
+ callback.executeExternalCommand();
+ nextExecDate = std::numeric_limits<time_t>::max();
+ }
+ };
+
+ for (;;)
+ try
+ {
+ execMonitoring(); //throw FileError
+ }
+ catch (const FileError& e)
+ {
+ callback.reportError(e.toString());
+ }
+}
|