summaryrefslogtreecommitdiff
path: root/library/dir_lock.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'library/dir_lock.cpp')
-rw-r--r--library/dir_lock.cpp448
1 files changed, 448 insertions, 0 deletions
diff --git a/library/dir_lock.cpp b/library/dir_lock.cpp
new file mode 100644
index 00000000..c5ea37b1
--- /dev/null
+++ b/library/dir_lock.cpp
@@ -0,0 +1,448 @@
+#include "dir_lock.h"
+#include <wx/intl.h>
+#include "../shared/string_conv.h"
+#include "../shared/system_func.h"
+#include <wx/utils.h>
+#include <wx/timer.h>
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+#include <boost/thread.hpp>
+#include "../shared/loki/ScopeGuard.h"
+#include <wx/msgdlg.h>
+#include "../shared/system_constants.h"
+#include "../shared/guid.h"
+#include "../shared/file_io.h"
+#include <utility>
+
+#ifdef FFS_WIN
+#include <wx/msw/wrapwin.h> //includes "windows.h"
+#include "../shared/long_path_prefix.h"
+
+#elif defined FFS_LINUX
+#include <sys/stat.h>
+#include <cerrno>
+#endif
+
+using namespace ffs3;
+using namespace std::rel_ops;
+
+
+namespace
+{
+const size_t EMIT_LIFE_SIGN_INTERVAL = 5000; //show life sign; unit [ms]
+const size_t POLL_LIFE_SIGN_INTERVAL = 6000; //poll for life sign; unit [ms]
+const size_t DETECT_EXITUS_INTERVAL = 30000; //assume abandoned lock; unit [ms]
+}
+
+class LifeSigns
+{
+public:
+ LifeSigns(const Zstring& lockfilename) : //throw()!!! siehe SharedDirLock()
+ lockfilename_(lockfilename.c_str()) //ref-counting structure is used by thread: make deep copy!
+ {
+ threadObj = boost::thread(boost::cref(*this)); //localize all thread logic to this class!
+ }
+
+ ~LifeSigns()
+ {
+ threadObj.interrupt(); //thread lifetime is subset of this instances's life
+ threadObj.join();
+ }
+
+ void operator()() const //thread entry
+ {
+ try
+ {
+ while (true)
+ {
+ boost::thread::sleep(boost::get_system_time() + boost::posix_time::milliseconds(EMIT_LIFE_SIGN_INTERVAL)); //interruption point!
+
+ //actual work
+ emitLifeSign(); //throw ()
+ }
+ }
+ catch (const std::exception& e) //exceptions must be catched per thread
+ {
+ wxMessageBox(wxString::FromAscii(e.what()), wxString(_("An exception occurred!")) + wxT("(Dirlock)"), wxOK | wxICON_ERROR);
+ }
+ }
+
+ void emitLifeSign() const //try to append one byte...; throw()
+ {
+ const char buffer[1] = {' '};
+
+#ifdef FFS_WIN
+ const HANDLE fileHandle = ::CreateFile(applyLongPathPrefix(lockfilename_).c_str(),
+ FILE_APPEND_DATA,
+ FILE_SHARE_READ,
+ NULL,
+ OPEN_EXISTING,
+ FILE_ATTRIBUTE_NORMAL,
+ NULL);
+ if (fileHandle == INVALID_HANDLE_VALUE)
+ return;
+
+ Loki::ScopeGuard dummy = Loki::MakeGuard(::CloseHandle, fileHandle);
+ (void)dummy; //silence warning "unused variable"
+
+ const DWORD fpLow = ::SetFilePointer(fileHandle, 0, NULL, FILE_END);
+ if (fpLow == INVALID_SET_FILE_POINTER)
+ return;
+
+ DWORD bytesWritten = 0;
+ ::WriteFile(
+ fileHandle, //__in HANDLE hFile,
+ buffer, //__out LPVOID lpBuffer,
+ 1, //__in DWORD nNumberOfBytesToRead,
+ &bytesWritten, //__out_opt LPDWORD lpNumberOfBytesWritten,
+ NULL); //__inout_opt LPOVERLAPPED lpOverlapped
+
+#elif defined FFS_LINUX
+ const int fileHandle = ::open(lockfilename_.c_str(), O_WRONLY | O_APPEND); //O_EXCL contains a race condition on NFS file systems: http://linux.die.net/man/2/open
+ if (fileHandle == -1)
+ return;
+
+ Loki::ScopeGuard dummy = Loki::MakeGuard(::close, fileHandle);
+ (void)dummy; //silence warning "unused variable"
+
+ const ssize_t bytesWritten = ::write(fileHandle, buffer, 1);
+ (void)bytesWritten;
+#endif
+ }
+
+private:
+ LifeSigns(const LifeSigns&); //just be sure this ref-counting Zstring doesn't bite
+ LifeSigns& operator=(const LifeSigns&); //
+
+ boost::thread threadObj;
+ const Zstring lockfilename_; //used by worker thread only! Not ref-counted!
+};
+
+
+namespace
+{
+bool somethingExists(const Zstring& objname) //throw() check whether any object with this name exists
+{
+#ifdef FFS_WIN
+ return ::GetFileAttributes(applyLongPathPrefix(objname).c_str()) != INVALID_FILE_ATTRIBUTES;
+
+#elif defined FFS_LINUX
+ struct stat fileInfo;
+ return ::lstat(objname.c_str(), &fileInfo) == 0;
+#endif
+}
+
+
+void deleteLockFile(const Zstring& filename) //throw (FileError)
+{
+#ifdef FFS_WIN
+ if (!::DeleteFile(applyLongPathPrefix(filename).c_str()))
+#elif defined FFS_LINUX
+ if (::unlink(filename.c_str()) != 0)
+#endif
+ {
+ wxString errorMessage = wxString(_("Error deleting file:")) + wxT("\n\"") + zToWx(filename) + wxT("\"");
+ throw FileError(errorMessage + wxT("\n\n") + getLastErrorFormatted());
+ }
+}
+
+
+wxULongLong getLockFileSize(const Zstring& filename) //throw (FileError, ErrorNotExisting)
+{
+#ifdef FFS_WIN
+ WIN32_FIND_DATA fileMetaData;
+ const HANDLE searchHandle = ::FindFirstFile(applyLongPathPrefix(filename).c_str(), &fileMetaData);
+ if (searchHandle == INVALID_HANDLE_VALUE)
+ {
+ const DWORD lastError = ::GetLastError();
+ const wxString errorMessage = wxString(_("Error reading file attributes:")) + wxT("\n\"") + zToWx(filename) + wxT("\"") +
+ wxT("\n\n") + getLastErrorFormatted(lastError);
+ if (lastError == ERROR_FILE_NOT_FOUND)
+ throw ErrorNotExisting(errorMessage);
+ else
+ throw FileError(errorMessage);
+ }
+
+ ::FindClose(searchHandle);
+
+ return wxULongLong(fileMetaData.nFileSizeHigh, fileMetaData.nFileSizeLow);
+
+#elif defined FFS_LINUX
+ struct stat fileInfo;
+ if (::stat(filename.c_str(), &fileInfo) != 0) //follow symbolic links
+ {
+ const int lastError = errno;
+ const wxString errorMessage = wxString(_("Error reading file attributes:")) + wxT("\n\"") + zToWx(filename) + wxT("\"") +
+ wxT("\n\n") + getLastErrorFormatted(lastError);
+ if (lastError == ENOENT)
+ throw ErrorNotExisting(errorMessage);
+ else
+ throw FileError(errorMessage);
+ }
+
+ return fileInfo.st_size;
+#endif
+}
+
+
+Zstring deleteAbandonedLockName(const Zstring& lockfilename)
+{
+ const size_t pos = lockfilename.Find(common::FILE_NAME_SEPARATOR, true); //search from end
+
+ return pos == Zstring::npos ? DefaultStr("Del.") + lockfilename :
+
+ Zstring(lockfilename.c_str(), pos + 1) + //include path separator
+ DefaultStr("Del.") +
+ lockfilename.AfterLast(common::FILE_NAME_SEPARATOR); //returns the whole string if ch not found
+}
+
+
+void writeLockId(const Zstring& lockfilename) //throw (FileError)
+{
+ //write GUID at the beginning of the file: this ID is a universal identifier for this lock (no matter what the path is, considering symlinks ,etc.)
+ FileOutputStream lockFile(lockfilename); //throw FileError()
+ util::UniqueId().toStream(lockFile); //
+}
+
+
+util::UniqueId retrieveLockId(const Zstring& lockfilename) //throw (FileError, ErrorNotExisting)
+{
+ //read GUID from beginning of file
+ FileInputStream lockFile(lockfilename); //throw (FileError, ErrorNotExisting)
+ return util::UniqueId(lockFile); //
+}
+
+
+void waitOnDirLock(const Zstring& lockfilename, DirLockCallback* callback) //throw (FileError)
+{
+ Zstring infoMsg;
+ infoMsg = wxToZ(_("Waiting while directory is locked (%x)..."));
+ infoMsg.Replace(DefaultStr("%x"), DefaultStr("\"") + lockfilename + DefaultStr("\""));
+ if (callback) callback->updateStatusText(infoMsg);
+ //---------------------------------------------------------------
+ try
+ {
+ const util::UniqueId lockId = retrieveLockId(lockfilename); //throw (FileError, ErrorNotExisting)
+
+ wxULongLong fileSizeOld;
+ wxLongLong lockSilentStart = wxGetLocalTimeMillis();
+
+ while (true)
+ {
+ const wxULongLong fileSizeNew = ::getLockFileSize(lockfilename); //throw (FileError, ErrorNotExisting)
+ const wxLongLong currentTime = wxGetLocalTimeMillis();
+
+ if (fileSizeNew != fileSizeOld)
+ {
+ //received life sign from lock
+ fileSizeOld = fileSizeNew;
+ lockSilentStart = currentTime;
+ }
+ else if (currentTime - lockSilentStart > DETECT_EXITUS_INTERVAL)
+ {
+ DirLock dummy(deleteAbandonedLockName(lockfilename), callback); //throw (FileError)
+
+ //now that the lock is in place check existence again: meanwhile another process may have deleted and created a new lock!
+
+ if (retrieveLockId(lockfilename) != lockId) //throw (FileError, ErrorNotExisting)
+ return; //another process has placed a new lock, leave scope: the wait for the old lock is technically over...
+
+ if (getLockFileSize(lockfilename) != fileSizeOld) //throw (FileError, ErrorNotExisting)
+ continue; //belated lifesign
+
+ //---------------------------------------------------------------
+ Zstring infoMsg2 = wxToZ(_("Removing abandoned directory lock (%x)..."));
+ infoMsg2.Replace(DefaultStr("%x"), DefaultStr("\"") + lockfilename + DefaultStr("\""));
+ if (callback) callback->updateStatusText(infoMsg2);
+ //---------------------------------------------------------------
+
+ ::deleteLockFile(lockfilename); //throw (FileError)
+ return;
+ }
+
+ //wait some time...
+ const size_t GUI_CALLBACK_INTERVAL = 100;
+ for (size_t i = 0; i < POLL_LIFE_SIGN_INTERVAL / GUI_CALLBACK_INTERVAL; ++i)
+ {
+ if (callback) callback->requestUiRefresh();
+ wxMilliSleep(GUI_CALLBACK_INTERVAL);
+
+ //show some countdown on abandoned locks
+ if (callback)
+ {
+ if (currentTime - lockSilentStart > EMIT_LIFE_SIGN_INTERVAL) //one signal missed: it's likely this is an abandoned lock:
+ {
+ long remainingSeconds = ((DETECT_EXITUS_INTERVAL - (wxGetLocalTimeMillis() - lockSilentStart)) / 1000).GetLo();
+ remainingSeconds = std::max(0L, remainingSeconds);
+
+ Zstring remSecMsg = wxToZ(_("%x sec"));
+ remSecMsg.Replace(DefaultStr("%x"), numberToZstring(remainingSeconds));
+ callback->updateStatusText(infoMsg + DefaultStr(" ") + remSecMsg);
+ }
+ else
+ callback->updateStatusText(infoMsg); //emit a message in any case (might clear other one)
+ }
+ }
+ }
+
+ }
+ catch (const ErrorNotExisting&)
+ {
+ return; //what we are waiting for...
+ }
+}
+
+
+void releaseLock(const Zstring& lockfilename) //throw ()
+{
+ try
+ {
+ ::deleteLockFile(lockfilename);
+ }
+ catch(...) {}
+}
+
+
+bool tryLock(const Zstring& lockfilename) //throw (FileError)
+{
+#ifdef FFS_WIN
+ const HANDLE fileHandle = ::CreateFile(applyLongPathPrefix(lockfilename).c_str(),
+ GENERIC_WRITE,
+ FILE_SHARE_READ,
+ NULL,
+ CREATE_NEW,
+ FILE_ATTRIBUTE_NORMAL,
+ NULL);
+ if (fileHandle == INVALID_HANDLE_VALUE)
+ {
+ if (::GetLastError() == ERROR_FILE_EXISTS)
+ return false;
+ else
+ throw FileError(wxString(_("Error setting directory lock:")) + wxT("\n\"") + zToWx(lockfilename) + wxT("\"") +
+ wxT("\n\n") + getLastErrorFormatted());
+ }
+ ::CloseHandle(fileHandle);
+
+#elif defined FFS_LINUX
+ //O_EXCL contains a race condition on NFS file systems: http://linux.die.net/man/2/open
+ ::umask(0); //important!
+ const int fileHandle = ::open(lockfilename.c_str(), O_CREAT | O_WRONLY | O_EXCL, S_IRWXU | S_IRWXG | S_IRWXO);
+ if (fileHandle == -1)
+ {
+ if (errno == EEXIST)
+ return false;
+ else
+ throw FileError(wxString(_("Error setting directory lock:")) + wxT("\n\"") + zToWx(lockfilename) + wxT("\"") +
+ wxT("\n\n") + getLastErrorFormatted());
+ }
+ ::close(fileHandle);
+#endif
+
+ Loki::ScopeGuard guardLockFile = Loki::MakeGuard(::releaseLock, lockfilename);
+
+ //write UUID at the beginning of the file: this ID is a universal identifier for this lock (no matter what the path is, considering symlinks ,etc.)
+ writeLockId(lockfilename); //throw (FileError)
+
+ guardLockFile.Dismiss(); //lockfile created successfully
+ return true;
+}
+}
+
+
+class DirLock::SharedDirLock
+{
+public:
+ SharedDirLock(const Zstring& lockfilename, DirLockCallback* callback = NULL) : //throw (FileError)
+ lockfilename_(lockfilename)
+ {
+ while (!::tryLock(lockfilename)) //throw (FileError)
+ ::waitOnDirLock(lockfilename, callback); //
+
+ emitLifeSigns.reset(new LifeSigns(lockfilename)); //throw()! ownership of lockfile not yet managed!
+ }
+
+ ~SharedDirLock()
+ {
+ emitLifeSigns.reset();
+
+ ::releaseLock(lockfilename_); //throw ()
+ }
+
+private:
+ SharedDirLock(const DirLock&);
+ SharedDirLock& operator=(const DirLock&);
+
+ const Zstring lockfilename_;
+
+ std::auto_ptr<LifeSigns> emitLifeSigns;
+};
+
+
+class DirLock::LockAdmin //administrate all locks of this process to avoid deadlock by recursion
+{
+public:
+ static LockAdmin& instance()
+ {
+ static LockAdmin inst;
+ return inst;
+ }
+
+ //create or retrieve a SharedDirLock
+ boost::shared_ptr<SharedDirLock> retrieve(const Zstring& lockfilename, DirLockCallback* callback) //throw (FileError)
+ {
+ //optimization: check if there is an active(!) lock for "lockfilename"
+ FileToUuidMap::const_iterator iterUuid = fileToUuid.find(lockfilename);
+ if (iterUuid != fileToUuid.end())
+ {
+ const boost::shared_ptr<SharedDirLock>& activeLock = findActive(iterUuid->second); //returns null-lock if not found
+ if (activeLock)
+ return activeLock; //SharedDirLock is still active -> enlarge circle of shared ownership
+ }
+
+ try //actual check based on lock UUID, deadlock prevention: "lockfilename" may be an alternative name for an already active lock
+ {
+ const util::UniqueId lockId = retrieveLockId(lockfilename); //throw (FileError, ErrorNotExisting)
+
+ const boost::shared_ptr<SharedDirLock>& activeLock = findActive(lockId); //returns null-lock if not found
+ if (activeLock)
+ {
+ fileToUuid[lockfilename] = lockId; //perf-optimization: update relation
+ return activeLock;
+ }
+ }
+ catch (const ErrorNotExisting&) {} //let other FileError(s) propagate!
+
+ //not yet in buffer, so create a new directory lock
+ boost::shared_ptr<SharedDirLock> newLock(new SharedDirLock(lockfilename, callback)); //throw (FileError)
+ const util::UniqueId newLockId = retrieveLockId(lockfilename); //throw (FileError, ErrorNotExisting)
+
+ //update registry
+ fileToUuid[lockfilename] = newLockId; //throw()
+ uuidToLock[newLockId] = newLock; //
+
+ return newLock;
+ }
+
+private:
+ LockAdmin() {}
+
+ boost::shared_ptr<SharedDirLock> findActive(const util::UniqueId& lockId) //returns null-lock if not found
+ {
+ UuidToLockMap::const_iterator iterLock = uuidToLock.find(lockId);
+ return iterLock != uuidToLock.end() ?
+ iterLock->second.lock() : //try to get shared_ptr; throw()
+ boost::shared_ptr<SharedDirLock>();
+ }
+
+ typedef boost::weak_ptr<SharedDirLock> SharedLock;
+
+ typedef std::map<Zstring, util::UniqueId, LessFilename> FileToUuidMap; //n:1 handle uppper/lower case correctly
+ typedef std::map<util::UniqueId, SharedLock> UuidToLockMap; //1:1
+
+ FileToUuidMap fileToUuid; //lockname |-> UUID; locks can be referenced by a lockfilename or alternatively a UUID
+ UuidToLockMap uuidToLock; //UUID |-> "shared lock ownership"
+};
+
+
+DirLock::DirLock(const Zstring& lockfilename, DirLockCallback* callback) : //throw (FileError)
+ sharedLock(LockAdmin::instance().retrieve(lockfilename, callback)) {}
bgstack15