561 lines
22 KiB
C++
561 lines
22 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 "dir_lock.h"
|
|
#include <memory>
|
|
#include <unordered_map>
|
|
#include <zen/crc.h>
|
|
#include <zen/sys_error.h>
|
|
#include <zen/thread.h>
|
|
#include <zen/scope_guard.h>
|
|
#include <zen/guid.h>
|
|
#include <zen/file_access.h>
|
|
#include <zen/file_io.h>
|
|
#include <zen/sys_info.h>
|
|
|
|
#include <iostream> //std::cerr
|
|
#include <fcntl.h> //open()
|
|
#include <unistd.h> //close()
|
|
#include <signal.h> //kill()
|
|
|
|
using namespace zen;
|
|
using namespace fff;
|
|
|
|
|
|
namespace
|
|
{
|
|
constexpr std::chrono::seconds EMIT_LIFE_SIGN_INTERVAL (5); //show life sign;
|
|
constexpr std::chrono::seconds POLL_LIFE_SIGN_INTERVAL (4); //poll for life sign;
|
|
constexpr std::chrono::seconds DETECT_ABANDONED_INTERVAL(30); //assume abandoned lock;
|
|
|
|
const char LOCK_FILE_DESCR[] = "FreeFileSync";
|
|
const int LOCK_FILE_VERSION = 3; //2020-02-07
|
|
const int ABANDONED_LOCK_LEVEL_MAX = 10;
|
|
}
|
|
|
|
|
|
Zstring fff::impl::getAbandonedLockFileName(const Zstring& lockFileName) //throw SysError
|
|
{
|
|
Zstring fileName = lockFileName;
|
|
int level = 0;
|
|
|
|
//recursive abandoned locks!? (almost) impossible, except for file system bugs: https://freefilesync.org/forum/viewtopic.php?t=6568
|
|
const Zstring tmp = afterFirst(fileName, Zstr("Delete."), IfNotFoundReturn::none); //e.g. Delete.1.sync.ffs_lock
|
|
if (!tmp.empty())
|
|
{
|
|
const Zstring levelStr = beforeFirst(tmp, Zstr('.'), IfNotFoundReturn::none);
|
|
if (!levelStr.empty() && std::all_of(levelStr.begin(), levelStr.end(), [](Zchar c) { return zen::isDigit(c); }))
|
|
{
|
|
fileName = afterFirst(tmp, Zstr('.'), IfNotFoundReturn::none);
|
|
level = stringTo<int>(levelStr) + 1;
|
|
|
|
if (level >= ABANDONED_LOCK_LEVEL_MAX)
|
|
throw SysError(L"Endless recursion.");
|
|
}
|
|
}
|
|
|
|
return Zstr("Delete.") + numberTo<Zstring>(level) + Zstr(".") + fileName; //preserve lock file extension!
|
|
}
|
|
|
|
|
|
namespace
|
|
{
|
|
//worker thread
|
|
class LifeSigns
|
|
{
|
|
public:
|
|
LifeSigns(const Zstring& lockFilePath) : lockFilePath_(lockFilePath)
|
|
{
|
|
}
|
|
|
|
void operator()() const //throw ThreadStopRequest
|
|
{
|
|
const std::optional<Zstring> parentDirPath = getParentFolderPath(lockFilePath_);
|
|
setCurrentThreadName(Zstr("DirLock: ") + (parentDirPath ? *parentDirPath : Zstr("")));
|
|
|
|
for (;;)
|
|
{
|
|
interruptibleSleep(EMIT_LIFE_SIGN_INTERVAL); //throw ThreadStopRequest
|
|
emitLifeSign(); //noexcept
|
|
}
|
|
}
|
|
|
|
private:
|
|
//try to append one byte...
|
|
void emitLifeSign() const //noexcept
|
|
{
|
|
try
|
|
{
|
|
#if 1
|
|
const int fdLockFile = ::open(lockFilePath_.c_str(), O_WRONLY | O_APPEND | O_CLOEXEC);
|
|
if (fdLockFile == -1)
|
|
THROW_LAST_SYS_ERROR("open");
|
|
ZEN_ON_SCOPE_EXIT(::close(fdLockFile));
|
|
|
|
#else //alternative using lseek => no apparent benefit https://freefilesync.org/forum/viewtopic.php?t=7553#p25505
|
|
const int fdLockFile = ::open(lockFilePath_.c_str(), O_WRONLY | O_CLOEXEC);
|
|
if (fdLockFile == -1)
|
|
THROW_LAST_SYS_ERROR("open");
|
|
ZEN_ON_SCOPE_EXIT(::close(fdLockFile));
|
|
|
|
if (const off_t offset = ::lseek(fdLockFile, 0, SEEK_END);
|
|
offset == -1)
|
|
THROW_LAST_SYS_ERROR("lseek");
|
|
#endif
|
|
const ssize_t bytesWritten = ::write(fdLockFile, " ", 1); //writes *up to* count bytes
|
|
if (bytesWritten <= 0)
|
|
{
|
|
if (bytesWritten == 0) //comment in safe-read.c suggests to treat this as an error due to buggy drivers
|
|
errno = ENOSPC;
|
|
THROW_LAST_SYS_ERROR("write");
|
|
}
|
|
ASSERT_SYSERROR(bytesWritten == 1); //better safe than sorry
|
|
}
|
|
catch (const SysError& e)
|
|
{
|
|
logExtraError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(lockFilePath_)) + L"\n\n" + e.toString());
|
|
}
|
|
}
|
|
|
|
const Zstring lockFilePath_; //thread-local!
|
|
};
|
|
|
|
|
|
using ProcessId = pid_t;
|
|
using SessionId = pid_t;
|
|
|
|
//return ppid on Windows, sid on Linux/Mac, "no value" if process corresponding to "processId" is not existing
|
|
std::optional<SessionId> getSessionId(ProcessId processId) //throw FileError
|
|
{
|
|
if (::kill(processId, 0) != 0) //sig == 0: no signal sent, just existence check
|
|
return {};
|
|
|
|
const pid_t procSid = ::getsid(processId); //NOT to be confused with "login session", e.g. not stable on OS X!!!
|
|
if (procSid < 0) //pids are never negative, empiric proof: https://linux.die.net/man/2/wait
|
|
THROW_LAST_FILE_ERROR(_("Cannot get process information."), "getsid");
|
|
|
|
return procSid;
|
|
}
|
|
|
|
|
|
struct LockInformation //throw FileError
|
|
{
|
|
std::string lockId; //16 byte GUID - a universal identifier for this lock (no matter what the path is, considering symlinks, distributed network, etc.)
|
|
|
|
//identify local computer
|
|
std::string computerName; //format: HostName.DomainName
|
|
std::string userId;
|
|
|
|
//identify running process
|
|
SessionId sessionId = 0; //Windows: parent process id; Linux/macOS: session of the process, NOT the user
|
|
ProcessId processId = 0;
|
|
};
|
|
|
|
|
|
LockInformation getLockInfoFromCurrentProcess() //throw FileError
|
|
{
|
|
LockInformation lockInfo =
|
|
{
|
|
.lockId = generateGUID(),
|
|
.userId = utfTo<std::string>(getLoginUser()), //throw FileError
|
|
};
|
|
|
|
const std::string osName = "Linux";
|
|
|
|
//wxGetFullHostName() is a performance killer and can hang for some users, so don't touch!
|
|
std::vector<char> buf(10000);
|
|
if (::gethostname(buf.data(), buf.size()) != 0)
|
|
THROW_LAST_FILE_ERROR(_("Cannot get process information."), "gethostname");
|
|
lockInfo.computerName = osName + ' ' + buf.data() + '.';
|
|
|
|
if (::getdomainname(buf.data(), buf.size()) != 0)
|
|
THROW_LAST_FILE_ERROR(_("Cannot get process information."), "getdomainname");
|
|
lockInfo.computerName += buf.data(); //can be "(none)"!
|
|
|
|
lockInfo.processId = ::getpid(); //never fails
|
|
|
|
std::optional<SessionId> sessionIdTmp = getSessionId(lockInfo.processId); //throw FileError
|
|
if (!sessionIdTmp)
|
|
throw FileError(_("Cannot get process information."), L"no session id found"); //should not happen?
|
|
lockInfo.sessionId = *sessionIdTmp;
|
|
|
|
return lockInfo;
|
|
}
|
|
|
|
|
|
std::string serialize(const LockInformation& lockInfo)
|
|
{
|
|
MemoryStreamOut streamOut;
|
|
writeArray(streamOut, LOCK_FILE_DESCR, sizeof(LOCK_FILE_DESCR));
|
|
writeNumber<int32_t>(streamOut, LOCK_FILE_VERSION);
|
|
|
|
static_assert(sizeof(lockInfo.processId) <= sizeof(uint64_t)); //ensure cross-platform compatibility!
|
|
static_assert(sizeof(lockInfo.sessionId) <= sizeof(uint64_t)); //
|
|
|
|
writeContainer(streamOut, lockInfo.lockId);
|
|
writeContainer(streamOut, lockInfo.computerName);
|
|
writeContainer(streamOut, lockInfo.userId);
|
|
writeNumber<uint64_t>(streamOut, lockInfo.sessionId);
|
|
writeNumber<uint64_t>(streamOut, lockInfo.processId);
|
|
|
|
writeNumber<uint32_t>(streamOut, getCrc32(streamOut.ref()));
|
|
writeArray(streamOut, "x", 1); //sentinel: mark logical end with a non-space character
|
|
return streamOut.ref();
|
|
}
|
|
|
|
|
|
LockInformation unserialize(const std::string& byteStream) //throw SysError
|
|
{
|
|
MemoryStreamIn streamIn(byteStream);
|
|
|
|
char formatDescr[sizeof(LOCK_FILE_DESCR)] = {};
|
|
readArray(streamIn, &formatDescr, sizeof(formatDescr)); //throw SysErrorUnexpectedEos
|
|
|
|
if (!std::equal(std::begin(formatDescr), std::end(formatDescr), std::begin(LOCK_FILE_DESCR)))
|
|
throw SysError(_("File content is corrupted.") + L" (invalid header)");
|
|
|
|
const int version = readNumber<int32_t>(streamIn); //throw SysErrorUnexpectedEos
|
|
if (version != LOCK_FILE_VERSION)
|
|
throw SysError(_("Unsupported data format.") + L' ' + replaceCpy(_("Version: %x"), L"%x", numberTo<std::wstring>(version)));
|
|
|
|
//--------------------------------------------------------------------
|
|
//catch data corruption ASAP + don't rely on std::bad_alloc for consistency checking
|
|
const size_t posEnd = byteStream.rfind('x'); //skip blanks (+ unrelated corrupted data e.g. nulls!)
|
|
if (posEnd == std::string::npos)
|
|
throw SysErrorUnexpectedEos();
|
|
|
|
const std::string_view byteStreamTrm = makeStringView(byteStream.begin(), posEnd);
|
|
|
|
MemoryStreamOut crcStreamOut;
|
|
writeNumber<uint32_t>(crcStreamOut, getCrc32(byteStreamTrm.begin(), byteStreamTrm.end() - sizeof(uint32_t)));
|
|
|
|
if (!endsWith(byteStreamTrm, crcStreamOut.ref()))
|
|
throw SysError(_("File content is corrupted.") + L" (invalid checksum)");
|
|
//--------------------------------------------------------------------
|
|
|
|
LockInformation lockInfo = {};
|
|
lockInfo.lockId = readContainer<std::string>(streamIn); //
|
|
lockInfo.computerName = readContainer<std::string>(streamIn); //SysErrorUnexpectedEos
|
|
lockInfo.userId = readContainer<std::string>(streamIn); //
|
|
lockInfo.sessionId = static_cast<SessionId>(readNumber<uint64_t>(streamIn)); //[!] conversion
|
|
lockInfo.processId = static_cast<ProcessId>(readNumber<uint64_t>(streamIn)); //[!] conversion
|
|
return lockInfo;
|
|
}
|
|
|
|
|
|
LockInformation retrieveLockInfo(const Zstring& lockFilePath) //throw FileError
|
|
{
|
|
const std::string byteStream = getFileContent(lockFilePath, nullptr /*notifyUnbufferedIO*/); //throw FileError
|
|
try
|
|
{
|
|
return unserialize(byteStream); //throw SysError
|
|
}
|
|
catch (const SysError& e)
|
|
{
|
|
throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(lockFilePath)), e.toString());
|
|
}
|
|
}
|
|
|
|
|
|
inline
|
|
std::string retrieveLockId(const Zstring& lockFilePath) //throw FileError
|
|
{
|
|
return retrieveLockInfo(lockFilePath).lockId; //throw FileError
|
|
}
|
|
|
|
|
|
enum class ProcessStatus
|
|
{
|
|
notRunning,
|
|
running,
|
|
itsUs,
|
|
noIdea,
|
|
};
|
|
|
|
ProcessStatus getProcessStatus(const LockInformation& lockInfo) //throw FileError
|
|
{
|
|
const LockInformation localInfo = getLockInfoFromCurrentProcess(); //throw FileError
|
|
|
|
if (lockInfo.computerName != localInfo.computerName ||
|
|
lockInfo.userId != localInfo.userId) //another user may run a session right now!
|
|
return ProcessStatus::noIdea; //lock owned by different computer in this network
|
|
|
|
if (lockInfo.sessionId == localInfo.sessionId &&
|
|
lockInfo.processId == localInfo.processId) //obscure, but possible: deletion failed or a lock file is "stolen" and put back while the program is running
|
|
return ProcessStatus::itsUs;
|
|
|
|
if (std::optional<SessionId> sessionId = getSessionId(lockInfo.processId)) //throw FileError
|
|
return *sessionId == lockInfo.sessionId ? ProcessStatus::running : ProcessStatus::notRunning;
|
|
return ProcessStatus::notRunning;
|
|
}
|
|
|
|
|
|
DEFINE_NEW_FILE_ERROR(ErrorFileNotExisting)
|
|
uint64_t getLockFileSize(const Zstring& filePath) //throw FileError, ErrorFileNotExisting
|
|
{
|
|
struct stat fileInfo = {};
|
|
if (::stat(filePath.c_str(), &fileInfo) == 0)
|
|
return fileInfo.st_size;
|
|
|
|
if (errno == ENOENT)
|
|
throw ErrorFileNotExisting(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(filePath)), formatSystemError("stat", errno));
|
|
THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(filePath)), "stat");
|
|
}
|
|
|
|
|
|
void waitOnDirLock(const Zstring& lockFilePath, const DirLockCallback& notifyStatus /*throw X*/, std::chrono::milliseconds cbInterval) //throw FileError
|
|
{
|
|
std::wstring infoMsg = _("Waiting while directory is in use:") + L' ' + fmtPath(lockFilePath);
|
|
|
|
if (notifyStatus) notifyStatus(std::wstring(infoMsg)); //throw X
|
|
|
|
//convenience optimization only: if we know the owning process crashed, we needn't wait DETECT_ABANDONED_INTERVAL sec
|
|
bool lockOwnderDead = false;
|
|
std::string originalLockId; //empty if it cannot be retrieved
|
|
try
|
|
{
|
|
const LockInformation& lockInfo = retrieveLockInfo(lockFilePath); //throw FileError
|
|
|
|
infoMsg += SPACED_DASH + _("Username:") + L' ' + utfTo<std::wstring>(lockInfo.userId);
|
|
|
|
originalLockId = lockInfo.lockId;
|
|
switch (getProcessStatus(lockInfo)) //throw FileError
|
|
{
|
|
case ProcessStatus::itsUs: //since we've already passed LockAdmin, the lock file seems abandoned ("stolen"?) although it's from this process
|
|
case ProcessStatus::notRunning:
|
|
lockOwnderDead = true;
|
|
break;
|
|
case ProcessStatus::running:
|
|
case ProcessStatus::noIdea:
|
|
break;
|
|
}
|
|
}
|
|
catch (FileError&) {} //logfile may be only partly written -> this is no error!
|
|
//------------------------------------------------------------------------------
|
|
|
|
uint64_t fileSizeOld = 0;
|
|
auto lastLifeSign = std::chrono::steady_clock::now();
|
|
|
|
for (;;)
|
|
{
|
|
uint64_t fileSizeNew = 0;
|
|
try
|
|
{
|
|
fileSizeNew = getLockFileSize(lockFilePath); //throw FileError, ErrorFileNotExisting
|
|
}
|
|
catch (ErrorFileNotExisting&) { return; } //what we are waiting for...
|
|
|
|
const auto lastCheckTime = std::chrono::steady_clock::now();
|
|
|
|
if (fileSizeNew != fileSizeOld) //received life sign from lock
|
|
{
|
|
fileSizeOld = fileSizeNew;
|
|
lastLifeSign = lastCheckTime;
|
|
}
|
|
|
|
if (lockOwnderDead || //no need to wait any longer...
|
|
lastCheckTime >= lastLifeSign + DETECT_ABANDONED_INTERVAL)
|
|
{
|
|
const Zstring lockFileName = [&]
|
|
{
|
|
try
|
|
{
|
|
return fff::impl::getAbandonedLockFileName(getItemName(lockFilePath)); //throw SysError
|
|
}
|
|
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(lockFilePath)), e.toString()); }
|
|
}();
|
|
|
|
DirLock guardDeletion(*getParentFolderPath(lockFilePath), lockFileName, notifyStatus, cbInterval); //throw FileError
|
|
|
|
//now that the lock is in place check existence again: meanwhile another process may have deleted and created a new lock!
|
|
std::string currentLockId;
|
|
try { currentLockId = retrieveLockId(lockFilePath); /*throw FileError*/ }
|
|
catch (FileError&) {}
|
|
|
|
if (currentLockId != originalLockId)
|
|
return; //another process has placed a new lock, leave scope: the wait for the old lock is technically over...
|
|
|
|
try
|
|
{
|
|
if (getLockFileSize(lockFilePath) != fileSizeOld) //throw FileError, ErrorFileNotExisting
|
|
return; //late life sign (or maybe even a different lock if retrieveLockId() failed!)
|
|
}
|
|
catch (ErrorFileNotExisting&) { return; } //what we are waiting for anyway...
|
|
|
|
removeFilePlain(lockFilePath); //throw FileError
|
|
return;
|
|
}
|
|
|
|
//wait some time...
|
|
const auto delayUntil = std::chrono::steady_clock::now() + POLL_LIFE_SIGN_INTERVAL;
|
|
for (auto now = std::chrono::steady_clock::now(); now < delayUntil; now = std::chrono::steady_clock::now())
|
|
{
|
|
if (notifyStatus)
|
|
{
|
|
//one signal missed: it's likely this is an abandoned lock => show countdown
|
|
if (lastCheckTime >= lastLifeSign + EMIT_LIFE_SIGN_INTERVAL + std::chrono::seconds(1))
|
|
{
|
|
const int remainingSeconds = std::max(0, static_cast<int>(std::chrono::duration_cast<std::chrono::seconds>(DETECT_ABANDONED_INTERVAL - (now - lastLifeSign)).count()));
|
|
notifyStatus(infoMsg + SPACED_DASH + _("Lock file apparently abandoned...") + L' ' + _P("1 sec", "%x sec", remainingSeconds)); //throw X
|
|
}
|
|
else
|
|
notifyStatus(std::wstring(infoMsg)); //throw X; emit a message in any case (might clear other one)
|
|
}
|
|
std::this_thread::sleep_for(cbInterval);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void releaseLock(const Zstring& lockFilePath) { removeFilePlain(lockFilePath); } //throw FileError
|
|
|
|
|
|
bool tryLock(const Zstring& lockFilePath) //throw FileError
|
|
{
|
|
//important: we want the lock file to have exactly the permissions specified
|
|
//=> yes, disabling umask() is messy (per-process!), but fchmod() may not be supported: https://freefilesync.org/forum/viewtopic.php?t=8096
|
|
const mode_t oldMask = ::umask(0); //always succeeds
|
|
ZEN_ON_SCOPE_EXIT(::umask(oldMask));
|
|
|
|
const mode_t lockFileMode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; //0666
|
|
|
|
//O_EXCL contains a race condition on NFS file systems: https://linux.die.net/man/2/open
|
|
const int hFile = ::open(lockFilePath.c_str(), //const char* pathname
|
|
O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, //int flags
|
|
lockFileMode); //mode_t mode
|
|
if (hFile == -1)
|
|
{
|
|
if (errno == EEXIST)
|
|
return false;
|
|
|
|
THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(lockFilePath)), "open");
|
|
}
|
|
FileOutputPlain fileOut(hFile, lockFilePath); //pass handle ownership
|
|
|
|
//write housekeeping info: user, process info, lock GUID
|
|
const std::string byteStream = serialize(getLockInfoFromCurrentProcess()); //throw FileError
|
|
|
|
unbufferedSave(byteStream, [&](const void* buffer, size_t bytesToWrite)
|
|
{
|
|
return fileOut.tryWrite(buffer, bytesToWrite); //throw FileError; may return short! CONTRACT: bytesToWrite > 0
|
|
},
|
|
fileOut.getBlockSize());
|
|
|
|
fileOut.close(); //throw FileError
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
class DirLock::SharedDirLock
|
|
{
|
|
public:
|
|
SharedDirLock(const Zstring& lockFilePath, const DirLockCallback& notifyStatus, std::chrono::milliseconds cbInterval) : //throw FileError
|
|
lockFilePath_(lockFilePath)
|
|
{
|
|
if (notifyStatus) notifyStatus(replaceCpy(_("Creating file %x"), L"%x", fmtPath(lockFilePath))); //throw X
|
|
|
|
while (!::tryLock(lockFilePath)) //throw FileError
|
|
{
|
|
::waitOnDirLock(lockFilePath, notifyStatus, cbInterval); //throw FileError
|
|
}
|
|
|
|
lifeSignthread_ = InterruptibleThread(LifeSigns(lockFilePath));
|
|
}
|
|
|
|
~SharedDirLock()
|
|
{
|
|
lifeSignthread_.requestStop(); //thread lifetime is subset of this instances's life
|
|
lifeSignthread_.join();
|
|
|
|
try
|
|
{
|
|
::releaseLock(lockFilePath_); //throw FileError
|
|
}
|
|
catch (const FileError& e) { logExtraError(e.toString()); } //inform user about remnant lock files *somehow*!
|
|
}
|
|
|
|
private:
|
|
SharedDirLock (const DirLock&) = delete;
|
|
SharedDirLock& operator=(const DirLock&) = delete;
|
|
|
|
const Zstring lockFilePath_;
|
|
InterruptibleThread lifeSignthread_;
|
|
};
|
|
|
|
|
|
class DirLock::LockAdmin //administrate all locks held by this process to avoid deadlock by recursion
|
|
{
|
|
public:
|
|
static LockAdmin& instance()
|
|
{
|
|
static LockAdmin inst;
|
|
return inst;
|
|
}
|
|
|
|
//create or retrieve a SharedDirLock
|
|
std::shared_ptr<SharedDirLock> retrieve(const Zstring& lockFilePath, const DirLockCallback& notifyStatus, std::chrono::milliseconds cbInterval) //throw FileError
|
|
{
|
|
assert(runningOnMainThread()); //function is not thread-safe!
|
|
|
|
tidyUp();
|
|
|
|
//optimization: check if we already own a lock for this path
|
|
if (auto itGuid = guidByPath_.find(lockFilePath);
|
|
itGuid != guidByPath_.end())
|
|
if (const std::shared_ptr<SharedDirLock>& activeLock = getActiveLock(itGuid->second)) //returns null-lock if not found
|
|
return activeLock; //SharedDirLock is still active -> enlarge circle of shared ownership
|
|
|
|
try //check based on lock GUID, deadlock prevention: "lockFilePath" may be an alternative name for a lock already owned by this process
|
|
{
|
|
const std::string lockId = retrieveLockId(lockFilePath); //throw FileError
|
|
if (const std::shared_ptr<SharedDirLock>& activeLock = getActiveLock(lockId)) //returns null-lock if not found
|
|
{
|
|
guidByPath_[lockFilePath] = lockId; //found an alias for one of our active locks
|
|
return activeLock;
|
|
}
|
|
}
|
|
catch (FileError&) {} //catch everything, let SharedDirLock constructor deal with errors, e.g. 0-sized/corrupted lock files
|
|
|
|
//lock not owned by us => create a new one
|
|
auto newLock = std::make_shared<SharedDirLock>(lockFilePath, notifyStatus, cbInterval); //throw FileError
|
|
const std::string& newLockGuid = retrieveLockId(lockFilePath); //throw FileError
|
|
|
|
guidByPath_[lockFilePath] = newLockGuid; //update registry
|
|
locksByGuid_[newLockGuid] = newLock; //
|
|
|
|
return newLock;
|
|
}
|
|
|
|
private:
|
|
LockAdmin() {}
|
|
LockAdmin (const LockAdmin&) = delete;
|
|
LockAdmin& operator=(const LockAdmin&) = delete;
|
|
|
|
using UniqueId = std::string;
|
|
|
|
std::shared_ptr<SharedDirLock> getActiveLock(const UniqueId& lockId) //returns null if none found
|
|
{
|
|
auto it = locksByGuid_.find(lockId);
|
|
return it != locksByGuid_.end() ? it->second.lock() : nullptr; //try to get shared_ptr; throw()
|
|
}
|
|
|
|
void tidyUp() //remove obsolete entries
|
|
{
|
|
std::erase_if(locksByGuid_, [](const auto& v) { return v.second.expired(); });
|
|
std::erase_if(guidByPath_, [&](const auto& v) { return !locksByGuid_.contains(v.second); });
|
|
}
|
|
|
|
std::unordered_map<Zstring, UniqueId> guidByPath_; //lockFilePath |-> GUID; n:1; locks can be referenced by a lockFilePath or alternatively a GUID
|
|
std::unordered_map<UniqueId, std::weak_ptr<SharedDirLock>> locksByGuid_; //GUID |-> "shared lock ownership"; 1:1
|
|
};
|
|
|
|
|
|
DirLock::DirLock(const Zstring& folderPath, const Zstring& fileName, const DirLockCallback& notifyStatus, std::chrono::milliseconds cbInterval) //throw FileError
|
|
{
|
|
sharedLock_ = LockAdmin::instance().retrieve(appendPath(folderPath, fileName), notifyStatus, cbInterval); //throw FileError
|
|
}
|