mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-25 09:50:59 +00:00
Some mounts refuse to replace an existing file in one rename, and the old remove-then-rename worked there where the atomic path now fails, so rename_file() takes the two-step route when a one-step replace is refused and write_file_atomically() writes in place whenever the rename fails, not only for the Windows sharing case. An instance that gave up waiting for the lock writes unlocked by design, so a peer holding the lock could sweep its in-flight temporary and make its rename fail; only temporaries older than ten minutes are removed now, and the sweep uses the error-code overloads so an entry vanishing between listing and stat cannot throw out of startup. The registry mutex is leaked like the map it guards, so a save during static destruction does not lock a destroyed mutex. The physical printer loader is never called, so its guard is gone, while the two delete paths that do run now hold the lock. The bundle metadata loader is lock-free again, since the zip import reads it from a scratch folder; the guard sits at the two scans that read the user's bundles. Preset::save() builds what it writes before taking the lock, the cache writer reserves its payload, and the retry test tolerates a slow runner.
181 lines
5.7 KiB
C++
181 lines
5.7 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 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
|