Fall Back When a Replace Is Refused and Sweep Only Old Temporaries

Some mounts refuse to replace an existing file in one rename, and the
old remove-then-rename worked there where the atomic path now fails, so
rename_file() takes the two-step route when a one-step replace is
refused and write_file_atomically() writes in place whenever the rename
fails, not only for the Windows sharing case.

An instance that gave up waiting for the lock writes unlocked by design,
so a peer holding the lock could sweep its in-flight temporary and make
its rename fail; only temporaries older than ten minutes are removed now,
and the sweep uses the error-code overloads so an entry vanishing between
listing and stat cannot throw out of startup. The registry mutex is
leaked like the map it guards, so a save during static destruction does
not lock a destroyed mutex.

The physical printer loader is never called, so its guard is gone, while
the two delete paths that do run now hold the lock. The bundle metadata
loader is lock-free again, since the zip import reads it from a scratch
folder; the guard sits at the two scans that read the user's bundles.
Preset::save() builds what it writes before taking the lock, the cache
writer reserves its payload, and the retry test tolerates a slow runner.
This commit is contained in:
Hanif Koh
2026-09-24 18:22:00 +08:00
parent 18a4d70d41
commit 0d32795603
9 changed files with 81 additions and 40 deletions
+26 -11
View File
@@ -707,7 +707,15 @@ std::error_code rename_file(const std::string &from, const std::string &to)
#else
// rename(2) replaces an existing target atomically; removing it first would
// leave a window in which the file does not exist at all.
return std::make_error_code(static_cast<std::errc>(boost::nowide::rename(from.c_str(), to.c_str()) == 0 ? 0 : errno));
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 &&
boost::nowide::rename(from.c_str(), to.c_str()) == 0)
return {};
return std::make_error_code(static_cast<std::errc>(err));
#endif
}
@@ -741,18 +749,16 @@ std::error_code write_file_atomically(const std::string &path, const std::string
if (target_exists)
boost::filesystem::permissions(tmp_path, target.permissions(), bec);
std::error_code ec = rename_file(tmp_path, path);
if (ec)
if (ec) {
boost::nowide::remove(tmp_path.c_str());
#ifdef _WIN32
// Windows refuses to replace a file another process holds open without
// FILE_SHARE_DELETE, which is how the C runtime opens files for reading.
// Losing the 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.
if (ec == std::errc::permission_denied) {
BOOST_LOG_TRIVIAL(warning) << "Cannot replace " << path << " while another process holds it open; writing in place";
// Windows refuses to replace a file another process holds open without
// FILE_SHARE_DELETE, which is how the C runtime opens files for reading,
// and some mounts cannot replace a file in one step at all. Losing the
// 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);
}
#endif
return ec;
}
@@ -771,11 +777,20 @@ size_t remove_stale_temp_files(const boost::filesystem::path &dir, const std::st
return false;
return std::all_of(name.begin() + dot + 1, name.begin() + digits_end, [](char c) { return c >= '0' && c <= '9'; });
};
// 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.
constexpr std::time_t stale_age = 10 * 60;
const std::time_t now = std::time(nullptr);
size_t removed = 0;
boost::system::error_code ec;
for (boost::filesystem::directory_iterator it(dir, ec), end; ! ec && it != end; it.increment(ec)) {
if (! boost::filesystem::is_regular_file(it->symlink_status()) || ! is_temp_name(it->path().filename().string()))
if (! boost::filesystem::is_regular_file(it->symlink_status(ec)) || ! is_temp_name(it->path().filename().string()))
continue;
const std::time_t written = boost::filesystem::last_write_time(it->path(), ec);
if (ec || now - written < stale_age) {
ec.clear();
continue;
}
if (boost::filesystem::remove(it->path(), ec)) {
BOOST_LOG_TRIVIAL(info) << "Removed stale temporary file " << it->path();
++ removed;