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