diff --git a/src/common/utility.h b/src/common/utility.h index bd62a5d8c78c..a76527c1caa8 100644 --- a/src/common/utility.h +++ b/src/common/utility.h @@ -50,6 +50,18 @@ Q_DECLARE_LOGGING_CATEGORY(lcUtility) * @{ */ namespace Utility { + struct ProcessInfosForOpenFile { + ulong processId; + QString processName; + }; + /** + * @brief Queries the OS for processes that are keeping the file open(using it) + * + * @param filePath absolute file path + * @return list of ProcessInfosForOpenFile + */ + OCSYNC_EXPORT QVector queryProcessInfosKeepingFileOpen(const QString &filePath); + OCSYNC_EXPORT int rand(); OCSYNC_EXPORT void sleep(int sec); OCSYNC_EXPORT void usleep(int usec); diff --git a/src/common/utility_mac.mm b/src/common/utility_mac.mm index 9d0f4f03d497..b7e74c713052 100644 --- a/src/common/utility_mac.mm +++ b/src/common/utility_mac.mm @@ -31,6 +31,12 @@ namespace OCC { +QVector Utility::queryProcessInfosKeepingFileOpen(const QString &filePath) +{ + Q_UNUSED(filePath) + return {}; +} + void Utility::setupFavLink(const QString &folder) { // Finder: Place under "Places"/"Favorites" on the left sidebar diff --git a/src/common/utility_unix.cpp b/src/common/utility_unix.cpp index 0b47ee78e13b..091f16ab6548 100644 --- a/src/common/utility_unix.cpp +++ b/src/common/utility_unix.cpp @@ -31,6 +31,12 @@ namespace OCC { +QVector Utility::queryProcessInfosKeepingFileOpen(const QString &filePath) +{ + Q_UNUSED(filePath) + return {}; +} + void Utility::setupFavLink(const QString &folder) { // Nautilus: add to ~/.gtk-bookmarks diff --git a/src/common/utility_win.cpp b/src/common/utility_win.cpp index 9852cde16b38..ba4d2ba85915 100644 --- a/src/common/utility_win.cpp +++ b/src/common/utility_win.cpp @@ -22,6 +22,8 @@ #include #include +#include +#include #include #include #include @@ -43,6 +45,72 @@ static const char runPathC[] = R"(HKEY_CURRENT_USER\Software\Microsoft\Windows\C namespace OCC { +QVector Utility::queryProcessInfosKeepingFileOpen(const QString &filePath) +{ + QVector results; + + DWORD restartManagerSession = 0; + WCHAR restartManagerSessionKey[CCH_RM_SESSION_KEY + 1] = {0}; + auto errorStatus = RmStartSession(&restartManagerSession, 0, restartManagerSessionKey); + if (errorStatus != ERROR_SUCCESS) { + return results; + } + + LPCWSTR files[] = {reinterpret_cast(filePath.utf16())}; + errorStatus = RmRegisterResources(restartManagerSession, 1, files, 0, NULL, 0, NULL); + if (errorStatus != ERROR_SUCCESS) { + RmEndSession(restartManagerSession); + return results; + } + + DWORD rebootReasons = 0; + UINT rmProcessInfosNeededCount = 0; + std::vector rmProcessInfos; + auto rmProcessInfosRequestedCount = static_cast(rmProcessInfos.size()); + errorStatus = RmGetList(restartManagerSession, &rmProcessInfosNeededCount, &rmProcessInfosRequestedCount, rmProcessInfos.data(), &rebootReasons); + + if (errorStatus == ERROR_MORE_DATA) { + rmProcessInfos.resize(rmProcessInfosNeededCount, {}); + rmProcessInfosRequestedCount = static_cast(rmProcessInfos.size()); + errorStatus = RmGetList(restartManagerSession, &rmProcessInfosNeededCount, &rmProcessInfosRequestedCount, rmProcessInfos.data(), &rebootReasons); + } + + if (errorStatus != ERROR_SUCCESS || rmProcessInfos.empty()) { + RmEndSession(restartManagerSession); + return results; + } + + for (size_t i = 0; i < rmProcessInfos.size(); ++i) { + const auto processHandle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, rmProcessInfos[i].Process.dwProcessId); + if (!processHandle) { + continue; + } + + FILETIME ftCreate, ftExit, ftKernel, ftUser; + + if (!GetProcessTimes(processHandle, &ftCreate, &ftExit, &ftKernel, &ftUser) + || CompareFileTime(&rmProcessInfos[i].Process.ProcessStartTime, &ftCreate) != 0) { + CloseHandle(processHandle); + continue; + } + + WCHAR processFullPath[MAX_PATH]; + DWORD processFullPathLength = MAX_PATH; + if (QueryFullProcessImageNameW(processHandle, 0, processFullPath, &processFullPathLength) && processFullPathLength <= MAX_PATH) { + const auto processFullPathString = QDir::fromNativeSeparators(QString::fromWCharArray(processFullPath)); + const QFileInfo fileInfoForProcess(processFullPathString); + const auto processName = fileInfoForProcess.fileName(); + if (!processName.isEmpty()) { + results.push_back(Utility::ProcessInfosForOpenFile{rmProcessInfos[i].Process.dwProcessId, processName}); + } + } + CloseHandle(processHandle); + } + RmEndSession(restartManagerSession); + + return results; +} + void Utility::setupFavLink(const QString &folder) { // First create a Desktop.ini so that the folder and favorite link show our application's icon. diff --git a/src/csync/ConfigureChecks.cmake b/src/csync/ConfigureChecks.cmake index 64bdf1770ee6..bd1a74be11f0 100644 --- a/src/csync/ConfigureChecks.cmake +++ b/src/csync/ConfigureChecks.cmake @@ -31,7 +31,7 @@ if (NOT LINUX) endif (NOT LINUX) if(WIN32) - set(CMAKE_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES} psapi kernel32) + set(CMAKE_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES} psapi kernel32 Rstrtmgr) endif() check_function_exists(utimes HAVE_UTIMES) diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index 35bbaed4acbb..2112df8765d0 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -34,6 +34,12 @@ #include "csync_exclude.h" #include "csync.h" +namespace +{ +constexpr const char *editorNamesForDelayedUpload[] = {"PowerPDF"}; +constexpr const char *fileExtensionsToCheckIfOpenForSigning[] = {".pdf"}; +constexpr auto delayIntervalForSyncRetryForOpenedForSigningFilesSeconds = 60; +} namespace OCC { @@ -1022,6 +1028,19 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( item->_status = SyncFileItem::Status::NormalError; } + { + const auto foundEditorsKeepingFileBusy = queryEditorsKeepingFileBusy(item, path); + if (!foundEditorsKeepingFileBusy.isEmpty()) { + item->_instruction = CSYNC_INSTRUCTION_ERROR; + const auto editorsString = foundEditorsKeepingFileBusy.join(", "); + qCInfo(lcDisco) << "Failed, because it is open in the editor." << item->_file << "direction" << item->_direction << editorsString; + item->_errorString = tr("Could not upload file, because it is open in \"%1\".").arg(editorsString); + item->_status = SyncFileItem::Status::SoftError; + _discoveryData->_anotherSyncNeeded = true; + _discoveryData->_filesNeedingScheduledSync.insert(path._original, delayIntervalForSyncRetryForOpenedForSigningFilesSeconds); + } + } + if (dbEntry.isValid() && item->isDirectory()) { item->_e2eEncryptionStatus = EncryptionStatusEnums::fromDbEncryptionStatus(dbEntry._e2eEncryptionStatus); if (item->isEncrypted()) { @@ -1839,6 +1858,43 @@ bool ProcessDirectoryJob::isRename(const QString &originalPath) const */ } +QStringList ProcessDirectoryJob::queryEditorsKeepingFileBusy(const SyncFileItemPtr &item, const PathTuple &path) const +{ + QStringList matchingEditorsKeepingFileBusy; + + if (item->isDirectory() || item->_direction != SyncFileItem::Up) { + return matchingEditorsKeepingFileBusy; + } + + const auto isMatchingFileExtension = std::find_if(std::cbegin(fileExtensionsToCheckIfOpenForSigning), std::cend(fileExtensionsToCheckIfOpenForSigning), + [path](const auto &matchingExtension) { + return path._local.endsWith(matchingExtension, Qt::CaseInsensitive); + }) != std::cend(fileExtensionsToCheckIfOpenForSigning); + + if (!isMatchingFileExtension) { + return matchingEditorsKeepingFileBusy; + } + + const QString fullLocalPath(_discoveryData->_localDir + path._local); + const auto editorsKeepingFileBusy = Utility::queryProcessInfosKeepingFileOpen(fullLocalPath); + + for (const auto &detectedEditorName : editorsKeepingFileBusy) { + const auto isMatchingEditorFound = std::find_if(std::cbegin(editorNamesForDelayedUpload), std::cend(editorNamesForDelayedUpload), + [detectedEditorName](const auto &matchingEditorName) { + return detectedEditorName.processName.startsWith(matchingEditorName, Qt::CaseInsensitive); + }) != std::cend(editorNamesForDelayedUpload); + if (isMatchingEditorFound) { + matchingEditorsKeepingFileBusy.push_back(detectedEditorName.processName); + } + } + + if (!matchingEditorsKeepingFileBusy.isEmpty()) { + matchingEditorsKeepingFileBusy.push_back("PowerPDF.exe"); + } + + return matchingEditorsKeepingFileBusy; +} + auto ProcessDirectoryJob::checkMovePermissions(RemotePermissions srcPerm, const QString &srcPath, bool isDirectory) -> MovePermissionResult diff --git a/src/libsync/discovery.h b/src/libsync/discovery.h index eaa2657697a9..62d1f493c513 100644 --- a/src/libsync/discovery.h +++ b/src/libsync/discovery.h @@ -191,6 +191,8 @@ class ProcessDirectoryJob : public QObject [[nodiscard]] bool isRename(const QString &originalPath) const; + [[nodiscard]] QStringList queryEditorsKeepingFileBusy(const SyncFileItemPtr &item, const PathTuple &path) const; + struct MovePermissionResult { // whether moving/renaming the source is ok