Merge bitcoin/bitcoin#29307: util: explicitly close all AutoFiles that have been written
c10e382d2aflatfile: check whether the file has been closed successfully (Vasil Dimov)4bb5dd78eautil: check that a file has been closed before ~AutoFile() is called (Vasil Dimov)8bb34f07dfExplicitly close all AutoFiles that have been written (Vasil Dimov)a69c4098b2rpc: take ownership of the file by WriteUTXOSnapshot() (Hodlinator) Pull request description: `fclose(3)` may fail to flush the previously written data to disk, thus a failing `fclose(3)` is as serious as a failing `fwrite(3)`. Previously the code ignored `fclose(3)` failures. This PR improves that by changing all users of `AutoFile` that use it to write data to explicitly close the file and handle a possible error. --- Other alternatives are: 1. `fflush(3)` after each write to the file (and throw if it fails from the `AutoFile::write()` method) and hope that `fclose(3)` will then always succeed. Assert that it succeeds from the destructor 🙄. Will hurt performance. 2. Throw nevertheless from the destructor. Exception within the exception in C++ I think results in terminating the program without a useful message. 3. (this is implemented in the latest incarnation of this PR) Redesign `AutoFile` so that its destructor cannot fail. Adjust _all_ its users 😭. For example, if the file has been written to, then require the callers to explicitly call the `AutoFile::fclose()` method before the object goes out of scope. In the destructor, as a sanity check, assume/assert that this is indeed the case. Defeats the purpose of a RAII wrapper for `FILE*` which automatically closes the file when it goes out of scope and there are a lot of users of `AutoFile`. 4. Pass a new callback function to the `AutoFile` constructor which will be called from the destructor to handle `fclose()` errors, as described in https://github.com/bitcoin/bitcoin/pull/29307#issuecomment-2243842400. My thinking is that if that callback is going to only log a message, then we can log the message directly from the destructor without needing a callback. If the callback is going to do more complicated error handling then it is easier to do that at the call site by directly calling `AutoFile::fclose()` instead of getting the `AutoFile` object out of scope (so that its destructor is called) and inspecting for side effects done by the callback (e.g. set a variable to indicate a failed `fclose()`). ACKs for top commit: l0rinc: ACKc10e382d2aachow101: ACKc10e382d2ahodlinator: re-ACKc10e382d2aTree-SHA512: 3994ca57e5b2b649fc84f24dad144173b7500fc0e914e06291d5c32fbbf8d2b1f8eae0040abd7a5f16095ddf4e11fe1636c6092f49058cda34f3eb2ee536d7ba
This commit is contained in:
20 files changed
+195
-54
No files matched your search
+29
-3
@@ -6,10 +6,13 @@
|
||||
#ifndef BITCOIN_STREAMS_H
|
||||
#define BITCOIN_STREAMS_H
|
||||
|
||||
#include <logging.h>
|
||||
#include <serialize.h>
|
||||
#include <span.h>
|
||||
#include <support/allocators/zeroafterfree.h>
|
||||
#include <util/check.h>
|
||||
#include <util/overflow.h>
|
||||
#include <util/syserror.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
@@ -387,7 +390,13 @@ public:
|
||||
*
|
||||
* Will automatically close the file when it goes out of scope if not null.
|
||||
* If you're returning the file pointer, return file.release().
|
||||
* If you need to close the file early, use file.fclose() instead of fclose(file).
|
||||
* If you need to close the file early, use autofile.fclose() instead of fclose(underlying_FILE).
|
||||
*
|
||||
* @note If the file has been written to, then the caller must close it
|
||||
* explicitly with the `fclose()` method, check if it returns an error and treat
|
||||
* such an error as if the `write()` method failed. The OS's `fclose(3)` may
|
||||
* fail to flush to disk data that has been previously written, rendering the
|
||||
* file corrupt.
|
||||
*/
|
||||
class AutoFile
|
||||
{
|
||||
@@ -395,11 +404,28 @@ protected:
|
||||
std::FILE* m_file;
|
||||
std::vector<std::byte> m_xor;
|
||||
std::optional<int64_t> m_position;
|
||||
bool m_was_written{false};
|
||||
|
||||
public:
|
||||
explicit AutoFile(std::FILE* file, std::vector<std::byte> data_xor={});
|
||||
|
||||
~AutoFile() { fclose(); }
|
||||
~AutoFile()
|
||||
{
|
||||
if (m_was_written) {
|
||||
// Callers that wrote to the file must have closed it explicitly
|
||||
// with the fclose() method and checked that the close succeeded.
|
||||
// This is because here in the destructor we have no way to signal
|
||||
// errors from fclose() which, after write, could mean the file is
|
||||
// corrupted and must be handled properly at the call site.
|
||||
// Destructors in C++ cannot signal an error to the callers because
|
||||
// they do not return a value and are not allowed to throw exceptions.
|
||||
Assume(IsNull());
|
||||
}
|
||||
|
||||
if (fclose() != 0) {
|
||||
LogError("Failed to close file: %s", SysErrorString(errno));
|
||||
}
|
||||
}
|
||||
|
||||
// Disallow copies
|
||||
AutoFile(const AutoFile&) = delete;
|
||||
@@ -407,7 +433,7 @@ public:
|
||||
|
||||
bool feof() const { return std::feof(m_file); }
|
||||
|
||||
int fclose()
|
||||
[[nodiscard]] int fclose()
|
||||
{
|
||||
if (auto rel{release()}) return std::fclose(rel);
|
||||
return 0;
|
||||
|
||||
Reference in new issue
Block a user