diff --git a/CMakeLists.txt b/CMakeLists.txt index 75ae599cc3c..effdfff2e8b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -163,7 +163,7 @@ else() set(LOGROTATE_CREATE "\n\tcreate 644 ${ICINGA2_USER} ${ICINGA2_GROUP}") endif() -find_package(Boost ${BOOST_MIN_VERSION} COMPONENTS coroutine context date_time filesystem thread system program_options regex REQUIRED) +find_package(Boost ${BOOST_MIN_VERSION} COMPONENTS coroutine context date_time filesystem iostreams thread system program_options regex REQUIRED) # Boost.Coroutine2 (the successor of Boost.Coroutine) # (1) doesn't even exist in old Boost versions and diff --git a/lib/base/CMakeLists.txt b/lib/base/CMakeLists.txt index 28747279db1..f872a69bef7 100644 --- a/lib/base/CMakeLists.txt +++ b/lib/base/CMakeLists.txt @@ -16,6 +16,7 @@ set(base_SOURCES application.cpp application.hpp application-ti.hpp application-version.cpp application-environment.cpp array.cpp array.hpp array-script.cpp atomic.hpp + atomic-file.cpp atomic-file.hpp base64.cpp base64.hpp boolean.cpp boolean.hpp boolean-script.cpp bulker.hpp diff --git a/lib/base/atomic-file.cpp b/lib/base/atomic-file.cpp new file mode 100644 index 00000000000..361a3d24c6a --- /dev/null +++ b/lib/base/atomic-file.cpp @@ -0,0 +1,116 @@ +/* Icinga 2 | (c) 2022 Icinga GmbH | GPLv2+ */ + +#include "base/atomic-file.hpp" +#include "base/exception.hpp" +#include "base/utility.hpp" +#include + +#ifdef _WIN32 +# include +# include +#else /* _WIN32 */ +# include +# include +#endif /* _WIN32 */ + +using namespace icinga; + +AtomicFile::AtomicFile(String path, int mode) : m_Path(std::move(path)) +{ + m_TempFilename = m_Path + ".tmp.XXXXXX"; + +#ifdef _WIN32 + m_Fd = Utility::MksTemp(&m_TempFilename[0]); +#else /* _WIN32 */ + m_Fd = mkstemp(&m_TempFilename[0]); +#endif /* _WIN32 */ + + if (m_Fd < 0) { + auto error (errno); + + BOOST_THROW_EXCEPTION(posix_error() + << boost::errinfo_api_function("mkstemp") + << boost::errinfo_errno(error) + << boost::errinfo_file_name(m_TempFilename)); + } + + try { + exceptions(failbit | badbit); + + open(boost::iostreams::file_descriptor( + m_Fd, + // Rationale: https://github.com/boostorg/iostreams/issues/152 + boost::iostreams::never_close_handle + )); + + if (chmod(m_TempFilename.CStr(), mode) < 0) { + auto error (errno); + + BOOST_THROW_EXCEPTION(posix_error() + << boost::errinfo_api_function("chmod") + << boost::errinfo_errno(error) + << boost::errinfo_file_name(m_TempFilename)); + } + } catch (...) { + if (is_open()) { + close(); + } + + (void)::close(m_Fd); + (void)unlink(m_TempFilename.CStr()); + throw; + } +} + +AtomicFile::~AtomicFile() +{ + if (is_open()) { + try { + close(); + } catch (...) { + // Destructor must not throw + } + } + + if (m_Fd >= 0) { + (void)::close(m_Fd); + } + + if (!m_TempFilename.IsEmpty()) { + (void)unlink(m_TempFilename.CStr()); + } +} + +void AtomicFile::Commit() +{ + flush(); + + auto h ((*this)->handle()); + +#ifdef _WIN32 + if (!FlushFileBuffers(h)) { + auto err (GetLastError()); + + BOOST_THROW_EXCEPTION(win32_error() + << boost::errinfo_api_function("FlushFileBuffers") + << errinfo_win32_error(err) + << boost::errinfo_file_name(m_TempFilename)); + } +#else /* _WIN32 */ + if (fsync(h)) { + auto err (errno); + + BOOST_THROW_EXCEPTION(posix_error() + << boost::errinfo_api_function("fsync") + << boost::errinfo_errno(err) + << boost::errinfo_file_name(m_TempFilename)); + } +#endif /* _WIN32 */ + + close(); + (void)::close(m_Fd); + m_Fd = -1; + + Utility::RenameFile(m_TempFilename, m_Path); + m_TempFilename = ""; +} diff --git a/lib/base/atomic-file.hpp b/lib/base/atomic-file.hpp new file mode 100644 index 00000000000..5ad79d914a1 --- /dev/null +++ b/lib/base/atomic-file.hpp @@ -0,0 +1,34 @@ +/* Icinga 2 | (c) 2022 Icinga GmbH | GPLv2+ */ + +#ifndef ATOMIC_FILE_H +#define ATOMIC_FILE_H + +#include "base/string.hpp" +#include +#include + +namespace icinga +{ + +/** + * Atomically replaces a file's content. + * + * @ingroup base + */ +class AtomicFile : public boost::iostreams::stream +{ +public: + AtomicFile(String path, int mode); + ~AtomicFile(); + + void Commit(); + +private: + String m_Path; + String m_TempFilename; + int m_Fd; +}; + +} + +#endif /* ATOMIC_FILE_H */ diff --git a/lib/base/configobject.cpp b/lib/base/configobject.cpp index d4592b4ef3a..144afa4a985 100644 --- a/lib/base/configobject.cpp +++ b/lib/base/configobject.cpp @@ -1,5 +1,6 @@ /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ +#include "base/atomic-file.hpp" #include "base/configobject.hpp" #include "base/configobject-ti.cpp" #include "base/configtype.hpp" @@ -468,13 +469,7 @@ void ConfigObject::DumpObjects(const String& filename, int attributeTypes) Log(LogWarning, "ConfigObject") << DiagnosticInformation(ex); } - std::fstream fp; - String tempFilename = Utility::CreateTempFile(filename + ".tmp.XXXXXX", 0600, fp); - fp.exceptions(std::ofstream::failbit | std::ofstream::badbit); - - if (!fp) - BOOST_THROW_EXCEPTION(std::runtime_error("Could not open '" + tempFilename + "' file")); - + AtomicFile fp (filename, 0600); StdioStream::Ptr sfp = new StdioStream(&fp, false); for (const Type::Ptr& type : Type::GetAllTypes()) { @@ -502,10 +497,7 @@ void ConfigObject::DumpObjects(const String& filename, int attributeTypes) } sfp->Close(); - - fp.close(); - - Utility::RenameFile(tempFilename, filename); + fp.Commit(); } void ConfigObject::RestoreObject(const String& message, int attributeTypes) diff --git a/lib/base/exception.cpp b/lib/base/exception.cpp index a194dafd590..57b324b4a7c 100644 --- a/lib/base/exception.cpp +++ b/lib/base/exception.cpp @@ -180,6 +180,13 @@ String icinga::DiagnosticInformation(const std::exception& ex, bool verbose, boo String message = ex.what(); +#ifdef _WIN32 + const auto *win32_err = dynamic_cast(&ex); + if (win32_err) { + message = to_string(*win32_err); + } +#endif /* _WIN32 */ + const auto *vex = dynamic_cast(&ex); if (message.IsEmpty()) @@ -424,6 +431,39 @@ std::string icinga::to_string(const StackTraceErrorInfo&) } #ifdef _WIN32 +const char *win32_error::what() const noexcept +{ + return "win32_error"; +} + +std::string icinga::to_string(const win32_error &e) { + std::ostringstream msgbuf; + + const char * const *func = boost::get_error_info(e); + + if (func) { + msgbuf << "Function call '" << *func << "'"; + } else { + msgbuf << "Function call"; + } + + const std::string *fname = boost::get_error_info(e); + + if (fname) { + msgbuf << " for file '" << *fname << "'"; + } + + msgbuf << " failed"; + + const int *errnum = boost::get_error_info(e); + + if (errnum) { + msgbuf << " with error code " << Utility::FormatErrorNumber(*errnum); + } + + return msgbuf.str(); +} + std::string icinga::to_string(const errinfo_win32_error& e) { return "[errinfo_win32_error] = " + Utility::FormatErrorNumber(e.value()) + "\n"; diff --git a/lib/base/exception.hpp b/lib/base/exception.hpp index c18bd7abed6..18dab650e6e 100644 --- a/lib/base/exception.hpp +++ b/lib/base/exception.hpp @@ -127,7 +127,12 @@ class posix_error : virtual public std::exception, virtual public boost::excepti }; #ifdef _WIN32 -class win32_error : virtual public std::exception, virtual public boost::exception { }; +class win32_error : virtual public std::exception, virtual public boost::exception { +public: + const char *what() const noexcept override; +}; + +std::string to_string(const win32_error& e); struct errinfo_win32_error_; typedef boost::error_info errinfo_win32_error; diff --git a/lib/base/utility.hpp b/lib/base/utility.hpp index 9ddb82dc4f8..6f9d03d447e 100644 --- a/lib/base/utility.hpp +++ b/lib/base/utility.hpp @@ -134,6 +134,10 @@ class Utility static String CreateTempFile(const String& path, int mode, std::fstream& fp); +#ifdef _WIN32 + static int MksTemp(char *tmpl); +#endif /* _WIN32 */ + #ifdef _WIN32 static String GetIcingaInstallPath(); static String GetIcingaDataPath(); @@ -185,10 +189,6 @@ class Utility private: Utility(); -#ifdef _WIN32 - static int MksTemp (char *tmpl); -#endif /* _WIN32 */ - #ifdef I2_DEBUG static double m_DebugTime; #endif /* I2_DEBUG */