Files
OrcaSlicer/tests/libslic3r/test_instance_lock.cpp
T
Hanif Koh 16f44a9dfd Move a Refused Target Aside Rather Than Removing It and Wait Only for a Reader
The two-step rename fallback removed the target before its second try,
and rename(2) reports the same errors for a source it cannot move, so a
refusal about the source cost the caller its existing file. The target
is moved aside and put back if the second step fails too.

The wait for a reader holding the target open was layered on the write
helper and ran on every platform and for every refusal, so a read-only
target on Windows cost half a second before failing anyway, while G-code
exports through rename_file() got no wait at all. It lives in
rename_file() now, on Windows only, and only for a target this process
could write. A failed config write clears the dirty flag as it always
did, so the idle handler does not repeat it on every tick.

The lock file is created readable and writable by every user, since
another user sharing the data dir has to open it read-write; the check
that the file behind the path is still the one opened runs at most every
few seconds rather than once per preset during a scan; the physical
printer loader reads under the lock; the stat headers join the existing
platform include block; and the utility tests keep their file's tag.
2026-09-24 22:05:30 +08:00

202 lines
6.5 KiB
C++

#include <catch2/catch_all.hpp>
#include <atomic>
#include <chrono>
#include <thread>
#include <boost/filesystem.hpp>
#include "libslic3r/InstanceLock.hpp"
#include "test_utils.hpp"
#ifndef _WIN32
#include <fcntl.h>
#include <sys/wait.h>
#include <unistd.h>
#endif
using namespace Slic3r;
using namespace std::chrono_literals;
TEST_CASE("InstanceLock creates its lock file and holds it for the guard's scope", "[InstanceLock]")
{
ScopedTemporaryFile lock_file(".lock");
const std::string path = lock_file.string();
{
InstanceLock lock(path);
REQUIRE(lock.locked());
REQUIRE(boost::filesystem::exists(path));
}
// Released: a fresh guard gets the lock at once instead of waiting out a timeout.
const auto started = std::chrono::steady_clock::now();
InstanceLock again(path, 5000ms);
REQUIRE(again.locked());
REQUIRE(std::chrono::steady_clock::now() - started < 1000ms);
}
TEST_CASE("InstanceLock nests within one thread", "[InstanceLock]")
{
ScopedTemporaryFile lock_file(".lock");
const std::string path = lock_file.string();
InstanceLock outer(path);
{
InstanceLock inner(path, 100ms);
REQUIRE(inner.locked());
}
// The inner guard leaving does not release the outer one.
REQUIRE(outer.locked());
}
TEST_CASE("InstanceLock is a no-op for an empty path and survives an unwritable one", "[InstanceLock]")
{
ScopedTemporaryDir dir;
InstanceLock none("");
REQUIRE_FALSE(none.locked());
// The directory does not exist, so the lock file cannot be created; the
// guard still constructs and the write it guards can go ahead.
InstanceLock unwritable((dir.path() / "missing" / "shared.lock").string(), 100ms);
REQUIRE_FALSE(unwritable.locked());
}
TEST_CASE("InstanceLock retries a lock file it could not open once the cool-down passes", "[InstanceLock]")
{
ScopedTemporaryDir dir;
const std::string path = (dir.path() / "later" / "shared.lock").string();
const auto saved_cooldown = InstanceLock::cooldown;
InstanceLock::cooldown = 300ms;
bool before_dir, during_cooldown, after_cooldown;
const auto started = std::chrono::steady_clock::now();
{
InstanceLock lock(path, 100ms);
before_dir = lock.locked();
}
boost::filesystem::create_directories(dir.path() / "later");
{
InstanceLock lock(path, 100ms);
during_cooldown = lock.locked();
}
const bool second_guard_inside_cooldown = std::chrono::steady_clock::now() - started < InstanceLock::cooldown;
std::this_thread::sleep_for(400ms);
{
InstanceLock lock(path, 100ms);
after_cooldown = lock.locked();
}
InstanceLock::cooldown = saved_cooldown;
REQUIRE_FALSE(before_dir);
// A loaded runner may take longer than the cool-down to get here; then the
// second guard legitimately retried, so only assert when the timing held.
if (second_guard_inside_cooldown)
REQUIRE_FALSE(during_cooldown);
REQUIRE(after_cooldown);
}
TEST_CASE("InstanceLock reopens a lock file that was replaced on disk", "[InstanceLock]")
{
ScopedTemporaryFile lock_file(".lock");
const std::string path = lock_file.string();
const auto saved_interval = InstanceLock::identity_check_interval;
InstanceLock::identity_check_interval = 0ms;
{
InstanceLock lock(path);
REQUIRE(lock.locked());
}
boost::filesystem::remove(path);
InstanceLock lock(path);
InstanceLock::identity_check_interval = saved_interval;
REQUIRE(lock.locked());
// Only a reopen recreates the file; a guard still holding the unlinked one
// would leave the path missing. (The inode number itself may be reused once
// the old handle is closed, so it is no proof either way.)
REQUIRE(boost::filesystem::exists(path));
}
TEST_CASE("InstanceLock serialises the threads of one process", "[InstanceLock]")
{
ScopedTemporaryFile lock_file(".lock");
const std::string path = lock_file.string();
std::atomic<bool> holder_ready{false};
std::atomic<bool> holder_released{false};
std::thread holder([&] {
InstanceLock lock(path);
holder_ready = true;
std::this_thread::sleep_for(150ms);
holder_released = true;
});
while (! holder_ready)
std::this_thread::yield();
bool released_before_acquire = false;
{
InstanceLock lock(path);
released_before_acquire = holder_released;
}
holder.join();
REQUIRE(released_before_acquire);
}
#ifndef _WIN32
// The cross-process side of the lock is a POSIX fcntl write lock, which a
// child process takes here directly; the same primitive backs the guard on
// Windows through LockFileEx, but spawning a child there is not worth a test.
TEST_CASE("InstanceLock yields to another process and reports it", "[InstanceLock]")
{
ScopedTemporaryFile lock_file(".lock");
const std::string path = lock_file.string();
int child_holds[2], child_may_exit[2];
REQUIRE(::pipe(child_holds) == 0);
REQUIRE(::pipe(child_may_exit) == 0);
const pid_t child = ::fork();
REQUIRE(child >= 0);
if (child == 0) {
int fd = ::open(path.c_str(), O_RDWR | O_CREAT, 0644);
struct flock lock{};
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
char byte = ::fcntl(fd, F_SETLK, &lock) == 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);
locked_while_child_holds = lock.locked();
}
// The timed-out wait starts a cool-down: the next guard does not wait again.
const auto started = std::chrono::steady_clock::now();
bool locked_during_cooldown;
{
InstanceLock lock(path, 5000ms);
locked_during_cooldown = lock.locked();
}
const auto cooldown_wait = std::chrono::steady_clock::now() - started;
REQUIRE(::write(child_may_exit[1], "x", 1) == 1);
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_during_cooldown);
REQUIRE(cooldown_wait < 1000ms);
// A guard inside the cool-down still takes the lock when it is free.
InstanceLock lock(path);
REQUIRE(lock.locked());
}
#endif