The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
fs_commit.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2016 - 2025
3 by Iris Morelle <shadowm2006@gmail.com>
4 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY.
12
13 See the COPYING file for more details.
14*/
15
17
18#include "log.hpp"
20
21#include <cerrno>
22#include <cstdio>
23#include <cstring>
24
25#include <boost/iostreams/device/file_descriptor.hpp>
26#include <boost/iostreams/stream.hpp>
27
28#ifndef _WIN32
29
30#include <unistd.h>
31
32#else
33
34#include "formatter.hpp"
36
37#include <boost/system/error_code.hpp>
38#include <boost/filesystem.hpp>
39
40#define WIN32_LEAN_AND_MEAN
41#include <windows.h>
42
43#endif
44
45static lg::log_domain log_filesystem("filesystem");
46
47#define DBG_FS LOG_STREAM(debug, log_filesystem)
48#define LOG_FS LOG_STREAM(info, log_filesystem)
49#define WRN_FS LOG_STREAM(warn, log_filesystem)
50#define ERR_FS LOG_STREAM(err, log_filesystem)
51
52namespace filesystem
53{
54
55namespace
56{
57namespace biostreams = boost::iostreams;
58
59// These types correspond to what's used by filesystem::ostream_file() in
60// filesystem.cpp.
61
62using sink_type = biostreams::file_descriptor_sink;
63using stream_type = biostreams::stream<sink_type>;
64using platform_file_handle_type = sink_type::handle_type;
65
66const platform_file_handle_type INVALID_FILE_HANDLE =
67#ifndef _WIN32
68 0
69#else
70 INVALID_HANDLE_VALUE
71#endif
72 ;
73
74inline void atomic_fail(const std::string& step_description)
75{
76 const std::string errno_desc = std::strerror(errno);
77 ERR_FS << "Atomic commit failed (" << step_description << "): " << errno_desc;
78 throw filesystem::io_exception(std::string("Atomic commit failed (") + step_description + ")");
79}
80
81/**
82 * Returns the real file descriptor/handle associated with the stream.
83 *
84 * This only makes sense for valid streams returned by ostream_file(). Anything
85 * else will yield an invalid value (e.g. 0 for POSIX, INVALID_HANDLE_VALUE for
86 * Windows).
87 */
88platform_file_handle_type get_stream_file_descriptor(std::ostream& os)
89{
90 stream_type* const real = dynamic_cast<stream_type*>(&os);
91 return real ? (*real)->handle() : INVALID_FILE_HANDLE;
92}
93
94#ifdef _WIN32
95
96/**
97 * Opens the specified file with FILE_SHARE_DELETE access.
98 *
99 * This is a drop-in replacement for filesystem::ostream_file. The special
100 * access is required on Windows to rename or delete the file while we hold
101 * handles to it.
102 */
103filesystem::scoped_ostream ostream_file_with_delete(const std::string& fname)
104{
105 LOG_FS << "streaming " << fname << " for writing with delete access.";
106
107 namespace bfs = boost::filesystem;
108 const auto& w_name = unicode_cast<std::wstring>(fname);
109
110 try {
111 HANDLE file = CreateFileW(w_name.c_str(),
112 GENERIC_WRITE | DELETE,
113 FILE_SHARE_WRITE | FILE_SHARE_DELETE,
114 nullptr,
115 CREATE_ALWAYS,
116 FILE_ATTRIBUTE_NORMAL,
117 nullptr);
118
119 if(file == INVALID_HANDLE_VALUE) {
120 throw BOOST_IOSTREAMS_FAILURE(formatter() << "CreateFile() failed: " << GetLastError());
121 }
122
123 // Transfer ownership to the sink post-haste
124 sink_type fd{file, biostreams::close_handle};
125 return std::make_unique<stream_type>(fd, 4096, 0);
126 } catch(const BOOST_IOSTREAMS_FAILURE& e) {
127 // Create directories if needed and try again
128 boost::system::error_code ec_unused;
129 if(bfs::create_directories(bfs::path{fname}.parent_path(), ec_unused)) {
130 return ostream_file_with_delete(fname);
131 }
132 // Creating directories was impossible, give up
133 throw filesystem::io_exception(e.what());
134 }
135}
136
137/**
138 * Renames an open file, potentially overwriting another (closed) existing file.
139 *
140 * @param new_name New path for the open file.
141 * @param open_handle Handle for the open file.
142 *
143 * @return @a true on success, @a false on failure. Passing an invalid handle
144 * will always result in failure.
145 */
146bool rename_open_file(const std::string& new_name, HANDLE open_handle)
147{
148 if(open_handle == INVALID_HANDLE_VALUE) {
149 ERR_FS << "replace_open_file(): Bad handle";
150 return false;
151 }
152
153 const auto& w_name = unicode_cast<std::wstring>(new_name);
154 const std::size_t buf_size = w_name.length()*sizeof(wchar_t) + sizeof(FILE_RENAME_INFO);
155
156 // Avert your eyes, children
157
158 std::unique_ptr<BYTE[]> fileinfo_buf{new BYTE[buf_size]};
159 FILE_RENAME_INFO& fri = *reinterpret_cast<FILE_RENAME_INFO*>(fileinfo_buf.get());
160
161 SecureZeroMemory(fileinfo_buf.get(), buf_size);
162 fri.ReplaceIfExists = TRUE;
163 fri.RootDirectory = nullptr;
164 fri.FileNameLength = static_cast<DWORD>(w_name.length());
165 ::wmemcpy(fri.FileName, w_name.c_str(), w_name.length());
166
167 // Okay, back to our regular programming
168
169 if(!SetFileInformationByHandle(open_handle,
170 FileRenameInfo,
171 fileinfo_buf.get(),
172 static_cast<DWORD>(buf_size)))
173 {
174 ERR_FS << "replace_open_file(): SetFileInformationByHandle() " << GetLastError();
175 return false;
176 }
177
178 return true;
179}
180
181#endif // !defined(_WIN32)
182
183} // unnamed namespace
184
186 : temp_name_(filename + ".new")
187 , dest_name_(filename)
188#ifndef _WIN32
189 , out_(filesystem::ostream_file(temp_name_))
190 , outfd_(filesystem::get_stream_file_descriptor(*out_))
191#else
192 , out_(filesystem::ostream_file_with_delete(temp_name_))
193 , handle_(filesystem::get_stream_file_descriptor(*out_))
194#endif
195{
196 LOG_FS << "Atomic write guard created for " << dest_name_ << " using " << temp_name_;
197}
198
200{
201 if(!temp_name_.empty()) {
202 ERR_FS << "Temporary file for atomic write leaked: " << temp_name_;
203 }
204}
205
207{
208 if(temp_name_.empty()) {
209 ERR_FS << "Attempted to commit " << dest_name_ << " more than once!";
210 return;
211 }
212
213#ifdef _WIN32
214 if(!rename_open_file(dest_name_, handle_)) {
215 atomic_fail("rename");
216 }
217#else
218 if(fsync(outfd_) != 0) {
219 atomic_fail("fsync");
220 }
221
222 if(std::rename(temp_name_.c_str(), dest_name_.c_str()) != 0) {
223 atomic_fail("rename");
224 }
225#endif
226
227 LOG_FS << "Atomic commit succeeded: " << temp_name_ << " -> " << dest_name_;
228
229 temp_name_.clear();
230}
231
232} // namespace filesystem
#define ERR_FS
Definition manager.cpp:35
void commit()
Commits the new file contents to disk atomically.
atomic_commit(const std::string &filename)
Constructor.
std::ostringstream wrapper.
Definition formatter.hpp:40
#define LOG_FS
static lg::log_domain log_filesystem("filesystem")
Atomic filesystem commit functions.
Standard logging facilities (interface).
filesystem::scoped_ostream ostream_file(const std::string &fname, std::ios_base::openmode mode=std::ios_base::binary, bool create_directory=true)
std::unique_ptr< std::ostream > scoped_ostream
std::string filename
Filename.
An exception object used when an IO error occurs.
#define e