The Battle for Wesnoth  1.17.0-dev
fs_commit.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2016 - 2021
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"
19 #include "serialization/parser.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 
45 static 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 
52 namespace filesystem
53 {
54 
55 namespace
56 {
57 namespace biostreams = boost::iostreams;
58 
59 // These types correspond to what's used by filesystem::ostream_file() in
60 // filesystem.cpp.
61 
62 using sink_type = biostreams::file_descriptor_sink;
63 using stream_type = biostreams::stream<sink_type>;
64 using platform_file_handle_type = sink_type::handle_type;
65 
66 const platform_file_handle_type INVALID_FILE_HANDLE =
67 #ifndef _WIN32
68  0
69 #else
70  INVALID_HANDLE_VALUE
71 #endif
72  ;
73 
74 inline 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 << '\n';
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  */
88 platform_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  */
103 filesystem::scoped_ostream ostream_file_with_delete(const std::string& fname)
104 {
105  LOG_FS << "streaming " << fname << " for writing with delete access.\n";
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  */
146 bool 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\n";
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() << '\n';
175  return false;
176  }
177 
178  return true;
179 }
180 
181 #endif // !defined(_WIN32)
182 
183 } // unnamed namespace
184 
185 atomic_commit::atomic_commit(const std::string& filename)
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_ << '\n';
197 }
198 
200 {
201  if(!temp_name_.empty()) {
202  ERR_FS << "Temporary file for atomic write leaked: " << temp_name_ << '\n';
203  }
204 }
205 
207 {
208  if(temp_name_.empty()) {
209  ERR_FS << "Attempted to commit " << dest_name_ << " more than once!\n";
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_ << '\n';
228 
229  temp_name_.clear();
230 }
231 
232 } // namespace filesystem
#define LOG_FS
Definition: fs_commit.cpp:48
atomic_commit(const std::string &filename)
Constructor.
Definition: fs_commit.cpp:185
ucs4_convert_impl::enableif< TD, typename TS::value_type >::type unicode_cast(const TS &source)
filesystem::scoped_ostream ostream_file(const std::string &fname, std::ios_base::openmode mode, bool create_directory)
std::ostringstream wrapper.
Definition: formatter.hpp:39
void commit()
Commits the new file contents to disk atomically.
Definition: fs_commit.cpp:206
std::string path
Definition: game_config.cpp:39
std::unique_ptr< std::ostream > scoped_ostream
Definition: filesystem.hpp:40
static lg::log_domain log_filesystem("filesystem")
Atomic filesystem commit functions.
An exception object used when an IO error occurs.
Definition: filesystem.hpp:48
#define ERR_FS
Definition: fs_commit.cpp:50
Standard logging facilities (interface).
#define e