Run the Cross-Process Lock Test on Every Platform

The test that checks the guard yields to a lock held elsewhere forked a
child to hold it, so it was left out on Windows. The OS lock belongs to
the handle on Windows and to the open file description elsewhere, so a
second handle in the same process is refused like another instance
would be. The test now holds the lock that way and runs everywhere.
This commit is contained in:
Hanif Koh
2026-10-01 15:00:32 +08:00
parent e00c3a3f26
commit da353c2fed
+58 -34
View File
@@ -9,10 +9,13 @@
#include "libslic3r/InstanceLock.hpp" #include "libslic3r/InstanceLock.hpp"
#include "test_utils.hpp" #include "test_utils.hpp"
#ifndef _WIN32 #ifdef _WIN32
#include <boost/interprocess/sync/file_lock.hpp>
#include <boost/nowide/convert.hpp>
#include <boost/nowide/fstream.hpp>
#else
#include <fcntl.h> #include <fcntl.h>
#include <sys/file.h> #include <sys/file.h>
#include <sys/wait.h>
#include <unistd.h> #include <unistd.h>
#endif #endif
@@ -146,38 +149,64 @@ TEST_CASE("InstanceLock serialises the threads of one process", "[InstanceLock]"
REQUIRE(released_before_acquire); REQUIRE(released_before_acquire);
} }
#ifndef _WIN32 // Holds the OS lock on a file through a handle of its own, as another instance would. The lock
// The cross-process side of the lock is a POSIX flock, which a child process // belongs to the handle on Windows and to the open file description elsewhere, so the guard's
// takes here directly; LockFileEx backs the guard on Windows, but spawning a // handle is refused while this one holds it.
// child there is not worth a test. class OtherHolder
TEST_CASE("InstanceLock yields to another process and reports it", "[InstanceLock]") {
public:
explicit OtherHolder(const std::string &path)
{
#ifdef _WIN32
boost::nowide::ofstream(path, std::ios::app).close();
m_lock = boost::interprocess::file_lock(boost::nowide::widen(path).c_str());
m_held = m_lock.try_lock();
#else
m_fd = ::open(path.c_str(), O_RDWR | O_CREAT, 0644);
m_held = m_fd >= 0 && ::flock(m_fd, LOCK_EX | LOCK_NB) == 0;
#endif
}
~OtherHolder() { release(); }
OtherHolder(const OtherHolder &) = delete;
OtherHolder &operator=(const OtherHolder &) = delete;
bool held() const { return m_held; }
void release()
{
#ifdef _WIN32
if (m_held)
m_lock.unlock();
m_lock = boost::interprocess::file_lock();
#else
if (m_fd >= 0)
::close(m_fd);
m_fd = -1;
#endif
m_held = false;
}
private:
#ifdef _WIN32
boost::interprocess::file_lock m_lock;
#else
int m_fd{-1};
#endif
bool m_held{false};
};
TEST_CASE("InstanceLock yields to a lock held through another handle and reports it", "[InstanceLock]")
{ {
ScopedTemporaryFile lock_file(".lock"); ScopedTemporaryFile lock_file(".lock");
const std::string path = lock_file.string(); const std::string path = lock_file.string();
ScopedStaticValue cooldown(InstanceLock::cooldown, 300ms); ScopedStaticValue cooldown(InstanceLock::cooldown, 300ms);
int child_holds[2], child_may_exit[2]; OtherHolder other(path);
REQUIRE(::pipe(child_holds) == 0); REQUIRE(other.held());
REQUIRE(::pipe(child_may_exit) == 0);
const pid_t child = ::fork(); bool locked_while_other_holds;
REQUIRE(child >= 0);
if (child == 0) {
int fd = ::open(path.c_str(), O_RDWR | O_CREAT, 0644);
char byte = ::flock(fd, LOCK_EX | LOCK_NB) == 0 ? '1' : '0';
if (::write(child_holds[1], &byte, 1) != 1 || ::read(child_may_exit[0], &byte, 1) != 1)
::_exit(1);
::_exit(0);
}
char byte = '0';
REQUIRE(::read(child_holds[0], &byte, 1) == 1);
REQUIRE(byte == '1');
bool locked_while_child_holds;
{ {
InstanceLock lock(path, 100ms); InstanceLock lock(path, 100ms);
locked_while_child_holds = lock.locked(); locked_while_other_holds = lock.locked();
} }
// The timed-out wait starts a cool-down: the next guard does not touch the file. // The timed-out wait starts a cool-down: the next guard does not touch the file.
const auto started = std::chrono::steady_clock::now(); const auto started = std::chrono::steady_clock::now();
@@ -187,18 +216,13 @@ TEST_CASE("InstanceLock yields to another process and reports it", "[InstanceLoc
locked_during_cooldown = lock.locked(); locked_during_cooldown = lock.locked();
} }
const auto cooldown_wait = std::chrono::steady_clock::now() - started; const auto cooldown_wait = std::chrono::steady_clock::now() - started;
REQUIRE(::write(child_may_exit[1], "x", 1) == 1); other.release();
int status = 0;
REQUIRE(::waitpid(child, &status, 0) == child);
for (int fd : {child_holds[0], child_holds[1], child_may_exit[0], child_may_exit[1]})
::close(fd);
REQUIRE_FALSE(locked_while_child_holds); REQUIRE_FALSE(locked_while_other_holds);
REQUIRE_FALSE(locked_during_cooldown); REQUIRE_FALSE(locked_during_cooldown);
REQUIRE(cooldown_wait < 4000ms); REQUIRE(cooldown_wait < 4000ms);
// Once the cool-down passes, the lock the child released is taken again. // Once the cool-down passes, the lock the other holder released is taken again.
std::this_thread::sleep_for(400ms); std::this_thread::sleep_for(400ms);
InstanceLock lock(path); InstanceLock lock(path);
REQUIRE(lock.locked()); REQUIRE(lock.locked());
} }
#endif