mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-25 09:50:59 +00:00
Before this change a config whose temporary could not be written stayed dirty, so the idle handler tried again; the last round cleared the flag regardless, which lost a pending change to a transient failure. The writer reports whether the config itself was written and the flag clears only then, as it always did. The Windows rename retry duplicated what WindowsSupport::rename already does, retrying and moving an open destination aside, and multiplied its error logging; it is gone. The file a refused rename moves aside is named so the sweep can clear it after a crash in between, and every successful atomic write sweeps leftovers of earlier writes to the same target, so the caches and the smaller state files are covered too; the name shapes are strict enough that nothing of the user's matches. The lock wait defaults to a second, long against a critical section of milliseconds and short against the GUI thread; a lock file that cannot be opened says to check its owner and permissions; the test timing bounds tolerate a loaded runner; and the helper's header includes what its declarations use.
203 lines
6.6 KiB
C++
203 lines
6.6 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());
|
|
// Well inside the timeout it would otherwise have waited out; loose enough for a loaded runner.
|
|
REQUIRE(std::chrono::steady_clock::now() - started < 4000ms);
|
|
}
|
|
|
|
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 < 4000ms);
|
|
// A guard inside the cool-down still takes the lock when it is free.
|
|
InstanceLock lock(path);
|
|
REQUIRE(lock.locked());
|
|
}
|
|
#endif
|