diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp --- a/src/test/util_tests.cpp +++ b/src/test/util_tests.cpp @@ -13,6 +13,10 @@ #include #include +#ifndef WIN32 +#include +#include +#endif #include @@ -1182,6 +1186,133 @@ BOOST_CHECK(!ParseFixedPoint("1.", 8, &amount)); } +static void TestOtherThread(fs::path dirname, std::string lockname, + bool *result) { + *result = LockDirectory(dirname, lockname); +} + +#ifndef WIN32 // Cannot do this test on WIN32 due to lack of fork() +static constexpr char LockCommand = 'L'; +static constexpr char UnlockCommand = 'U'; +static constexpr char ExitCommand = 'X'; + +static void TestOtherProcess(fs::path dirname, std::string lockname, int fd) { + char ch; + int rv; + while (true) { + rv = read(fd, &ch, 1); // Wait for command + assert(rv == 1); + switch (ch) { + case LockCommand: + ch = LockDirectory(dirname, lockname); + rv = write(fd, &ch, 1); + assert(rv == 1); + break; + case UnlockCommand: + ReleaseDirectoryLocks(); + ch = true; // Always succeeds + rv = write(fd, &ch, 1); + break; + case ExitCommand: + close(fd); + exit(0); + default: + assert(0); + } + } +} +#endif + +BOOST_AUTO_TEST_CASE(test_LockDirectory) { + fs::path dirname = fs::temp_directory_path() / fs::unique_path(); + const std::string lockname = ".lock"; +#ifndef WIN32 + // Revert SIGCHLD to default, otherwise boost.test will catch and fail on + // it: there is BOOST_TEST_IGNORE_SIGCHLD but that only works when defined + // at build-time of the boost library + void (*old_handler)(int) = signal(SIGCHLD, SIG_DFL); + + // Fork another process for testing before creating the lock, so that we + // won't fork while holding the lock (which might be undefined, and is not + // relevant as test case as that is avoided with -daemonize). + int fd[2]; + BOOST_CHECK_EQUAL(socketpair(AF_UNIX, SOCK_STREAM, 0, fd), 0); + pid_t pid = fork(); + if (!pid) { + BOOST_CHECK_EQUAL(close(fd[1]), 0); // Child: close parent end + TestOtherProcess(dirname, lockname, fd[0]); + } + BOOST_CHECK_EQUAL(close(fd[0]), 0); // Parent: close child end +#endif + // Lock on non-existent directory should fail + BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname), false); + + fs::create_directories(dirname); + + // Probing lock on new directory should succeed + BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), true); + + // Persistent lock on new directory should succeed + BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname), true); + + // Another lock on the directory from the same thread should succeed + BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname), true); + + // Another lock on the directory from a different thread within the same + // process should succeed + bool threadresult; + std::thread thr(TestOtherThread, dirname, lockname, &threadresult); + thr.join(); + BOOST_CHECK_EQUAL(threadresult, true); +#ifndef WIN32 + // Try to aquire lock in child process while we're holding it, this should + // fail. + char ch; + BOOST_CHECK_EQUAL(write(fd[1], &LockCommand, 1), 1); + BOOST_CHECK_EQUAL(read(fd[1], &ch, 1), 1); + BOOST_CHECK_EQUAL((bool)ch, false); + + // Give up our lock + ReleaseDirectoryLocks(); + // Probing lock from our side now should succeed, but not hold on to the + // lock. + BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), true); + + // Try to acquire the lock in the child process, this should be succesful. + BOOST_CHECK_EQUAL(write(fd[1], &LockCommand, 1), 1); + BOOST_CHECK_EQUAL(read(fd[1], &ch, 1), 1); + BOOST_CHECK_EQUAL((bool)ch, true); + + // When we try to probe the lock now, it should fail. + BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), false); + + // Unlock the lock in the child process + BOOST_CHECK_EQUAL(write(fd[1], &UnlockCommand, 1), 1); + BOOST_CHECK_EQUAL(read(fd[1], &ch, 1), 1); + BOOST_CHECK_EQUAL((bool)ch, true); + + // When we try to probe the lock now, it should succeed. + BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), true); + + // Re-lock the lock in the child process, then wait for it to exit, check + // successful return. After that, we check that exiting the process + // has released the lock as we would expect by probing it. + int processstatus; + BOOST_CHECK_EQUAL(write(fd[1], &LockCommand, 1), 1); + BOOST_CHECK_EQUAL(write(fd[1], &ExitCommand, 1), 1); + BOOST_CHECK_EQUAL(waitpid(pid, &processstatus, 0), pid); + BOOST_CHECK_EQUAL(processstatus, 0); + BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), true); + + // Restore SIGCHLD + signal(SIGCHLD, old_handler); + BOOST_CHECK_EQUAL(close(fd[1]), 0); // Close our side of the socketpair +#endif + // Clean up + ReleaseDirectoryLocks(); + fs::remove_all(dirname); +} + template static void CheckConvertBits(const std::vector &in, const std::vector &expected) { diff --git a/src/util.h b/src/util.h --- a/src/util.h +++ b/src/util.h @@ -74,6 +74,13 @@ bool RenameOver(fs::path src, fs::path dest); bool LockDirectory(const fs::path &directory, const std::string lockfile_name, bool probe_only = false); + +/** + * Release all directory locks. This is used for unit testing only, at runtime + * the global destructor will take care of the locks. + */ +void ReleaseDirectoryLocks(); + bool TryCreateDirectories(const fs::path &p); fs::path GetDefaultDataDir(); const fs::path &GetDataDir(bool fNetSpecific = true); diff --git a/src/util.cpp b/src/util.cpp --- a/src/util.cpp +++ b/src/util.cpp @@ -145,25 +145,42 @@ } } instance_of_cinit; +/** + * A map that contains all the currently held directory locks. After successful + * locking, these will be held here until the global destructor cleans them up + * and thus automatically unlocks them, or ReleaseDirectoryLocks is called. + */ +static std::map> + dir_locks; +/** Mutex to protect dir_locks. */ +static std::mutex cs_dir_locks; + bool LockDirectory(const fs::path &directory, const std::string lockfile_name, bool probe_only) { + std::lock_guard ulock(cs_dir_locks); fs::path pathLockFile = directory / lockfile_name; - // empty lock file; created if it doesn't exist. + + // If a lock for this directory already exists in the map, don't try to + // re-lock it + if (dir_locks.count(pathLockFile.string())) { + return true; + } + + // Create empty lock file if it doesn't exist. FILE *file = fsbridge::fopen(pathLockFile, "a"); if (file) { fclose(file); } try { - static std::map locks; - boost::interprocess::file_lock &lock = - locks.emplace(pathLockFile.string(), pathLockFile.string().c_str()) - .first->second; - if (!lock.try_lock()) { + auto lock = std::make_unique( + pathLockFile.string().c_str()); + if (!lock->try_lock()) { return false; } - if (probe_only) { - lock.unlock(); + if (!probe_only) { + // Lock successful and we're not just probing, put it into the map + dir_locks.emplace(pathLockFile.string(), std::move(lock)); } } catch (const boost::interprocess::interprocess_exception &e) { return error("Error while attempting to lock directory %s: %s", @@ -172,6 +189,11 @@ return true; } +void ReleaseDirectoryLocks() { + std::lock_guard ulock(cs_dir_locks); + dir_locks.clear(); +} + /** * Interpret a string argument as a boolean. *