/* LLFIO error handling (C) 2018 Niall Douglas (24 commits) File Created: June 2018 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License in the accompanying file Licence.txt or at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Distributed under the Boost Software License, Version 1.0. (See accompanying file Licence.txt or copy at http://www.boost.org/LICENSE_1_0.txt) */ #ifndef LLFIO_STATUS_CODE_HPP #define LLFIO_STATUS_CODE_HPP #include "logging.hpp" /* The SG14 status code implementation is quite profoundly different to the error code implementation. In the error code implementation, std::error_code is fixed by the standard library, so we wrap it with extra metadata into an error_info type. error_info then constructs off a code and a code domain tag. Status code, on the other hand, is templated and is designed for custom domains which can set arbitrary payloads. So we define custom domains and status codes for LLFIO with these combinations: - win32_error{ DWORD } - ntkernel_error{ LONG } - posix_error{ int } - generic_error{ errc } Each of these is a separate LLFIO custom status code domain. We also define an erased form of these custom domains, and that is typedefed to file_io_error_domain::value_type. This design ensure that LLFIO can be configured into either std-based error handling or SG14 experimental status code handling. It defaults to the latter as that (a) enables safe header only LLFIO on Windows (b) produces better codegen (c) drags in far fewer STL headers. */ #if LLFIO_EXPERIMENTAL_STATUS_CODE // Bring in a result implementation based on status_code #include "outcome/include/outcome/experimental/status_result.hpp" #include "outcome/include/outcome/try.hpp" LLFIO_V2_NAMESPACE_BEGIN #ifndef LLFIO_DISABLE_PATHS_IN_FAILURE_INFO namespace detail { template struct file_io_error_value_type { //! \brief The type of code T sc{}; // The id of the thread where this failure occurred uint32_t _thread_id{0}; // The TLS path store entry uint16_t _tls_path_id1{static_cast(-1)}, _tls_path_id2{static_cast(-1)}; // The id of the relevant log entry in the LLFIO log (if logging enabled) size_t _log_id{static_cast(-1)}; //! Default construction file_io_error_value_type() = default; //! Implicitly constructs an instance constexpr inline file_io_error_value_type(T _sc) : sc(_sc) { } // NOLINT //! Compares to a T constexpr bool operator==(const T &b) const noexcept { return sc == b; } }; } template class file_io_error_domain; LLFIO_V2_NAMESPACE_END // Inject a mixin for our custom status codes SYSTEM_ERROR2_NAMESPACE_BEGIN namespace mixins { template struct mixin> : public Base { using Base::Base; //! Retrieve the paths associated with this failure std::pair _paths() const noexcept { if(QUICKCPPLIB_NAMESPACE::utils::thread::this_thread_id() == this->value()._thread_id) { auto &tls = ::LLFIO_V2_NAMESPACE::detail::tls_errored_results(); const char *path1 = tls.get(this->value()._tls_path_id1); const char *path2 = tls.get(this->value()._tls_path_id2); return {path1, path2}; } return {}; } //! Retrieve the first path associated with this failure ::LLFIO_V2_NAMESPACE::filesystem::path path1() const { if(QUICKCPPLIB_NAMESPACE::utils::thread::this_thread_id() == this->value()._thread_id) { auto &tls = ::LLFIO_V2_NAMESPACE::detail::tls_errored_results(); const char *path1 = tls.get(this->value()._tls_path_id1); if(path1 != nullptr) { return ::LLFIO_V2_NAMESPACE::filesystem::path(path1); } } return {}; } //! Retrieve the second path associated with this failure ::LLFIO_V2_NAMESPACE::filesystem::path path2() const { if(QUICKCPPLIB_NAMESPACE::utils::thread::this_thread_id() == this->value()._thread_id) { auto &tls = ::LLFIO_V2_NAMESPACE::detail::tls_errored_results(); const char *path2 = tls.get(this->value()._tls_path_id2); if(path2 != nullptr) { return ::LLFIO_V2_NAMESPACE::filesystem::path(path2); } } return {}; } }; } SYSTEM_ERROR2_NAMESPACE_END LLFIO_V2_NAMESPACE_BEGIN /*! \class file_io_error_domain \brief The SG14 status code domain for errors in LLFIO. */ template class file_io_error_domain : public BaseStatusCodeDomain { friend class SYSTEM_ERROR2_NAMESPACE::status_code; using _base = BaseStatusCodeDomain; public: using string_ref = typename BaseStatusCodeDomain::string_ref; using atomic_refcounted_string_ref = typename BaseStatusCodeDomain::atomic_refcounted_string_ref; //! \brief The value type of errors in LLFIO using value_type = detail::file_io_error_value_type; file_io_error_domain() = default; file_io_error_domain(const file_io_error_domain &) = default; file_io_error_domain(file_io_error_domain &&) = default; file_io_error_domain &operator=(const file_io_error_domain &) = default; file_io_error_domain &operator=(file_io_error_domain &&) = default; ~file_io_error_domain() = default; protected: virtual inline string_ref _do_message(const SYSTEM_ERROR2_NAMESPACE::status_code &code) const noexcept override final { assert(code.domain() == *this); const auto &v = static_cast &>(code); // NOLINT // Get the paths for this failure, if any, using the mixins from above auto paths = v._paths(); // Get the base message for this failure auto msg = _base::_do_message(code); if(paths.first == nullptr && paths.second == nullptr) { return msg; } std::string ret; try { ret = msg.c_str(); if(paths.first != nullptr) { ret.append(" [path1 = "); ret.append(paths.first); if(paths.second != nullptr) { ret.append(", path2 = "); ret.append(paths.second); } ret.append("]"); } #if LLFIO_LOGGING_LEVEL >= 2 if(v.value()._log_id != static_cast(-1)) { if(log().valid(v.value()._log_id)) { ret.append(" [location = "); ret.append(location(log()[v.value()._log_id])); ret.append("]"); } } #endif } catch(...) { return string_ref("Failed to retrieve message for status code"); } char *p = (char *) malloc(ret.size() + 1); if(p == nullptr) { return string_ref("Failed to allocate memory to store error string"); } memcpy(p, ret.c_str(), ret.size() + 1); return atomic_refcounted_string_ref(p, ret.size()); } }; #else // LLFIO_DISABLE_PATHS_IN_FAILURE_INFO template using file_io_error_domain = BaseStatusCodeDomain; #endif // LLFIO_DISABLE_PATHS_IN_FAILURE_INFO namespace detail { using file_io_error_domain_value_system_code = file_io_error_value_type; } //! An erased status code using file_io_error = SYSTEM_ERROR2_NAMESPACE::errored_status_code>; template using result = OUTCOME_V2_NAMESPACE::experimental::erased_result; using OUTCOME_V2_NAMESPACE::success; using OUTCOME_V2_NAMESPACE::failure; using OUTCOME_V2_NAMESPACE::in_place_type; //! Choose an errc implementation using SYSTEM_ERROR2_NAMESPACE::errc; //! Helper for constructing an error code from an errc inline file_io_error generic_error(errc c); #ifndef _WIN32 //! Helper for constructing an error code from a POSIX errno inline file_io_error posix_error(int c = errno); #else //! Helper for constructing an error code from a DWORD inline file_io_error win32_error(SYSTEM_ERROR2_NAMESPACE::win32::DWORD c = SYSTEM_ERROR2_NAMESPACE::win32::GetLastError()); //! Helper for constructing an error code from a NTSTATUS inline file_io_error ntkernel_error(SYSTEM_ERROR2_NAMESPACE::win32::NTSTATUS c); #endif namespace detail { inline std::ostream &operator<<(std::ostream &s, const file_io_error &v) { return s << "llfio::file_io_error(" << v.message().c_str() << ")"; } } inline file_io_error error_from_exception(std::exception_ptr &&ep = std::current_exception(), file_io_error not_matched = generic_error(errc::resource_unavailable_try_again)) noexcept { if(!ep) { return generic_error(errc::success); } try { std::rethrow_exception(ep); } catch(const std::invalid_argument & /*unused*/) { ep = std::exception_ptr(); return generic_error(errc::invalid_argument); } catch(const std::domain_error & /*unused*/) { ep = std::exception_ptr(); return generic_error(errc::argument_out_of_domain); } catch(const std::length_error & /*unused*/) { ep = std::exception_ptr(); return generic_error(errc::argument_list_too_long); } catch(const std::out_of_range & /*unused*/) { ep = std::exception_ptr(); return generic_error(errc::result_out_of_range); } catch(const std::logic_error & /*unused*/) /* base class for this group */ { ep = std::exception_ptr(); return generic_error(errc::invalid_argument); } catch(const std::system_error &e) /* also catches ios::failure */ { ep = std::exception_ptr(); if(e.code().category() == std::generic_category()) { return generic_error(static_cast(static_cast(e.code().value()))); } // Don't know this error code category, so fall through } catch(const std::overflow_error & /*unused*/) { ep = std::exception_ptr(); return generic_error(errc::value_too_large); } catch(const std::range_error & /*unused*/) { ep = std::exception_ptr(); return generic_error(errc::result_out_of_range); } catch(const std::runtime_error & /*unused*/) /* base class for this group */ { ep = std::exception_ptr(); return generic_error(errc::resource_unavailable_try_again); } catch(const std::bad_alloc & /*unused*/) { ep = std::exception_ptr(); return generic_error(errc::not_enough_memory); } catch(...) { } return not_matched; } LLFIO_V2_NAMESPACE_END #else // LLFIO_EXPERIMENTAL_STATUS_CODE // Bring in a result implementation based on std::error_code #include "outcome/include/outcome.hpp" LLFIO_V2_NAMESPACE_BEGIN namespace detail { template inline void fill_failure_info(Dest &dest, const Src &src); template inline void append_path_info(Src &src, std::string &ret); } struct error_info; inline std::error_code make_error_code(error_info ei); /*! \struct error_info \brief The cause of the failure of an operation in LLFIO. */ struct error_info { friend inline std::error_code make_error_code(error_info ei); template friend inline void detail::append_path_info(Src &src, std::string &ret); template friend inline void detail::fill_failure_info(Dest &dest, const Src &src); private: // The error code for the failure std::error_code ec; #ifndef LLFIO_DISABLE_PATHS_IN_FAILURE_INFO // The id of the thread where this failure occurred uint32_t _thread_id{0}; // The TLS path store entry uint16_t _tls_path_id1{static_cast(-1)}, _tls_path_id2{static_cast(-1)}; // The id of the relevant log entry in the LLFIO log (if logging enabled) size_t _log_id{static_cast(-1)}; public: #endif //! Default constructor error_info() = default; // Explicit construction from an error code explicit inline error_info(std::error_code _ec); // NOLINT /* NOTE TO SELF: The error_info constructor implementation is in handle.hpp as we need that defined before we can do useful logging. */ //! Implicit construct from an error condition enum OUTCOME_TEMPLATE(class ErrorCondEnum) OUTCOME_TREQUIRES(OUTCOME_TPRED(std::is_error_condition_enum::value)) error_info(ErrorCondEnum &&v) // NOLINT : error_info(make_error_code(std::forward(v))) { } //! Retrieve the value of the error code int value() const noexcept { return ec.value(); } //! Retrieve any first path associated with this failure. Note this only works if called from the same thread as where the failure occurred. inline filesystem::path path1() const { #ifndef LLFIO_DISABLE_PATHS_IN_FAILURE_INFO if(QUICKCPPLIB_NAMESPACE::utils::thread::this_thread_id() == _thread_id) { auto &tls = detail::tls_errored_results(); const char *path1 = tls.get(_tls_path_id1); if(path1 != nullptr) { return filesystem::path(path1); } } #endif return {}; } //! Retrieve any second path associated with this failure. Note this only works if called from the same thread as where the failure occurred. inline filesystem::path path2() const { #ifndef LLFIO_DISABLE_PATHS_IN_FAILURE_INFO if(QUICKCPPLIB_NAMESPACE::utils::thread::this_thread_id() == _thread_id) { auto &tls = detail::tls_errored_results(); const char *path2 = tls.get(_tls_path_id2); if(path2 != nullptr) { return filesystem::path(path2); } } #endif return {}; } //! Retrieve a descriptive message for this failure, possibly with paths and stack backtraces. Extra detail only appears if called from the same thread as where the failure occurred. inline std::string message() const { std::string ret(ec.message()); #ifndef LLFIO_DISABLE_PATHS_IN_FAILURE_INFO detail::append_path_info(*this, ret); #endif return ret; } /*! Throw this failure as a C++ exception. Firstly if the error code matches any of the standard C++ exception types e.g. `bad_alloc`, we throw those types using the string from `message()` where possible. We then will throw an `error` exception type. */ inline void throw_exception() const; }; inline bool operator==(const error_info &a, const error_info &b) { return make_error_code(a) == make_error_code(b); } inline bool operator!=(const error_info &a, const error_info &b) { return make_error_code(a) != make_error_code(b); } OUTCOME_TEMPLATE(class ErrorCondEnum) OUTCOME_TREQUIRES(OUTCOME_TPRED(std::is_error_condition_enum::value)) inline bool operator==(const error_info &a, const ErrorCondEnum &b) { auto _a = make_error_code(a); auto _b = std::error_condition(b); #ifndef _WIN32 // Looks like libstdc++ doesn't map system category to generic category, which is a bug if(_a.category() == std::system_category() && _b.category() == std::generic_category() && _a.value() == static_cast(b)) return true; #endif return _a == _b; } OUTCOME_TEMPLATE(class ErrorCondEnum) OUTCOME_TREQUIRES(OUTCOME_TPRED(std::is_error_condition_enum::value)) inline bool operator==(const ErrorCondEnum &a, const error_info &b) { auto _a = std::error_condition(a); auto _b = make_error_code(b); #ifndef _WIN32 // Looks like libstdc++ doesn't map system category to generic category, which is a bug if(_a.category() == std::generic_category() && _b.category() == std::system_category() && _b.value() == static_cast(a)) return true; #endif return _a == _b; } OUTCOME_TEMPLATE(class ErrorCondEnum) OUTCOME_TREQUIRES(OUTCOME_TPRED(std::is_error_condition_enum::value)) inline bool operator!=(const error_info &a, const ErrorCondEnum &b) { auto _a = make_error_code(a); auto _b = std::error_condition(b); #ifndef _WIN32 // Looks like libstdc++ doesn't map system category to generic category, which is a bug if(_a.category() == std::system_category() && _b.category() == std::generic_category() && _a.value() == static_cast(b)) return false; #endif return _a != _b; } OUTCOME_TEMPLATE(class ErrorCondEnum) OUTCOME_TREQUIRES(OUTCOME_TPRED(std::is_error_condition_enum::value)) inline bool operator!=(const ErrorCondEnum &a, const error_info &b) { auto _a = std::error_condition(a); auto _b = make_error_code(b); #ifndef _WIN32 // Looks like libstdc++ doesn't map system category to generic category, which is a bug if(_a.category() == std::generic_category() && _b.category() == std::system_category() && _b.value() == static_cast(a)) return false; #endif return _a != _b; } #ifndef NDEBUG // Is trivial in all ways, except default constructibility static_assert(std::is_trivially_copyable::value, "error_info is not a trivially copyable!"); #endif inline std::ostream &operator<<(std::ostream &s, const error_info &v) { if(make_error_code(v)) { return s << "llfio::error_info(" << v.message() << ")"; } return s << "llfio::error_info(null)"; } // Tell Outcome that error_info is to be treated as an error_code inline std::error_code make_error_code(error_info ei) { return ei.ec; } // Tell Outcome to call error_info::throw_exception() on no-value observation inline void outcome_throw_as_system_error_with_payload(const error_info &ei) { ei.throw_exception(); } /*! \class error \brief The exception type synthesised and thrown when an `llfio::result` or `llfio::outcome` is no-value observed. */ class error : public filesystem::filesystem_error { public: error_info ei; //! Constructs from an error_info explicit error(error_info _ei) : filesystem::filesystem_error(_ei.message(), _ei.path1(), _ei.path2(), make_error_code(_ei)) , ei(_ei) { } }; inline void error_info::throw_exception() const { std::string msg; try { msg = message(); } catch(...) { } OUTCOME_V2_NAMESPACE::try_throw_std_exception_from_error(ec, msg); throw error(*this); } template using result = OUTCOME_V2_NAMESPACE::result; template using outcome = OUTCOME_V2_NAMESPACE::outcome; using OUTCOME_V2_NAMESPACE::success; using OUTCOME_V2_NAMESPACE::failure; inline error_info error_from_exception(std::exception_ptr &&ep = std::current_exception(), std::error_code not_matched = std::make_error_code(std::errc::resource_unavailable_try_again)) noexcept { return error_info(OUTCOME_V2_NAMESPACE::error_from_exception(std::move(ep), not_matched)); } using OUTCOME_V2_NAMESPACE::in_place_type; static_assert(OUTCOME_V2_NAMESPACE::trait::has_error_code_v, "error_info is not detected to be an error code"); //! Choose an errc implementation using std::errc; //! Helper for constructing an error info from an errc inline error_info generic_error(errc c) { return error_info(make_error_code(c)); } #ifndef _WIN32 //! Helper for constructing an error info from a POSIX errno inline error_info posix_error(int c = errno) { return error_info(std::error_code(c, std::system_category())); } #endif LLFIO_V2_NAMESPACE_END #endif // LLFIO_EXPERIMENTAL_STATUS_CODE #endif