OpenSWMM Engine  6.0.0-alpha.4
Data-oriented, plugin-extensible SWMM Engine (6.0.0-alpha.4)
Loading...
Searching...
No Matches
AtomicOutputFile.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
11
12#pragma once
13
14#include "FileIO.hpp"
15#include <atomic>
16#include <cerrno>
17#include <chrono>
18#include <system_error>
19#ifdef _WIN32
20#include <io.h>
21#else
22#include <unistd.h>
23#endif
24
25namespace openswmm::io {
26
27// One output only: checked serialization into an exclusively created sibling,
28// followed by atomic replacement. This is not a multi-file transaction.
37public:
38 // `binary` publishes exactly the bytes written; the default text mode lets
39 // Windows turn "\n" into "\r\n".
53 explicit AtomicOutputFile(const std::filesystem::path& destination, bool binary = false)
54 : destination_(destination) {
55 namespace fs = std::filesystem;
56 std::error_code ec;
57 // Preserve ordinary Save-through-symlink semantics. A dangling link
58 // is refused rather than silently replacing the link itself.
59 if (fs::is_symlink(destination_, ec)) {
60 const auto resolved = fs::canonical(destination_, ec);
61 if (ec) {
62 fail("resolve destination", ec);
63 return;
64 }
65 destination_ = resolved;
66 } else if (ec && ec != std::errc::no_such_file_or_directory) {
67 fail("inspect destination", ec);
68 return;
69 }
70 ec.clear();
71 const auto status = fs::status(destination_, ec);
72 const bool exists = fs::exists(status);
73 if (ec && ec != std::errc::no_such_file_or_directory) {
74 fail("inspect destination", ec);
75 return;
76 }
77 if (exists && !fs::is_regular_file(status)) {
78 fail("replace a non-regular destination",
79 std::make_error_code(std::errc::invalid_argument));
80 return;
81 }
82 const auto writable =
83 fs::perms::owner_write | fs::perms::group_write | fs::perms::others_write;
84 if (exists && (status.permissions() & writable) == fs::perms::none) {
85 fail("write a read-only destination",
86 std::make_error_code(std::errc::permission_denied));
87 return;
88 }
89 static std::atomic<unsigned long long> sequence{0};
90 const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
91 for (int attempt = 0; attempt < 32; ++attempt) {
92 const auto candidate =
93 destination_.parent_path() /
94 (".openswmm-save-" + std::to_string(stamp) + "-" + std::to_string(sequence++));
95 file_ = fopen_path(candidate, binary ? "wbx" : "wx");
96 if (file_) {
97 staging_ = candidate;
98 break;
99 }
100 if (errno != EEXIST)
101 break;
102 }
103 if (!file_) {
104 fail("create temporary output", systemError());
105 return;
106 }
107 if (exists) {
108 fs::permissions(staging_, status.permissions(), ec);
109 if (ec) {
110 fail("preserve destination permissions", ec);
111 closeAndDiscard();
112 }
113 }
114 }
115
122 ~AtomicOutputFile() { closeAndDiscard(); }
131
139 FILE* stream() const { return file_; }
145 const std::string& error() const { return error_; }
146
157 bool commit() {
158 if (!file_)
159 return false;
160 bool ok = std::ferror(file_) == 0;
161 if (!ok)
162 fail("write output", systemError());
163 if (std::fflush(file_) != 0) {
164 if (ok)
165 fail("flush output", systemError());
166 ok = false;
167 }
168 if (ok) {
169#ifdef _WIN32
170 const int sync = ::_commit(::_fileno(file_));
171#else
172 int sync;
173 do {
174 sync = ::fsync(::fileno(file_));
175 } while (sync != 0 && errno == EINTR);
176#endif
177 if (sync != 0) {
178 fail("sync output", systemError());
179 ok = false;
180 }
181 }
182 if (std::fclose(file_) != 0) {
183 if (ok)
184 fail("close output", systemError());
185 ok = false;
186 }
187 file_ = nullptr;
188 if (!ok)
189 return false;
190 std::error_code ec;
191 std::filesystem::rename(staging_, destination_, ec);
192 if (ec) {
193 fail("publish output", ec);
194 return false;
195 }
196 staging_.clear();
197 return true;
198 }
199
200private:
207 static std::error_code systemError() {
208 return errno ? std::error_code(errno, std::generic_category())
209 : std::make_error_code(std::errc::io_error);
210 }
220 void fail(const char* operation, const std::error_code& ec) {
221 error_ = "Cannot " + std::string(operation) + " '" + path_utf8(destination_) +
222 "': " + ec.message();
223 }
231 void closeAndDiscard() {
232 if (file_) {
233 std::fclose(file_);
234 file_ = nullptr;
235 }
236 if (!staging_.empty()) {
237 std::error_code ec;
238 std::filesystem::remove(staging_, ec);
239 }
240 }
241 std::filesystem::path destination_, staging_;
242 FILE* file_ = nullptr;
243 std::string error_;
244};
245} // namespace openswmm::io
Opening files by UTF-8 path on every platform.
AtomicOutputFile & operator=(const AtomicOutputFile &)=delete
Disallow copy replacement of a pending file publication.
FILE * stream() const
Borrow the writable staging stream.
Definition AtomicOutputFile.hpp:139
AtomicOutputFile(const std::filesystem::path &destination, bool binary=false)
Create an exclusive staging sibling for a single output destination.
Definition AtomicOutputFile.hpp:53
~AtomicOutputFile()
Close any open staging stream and best-effort discard unpublished output.
Definition AtomicOutputFile.hpp:122
AtomicOutputFile(const AtomicOutputFile &)=delete
Disallow copying ownership of the staging stream and publication state.
const std::string & error() const
Borrow the latest stored file-operation diagnostic.
Definition AtomicOutputFile.hpp:145
bool commit()
Flush, sync, close and attempt to rename staging over the destination.
Definition AtomicOutputFile.hpp:157
Definition AtomicOutputFile.hpp:25
std::string path_utf8(const std::filesystem::path &p, bool generic=false)
Copy a native path into the engine byte-string representation.
Definition FileIO.hpp:99
std::FILE * fopen_path(const std::filesystem::path &p, const char *mode)
Open a borrowed native path using platform C stdio.
Definition FileIO.hpp:135