Lock Config and Preset Files Across Instances and Write Them Atomically (#15861)

* Lock Config and Preset Files Across Instances and Write Them Atomically

Every running instance shares one OrcaSlicer.conf and one user preset
tree, and nothing kept their writers apart. Two instances saving at the
same moment, or the cloud preset sync thread writing while the GUI thread
saved, could interleave, and a reader in another instance could open a
preset JSON or .info file between truncate and close and get a partial
file, dropping that preset for the session with a parse error.

Add InstanceLock, a scoped guard that serialises the threads of one
process through a recursive mutex and other processes through an advisory
OS file lock: flock on POSIX, held on the guard's own descriptor so no
other close in the process can drop it, and LockFileEx on Windows. The
outermost guard opens the lock file and closes it on release, so nothing
stays open between saves and a data dir can be removed once nothing is
saving into it; the file itself is kept, since deleting it would let a
third instance lock a fresh file while the second still holds the old
one. It is best effort: when the lock file cannot be opened or locked, or
another instance still holds it after a second, the guard logs once and
lets the write proceed, then leaves the file alone for ten seconds, so a
hung instance never blocks every other one and a holder stuck in a
debugger does not cost a stall per save. The guard sits at the leaf
readers and writers: set_sync_info_and_save() calls save_info() under the
preset collection mutex, so a batch lock around save_user_presets() would
invert the order against the sync thread. The user preset scan reads its
files on worker threads without the guard, since the mutex would
serialise them, and takes it per file in the serial commit step, so a
save never waits for the whole scan. Each read keeps the bytes of the
preset and its .info as they were before parsing; commit compares them
with the disk under the guard and reads a file that changed again, so it
never deletes or writes back over another instance's newer save, nor
installs a .json and .info from two different saves; a preset another
instance removed in the meantime is not installed. Without the guard, in
a cool-down, the scan still loads the presets but leaves their files
alone: an unreadable file stays for the next scan, and a derived
compatible printer is not written back. Read-only scans, which is what
the CLI does, take no lock and create no lock file.

AppConfig holds OrcaSlicer.conf.lock in load() and save(); load is
included because the Windows path restores from the .bak copy. Every
user preset writer and reader holds user.lock: Preset::save(), which
writes no .info when the preset itself could not be written, since an
.info without its preset reads as a cloud deletion request, save_info(),
reload() and remove_files(), each preset the scan commits, the
bundle metadata reads and write, the .info removal after a cloud-confirmed
delete, the orphaned-.info scan on the sync thread, the bundle folder
removal on unsubscribe and the physical printer writers and delete
paths. A bundle import extracts under cache/ into a folder per process
and per import, where no scan reads.

Preset JSON, .info, bundle metadata, physical printer and config files,
and the caches and state files that already used a temporary by hand,
now go through write_file_atomically(), which writes <file>.<pid>.<n>.tmp
beside the target and renames it over, so a reader that never waits sees
a complete old or new file. A symlink is followed; a target that is not
a regular file is written in place; and when no temporary can be created
beside an existing target, or the rename itself is refused, by a Windows
reader holding the file open or a mount that cannot replace in one step,
the helper writes in place as before, since losing the save is worse
than a torn read. On POSIX the rename replaces the
target atomically where the old code removed it first and left a window
with no file at all; only a mount that refuses a one-step replace gets
the old remove-then-rename. A crash between temporary and rename leaves
the temporary behind, which no scan reads. Preset::save() returns
whether it wrote the preset, so the scan counts a compatible printer it
could not write back as an error. A failed config write keeps
the config dirty, and the idle handler waits ten seconds before retrying
while an explicit save always tries.

* 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:
HanifKoh
2026-10-01 22:18:21 +08:00
committed by GitHub
parent b102ae3aaa
commit c023632a7d
22 changed files with 1186 additions and 236 deletions
+1
View File
@@ -57,6 +57,7 @@ add_executable(${_TEST_NAME}_tests
# test_png_io.cpp
test_indexed_triangle_set.cpp
test_texture_displacement.cpp
test_instance_lock.cpp
../libnest2d/printer_parts.cpp
)
+228
View File
@@ -0,0 +1,228 @@
#include <catch2/catch_all.hpp>
#include <atomic>
#include <chrono>
#include <thread>
#include <boost/filesystem.hpp>
#include "libslic3r/InstanceLock.hpp"
#include "test_utils.hpp"
#ifdef _WIN32
#include <boost/interprocess/sync/file_lock.hpp>
#include <boost/nowide/convert.hpp>
#include <boost/nowide/fstream.hpp>
#else
#include <fcntl.h>
#include <sys/file.h>
#include <unistd.h>
#endif
using namespace Slic3r;
using namespace std::chrono_literals;
// Sets a process-wide knob for one test and restores it however the test ends.
template<typename T> struct ScopedStaticValue
{
T &ref;
T saved;
ScopedStaticValue(T &ref, T value) : ref(ref), saved(ref) { ref = value; }
~ScopedStaticValue() { ref = saved; }
};
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();
ScopedStaticValue 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();
}
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();
{
InstanceLock lock(path);
REQUIRE(lock.locked());
}
boost::filesystem::remove(path);
InstanceLock lock(path);
REQUIRE(lock.locked());
// Each outermost guard opens the file afresh, so the deleted path is back.
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);
}
// Holds the OS lock on a file through a handle of its own, as another instance would. The lock
// belongs to the handle on Windows and to the open file description elsewhere, so the guard's
// handle is refused while this one holds it.
class OtherHolder
{
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");
const std::string path = lock_file.string();
ScopedStaticValue cooldown(InstanceLock::cooldown, 300ms);
OtherHolder other(path);
REQUIRE(other.held());
bool locked_while_other_holds;
{
InstanceLock lock(path, 100ms);
locked_while_other_holds = lock.locked();
}
// The timed-out wait starts a cool-down: the next guard does not touch the file.
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;
other.release();
REQUIRE_FALSE(locked_while_other_holds);
REQUIRE_FALSE(locked_during_cooldown);
REQUIRE(cooldown_wait < 4000ms);
// Once the cool-down passes, the lock the other holder released is taken again.
std::this_thread::sleep_for(400ms);
InstanceLock lock(path);
REQUIRE(lock.locked());
}
@@ -5,6 +5,7 @@
#include <fstream>
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/ParallelResolve.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleMesh.hpp"
@@ -17,6 +18,10 @@
#include <iostream>
#include <initializer_list>
#ifndef _WIN32
#include <unistd.h> // geteuid
#endif
using namespace Slic3r;
namespace {
@@ -5956,3 +5961,214 @@ TEST_CASE("A filament's variant index follows the extruder type and nozzle volum
// the filament defines no Bowden High Flow variant
CHECK(PresetBundle::get_filament_variant_index(filament, printer, 1, nvtHighFlow) == 0);
}
TEST_CASE("A user preset saved over by another instance while its directory loads is read again under the lock", "[Preset][Bundle][InstanceLock]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data_dir_scope(temp_dir.path());
RenameTestCollection coll;
Preset &parent = add_inmemory_preset(coll, "Parent Process");
parent.config.option<ConfigOptionFloat>("layer_height", true)->value = 0.24;
parent.is_system = true;
// Fewer than one batch, so every file is read before the first one is committed.
constexpr int children = 8;
static_assert(children <= int(resolve_batch_size));
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
for (int i = 0; i < children; ++ i)
write_minimal_child(dir / ("Child " + std::to_string(i) + ".json"), "Child " + std::to_string(i), "Parent Process");
// The first commit stands in for another instance saving every other preset after
// they were read and before they are committed.
std::string first;
auto save_the_others = [&](Preset &preset) {
if (! first.empty())
return;
first = preset.name;
for (int i = 0; i < children; ++ i) {
const std::string name = "Child " + std::to_string(i);
if (name != first)
std::ofstream((dir / (name + ".json")).string())
<< R"({"type":"process","name":")" << name
<< R"(","from":"User","version":"1.0.0","inherits":"Parent Process","layer_height":"0.3"})";
}
};
PresetsConfigSubstitutions substitutions;
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable, save_the_others);
REQUIRE_FALSE(first.empty());
CHECK(coll.error_count() == 0);
for (int i = 0; i < children; ++ i) {
const std::string name = "Child " + std::to_string(i);
const Preset *child = coll.find_preset(name);
REQUIRE(child != nullptr);
CHECK_THAT(child->config.opt_float("layer_height"), Catch::Matchers::WithinAbs(name == first ? 0.24 : 0.3, 1e-9));
}
}
TEST_CASE("A user preset removed by another instance while its directory loads is not installed", "[Preset][Bundle][InstanceLock]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data_dir_scope(temp_dir.path());
RenameTestCollection coll;
Preset &parent = add_inmemory_preset(coll, "Parent Process");
parent.is_system = true;
// Fewer than one batch, so every file is read before the first one is committed.
constexpr int children = 8;
static_assert(children <= int(resolve_batch_size));
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
for (int i = 0; i < children; ++ i)
write_minimal_child(dir / ("Child " + std::to_string(i) + ".json"), "Child " + std::to_string(i), "Parent Process");
std::string first;
auto remove_the_others = [&](Preset &preset) {
if (! first.empty())
return;
first = preset.name;
for (int i = 0; i < children; ++ i)
if (const std::string name = "Child " + std::to_string(i); name != first)
fs::remove(dir / (name + ".json"));
};
PresetsConfigSubstitutions substitutions;
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable, remove_the_others);
REQUIRE_FALSE(first.empty());
CHECK(coll.error_count() == 0);
CHECK(coll.size() == 3); // the default preset, the parent and the first child
CHECK(coll.find_preset(first) != nullptr);
}
TEST_CASE("Without the instance lock a user preset loads but its file is not written back", "[Preset][Bundle][InstanceLock]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data_dir_scope(temp_dir.path());
// A directory where the lock file belongs, so the lock cannot be taken.
fs::create_directories(fs::path(user_presets_lock_path()));
PresetBundle bundle;
const fs::path file = temp_dir.path() / PRESET_FILAMENT_NAME / "My PLA @Test Printer.json";
write_preset_with_inherits(bundle.filaments.default_preset().config, file, "My PLA @Test Printer", std::string());
const std::string before = read_file(file);
PresetsConfigSubstitutions substitutions;
bundle.filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::EnableSilent);
const Preset *preset = bundle.filaments.find_preset("My PLA @Test Printer");
REQUIRE(preset != nullptr);
CHECK(preset->config.option<ConfigOptionStrings>("compatible_printers")->values == std::vector<std::string>{ "Test Printer" });
CHECK(read_file(file) == before);
}
#ifndef _WIN32
// File permissions stop reading only on POSIX.
TEST_CASE("A user preset that cannot be read under the instance lock is counted and removed", "[Preset][Bundle][InstanceLock]")
{
if (::geteuid() == 0)
SKIP("file permissions do not stop root");
ScopedTemporaryDir temp_dir;
ScopedDataDir data_dir_scope(temp_dir.path());
RenameTestCollection coll;
Preset &parent = add_inmemory_preset(coll, "Parent Process");
parent.is_system = true;
const fs::path file = temp_dir.path() / PRESET_PRINT_NAME / "Unreadable.json";
write_minimal_child(file, "Unreadable", "Parent Process");
fs::permissions(file, fs::no_perms);
PresetsConfigSubstitutions substitutions;
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable);
CHECK(coll.error_count() == 1);
CHECK(coll.find_preset("Unreadable") == nullptr);
CHECK_FALSE(fs::exists(file));
}
#endif
#ifndef _WIN32
// Creating a symlink needs no privilege only on POSIX.
TEST_CASE("A user preset that is a symlink to itself is counted and removed while the rest still load", "[Preset][Bundle][InstanceLock]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data_dir_scope(temp_dir.path());
RenameTestCollection coll;
Preset &parent = add_inmemory_preset(coll, "Parent Process");
parent.is_system = true;
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
write_minimal_child(dir / "Good A.json", "Good A", "Parent Process");
write_minimal_child(dir / "Good B.json", "Good B", "Parent Process");
const fs::path loop = dir / "Loop.json";
fs::create_symlink(loop.filename(), loop);
PresetsConfigSubstitutions substitutions;
REQUIRE_NOTHROW(coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable));
CHECK(coll.error_count() == 1);
CHECK(coll.find_preset("Good A") != nullptr);
CHECK(coll.find_preset("Good B") != nullptr);
CHECK(coll.find_preset("Loop") == nullptr);
boost::system::error_code ec;
CHECK(fs::symlink_status(loop, ec).type() == fs::file_not_found);
}
#endif
#ifndef _WIN32
// Creating a symlink needs no privilege only on POSIX.
TEST_CASE("A user preset symlinked to a missing target is counted but the link is kept", "[Preset][Bundle][InstanceLock]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data_dir_scope(temp_dir.path());
RenameTestCollection coll;
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
const fs::path link = dir / "Linked.json";
fs::create_directories(dir);
fs::create_symlink(temp_dir.path() / "unmounted" / "Linked.json", link);
PresetsConfigSubstitutions substitutions;
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable);
CHECK(coll.error_count() == 1);
CHECK(coll.find_preset("Linked") == nullptr);
boost::system::error_code ec;
CHECK(fs::symlink_status(link, ec).type() == fs::symlink_file);
}
#endif
#ifndef _WIN32
// The read-only bit on a directory stops file creation only on POSIX.
TEST_CASE("A user filament whose derived compatible printer cannot be written back still loads with the failure counted", "[Preset][Bundle]")
{
if (::geteuid() == 0)
SKIP("a read-only directory does not stop root");
ScopedTemporaryDir temp_dir;
PresetBundle bundle;
const fs::path dir = temp_dir.path() / PRESET_FILAMENT_NAME;
const fs::path file = dir / "My PLA @Test Printer.json";
write_preset_with_inherits(bundle.filaments.default_preset().config, file, "My PLA @Test Printer", std::string());
const std::string before = read_file(file);
fs::permissions(file, fs::owner_read);
fs::permissions(dir, fs::owner_read | fs::owner_exe);
PresetsConfigSubstitutions substitutions;
bundle.filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::EnableSilent);
// Restored before any assertion, so a failure never leaves an unremovable directory behind.
fs::permissions(dir, fs::owner_all);
fs::permissions(file, fs::owner_read | fs::owner_write);
const Preset *preset = bundle.filaments.find_preset("My PLA @Test Printer");
REQUIRE(preset != nullptr);
CHECK(preset->config.option<ConfigOptionStrings>("compatible_printers")->values == std::vector<std::string>{ "Test Printer" });
CHECK(bundle.filaments.error_count() == 1);
CHECK(read_file(file) == before);
}
#endif
+109
View File
@@ -10,6 +10,8 @@
#include <cctype>
#include <fstream>
#include <string>
#include <thread>
#include <system_error>
#ifndef _WIN32
#include <unistd.h> // getuid
@@ -62,6 +64,113 @@ TEST_CASE("per-user temp root is unchanged on Windows, isolated elsewhere", "[ut
#endif
}
TEST_CASE("write_file_atomically replaces the target and leaves no temporary file", "[utils]") {
ScopedTemporaryDir dir;
const boost::filesystem::path target = dir.path() / "preset.json";
REQUIRE_FALSE(write_file_atomically(target.string(), "first"));
REQUIRE_FALSE(write_file_atomically(target.string(), "second"));
std::string content;
load_string_file(target, content);
REQUIRE(content == "second");
size_t entries = 0;
for (auto &entry : boost::filesystem::directory_iterator(dir.path())) {
(void) entry;
++entries;
}
REQUIRE(entries == 1);
}
TEST_CASE("write_file_atomically reports a missing directory and writes nothing", "[utils]") {
ScopedTemporaryDir dir;
const boost::filesystem::path target = dir.path() / "missing" / "preset.json";
const std::error_code ec = write_file_atomically(target.string(), "x");
REQUIRE(ec == std::errc::no_such_file_or_directory);
REQUIRE_FALSE(boost::filesystem::exists(target));
}
#ifndef _WIN32
// The read-only bit on a directory stops file creation only on POSIX.
TEST_CASE("write_file_atomically writes in place when no temporary can be created beside an existing target", "[utils]") {
if (::geteuid() == 0)
SKIP("a read-only directory does not stop root");
ScopedTemporaryDir dir;
const boost::filesystem::path target = dir.path() / "preset.json";
REQUIRE_FALSE(write_file_atomically(target.string(), "first"));
boost::filesystem::permissions(dir.path(), boost::filesystem::owner_read | boost::filesystem::owner_exe);
const std::error_code replaced = write_file_atomically(target.string(), "second");
const std::error_code created = write_file_atomically((dir.path() / "new.json").string(), "x");
// Restored before any assertion, so a failure never leaves an unremovable directory behind.
boost::filesystem::permissions(dir.path(), boost::filesystem::owner_all);
REQUIRE_FALSE(replaced);
REQUIRE(created == std::errc::permission_denied);
std::string content;
load_string_file(target, content);
REQUIRE(content == "second");
}
#endif
TEST_CASE("write_file_atomically keeps bytes intact in binary mode", "[utils]") {
ScopedTemporaryDir dir;
const boost::filesystem::path target = dir.path() / "blob.bin";
const std::string bytes("a\r\nb\0c", 6);
REQUIRE_FALSE(write_file_atomically(target.string(), bytes, /*binary=*/true));
REQUIRE(boost::filesystem::file_size(target) == bytes.size());
}
#ifndef _WIN32
TEST_CASE("write_file_atomically writes through a symlink and keeps the target's permissions", "[utils]") {
ScopedTemporaryDir dir;
const boost::filesystem::path real = dir.path() / "real.json";
const boost::filesystem::path link = dir.path() / "link.json";
REQUIRE_FALSE(write_file_atomically(real.string(), "first"));
boost::filesystem::permissions(real, boost::filesystem::owner_read | boost::filesystem::owner_write);
boost::filesystem::create_symlink(real, link);
REQUIRE_FALSE(write_file_atomically(link.string(), "second"));
REQUIRE(boost::filesystem::is_symlink(boost::filesystem::symlink_status(link)));
std::string content;
load_string_file(real, content);
REQUIRE(content == "second");
REQUIRE_FALSE(write_file_atomically(real.string(), "third"));
const auto perms = boost::filesystem::status(real).permissions() & boost::filesystem::all_all;
REQUIRE(perms == (boost::filesystem::owner_read | boost::filesystem::owner_write));
}
#endif
TEST_CASE("write_file_atomically survives two threads writing one target", "[utils]") {
ScopedTemporaryDir dir;
const boost::filesystem::path target = dir.path() / "shared.json";
const std::string a(20000, 'a'), b(20000, 'b');
std::thread other([&] {
for (int i = 0; i < 50; ++i)
write_file_atomically(target.string(), a);
});
for (int i = 0; i < 50; ++i)
write_file_atomically(target.string(), b);
other.join();
std::string content;
load_string_file(target, content);
const bool whole = content == a || content == b;
REQUIRE(whole);
// No temporary may be left; a scanner on Windows may briefly hold the old
// file under another name, so only the temporaries are counted.
size_t temporaries = 0;
for (auto &entry : boost::filesystem::directory_iterator(dir.path()))
if (entry.path().extension() == ".tmp")
++temporaries;
REQUIRE(temporaries == 0);
}
TEST_CASE("copy_file reports the OS error when the destination cannot be written", "[utils]") {
ScopedTemporaryFile source(".txt");
{
-18
View File
@@ -1804,24 +1804,6 @@ TEST_CASE("a header claiming more body than the file holds is rejected", "[Vendo
REQUIRE_FALSE(bundle.load_vendor_cache(cache, "Bounded", Semver(1, 0, 0)));
}
TEST_CASE("a failed write leaves the previous cache in place", "[VendorCache]")
{
TempDir tmp;
const std::string cache = (tmp.path / "Durable.opc").string();
REQUIRE(save_one_vendor(cache, one_vendor("Durable"), "Durable", "1.0.0"));
const std::string before = slurp(cache);
// A directory where the temp file wants to go: the write cannot complete,
// and must not have destroyed what was already there to find that out.
const fs::path blocker = fs::path(cache + "." + std::to_string(get_current_pid()) + ".tmp");
fs::create_directories(blocker);
REQUIRE_FALSE(save_one_vendor(cache, one_vendor("Durable"), "Durable", "2.0.0"));
CHECK(slurp(cache) == before);
fs::remove_all(blocker);
}
TEST_CASE("a cache written by another build's option ordering still loads", "[VendorCache]")
{
// The regression the fingerprint used to prevent by refusing the file