diff options
Diffstat (limited to 'FreeFileSync/Source/RealTimeSync/monitor.cpp')
-rwxr-xr-x | FreeFileSync/Source/RealTimeSync/monitor.cpp | 556 |
1 files changed, 278 insertions, 278 deletions
diff --git a/FreeFileSync/Source/RealTimeSync/monitor.cpp b/FreeFileSync/Source/RealTimeSync/monitor.cpp index 2d88dc3e..8e3531ad 100755 --- a/FreeFileSync/Source/RealTimeSync/monitor.cpp +++ b/FreeFileSync/Source/RealTimeSync/monitor.cpp @@ -1,278 +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 (startsWith(trimCpy(phrase), Zstr("ftp:"), CmpAsciiNoCase()) ||
- startsWith(trimCpy(phrase), Zstr("sftp:"), CmpAsciiNoCase()) ||
- startsWith(trimCpy(phrase), Zstr("mtp:"), CmpAsciiNoCase()))
- 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", utfTo<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 (startsWith(trimCpy(phrase), Zstr("ftp:"), CmpAsciiNoCase()) || + startsWith(trimCpy(phrase), Zstr("sftp:"), CmpAsciiNoCase()) || + startsWith(trimCpy(phrase), Zstr("mtp:"), CmpAsciiNoCase())) + 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", utfTo<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()); + } +} |