Skip to content

Commit 4d54e7a

Browse files
committed
test: Add unit test for LockDirectory
Add a unit test for LockDirectory, introduced in #11281. Github-Pull: #12422 Rebased-From: 1d4cbd2 Tree-SHA512: 8186f4b22c65153e30ec1e0b68be20fd59d34e1fe565d99f3b5a302780125953b867fb14c37144de3fd7baf5751df94bf3901eebbb7b820491ca452706d4e205
1 parent 32a7268 commit 4d54e7a

File tree

3 files changed

+154
-9
lines changed

3 files changed

+154
-9
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: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -376,19 +376,22 @@ int LogPrintStr(const std::string &str)
376376
return ret;
377377
}
378378

379+
/** A map that contains all the currently held directory locks. After
380+
* successful locking, these will be held here until the global destructor
381+
* cleans them up and thus automatically unlocks them, or ReleaseDirectoryLocks
382+
* is called.
383+
*/
384+
static std::map<std::string, std::unique_ptr<boost::interprocess::file_lock>> dir_locks;
385+
/** Mutex to protect dir_locks. */
386+
static std::mutex cs_dir_locks;
387+
379388
bool LockDirectory(const fs::path& directory, const std::string lockfile_name, bool probe_only)
380389
{
381-
// A map that contains all the currently held directory locks. After
382-
// successful locking, these will be held here until the global
383-
// destructor cleans them up and thus automatically unlocks them.
384-
static std::map<std::string, std::unique_ptr<boost::interprocess::file_lock>> locks;
385-
// Protect the map with a mutex
386-
static std::mutex cs;
387-
std::lock_guard<std::mutex> ulock(cs);
390+
std::lock_guard<std::mutex> ulock(cs_dir_locks);
388391
fs::path pathLockFile = directory / lockfile_name;
389392

390393
// If a lock for this directory already exists in the map, don't try to re-lock it
391-
if (locks.count(pathLockFile.string())) {
394+
if (dir_locks.count(pathLockFile.string())) {
392395
return true;
393396
}
394397

@@ -403,14 +406,20 @@ bool LockDirectory(const fs::path& directory, const std::string lockfile_name, b
403406
}
404407
if (!probe_only) {
405408
// Lock successful and we're not just probing, put it into the map
406-
locks.emplace(pathLockFile.string(), std::move(lock));
409+
dir_locks.emplace(pathLockFile.string(), std::move(lock));
407410
}
408411
} catch (const boost::interprocess::interprocess_exception& e) {
409412
return error("Error while attempting to lock directory %s: %s", directory.string(), e.what());
410413
}
411414
return true;
412415
}
413416

417+
void ReleaseDirectoryLocks()
418+
{
419+
std::lock_guard<std::mutex> ulock(cs_dir_locks);
420+
dir_locks.clear();
421+
}
422+
414423
/** Interpret string as boolean, for argument parsing */
415424
static bool InterpretBool(const std::string& strValue)
416425
{

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)