mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-25 09:50:59 +00:00
A lock file that failed to open once stayed unopened for the rest of the process, so a scanner holding the fresh file for a moment on Windows silently disabled the lock for the session, and a lock call that throws, as it does on a share without a lock service, was re-attempted and logged by every guard. Both are now left alone for the cool-down and then retried. Renaming a fresh file over the target turned a symlinked preset into a plain file, reset its permissions to the umask default, and could not work at all for a settings export to a device or pipe, since a temporary cannot be created beside /dev/stdout. A target that is not a regular file is now written in place, an existing target keeps its permissions, and a directory that refuses the temporary but not the file falls back too. The inline PhysicalPrinter::save() overload was the one physical printer writer without the guard. load_info() took the lock redundantly under the scan guard and, in read-only mode, created user.lock per file; the bundle metadata reader now honours read_only too. The plugin config, the setup guide's profile cache and the vendor preset cache wrote through a temporary by hand; they use the helper, with a binary mode for the cache. A crash between temporary and rename left a <name>.<pid>.tmp behind for good; AppConfig::load() and the preset scan remove stale ones while they hold the lock, and AppConfig::save() names its temporary the same way.
176 lines
5.4 KiB
C++
176 lines
5.4 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 = 50ms;
|
|
|
|
bool before_dir, during_cooldown, after_cooldown;
|
|
{
|
|
InstanceLock lock(path, 100ms);
|
|
before_dir = lock.locked();
|
|
}
|
|
boost::filesystem::create_directories(dir.path() / "later");
|
|
{
|
|
InstanceLock lock(path, 100ms);
|
|
during_cooldown = lock.locked();
|
|
}
|
|
std::this_thread::sleep_for(80ms);
|
|
{
|
|
InstanceLock lock(path, 100ms);
|
|
after_cooldown = lock.locked();
|
|
}
|
|
InstanceLock::cooldown = saved_cooldown;
|
|
|
|
REQUIRE_FALSE(before_dir);
|
|
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
|