Skip to content

Commit 58715f6

Browse files
committed
Merge #12422: util: Make LockDirectory thread-safe, consistent, and fix OpenBSD 6.2 build
1d4cbd2 test: Add unit test for LockDirectory (Wladimir J. van der Laan) fc888bf util: Fix multiple use of LockDirectory (Wladimir J. van der Laan) Pull request description: Wrap the `boost::interprocess::file_lock` in a `std::unique_ptr` inside the map that keeps track of per-directory locks. This fixes a build issue with the clang 4.0.0+boost-1.58.0p8 version combo on OpenBSD 6.2, and should have no effect otherwise. Also add a unit test, make the function thread-safe, and fix Linux versus Windows behavior inconsistency. Meant to fix #12413. Tree-SHA512: 1a94c714c932524a51212c46e8951c129337d57b00fd3da5a347c6bcf6a947706cd440f39df935591b2079995136917f71ca7435fb356f6e8a128c509a62ec32
2 parents d09968f + 1d4cbd2 commit 58715f6

File tree

3 files changed

+165
-6
lines changed

3 files changed

+165
-6
lines changed

src/test/util_tests.cpp

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313

1414
#include <stdint.h>
1515
#include <vector>
16+
#ifndef WIN32
17+
#include <sys/types.h>
18+
#include <sys/wait.h>
19+
#endif
1620

1721
#include <boost/test/unit_test.hpp>
1822

@@ -603,4 +607,130 @@ BOOST_AUTO_TEST_CASE(test_ParseFixedPoint)
603607
BOOST_CHECK(!ParseFixedPoint("1.", 8, &amount));
604608
}
605609

610+
static void TestOtherThread(fs::path dirname, std::string lockname, bool *result)
611+
{
612+
*result = LockDirectory(dirname, lockname);
613+
}
614+
615+
#ifndef WIN32 // Cannot do this test on WIN32 due to lack of fork()
616+
static constexpr char LockCommand = 'L';
617+
static constexpr char UnlockCommand = 'U';
618+
static constexpr char ExitCommand = 'X';
619+
620+
static void TestOtherProcess(fs::path dirname, std::string lockname, int fd)
621+
{
622+
char ch;
623+
int rv;
624+
while (true) {
625+
rv = read(fd, &ch, 1); // Wait for command
626+
assert(rv == 1);
627+
switch(ch) {
628+
case LockCommand:
629+
ch = LockDirectory(dirname, lockname);
630+
rv = write(fd, &ch, 1);
631+
assert(rv == 1);
632+
break;
633+
case UnlockCommand:
634+
ReleaseDirectoryLocks();
635+
ch = true; // Always succeeds
636+
rv = write(fd, &ch, 1);
637+
break;
638+
case ExitCommand:
639+
close(fd);
640+
exit(0);
641+
default:
642+
assert(0);
643+
}
644+
}
645+
}
646+
#endif
647+
648+
BOOST_AUTO_TEST_CASE(test_LockDirectory)
649+
{
650+
fs::path dirname = fs::temp_directory_path() / fs::unique_path();
651+
const std::string lockname = ".lock";
652+
#ifndef WIN32
653+
// Revert SIGCHLD to default, otherwise boost.test will catch and fail on
654+
// it: there is BOOST_TEST_IGNORE_SIGCHLD but that only works when defined
655+
// at build-time of the boost library
656+
void (*old_handler)(int) = signal(SIGCHLD, SIG_DFL);
657+
658+
// Fork another process for testing before creating the lock, so that we
659+
// won't fork while holding the lock (which might be undefined, and is not
660+
// relevant as test case as that is avoided with -daemonize).
661+
int fd[2];
662+
BOOST_CHECK_EQUAL(socketpair(AF_UNIX, SOCK_STREAM, 0, fd), 0);
663+
pid_t pid = fork();
664+
if (!pid) {
665+
BOOST_CHECK_EQUAL(close(fd[1]), 0); // Child: close parent end
666+
TestOtherProcess(dirname, lockname, fd[0]);
667+
}
668+
BOOST_CHECK_EQUAL(close(fd[0]), 0); // Parent: close child end
669+
#endif
670+
// Lock on non-existent directory should fail
671+
BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname), false);
672+
673+
fs::create_directories(dirname);
674+
675+
// Probing lock on new directory should succeed
676+
BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), true);
677+
678+
// Persistent lock on new directory should succeed
679+
BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname), true);
680+
681+
// Another lock on the directory from the same thread should succeed
682+
BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname), true);
683+
684+
// Another lock on the directory from a different thread within the same process should succeed
685+
bool threadresult;
686+
std::thread thr(TestOtherThread, dirname, lockname, &threadresult);
687+
thr.join();
688+
BOOST_CHECK_EQUAL(threadresult, true);
689+
#ifndef WIN32
690+
// Try to aquire lock in child process while we're holding it, this should fail.
691+
char ch;
692+
BOOST_CHECK_EQUAL(write(fd[1], &LockCommand, 1), 1);
693+
BOOST_CHECK_EQUAL(read(fd[1], &ch, 1), 1);
694+
BOOST_CHECK_EQUAL((bool)ch, false);
695+
696+
// Give up our lock
697+
ReleaseDirectoryLocks();
698+
// Probing lock from our side now should succeed, but not hold on to the lock.
699+
BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), true);
700+
701+
// Try to acquire the lock in the child process, this should be succesful.
702+
BOOST_CHECK_EQUAL(write(fd[1], &LockCommand, 1), 1);
703+
BOOST_CHECK_EQUAL(read(fd[1], &ch, 1), 1);
704+
BOOST_CHECK_EQUAL((bool)ch, true);
705+
706+
// When we try to probe the lock now, it should fail.
707+
BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), false);
708+
709+
// Unlock the lock in the child process
710+
BOOST_CHECK_EQUAL(write(fd[1], &UnlockCommand, 1), 1);
711+
BOOST_CHECK_EQUAL(read(fd[1], &ch, 1), 1);
712+
BOOST_CHECK_EQUAL((bool)ch, true);
713+
714+
// When we try to probe the lock now, it should succeed.
715+
BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), true);
716+
717+
// Re-lock the lock in the child process, then wait for it to exit, check
718+
// successful return. After that, we check that exiting the process
719+
// has released the lock as we would expect by probing it.
720+
int processstatus;
721+
BOOST_CHECK_EQUAL(write(fd[1], &LockCommand, 1), 1);
722+
BOOST_CHECK_EQUAL(write(fd[1], &ExitCommand, 1), 1);
723+
BOOST_CHECK_EQUAL(waitpid(pid, &processstatus, 0), pid);
724+
BOOST_CHECK_EQUAL(processstatus, 0);
725+
BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), true);
726+
727+
// Restore SIGCHLD
728+
signal(SIGCHLD, old_handler);
729+
BOOST_CHECK_EQUAL(close(fd[1]), 0); // Close our side of the socketpair
730+
#endif
731+
// Clean up
732+
ReleaseDirectoryLocks();
733+
fs::remove_all(dirname);
734+
}
735+
606736
BOOST_AUTO_TEST_SUITE_END()

src/util.cpp

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -373,27 +373,50 @@ int LogPrintStr(const std::string &str)
373373
return ret;
374374
}
375375

376+
/** A map that contains all the currently held directory locks. After
377+
* successful locking, these will be held here until the global destructor
378+
* cleans them up and thus automatically unlocks them, or ReleaseDirectoryLocks
379+
* is called.
380+
*/
381+
static std::map<std::string, std::unique_ptr<boost::interprocess::file_lock>> dir_locks;
382+
/** Mutex to protect dir_locks. */
383+
static std::mutex cs_dir_locks;
384+
376385
bool LockDirectory(const fs::path& directory, const std::string lockfile_name, bool probe_only)
377386
{
387+
std::lock_guard<std::mutex> ulock(cs_dir_locks);
378388
fs::path pathLockFile = directory / lockfile_name;
379-
FILE* file = fsbridge::fopen(pathLockFile, "a"); // empty lock file; created if it doesn't exist.
389+
390+
// If a lock for this directory already exists in the map, don't try to re-lock it
391+
if (dir_locks.count(pathLockFile.string())) {
392+
return true;
393+
}
394+
395+
// Create empty lock file if it doesn't exist.
396+
FILE* file = fsbridge::fopen(pathLockFile, "a");
380397
if (file) fclose(file);
381398

382399
try {
383-
static std::map<std::string, boost::interprocess::file_lock> locks;
384-
boost::interprocess::file_lock& lock = locks.emplace(pathLockFile.string(), pathLockFile.string().c_str()).first->second;
385-
if (!lock.try_lock()) {
400+
auto lock = MakeUnique<boost::interprocess::file_lock>(pathLockFile.string().c_str());
401+
if (!lock->try_lock()) {
386402
return false;
387403
}
388-
if (probe_only) {
389-
lock.unlock();
404+
if (!probe_only) {
405+
// Lock successful and we're not just probing, put it into the map
406+
dir_locks.emplace(pathLockFile.string(), std::move(lock));
390407
}
391408
} catch (const boost::interprocess::interprocess_exception& e) {
392409
return error("Error while attempting to lock directory %s: %s", directory.string(), e.what());
393410
}
394411
return true;
395412
}
396413

414+
void ReleaseDirectoryLocks()
415+
{
416+
std::lock_guard<std::mutex> ulock(cs_dir_locks);
417+
dir_locks.clear();
418+
}
419+
397420
/** Interpret string as boolean, for argument parsing */
398421
static bool InterpretBool(const std::string& strValue)
399422
{

src/util.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,12 @@ int RaiseFileDescriptorLimit(int nMinFD);
174174
void AllocateFileRange(FILE *file, unsigned int offset, unsigned int length);
175175
bool RenameOver(fs::path src, fs::path dest);
176176
bool LockDirectory(const fs::path& directory, const std::string lockfile_name, bool probe_only=false);
177+
178+
/** Release all directory locks. This is used for unit testing only, at runtime
179+
* the global destructor will take care of the locks.
180+
*/
181+
void ReleaseDirectoryLocks();
182+
177183
bool TryCreateDirectories(const fs::path& p);
178184
fs::path GetDefaultDataDir();
179185
const fs::path &GetDataDir(bool fNetSpecific = true);

0 commit comments

Comments
 (0)