mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 02:41:17 +00:00
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, which the OS releases when its holder dies. It is best effort: when the lock file cannot be created, or another instance still holds it after two seconds, the guard logs a warning and lets the write proceed rather than letting a hung instance block every other one. AppConfig::save() and load() hold OrcaSlicer.conf.lock; load is included because the Windows path restores from the .bak copy, and because Windows cannot replace a file another process has open, so an unlocked reader there made the other instance's save fail. Every user preset writer and reader holds user.lock: Preset::save(), save_info(), load_info() and remove_files(), the whole directory scan in load_presets(), the bundle metadata file, the .info removal after a cloud-confirmed delete and the bundle folder removal on unsubscribe. The guard sits at the leaf writers on purpose: set_sync_info_and_save() calls save_info() under the preset collection mutex, so a batch lock around save_user_presets(), which takes that mutex through delete_preset(), would invert the order. Write preset JSON, .info and bundle metadata files through a temporary file beside the target that is renamed over it, so a reader that never waits still sees a complete old or new file. On Linux and macOS rename_file() deleted the target before renaming, leaving a window in which the file did not exist at all, and returned a meaningless error code on failure; rename() replaces atomically, so call it directly. AppConfig::save() now logs a failed final rename instead of dropping the save silently.
This commit is contained in:
@@ -62,6 +62,32 @@ 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";
|
||||
|
||||
REQUIRE(write_file_atomically(target.string(), "x"));
|
||||
REQUIRE_FALSE(boost::filesystem::exists(target));
|
||||
}
|
||||
|
||||
TEST_CASE("copy_file reports the OS error when the destination cannot be written", "[utils]") {
|
||||
ScopedTemporaryFile source(".txt");
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user