mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-25 01:40:57 +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:
+36
-17
@@ -710,44 +710,53 @@ std::error_code rename_file(const std::string &from, const std::string &to)
|
||||
if (boost::nowide::rename(from.c_str(), to.c_str()) == 0)
|
||||
return {};
|
||||
const int err = errno;
|
||||
// Some mounts (sshfs without its rename workaround, for one) refuse to
|
||||
// replace an existing target in one step; take the old two-step route there.
|
||||
if ((err == EPERM || err == EEXIST || err == ENOTEMPTY) && boost::nowide::remove(to.c_str()) == 0 &&
|
||||
// Some mounts (sshfs, gvfs, MTP and a few SMB setups) refuse to replace an
|
||||
// existing target in one step, with whichever error they see fit; take the
|
||||
// old two-step route whenever the target is still there to be replaced.
|
||||
boost::system::error_code ec;
|
||||
if (err != ENOENT && err != EXDEV && boost::filesystem::exists(to, ec) && boost::nowide::remove(to.c_str()) == 0 &&
|
||||
boost::nowide::rename(from.c_str(), to.c_str()) == 0)
|
||||
return {};
|
||||
return std::make_error_code(static_cast<std::errc>(err));
|
||||
#endif
|
||||
}
|
||||
|
||||
static std::error_code write_whole_file(const std::string &path, const std::string &content, bool binary)
|
||||
static std::error_code write_whole_file(const std::string &path, std::initializer_list<std::string_view> chunks, bool binary)
|
||||
{
|
||||
errno = 0;
|
||||
boost::nowide::ofstream out(path, std::ios::out | std::ios::trunc | (binary ? std::ios::binary : std::ios::openmode{}));
|
||||
out << content;
|
||||
for (const std::string_view chunk : chunks)
|
||||
out.write(chunk.data(), static_cast<std::streamsize>(chunk.size()));
|
||||
out.close();
|
||||
if (! out.fail())
|
||||
return {};
|
||||
return std::make_error_code(errno != 0 ? static_cast<std::errc>(errno) : std::errc::io_error);
|
||||
}
|
||||
|
||||
std::error_code write_file_atomically(const std::string &path, const std::string &content, bool binary)
|
||||
std::error_code write_file_atomically(const std::string &path, std::initializer_list<std::string_view> chunks, bool binary)
|
||||
{
|
||||
boost::system::error_code bec;
|
||||
const boost::filesystem::file_status target = boost::filesystem::symlink_status(path, bec);
|
||||
const bool target_exists = ! bec && boost::filesystem::exists(target);
|
||||
if (target_exists && ! boost::filesystem::is_regular_file(target))
|
||||
return write_whole_file(path, content, binary);
|
||||
return write_whole_file(path, chunks, binary);
|
||||
|
||||
const std::string tmp_path = path + "." + std::to_string(get_current_pid()) + ".tmp";
|
||||
if (std::error_code ec = write_whole_file(tmp_path, content, binary)) {
|
||||
// Unique per process and per call, so two threads writing one target
|
||||
// without a lock never share a temporary.
|
||||
static std::atomic<unsigned> counter{0};
|
||||
const std::string tmp_path = path + "." + std::to_string(get_current_pid()) + "." + std::to_string(counter++) + ".tmp";
|
||||
if (std::error_code ec = write_whole_file(tmp_path, chunks, binary)) {
|
||||
boost::nowide::remove(tmp_path.c_str());
|
||||
// A directory that allows writing the file but not creating one beside it.
|
||||
if (target_exists && ec == std::errc::permission_denied)
|
||||
return write_whole_file(path, content, binary);
|
||||
return write_whole_file(path, chunks, binary);
|
||||
return ec;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
// On Windows a read-only bit on the temporary would stop the rename itself.
|
||||
if (target_exists)
|
||||
boost::filesystem::permissions(tmp_path, target.permissions(), bec);
|
||||
#endif
|
||||
std::error_code ec = rename_file(tmp_path, path);
|
||||
if (ec) {
|
||||
boost::nowide::remove(tmp_path.c_str());
|
||||
@@ -757,25 +766,35 @@ std::error_code write_file_atomically(const std::string &path, const std::string
|
||||
// save is worse than a reader seeing a partial file, so write in place
|
||||
// the way this used to work before the atomic path existed.
|
||||
BOOST_LOG_TRIVIAL(warning) << "Cannot replace " << path << " (" << ec.message() << "); writing in place";
|
||||
ec = write_whole_file(path, content, binary);
|
||||
return write_whole_file(path, chunks, binary);
|
||||
}
|
||||
return ec;
|
||||
#ifdef _WIN32
|
||||
if (target_exists)
|
||||
boost::filesystem::permissions(path, target.permissions(), bec);
|
||||
#endif
|
||||
const boost::filesystem::path target_path(path);
|
||||
remove_stale_temp_files(target_path.parent_path(), target_path.filename().string());
|
||||
return {};
|
||||
}
|
||||
|
||||
size_t remove_stale_temp_files(const boost::filesystem::path &dir, const std::string &name_prefix)
|
||||
{
|
||||
// <anything>.<digits>.tmp
|
||||
auto is_temp_name = [&name_prefix](const std::string &name) {
|
||||
auto all_digits = [](std::string::const_iterator begin, std::string::const_iterator end) {
|
||||
return begin != end && std::all_of(begin, end, [](char c) { return c >= '0' && c <= '9'; });
|
||||
};
|
||||
// <name_prefix>...<digits>.tmp, or the older <name_prefix>.<digits> when a prefix is given.
|
||||
auto is_temp_name = [&](const std::string &name) {
|
||||
if (name.compare(0, name_prefix.size(), name_prefix) != 0)
|
||||
return false;
|
||||
if (! name_prefix.empty() && name.size() > name_prefix.size() + 1 && name[name_prefix.size()] == '.' &&
|
||||
all_digits(name.begin() + name_prefix.size() + 1, name.end()))
|
||||
return true;
|
||||
static const std::string suffix = ".tmp";
|
||||
if (name.size() <= suffix.size() || name.compare(name.size() - suffix.size(), suffix.size(), suffix) != 0)
|
||||
return false;
|
||||
const size_t digits_end = name.size() - suffix.size();
|
||||
const size_t dot = name.rfind('.', digits_end - 1);
|
||||
if (dot == std::string::npos || dot == 0 || dot + 1 == digits_end)
|
||||
return false;
|
||||
return std::all_of(name.begin() + dot + 1, name.begin() + digits_end, [](char c) { return c >= '0' && c <= '9'; });
|
||||
return dot != std::string::npos && dot != 0 && all_digits(name.begin() + dot + 1, name.begin() + digits_end);
|
||||
};
|
||||
// An instance that gave up waiting for the lock writes unlocked by design,
|
||||
// so a temporary this young may still be in flight; a crash leftover is old.
|
||||
|
||||
Reference in New Issue
Block a user