mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-26 10:21:00 +00:00
Give Each Atomic Write Its Own Temporary and Route the Config Through It
Two threads writing one target without a lock shared the same temporary name, so one could truncate it under the other; the name now carries a per-call counter. The helper takes its content as chunks, which lets the vendor cache hand over its header and body without copying the blob, and AppConfig::save() goes through it too, including the Windows backup copy, so the config's temporaries get the same handling as everyone else's. Copying the target's permissions onto the temporary set a read-only bit that stopped the rename itself on Windows; they are applied after the rename there. The two-step rename fallback triggers on whatever error a mount reports when the target is still there, since FUSE, gvfs and MTP refuse a replacing rename with errors other than the three it handled. Every successful write sweeps stale temporaries of its own target, so leftovers beside the bundle metadata, physical printers, plugin config and profile cache are covered, and the older OrcaSlicer.conf.<pid> form with them. The bundle metadata guard is taken per file, never while the bundle registry's writer lock is held, so the two are not taken in both orders. The lock guard's open-and-lock steps are one block, and the read-only choice lives in user_presets_lock_path() instead of at each call site. The vendor cache test that blocked the old fixed temporary name with a directory provokes the failure through permissions instead.
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
#include <ctime>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <system_error>
|
||||
|
||||
#ifndef _WIN32
|
||||
@@ -122,9 +123,34 @@ TEST_CASE("write_file_atomically writes through a symlink and keeps the target's
|
||||
}
|
||||
#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);
|
||||
size_t entries = 0;
|
||||
for (auto &entry : boost::filesystem::directory_iterator(dir.path())) {
|
||||
(void) entry;
|
||||
++entries;
|
||||
}
|
||||
REQUIRE(entries == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("remove_stale_temp_files removes only old <name>.<pid>.tmp files", "[utils]") {
|
||||
ScopedTemporaryDir dir;
|
||||
for (const char *name : { "a.json.123.tmp", "b.info.4.tmp", "c.json", "d.tmp", "e.json.x.tmp", "f.json..tmp" }) {
|
||||
for (const char *name : { "a.json.123.tmp", "b.info.4.tmp", "c.json", "d.tmp", "e.json.x.tmp", "f.json..tmp", "a.json.99", "a.json.12.3.tmp" }) {
|
||||
REQUIRE_FALSE(write_file_atomically((dir.path() / name).string(), "x"));
|
||||
// An hour old: long past the age below which a temporary may still be in flight.
|
||||
boost::filesystem::last_write_time(dir.path() / name, std::time(nullptr) - 3600);
|
||||
@@ -132,19 +158,22 @@ TEST_CASE("remove_stale_temp_files removes only old <name>.<pid>.tmp files", "[u
|
||||
// Just written: possibly another instance's in-flight save, so it stays.
|
||||
REQUIRE_FALSE(write_file_atomically((dir.path() / "g.json.7.tmp").string(), "x"));
|
||||
|
||||
SECTION("with a name prefix only matching names go") {
|
||||
REQUIRE(remove_stale_temp_files(dir.path(), "a.json") == 1);
|
||||
SECTION("with a name prefix only matching names go, including the older <name>.<pid> form") {
|
||||
REQUIRE(remove_stale_temp_files(dir.path(), "a.json") == 3);
|
||||
REQUIRE_FALSE(boost::filesystem::exists(dir.path() / "a.json.123.tmp"));
|
||||
REQUIRE_FALSE(boost::filesystem::exists(dir.path() / "a.json.99"));
|
||||
REQUIRE_FALSE(boost::filesystem::exists(dir.path() / "a.json.12.3.tmp"));
|
||||
REQUIRE(boost::filesystem::exists(dir.path() / "b.info.4.tmp"));
|
||||
}
|
||||
SECTION("without a prefix every stale temporary goes and nothing else") {
|
||||
REQUIRE(remove_stale_temp_files(dir.path()) == 2);
|
||||
REQUIRE(remove_stale_temp_files(dir.path()) == 3);
|
||||
size_t entries = 0;
|
||||
for (auto &entry : boost::filesystem::directory_iterator(dir.path())) {
|
||||
(void) entry;
|
||||
++entries;
|
||||
}
|
||||
REQUIRE(entries == 5);
|
||||
REQUIRE(entries == 6);
|
||||
REQUIRE(boost::filesystem::exists(dir.path() / "a.json.99"));
|
||||
REQUIRE(boost::filesystem::exists(dir.path() / "g.json.7.tmp"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user