Retry a Failed Lock File, Keep Special Targets in Place and Sweep Stale Temporaries

A lock file that failed to open once stayed unopened for the rest of the
process, so a scanner holding the fresh file for a moment on Windows
silently disabled the lock for the session, and a lock call that throws,
as it does on a share without a lock service, was re-attempted and logged
by every guard. Both are now left alone for the cool-down and then retried.

Renaming a fresh file over the target turned a symlinked preset into a
plain file, reset its permissions to the umask default, and could not
work at all for a settings export to a device or pipe, since a temporary
cannot be created beside /dev/stdout. A target that is not a regular file
is now written in place, an existing target keeps its permissions, and a
directory that refuses the temporary but not the file falls back too.

The inline PhysicalPrinter::save() overload was the one physical printer
writer without the guard. load_info() took the lock redundantly under the
scan guard and, in read-only mode, created user.lock per file; the bundle
metadata reader now honours read_only too. The plugin config, the setup
guide's profile cache and the vendor preset cache wrote through a
temporary by hand; they use the helper, with a binary mode for the cache.
A crash between temporary and rename left a <name>.<pid>.tmp behind for
good; AppConfig::load() and the preset scan remove stale ones while they
hold the lock, and AppConfig::save() names its temporary the same way.
This commit is contained in:
Hanif Koh
2026-09-24 17:26:46 +08:00
parent 651e48723d
commit 18a4d70d41
14 changed files with 216 additions and 97 deletions
+45 -5
View File
@@ -711,10 +711,10 @@ std::error_code rename_file(const std::string &from, const std::string &to)
#endif
}
static std::error_code write_whole_file(const std::string &path, const std::string &content)
static std::error_code write_whole_file(const std::string &path, const std::string &content, bool binary)
{
errno = 0;
boost::nowide::ofstream out(path, std::ios::out | std::ios::trunc);
boost::nowide::ofstream out(path, std::ios::out | std::ios::trunc | (binary ? std::ios::binary : std::ios::openmode{}));
out << content;
out.close();
if (! out.fail())
@@ -722,13 +722,24 @@ static std::error_code write_whole_file(const std::string &path, const std::stri
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)
std::error_code write_file_atomically(const std::string &path, const std::string &content, 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);
const std::string tmp_path = path + "." + std::to_string(get_current_pid()) + ".tmp";
if (std::error_code ec = write_whole_file(tmp_path, content)) {
if (std::error_code ec = write_whole_file(tmp_path, content, 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 ec;
}
if (target_exists)
boost::filesystem::permissions(tmp_path, target.permissions(), bec);
std::error_code ec = rename_file(tmp_path, path);
if (ec)
boost::nowide::remove(tmp_path.c_str());
@@ -739,12 +750,41 @@ std::error_code write_file_atomically(const std::string &path, const std::string
// 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";
ec = write_whole_file(path, content);
ec = write_whole_file(path, content, binary);
}
#endif
return ec;
}
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) {
if (name.compare(0, name_prefix.size(), name_prefix) != 0)
return false;
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'; });
};
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()))
continue;
if (boost::filesystem::remove(it->path(), ec)) {
BOOST_LOG_TRIVIAL(info) << "Removed stale temporary file " << it->path();
++ removed;
}
ec.clear();
}
return removed;
}
#ifdef __linux__
// Copied from boost::filesystem.
// Called by copy_file_linux() in case linux sendfile() API is not supported.