Files
2025-12-10 14:38:26 -08:00

281 lines
12 KiB
C++

// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************
#include "monitor.h"
#include <zen/file_access.h>
#include <zen/dir_watcher.h>
#include <zen/thread.h>
#include <zen/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
{
constexpr std::chrono::seconds FOLDER_EXISTENCE_CHECK_INTERVAL(1);
//wait until all directories become available (again) + logs in network share
std::set<Zstring, LessNativePath> waitForMissingDirs(const std::vector<Zstring>& folderPathPhrases, //throw FileError
const std::function<void(const Zstring& folderPath)>& requestUiUpdate, std::chrono::milliseconds cbInterval)
{
//early failure! check for unsupported folder paths:
for (const char* protoName : {"ftp", "sftp", "mtp", "gdrive"})
for (const Zstring& phrase : folderPathPhrases)
//hopefully clear enough now: https://freefilesync.org/forum/viewtopic.php?t=4302
if (startsWithAsciiNoCase(trimCpy(phrase), std::string(protoName) + ':'))
throw FileError(replaceCpy(_("The %x protocol does not support directory monitoring:"), L"%x", utfTo<std::wstring>(protoName)) + L"\n\n" + fmtPath(phrase));
for (;;)
{
struct FolderInfo
{
Zstring folderPathPhrase;
std::future<bool> folderAvailable;
};
std::map<Zstring /*folderPath*/, FolderInfo, LessNativePath> folderInfos;
for (const Zstring& phrase : folderPathPhrases)
{
const Zstring& folderPath = getResolvedFilePath(phrase);
//start all folder checks asynchronously (non-existent network path may block)
if (!folderInfos.contains(folderPath))
folderInfos[folderPath] = { phrase, runAsync([folderPath]
{
try
{
getItemType(folderPath); //throw FileError
return true;
}
catch (FileError&) { return false; }
})
};
}
std::set<Zstring, LessNativePath> availablePaths;
std::set<Zstring, LessNativePath> missingPathPhrases;
for (auto& [folderPath, folderInfo] : folderInfos)
{
std::future<bool>& folderAvailable = folderInfo.folderAvailable;
while (folderAvailable.wait_for(cbInterval) == std::future_status::timeout)
requestUiUpdate(folderPath); //throw X
if (folderAvailable.get())
availablePaths.insert(folderPath);
else
missingPathPhrases.insert(folderInfo.folderPathPhrase);
}
if (missingPathPhrases.empty())
return availablePaths; //only return when all folders were found on *first* try!
auto delayUntil = std::chrono::steady_clock::now() + FOLDER_EXISTENCE_CHECK_INTERVAL;
for (const Zstring& folderPathPhrase : missingPathPhrases)
for (;;)
{
//support specifying volume by name => call getResolvedFilePath() repeatedly
const Zstring folderPath = getResolvedFilePath(folderPathPhrase);
//wait some time...
for (auto now = std::chrono::steady_clock::now(); now < delayUntil; now = std::chrono::steady_clock::now())
{
requestUiUpdate(folderPath); //throw X
std::this_thread::sleep_for(cbInterval);
}
std::future<bool> folderAvailable = runAsync([folderPath]
{
try
{
getItemType(folderPath); //throw FileError
return true;
}
catch (FileError&) { return false; }
});
while (folderAvailable.wait_for(cbInterval) == std::future_status::timeout)
requestUiUpdate(folderPath); //throw X
if (folderAvailable.get())
break;
//else: wait until folder is available: do not needlessly poll existing folders again!
delayUntil = std::chrono::steady_clock::now() + FOLDER_EXISTENCE_CHECK_INTERVAL;
}
}
}
//wait until changes are detected or if a directory is not available (anymore)
DirWatcher::Change waitForChanges(const std::set<Zstring, LessNativePath>& folderPaths, //throw FileError
const std::function<void(bool readyForSync)>& requestUiUpdate, std::chrono::milliseconds cbInterval)
{
if (folderPaths.empty()) //pathological case, but we have to check or this function waits forever
throw FileError(_("A folder input field is empty.")); //should have been checked by caller!
std::vector<std::pair<Zstring, std::unique_ptr<DirWatcher>>> watches;
for (const Zstring& folderPath : folderPaths)
try
{
watches.emplace_back(folderPath, std::make_unique<DirWatcher>(folderPath)); //throw FileError
}
catch (FileError&)
{
try { getItemType(folderPath); } //throw FileError
catch (FileError&)
{
assert(false); //why "unavailable"!? violating waitForChanges() precondition!
return {DirWatcher::ChangeType::baseFolderUnavailable, folderPath};
}
throw;
}
auto lastCheckTime = std::chrono::steady_clock::now();
for (;;)
{
const bool checkDirNow = [&] //checking once per sec should suffice
{
const auto now = std::chrono::steady_clock::now();
if (now > lastCheckTime + FOLDER_EXISTENCE_CHECK_INTERVAL)
{
lastCheckTime = now;
return true;
}
return false;
}();
for (const auto& [folderPath, watcher] : watches)
{
//IMPORTANT CHECK: DirWatcher has problems detecting removal of top watched directories!
if (checkDirNow)
try //catch errors related to directory removal, e.g. ERROR_NETNAME_DELETED
{
getItemType(folderPath); //throw FileError
}
catch (FileError&) { return {DirWatcher::ChangeType::baseFolderUnavailable, folderPath}; }
try
{
std::vector<DirWatcher::Change> changes = watcher->fetchChanges([&] { requestUiUpdate(false /*readyForSync*/); /*throw X*/ },
cbInterval); //throw FileError
//give precedence to ChangeType::baseFolderUnavailable
for (const DirWatcher::Change& change : changes)
if (change.type == DirWatcher::ChangeType::baseFolderUnavailable)
return change;
std::erase_if(changes, [](const DirWatcher::Change& e)
{
return
endsWith(e.itemPath, Zstr(".ffs_tmp")) || //sync.8ea2.ffs_tmp
endsWith(e.itemPath, Zstr(".ffs_lock")) || //sync.ffs_lock, sync.Del.ffs_lock
endsWith(e.itemPath, Zstr(".ffs_db")); //sync.ffs_db
//no need to ignore temporary recycle bin directory: this must be caused by a file deletion anyway
});
if (!changes.empty())
return changes[0];
}
catch (FileError&)
{
try { getItemType(folderPath); } //throw FileError
catch (FileError&) { return {DirWatcher::ChangeType::baseFolderUnavailable, folderPath}; }
throw;
}
}
std::this_thread::sleep_for(cbInterval);
requestUiUpdate(true /*readyForSync*/); //throw X: may start sync at this presumably idle time
}
}
std::wstring getChangeTypeName(DirWatcher::ChangeType type)
{
switch (type)
{
case DirWatcher::ChangeType::create:
return L"Create";
case DirWatcher::ChangeType::update:
return L"Update";
case DirWatcher::ChangeType::remove:
return L"Delete";
case DirWatcher::ChangeType::baseFolderUnavailable:
return L"Base Folder Unavailable";
}
assert(false);
return L"Error";
}
struct ExecCommandNowException {};
}
void rts::monitorDirectories(const std::vector<Zstring>& folderPathPhrases, std::chrono::seconds delay,
const std::function<void(const Zstring& itemPath, const std::wstring& actionName)>& executeExternalCommand /*throw FileError*/,
const std::function<void(const Zstring* missingFolderPath)>& requestUiUpdate,
const std::function<void(const std::wstring& msg )>& reportError,
std::chrono::milliseconds cbInterval)
{
assert(!folderPathPhrases.empty());
if (folderPathPhrases.empty())
return;
for (;;)
try
{
std::set<Zstring, LessNativePath> folderPaths = waitForMissingDirs(folderPathPhrases, [&](const Zstring& folderPath) { requestUiUpdate(&folderPath); }, cbInterval); //throw FileError
//schedule initial execution (*after* all directories have arrived)
auto nextExecTime = std::chrono::steady_clock::now() + delay;
for (;;) //command executions
{
DirWatcher::Change lastChangeDetected;
try
{
for (;;) //detected changes
{
lastChangeDetected = waitForChanges(folderPaths, [&](bool readyForSync) //throw FileError, ExecCommandNowException
{
requestUiUpdate(nullptr);
if (readyForSync && std::chrono::steady_clock::now() >= nextExecTime)
throw ExecCommandNowException(); //abort wait and start sync
}, cbInterval);
if (lastChangeDetected.type == DirWatcher::ChangeType::baseFolderUnavailable)
//don't execute the command before all directories are available!
folderPaths = waitForMissingDirs(folderPathPhrases, [&](const Zstring& folderPath) { requestUiUpdate(&folderPath); }, cbInterval); //throw FileError
nextExecTime = std::chrono::steady_clock::now() + delay;
}
}
catch (ExecCommandNowException&) {}
try
{
executeExternalCommand(lastChangeDetected.itemPath, getChangeTypeName(lastChangeDetected.type)); //throw FileError
}
catch (const FileError& e) { reportError(e.toString()); }
nextExecTime = std::chrono::steady_clock::time_point::max();
}
}
catch (const FileError& e)
{
reportError(e.toString());
}
}