From f9de444a53904dd2315460891948c869265a3456 Mon Sep 17 00:00:00 2001 From: Mathew Sutcliffe Date: Sun, 10 Jan 2016 23:39:26 +0000 Subject: [PATCH] refs #545 CAtomicFile used in CValueCache to avoid corruption in case serialization is interrupted. --- src/blackmisc/atomicfile.cpp | 91 ++++++++++++++++++++++++++++++++++++ src/blackmisc/atomicfile.h | 61 ++++++++++++++++++++++++ src/blackmisc/valuecache.cpp | 17 ++++--- 3 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 src/blackmisc/atomicfile.cpp create mode 100644 src/blackmisc/atomicfile.h diff --git a/src/blackmisc/atomicfile.cpp b/src/blackmisc/atomicfile.cpp new file mode 100644 index 000000000..7eabdeaca --- /dev/null +++ b/src/blackmisc/atomicfile.cpp @@ -0,0 +1,91 @@ +/* Copyright (C) 2015 + * swift project Community / Contributors + * + * This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level + * directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project, + * including this file, may be copied, modified, propagated, or distributed except according to the terms + * contained in the LICENSE file. + */ + +#include "blackmisc/atomicfile.h" +#include "blackmisc/algorithm.h" +#include +#include +#if defined(Q_OS_POSIX) +#include +#elif defined(Q_OS_WIN32) +#include +#endif + +namespace BlackMisc +{ + + bool CAtomicFile::open(CAtomicFile::OpenMode mode) + { + Q_ASSERT_X(!(mode & (ReadOnly | Append)), Q_FUNC_INFO, "ReadOnly and Append flags are incompatible with CAtomicFile"); + + m_originalFilename = fileName(); + QFileInfo fileInfo(fileName()); + setFileName(QFileInfo(fileInfo.dir(), ".tmp." + fileInfo.fileName() + "." + randomSuffix()).filePath()); + if (exists()) { remove(); } + + bool ok = QFile::open(mode); + if (! ok) { setFileName(m_originalFilename); } + return ok; + } + + void CAtomicFile::close() + { + if (! isOpen()) { return; } + + QFile::close(); + + if (error() == NoError) { replaceOriginal(); } + setFileName(m_originalFilename); + } + + bool CAtomicFile::checkedClose() + { + close(); + return error() == NoError; + } + + CAtomicFile::FileError CAtomicFile::error() const + { + if (m_renameError) { return RenameError; } + return QFile::error(); + } + + void CAtomicFile::unsetError() + { + m_renameError = false; + QFile::unsetError(); + } + + QString CAtomicFile::randomSuffix() + { + Q_CONSTEXPR auto max = 2176782335; + return QString::number(std::uniform_int_distribution::type>(0, max)(Private::defaultRandomGenerator()), 36); + } + +#if defined(Q_OS_POSIX) + void CAtomicFile::replaceOriginal() + { + auto result = ::rename(qPrintable(fileName()), qPrintable(m_originalFilename)); + if (result < 0) { m_renameError = true; } + } +#elif defined(Q_OS_WIN32) + void CAtomicFile::replaceOriginal() + { + auto result = MoveFileExA(qPrintable(fileName()), qPrintable(m_originalFilename), MOVEFILE_REPLACE_EXISTING); + if (! result) { m_renameError = true; } + } +#else + void CAtomicFile::replaceOriginal() + { + if (exists(m_originalFilename)) { remove(m_originalFilename); } + rename(m_originalFilename); + } +#endif + +} diff --git a/src/blackmisc/atomicfile.h b/src/blackmisc/atomicfile.h new file mode 100644 index 000000000..03ae71f8f --- /dev/null +++ b/src/blackmisc/atomicfile.h @@ -0,0 +1,61 @@ +/* Copyright (C) 2015 + * swift project Community / Contributors + * + * This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level + * directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project, + * including this file, may be copied, modified, propagated, or distributed except according to the terms + * contained in the LICENSE file. + */ + +//! \file + +#ifndef BLACKMISC_ATOMICFILE_H +#define BLACKMISC_ATOMICFILE_H + +#include +#include "blackmisc/blackmiscexport.h" + +namespace BlackMisc +{ + /*! + * A subclass of QFile which writes to a temporary file while it is open, then renames the file + * when it is closed, so that it overwrites the target file as a single, atomic transaction. + * + * If the application crashes while data is still being written, the original file is unchanged. + */ + class BLACKMISC_EXPORT CAtomicFile : public QFile + { + public: + //! \copydoc QFile::QFile(const QString &) + CAtomicFile(const QString &filename) : QFile(filename) {} + + //! \copydoc QFile::~QFile + ~CAtomicFile() { close(); } + + //! \copydoc QFile::open + //! Just before opening the file, the filename is changed so we actually write to a temporary file. + virtual bool open(OpenMode mode) override; + + //! \copydoc QFileDevice::close + //! After closing the file, it is renamed so that it overwrites the target file. + virtual void close() override; + + //! Calls close() and returns false if there was an error at any stage. + bool checkedClose(); + + //! \copydoc QFileDevice::error + FileError error() const; + + //! \copydoc QFileDevice::unsetError + void unsetError(); + + private: + static QString randomSuffix(); + void replaceOriginal(); + + QString m_originalFilename; + bool m_renameError = false; + }; +} + +#endif diff --git a/src/blackmisc/valuecache.cpp b/src/blackmisc/valuecache.cpp index b7ac01498..c0a05b1ce 100644 --- a/src/blackmisc/valuecache.cpp +++ b/src/blackmisc/valuecache.cpp @@ -12,6 +12,7 @@ #include "blackmisc/logmessage.h" #include "blackmisc/algorithm.h" #include "blackmisc/lockfree.h" +#include "blackmisc/atomicfile.h" #include #include @@ -230,23 +231,25 @@ namespace BlackMisc } for (auto it = namespaces.cbegin(); it != namespaces.cend(); ++it) { - QFile file(dir + "/" + it.key() + ".json"); - if (! file.open(QFile::ReadWrite | QFile::Text)) + QFile readFile(dir + "/" + it.key() + ".json"); + if (! readFile.open(QFile::ReadWrite | QFile::Text)) { - return CLogMessage(this).error("Failed to open %1: %2") << file.fileName() << file.errorString(); + return CLogMessage(this).error("Failed to open %1: %2") << readFile.fileName() << readFile.errorString(); } - auto json = QJsonDocument::fromJson(file.readAll()); + auto json = QJsonDocument::fromJson(readFile.readAll()); if (json.isArray() || (json.isNull() && ! json.isEmpty())) { - return CLogMessage(this).error("Invalid JSON format in %1") << file.fileName(); + return CLogMessage(this).error("Invalid JSON format in %1") << readFile.fileName(); } CVariantMap storedValues; storedValues.convertFromJson(json.object()); storedValues.insert(*it); json.setObject(storedValues.toJson()); - if (! (file.seek(0) && file.resize(0) && file.write(json.toJson()) > 0)) + readFile.close(); + CAtomicFile writeFile(readFile.fileName()); + if (! (writeFile.open(QFile::WriteOnly | QFile::Text) && writeFile.write(json.toJson()) > 0 && writeFile.checkedClose())) { - return CLogMessage(this).error("Failed to write to %1: %2") << file.fileName() << file.errorString(); + return CLogMessage(this).error("Failed to write to %1: %2") << writeFile.fileName() << writeFile.errorString(); } } return {};