Merge lp:~widelands-dev/widelands/robust-file-saving into lp:widelands
- robust-file-saving
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 8987 |
Proposed branch: | lp:~widelands-dev/widelands/robust-file-saving |
Merge into: | lp:widelands |
Diff against target: |
1381 lines (+790/-261) 13 files modified
src/editor/CMakeLists.txt (+1/-0) src/editor/ui_menus/main_menu_save_map.cc (+37/-37) src/logic/CMakeLists.txt (+14/-1) src/logic/filesystem_constants.h (+5/-0) src/logic/generic_save_handler.cc (+308/-0) src/logic/generic_save_handler.h (+151/-0) src/logic/save_handler.cc (+113/-134) src/logic/save_handler.h (+8/-6) src/wlapplication.cc (+69/-29) src/wlapplication.h (+2/-2) src/wui/CMakeLists.txt (+1/-0) src/wui/game_main_menu_save_game.cc (+79/-52) src/wui/game_main_menu_save_game.h (+2/-0) |
To merge this branch: | bzr merge lp:~widelands-dev/widelands/robust-file-saving |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
GunChleoc | Approve | ||
Klaus Halfmann | Needs Resubmitting | ||
Review via email: mp+358718@code.launchpad.net |
Commit message
Introduces a new class to robustly handle saving of files (incl. dealing with backups, handling errors, etc.) and use it whenever maps/games are being saved.
Description of the change
bunnybot (widelandsofficial) wrote : | # |
GunChleoc (gunchleoc) wrote : | # |
I have added 2 comments. I'll do another review round when you have looked at them.
Arty (artydent) wrote : | # |
I admit the bitmask stuff is still my default way from before C++11, without having to re-define operators or typecast all the time. But you are right, I should do this properly with enum class now. I'll do that.
As for the code duplication between the game saving and map saving, I feel that this doesn't really belong into the GenericSaveHandler. I wanted to keep it on a separate abstraction level, in particular without relying on any UI stuff or other user interaction. (I am not even sure I should have put the generation of localized messages in there.)
The duplication part you pointed out isn't even that big (and half of it just comments), but there is overall a lot of code duplication between the map saving UI and the game saving UI. I think we should rather try to merge those into a single class. There are of course a few key differences between handling games and maps there, but those could possibly be dealt with via overloading or templates or even just via some parameters (including using lambda functions). Same goes for the loading UI and possibly a few others. I wouldn't mind looking into that, but that would be a somewhat bigger change (and require some more consideration how feasible this even is), so that would rather be something for the build21.
Arty (artydent) wrote : | # |
Switched to a type safe error type.
bunnybot (widelandsofficial) wrote : | # |
Continuous integration builds have changed state:
Travis build 4242. State: failed. Details: https:/
Appveyor build 4037. State: failed. Details: https:/
Arty (artydent) wrote : | # |
Oops, forgot to enable codecheck again. Will be fixed in the next round.
bunnybot (widelandsofficial) wrote : | # |
Continuous integration builds have changed state:
Travis build 4287. State: failed. Details: https:/
Appveyor build 4081. State: success. Details: https:/
GunChleoc (gunchleoc) wrote : | # |
Error from Travis log:
/home/travis/
uint32_t GenericSaveHand
bunnybot (widelandsofficial) wrote : | # |
Continuous integration builds have changed state:
Travis build 4296. State: passed. Details: https:/
Appveyor build 4089. State: success. Details: https:/
Klaus Halfmann (klaus-halfmann) wrote : | # |
Hello artydent,
I checked out this code an attached a debugger,
I will try to gain some coverage an this way understand it.
Gun: shall this become part of R20 or is this is "only" an improvemnet?
Artydent: Im am using OSX, anything special that I should look for?
Klaus Halfmann (klaus-halfmann) wrote : | # |
OK, I did some tests with normal saves and some ways to break it.
Experience tells me that Windows is more picky about such errors
(as it locks open files). I was unable to reproduce all the errors.
But anway, basic Saving is ok and nothing is broken.
The code looks ok for me so it can go in.
Gun: Do we have somone on Widows to do some more debugging?
I could spend some more time and try to break mores tuff :-)
Arty (artydent) wrote : | # |
Merry Christmas, and thanks for testing.
As for "ways to break it", there *should* at least be no new ways to do so. After all, the main purpose of this branch is to prevent any such breaking when we save maps/games. (And there aren't a lot of new things that could break on their own.) The actual saving routine itself could potentially break somewhere (I didn't change anything there), but as long as that involves throwing an exception, it will be caught.
Under Windows I tested this quite a lot myself. Usually, errors should "normally" not happen without someone trying to force them. Error scenarios that could be forced for testing would be:
1) after choosing a dir and filename, replace the dir by a non-dir file of that name, then try saving -> dir creation fails
2) lock a file (e.g. open it with a zip packer or so that locks the file), then try saving to that file -> renaming (for backup) fails
3) make a file read-only, then try saving to that file -> deleting backup (renamed file) fails (no ingame message though)
4) try saving on a read-only medium -> saving data fails
(There might be slight differences between platforms, especially with the file locking.)
Other errors would actually be hard to set off on purpose. That would likely require different processes to access the same file at the same time. Hard to even force, and extremely unlikely to happen by accident, but such errors are caught just in case. (In my testing for those scenarios I just temporarily added exceptions throws at some places.)
Klaus Halfmann (klaus-halfmann) wrote : | # |
I think should get this in now,
I dont think it will break anything.
@bunnybot merge
bunnybot (widelandsofficial) wrote : | # |
Error merging this proposal:
Output:
stdout:
stderr:
+N src/logic/
+N src/logic/
M src/editor/
M src/editor/
M src/logic/
M src/logic/
M src/logic/
M src/logic/
M src/wlapplicati
M src/wlapplication.h
M src/wui/
M src/wui/
M src/wui/
Text conflict in src/wlapplicati
1 conflicts encountered.
Klaus Halfmann (klaus-halfmann) wrote : | # |
Hello Arty I try to fix these conflicts, but maybe I will need you help?
Klaus Halfmann (klaus-halfmann) wrote : | # |
Arty can, you take another look? Maybe my merge broke it ....
lets try to compile this at leasst
Arty (artydent) wrote : | # |
Sorry, I am very busy lately and was not able to do anything Widelands-related or even look here at all. This will likely continue for quite some time, but I'll have a look at it this weekend.
GunChleoc (gunchleoc) wrote : | # |
I have had a look at the diff for the trunk merge and the resolution of the merge conflict looks fine.
Klaus Halfmann (klaus-halfmann) wrote : | # |
Uhm, why dont we get another build?
I foundd soum compile warnigns about the new JSON Objects not having virtual DTors.
but that is some other story.
GunChleoc (gunchleoc) wrote : | # |
Bunnybot will only post here if there is a status change to prevent spamming the discussion. You'll need to visit Travis & AppVeyor manually when there is no change.
bunnybot (widelandsofficial) wrote : | # |
Continuous integration builds have changed state:
Travis build 4498. State: errored. Details: https:/
Appveyor build 4285. State: success. Details: https:/
GunChleoc (gunchleoc) wrote : | # |
Klaus & I have tested this on Linux, Mac and Windows now. All working as expected. Thanks for the great fix!
@bunnybot merge
bunnybot (widelandsofficial) wrote : | # |
Refusing to merge, since Travis is not green. Use @bunnybot merge force for merging anyways.
Travis build 4498. State: errored. Details: https:/
GunChleoc (gunchleoc) wrote : | # |
@bunnybot merge force
Preview Diff
1 | === modified file 'src/editor/CMakeLists.txt' |
2 | --- src/editor/CMakeLists.txt 2018-07-19 16:43:14 +0000 |
3 | +++ src/editor/CMakeLists.txt 2019-02-20 09:56:48 +0000 |
4 | @@ -99,6 +99,7 @@ |
5 | logic |
6 | logic_constants |
7 | logic_filesystem_constants |
8 | + logic_generic_save_handler |
9 | logic_tribe_basic_info |
10 | logic_widelands_geometry |
11 | map_io |
12 | |
13 | === modified file 'src/editor/ui_menus/main_menu_save_map.cc' |
14 | --- src/editor/ui_menus/main_menu_save_map.cc 2018-11-23 17:10:25 +0000 |
15 | +++ src/editor/ui_menus/main_menu_save_map.cc 2019-02-20 09:56:48 +0000 |
16 | @@ -37,6 +37,7 @@ |
17 | #include "io/filesystem/layered_filesystem.h" |
18 | #include "io/filesystem/zip_filesystem.h" |
19 | #include "logic/filesystem_constants.h" |
20 | +#include "logic/generic_save_handler.h" |
21 | #include "map_io/map_saver.h" |
22 | #include "map_io/widelands_map_loader.h" |
23 | #include "ui_basic/messagebox.h" |
24 | @@ -119,7 +120,9 @@ |
25 | * Called when the ok button was pressed or a file in list was double clicked. |
26 | */ |
27 | void MainMenuSaveMap::clicked_ok() { |
28 | - assert(ok_.enabled()); |
29 | + if (!ok_.enabled()) { |
30 | + return; |
31 | + } |
32 | std::string filename = editbox_->text(); |
33 | std::string complete_filename; |
34 | |
35 | @@ -269,15 +272,12 @@ |
36 | |
37 | /** |
38 | * Save the map in the current directory with |
39 | - * the current filename |
40 | + * the given filename |
41 | * |
42 | * returns true if dialog should close, false if it |
43 | * should stay open |
44 | */ |
45 | bool MainMenuSaveMap::save_map(std::string filename, bool binary) { |
46 | - // Make sure that the current directory exists and is writeable. |
47 | - g_fs->ensure_directory_exists(curdir_); |
48 | - |
49 | // Trim it for preceding/trailing whitespaces in user input |
50 | boost::trim(filename); |
51 | |
52 | @@ -301,22 +301,6 @@ |
53 | } |
54 | } |
55 | |
56 | - // Try deleting file (if it exists). If it fails, give a message and let the player choose a new |
57 | - // name. |
58 | - try { |
59 | - g_fs->fs_unlink(complete_filename); |
60 | - } catch (const std::exception& e) { |
61 | - log("Unable to delete old map file %s while saving map: %s\n", complete_filename.c_str(), |
62 | - e.what()); |
63 | - const std::string s = (boost::format(_("File ‘%s’ could not be deleted.")) % |
64 | - FileSystem::fs_filename(filename.c_str())) |
65 | - .str() + |
66 | - " " + _("Try saving under a different name!"); |
67 | - UI::WLMessageBox mbox(&eia(), _("Error Saving Map!"), s, UI::WLMessageBox::MBoxType::kOk); |
68 | - mbox.run<UI::Panel::Returncodes>(); |
69 | - return false; |
70 | - } |
71 | - |
72 | Widelands::EditorGameBase& egbase = eia().egbase(); |
73 | Widelands::Map* map = egbase.mutable_map(); |
74 | |
75 | @@ -334,21 +318,37 @@ |
76 | map->delete_tag("artifacts"); |
77 | } |
78 | |
79 | - // Try saving. |
80 | - try { |
81 | - std::unique_ptr<FileSystem> fs(g_fs->create_sub_file_system( |
82 | - complete_filename, binary ? FileSystem::ZIP : FileSystem::DIR)); |
83 | - std::unique_ptr<Widelands::MapSaver> wms(new Widelands::MapSaver(*fs, egbase)); |
84 | - wms->save(); |
85 | - fs.reset(); |
86 | - } catch (const std::exception& e) { |
87 | - std::string s = _("Error Saving Map!\nSaved map file may be corrupt!\n\nReason " |
88 | - "given:\n"); |
89 | - s += e.what(); |
90 | - UI::WLMessageBox mbox(&eia(), _("Error Saving Map!"), s, UI::WLMessageBox::MBoxType::kOk); |
91 | - mbox.run<UI::Panel::Returncodes>(); |
92 | - } |
93 | - |
94 | - die(); |
95 | + // Try saving the map. |
96 | + GenericSaveHandler gsh( |
97 | + [&egbase](FileSystem& fs) { |
98 | + Widelands::MapSaver wms(fs, egbase); |
99 | + wms.save(); |
100 | + }, |
101 | + complete_filename, |
102 | + binary ? FileSystem::ZIP : FileSystem::DIR |
103 | + ); |
104 | + GenericSaveHandler::Error error = gsh.save(); |
105 | + |
106 | + // If only the temporary backup couldn't be deleted, we still treat it as |
107 | + // success. Automatic cleanup will deal with later. No need to bother the |
108 | + // player with it. |
109 | + if (error == GenericSaveHandler::Error::kSuccess || |
110 | + error == GenericSaveHandler::Error::kDeletingBackupFailed) { |
111 | + egbase.get_ibase()->log_message(_("Map saved")); |
112 | + return true; |
113 | + } |
114 | + |
115 | + std::string msg = gsh.localized_formatted_result_message(); |
116 | + UI::WLMessageBox mbox( |
117 | + this, _("Error Saving Map!"), msg, UI::WLMessageBox::MBoxType::kOk); |
118 | + mbox.run<UI::Panel::Returncodes>(); |
119 | + |
120 | + // If only the backup failed (likely just because of a file lock), |
121 | + // then leave the dialog open for the player to try with a new filename. |
122 | + if (error == GenericSaveHandler::Error::kBackupFailed) { |
123 | + return false; |
124 | + } |
125 | + |
126 | + // In the other error cases close the dialog. |
127 | return true; |
128 | } |
129 | |
130 | === modified file 'src/logic/CMakeLists.txt' |
131 | --- src/logic/CMakeLists.txt 2018-08-24 07:12:20 +0000 |
132 | +++ src/logic/CMakeLists.txt 2019-02-20 09:56:48 +0000 |
133 | @@ -7,7 +7,6 @@ |
134 | base_i18n |
135 | ) |
136 | |
137 | - |
138 | wl_library(logic_widelands_geometry |
139 | SRCS |
140 | widelands_geometry.cc |
141 | @@ -81,6 +80,7 @@ |
142 | filesystem_constants.h |
143 | filesystem_constants.cc |
144 | ) |
145 | + |
146 | wl_library(logic_tribe_basic_info |
147 | SRCS |
148 | map_objects/tribes/tribe_basic_info.cc |
149 | @@ -93,6 +93,18 @@ |
150 | |
151 | ) |
152 | |
153 | +wl_library(logic_generic_save_handler |
154 | + SRCS |
155 | + generic_save_handler.h |
156 | + generic_save_handler.cc |
157 | + DEPENDS |
158 | + base_i18n |
159 | + base_log |
160 | + base_time_string |
161 | + io_filesystem |
162 | + logic_filesystem_constants |
163 | +) |
164 | + |
165 | wl_library(logic |
166 | SRCS |
167 | ai_dna_handler.cc |
168 | @@ -281,6 +293,7 @@ |
169 | logic_filesystem_constants |
170 | logic_game_controller |
171 | logic_game_settings |
172 | + logic_generic_save_handler |
173 | logic_tribe_basic_info |
174 | logic_widelands_geometry |
175 | map_io |
176 | |
177 | === modified file 'src/logic/filesystem_constants.h' |
178 | --- src/logic/filesystem_constants.h 2019-01-16 22:22:41 +0000 |
179 | +++ src/logic/filesystem_constants.h 2019-02-20 09:56:48 +0000 |
180 | @@ -45,6 +45,11 @@ |
181 | // We delete (accidentally remaining) temp files older than a week |
182 | constexpr double kTempFilesKeepAroundTime = 7 * 24 * 60 * 60; |
183 | |
184 | +/// Filesystem names for for temporary backup when overwriting files during saving |
185 | +const std::string kTempBackupExtension = ".tmp"; |
186 | +// We delete (accidentally remaining) temp backup files older than a day |
187 | +constexpr double kTempBackupsKeepAroundTime = 24 * 60 * 60; |
188 | + |
189 | /// Filesystem names and timeouts for replays |
190 | const std::string kReplayDir = "replays"; |
191 | const std::string kReplayExtension = ".wrpl"; |
192 | |
193 | === added file 'src/logic/generic_save_handler.cc' |
194 | --- src/logic/generic_save_handler.cc 1970-01-01 00:00:00 +0000 |
195 | +++ src/logic/generic_save_handler.cc 2019-02-20 09:56:48 +0000 |
196 | @@ -0,0 +1,308 @@ |
197 | +/* |
198 | + * Copyright (C) 2002-2018 by the Widelands Development Team |
199 | + * |
200 | + * This program is free software; you can redistribute it and/or |
201 | + * modify it under the terms of the GNU General Public License |
202 | + * as published by the Free Software Foundation; either version 2 |
203 | + * of the License, or (at your option) any later version. |
204 | + * |
205 | + * This program is distributed in the hope that it will be useful, |
206 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
207 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
208 | + * GNU General Public License for more details. |
209 | + * |
210 | + * You should have received a copy of the GNU General Public License |
211 | + * along with this program; if not, write to the Free Software |
212 | + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
213 | + * |
214 | + */ |
215 | + |
216 | +#include "logic/generic_save_handler.h" |
217 | + |
218 | +#include <memory> |
219 | +#include <string> |
220 | + |
221 | +#include <boost/format.hpp> |
222 | + |
223 | +#include "base/i18n.h" |
224 | +#include "base/log.h" |
225 | +#include "base/time_string.h" |
226 | +#include "io/filesystem/filesystem.h" |
227 | +#include "io/filesystem/filesystem_exceptions.h" |
228 | +#include "io/filesystem/layered_filesystem.h" |
229 | +#include "logic/filesystem_constants.h" |
230 | + |
231 | +void GenericSaveHandler::clear() { |
232 | + error_ = Error::kNone; |
233 | + for (uint32_t index = 0; index < maxErrors_; index++) { |
234 | + error_msg_[index].clear(); |
235 | + } |
236 | + backup_filename_.clear(); |
237 | + return; |
238 | +} |
239 | + |
240 | +uint32_t GenericSaveHandler::get_index(GenericSaveHandler::Error err) { |
241 | + if (err == Error::kNone) { |
242 | + return maxErrors_; |
243 | + } |
244 | + uint32_t error_uint = static_cast<uint32_t>(err); |
245 | + for (uint32_t index = 0; index < maxErrors_; index++) { |
246 | + if (error_uint & 1) { |
247 | + return index; |
248 | + } |
249 | + error_uint >>= 1; |
250 | + } |
251 | + return maxErrors_; |
252 | +} |
253 | + |
254 | +void GenericSaveHandler::make_backup() { |
255 | + std::string backup_filename_base = |
256 | + dir_ + g_fs->file_separator() + timestring() + "_" + filename_; |
257 | + backup_filename_ = backup_filename_base + kTempBackupExtension; |
258 | + |
259 | + // If a file with that name already exists, then try some name modifications. |
260 | + if (g_fs->file_exists(backup_filename_)) |
261 | + { |
262 | + int suffix; |
263 | + for (suffix = 0; suffix <= 9; suffix++) |
264 | + { |
265 | + backup_filename_ = backup_filename_base + "-" + std::to_string(suffix) |
266 | + + kTempBackupExtension; |
267 | + if (!g_fs->file_exists(backup_filename_)) { |
268 | + break; |
269 | + } |
270 | + } |
271 | + if (suffix > 9) { |
272 | + error_ |= Error::kBackupFailed; |
273 | + uint32_t index = get_index(Error::kBackupFailed); |
274 | + error_msg_[index] = |
275 | + (boost::format("GenericSaveHandler::make_backup: %s: for all " |
276 | + "considered filenames a file already existed (last filename tried " |
277 | + "was %s)\n") % complete_filename_.c_str() % backup_filename_).str(); |
278 | + log("%s", error_msg_[index].c_str()); |
279 | + return; |
280 | + } |
281 | + } |
282 | + |
283 | + // Try to rename file. |
284 | + try { |
285 | + g_fs->fs_rename(complete_filename_, backup_filename_); |
286 | + } catch (const FileError& e) { |
287 | + error_ |= Error::kBackupFailed; |
288 | + uint32_t index = get_index(Error::kBackupFailed); |
289 | + error_msg_[index] = |
290 | + (boost::format("GenericSaveHandler::make_backup: file %s " |
291 | + "could not be renamed: %s\n") % complete_filename_.c_str() |
292 | + % backup_filename_).str(); |
293 | + log("%s", error_msg_[index].c_str()); |
294 | + return; |
295 | + } |
296 | + |
297 | + return; |
298 | +} |
299 | + |
300 | +void GenericSaveHandler::save_file() { |
301 | + // Write data to file/dir. |
302 | + try { |
303 | + std::unique_ptr<FileSystem> |
304 | + fs(g_fs->create_sub_file_system(complete_filename_, type_)); |
305 | + do_save_(*fs); |
306 | + } catch (const std::exception& e) { |
307 | + error_ |= Error::kSavingDataFailed; |
308 | + uint32_t index = get_index(Error::kSavingDataFailed); |
309 | + error_msg_[index] = |
310 | + (boost::format("GenericSaveHandler::save_file: data could not be " |
311 | + "written to file %s: %s\n") % complete_filename_.c_str() % e.what() |
312 | + ).str(); |
313 | + log("%s", error_msg_[index].c_str()); |
314 | + } |
315 | + |
316 | + if ((error_ & Error::kSavingDataFailed) != Error::kNone) { |
317 | + // Delete remnants of the failed save attempt. |
318 | + if (g_fs->file_exists(complete_filename_)) { |
319 | + try { |
320 | + g_fs->fs_unlink(complete_filename_); |
321 | + } catch (const FileError& e) { |
322 | + error_ |= Error::kCorruptFileLeft; |
323 | + uint32_t index = get_index(Error::kCorruptFileLeft); |
324 | + error_msg_[index] = |
325 | + (boost::format("GenericSaveHandler::save_file: possibly corrupt " |
326 | + "file %s could not be deleted: %s\n") % complete_filename_.c_str() |
327 | + % e.what()).str(); |
328 | + log("%s", error_msg_[index].c_str()); |
329 | + } |
330 | + } |
331 | + } |
332 | + return; |
333 | +} |
334 | + |
335 | +GenericSaveHandler::Error GenericSaveHandler::save() { |
336 | + try { // everything additionally in one big try block |
337 | + // to catch any unexpected errors |
338 | + clear(); |
339 | + |
340 | + // Make sure that the current directory exists and is writeable. |
341 | + try { |
342 | + g_fs->ensure_directory_exists(dir_); |
343 | + } catch (const FileError& e) { |
344 | + error_ |= Error::kCreatingDirFailed; |
345 | + uint32_t index = get_index(Error::kCreatingDirFailed); |
346 | + error_msg_[index] = |
347 | + (boost::format("GenericSaveHandler::save: directory %s could not be " |
348 | + "created: %s\n") % dir_.c_str() % e.what()).str(); |
349 | + log("%s", error_msg_[index].c_str()); |
350 | + return error_; |
351 | + } |
352 | + |
353 | + // Make a backup if file already exists. |
354 | + if (g_fs->file_exists(complete_filename_)) { |
355 | + make_backup(); |
356 | + } |
357 | + if (error_ != Error::kNone) { |
358 | + return error_; |
359 | + } |
360 | + |
361 | + // Write data to file/dir. |
362 | + save_file(); |
363 | + |
364 | + // Restore or delete backup if one was made. |
365 | + if (!backup_filename_.empty()) |
366 | + { |
367 | + if (error_ == Error::kNone) { |
368 | + // Delete backup. |
369 | + try { |
370 | + g_fs->fs_unlink(backup_filename_); |
371 | + } catch (const FileError& e) { |
372 | + error_ |= Error::kDeletingBackupFailed; |
373 | + uint32_t index = get_index(Error::kDeletingBackupFailed); |
374 | + error_msg_[index] = |
375 | + (boost::format("GenericSaveHandler::save: backup file %s could " |
376 | + "not be deleted: %s\n") % backup_filename_.c_str() % e.what() |
377 | + ).str(); |
378 | + log("%s", error_msg_[index].c_str()); |
379 | + } |
380 | + |
381 | + } else { |
382 | + if ((error_ & Error::kCorruptFileLeft) != Error::kNone) { |
383 | + error_ |= Error::kRestoringBackupFailed; |
384 | + uint32_t index = get_index(Error::kRestoringBackupFailed); |
385 | + error_msg_[index] = |
386 | + (boost::format("GenericSaveHandler::save: file %s could not be " |
387 | + "restored from backup %s: file still exists\n") |
388 | + % complete_filename_.c_str() % backup_filename_.c_str()).str(); |
389 | + log("%s", error_msg_[index].c_str()); |
390 | + } |
391 | + else { |
392 | + // Restore backup. |
393 | + try { |
394 | + g_fs->fs_rename(backup_filename_, complete_filename_); |
395 | + } catch (const FileError& e) { |
396 | + error_ |= Error::kRestoringBackupFailed; |
397 | + uint32_t index = get_index(Error::kRestoringBackupFailed); |
398 | + error_msg_[index] = |
399 | + (boost::format("GenericSaveHandler::save: file %s could not " |
400 | + "be restored from backup %s: %s\n") % backup_filename_.c_str() |
401 | + % backup_filename_.c_str() % e.what()).str(); |
402 | + log("%s", error_msg_[index].c_str()); |
403 | + } |
404 | + } |
405 | + } |
406 | + } |
407 | + |
408 | + } catch (const std::exception& e) { |
409 | + error_ |= Error::kUnexpectedError; |
410 | + uint32_t index = get_index(Error::kUnexpectedError); |
411 | + error_msg_[index] = |
412 | + (boost::format("GenericSaveHandler::save: unknown error: %s\n") |
413 | + % e.what()).str(); |
414 | + log("%s", error_msg_[index].c_str()); |
415 | + } |
416 | + |
417 | + return error_; |
418 | +} |
419 | + |
420 | +std::string GenericSaveHandler::error_message(GenericSaveHandler::Error error_mask) { |
421 | + uint32_t error_uint = |
422 | + static_cast<uint32_t>(error_mask) & static_cast<uint32_t>(error_); |
423 | + std::string err_msg; |
424 | + for (uint32_t index = 0; index < maxErrors_; index++) { |
425 | + if ((error_uint >> index) & 1) { |
426 | + err_msg += error_msg_[index]; |
427 | + } |
428 | + } |
429 | + return err_msg; |
430 | +} |
431 | + |
432 | +std::string GenericSaveHandler::localized_formatted_result_message() { |
433 | + std::string msg; |
434 | + |
435 | + if (error_ == Error::kSuccess) { |
436 | + return _("File successfully saved!"); |
437 | + } |
438 | + |
439 | + if (error_ == Error::kDeletingBackupFailed) { |
440 | + return std::string(_("File successfully saved!")) + "\n" + |
441 | + (boost::format(_("Backup file ‘%s’ could not be deleted."))).str(); |
442 | + } |
443 | + |
444 | + if (error_ == Error::kCreatingDirFailed) { |
445 | + return |
446 | + (boost::format(_("Directory ‘%s’ could not be created!")) |
447 | + % dir_).str(); |
448 | + } |
449 | + |
450 | + if (error_ == Error::kBackupFailed) { |
451 | + return |
452 | + (boost::format(_("File ‘%s’ could not be removed!")) |
453 | + % complete_filename_.c_str()).str() + "\n" |
454 | + + _("Try saving under a different name!"); |
455 | + } |
456 | + |
457 | + // from here on multiple errors might have occurred |
458 | + if ((error_ & Error::kSavingDataFailed) != Error::kNone) { |
459 | + msg = |
460 | + (boost::format(_("Error writing data to file ‘%s’!")) |
461 | + % complete_filename_.c_str()).str(); |
462 | + } |
463 | + |
464 | + if ((error_ & Error::kCorruptFileLeft) != Error::kNone) { |
465 | + if (!msg.empty()) { |
466 | + msg += "\n"; |
467 | + } |
468 | + msg += (boost::format(_("Saved file may be corrupt!"))).str(); |
469 | + } |
470 | + |
471 | + if ((error_ & Error::kRestoringBackupFailed) != Error::kNone) { |
472 | + if (!msg.empty()) { |
473 | + msg += "\n"; |
474 | + } |
475 | + msg += |
476 | + (boost::format(_("File ‘%s’ could not be restored!")) |
477 | + % complete_filename_.c_str()).str() + "\n" + |
478 | + (boost::format(_("Backup file ‘%s’ will be available for some time.")) |
479 | + % backup_filename_.c_str()).str(); |
480 | + } |
481 | + |
482 | + if (!backup_filename_.empty() && |
483 | + ((error_ & Error::kSavingDataFailed) != Error::kNone) && |
484 | + !((error_ & Error::kCorruptFileLeft) != Error::kNone) && |
485 | + !((error_ & Error::kRestoringBackupFailed) != Error::kNone)) { |
486 | + if (!msg.empty()) { |
487 | + msg += "\n"; |
488 | + } |
489 | + msg += |
490 | + (boost::format(_("File ‘%s’ was restored from backup.")) |
491 | + % complete_filename_.c_str()).str(); |
492 | + } |
493 | + |
494 | + if ((error_ & Error::kUnexpectedError) != Error::kNone) { |
495 | + if (!msg.empty()) { |
496 | + msg += "\n"; |
497 | + } |
498 | + msg += |
499 | + (boost::format(_("An unexpected error occurred:" ))).str() |
500 | + + "\n" + error_message(Error::kUnexpectedError); |
501 | + } |
502 | + |
503 | + return msg; |
504 | +} |
505 | |
506 | === added file 'src/logic/generic_save_handler.h' |
507 | --- src/logic/generic_save_handler.h 1970-01-01 00:00:00 +0000 |
508 | +++ src/logic/generic_save_handler.h 2019-02-20 09:56:48 +0000 |
509 | @@ -0,0 +1,151 @@ |
510 | +/* |
511 | + * Copyright (C) 2002-2018 by the Widelands Development Team |
512 | + * |
513 | + * This program is free software; you can redistribute it and/or |
514 | + * modify it under the terms of the GNU General Public License |
515 | + * as published by the Free Software Foundation; either version 2 |
516 | + * of the License, or (at your option) any later version. |
517 | + * |
518 | + * This program is distributed in the hope that it will be useful, |
519 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
520 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
521 | + * GNU General Public License for more details. |
522 | + * |
523 | + * You should have received a copy of the GNU General Public License |
524 | + * along with this program; if not, write to the Free Software |
525 | + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
526 | + * |
527 | + */ |
528 | + |
529 | +#ifndef WL_LOGIC_GENERIC_SAVE_HANDLER_H |
530 | +#define WL_LOGIC_GENERIC_SAVE_HANDLER_H |
531 | + |
532 | +#include <functional> |
533 | +#include <string> |
534 | + |
535 | +#include <stdint.h> |
536 | + |
537 | +#include "io/filesystem/filesystem.h" |
538 | + |
539 | +/** |
540 | + * Class that provides handling all errors for generic file saving. |
541 | + * It stores error codes and error messages. |
542 | + * It can also generate an overview message aimed at a human user. |
543 | + * |
544 | + * The saving routine (what actually writes data to a file system) |
545 | + * must be provided. |
546 | + */ |
547 | +// TODO(Arty): Possibly make it more generic, allowing to provide options |
548 | +// whether to overwrite files, whether to make backups, maybe allow |
549 | +// to provide a naming scheme, etc. |
550 | +class GenericSaveHandler { |
551 | +public: |
552 | + // error constants; also usable as bit masks |
553 | + enum class Error : uint32_t { |
554 | + kNone = 0, |
555 | + kSuccess = 0, |
556 | + kCreatingDirFailed = 1, |
557 | + kBackupFailed = 2, |
558 | + kDeletingBackupFailed = 4, |
559 | + kSavingDataFailed = 8, |
560 | + kCorruptFileLeft = 16, |
561 | + kRestoringBackupFailed = 32, |
562 | + kUnexpectedError = 64, |
563 | + kAllErrors = 127 |
564 | + }; |
565 | + |
566 | + explicit GenericSaveHandler( |
567 | + std::function<void(FileSystem&)> do_save, // function that actually saves data to the filesystem |
568 | + std::string complete_filename, |
569 | + FileSystem::Type type) |
570 | + : do_save_(do_save), |
571 | + complete_filename_(complete_filename), |
572 | + dir_(FileSystem::fs_dirname(complete_filename.c_str())), |
573 | + filename_(FileSystem::fs_filename(complete_filename.c_str())), |
574 | + type_(type), |
575 | +// error_(Error::kSuccess) {}; |
576 | + error_(static_cast<Error>(1132)) {}; |
577 | + |
578 | + /** |
579 | + * Tries to save a file. |
580 | + * |
581 | + * If the file already exists, it will be overwritten but a temporary backup |
582 | + * is made, which will be restored if saving fails and deleted otherwise. |
583 | + * |
584 | + * Catches ALL errors. |
585 | + * |
586 | + * Error messages for all errors are written to the log but also stored. |
587 | + * Stores and returns an error code (bit mask of all occurred errors). |
588 | + */ |
589 | + Error save(); |
590 | + |
591 | + // returns the stored error code (of the last saving operation) |
592 | + Error error() { return error_; }; |
593 | + |
594 | + // Returns the combination of error_messages (of occurred errors) |
595 | + // specified by a bit mask. |
596 | + std::string error_message(Error error_mask = Error::kAllErrors); |
597 | + |
598 | + // Generates a localized formatted message describing the result of |
599 | + // the last saving attempt. |
600 | + // Aimed to be sufficiently informative for a human user. |
601 | + std::string localized_formatted_result_message(); |
602 | + |
603 | +private: |
604 | + std::function<void(FileSystem&)> do_save_; |
605 | + std::string complete_filename_; |
606 | + std::string dir_; |
607 | + std::string filename_; |
608 | + FileSystem::Type type_; |
609 | + |
610 | + // Backup filename is automatically generated when saving but is also |
611 | + // stored for generating messages containing backup-related things. |
612 | + std::string backup_filename_; |
613 | + |
614 | + Error error_; |
615 | + |
616 | + static constexpr uint32_t maxErrors_ = 7; |
617 | + static_assert( |
618 | + (1ul << maxErrors_) == static_cast<uint32_t>(Error::kAllErrors) + 1, |
619 | + "value of maxErrors_ doesn't match!"); |
620 | + std::string error_msg_[maxErrors_]; |
621 | + |
622 | + // Returns the lowest array index of the an error. |
623 | + // Intended for use with single errors to get their array index. |
624 | + uint32_t get_index(Error); |
625 | + |
626 | + void clear(); |
627 | + |
628 | + // Finds a suitable backup filename and tries to rename a file. |
629 | + // Stores an errorcode and error message (if applicable). |
630 | + void make_backup(); |
631 | + |
632 | + // Saves a file. Assumes file doesn't exist yet. |
633 | + // Stores an errorcode and error message (if applicable). |
634 | + void save_file(); |
635 | +}; |
636 | + |
637 | + |
638 | +inline constexpr GenericSaveHandler::Error |
639 | +operator|(GenericSaveHandler::Error e1, GenericSaveHandler::Error e2) { |
640 | + return static_cast<GenericSaveHandler::Error> |
641 | + (static_cast<uint32_t>(e1) | static_cast<uint32_t>(e2)); |
642 | +} |
643 | + |
644 | +inline constexpr GenericSaveHandler::Error |
645 | +operator&(GenericSaveHandler::Error e1, GenericSaveHandler::Error e2) { |
646 | + return static_cast<GenericSaveHandler::Error> |
647 | + (static_cast<uint32_t>(e1) & static_cast<uint32_t>(e2)); |
648 | +} |
649 | + |
650 | +inline GenericSaveHandler::Error& |
651 | +operator|=(GenericSaveHandler::Error& e1, GenericSaveHandler::Error e2) { |
652 | + return e1 = e1 | e2; |
653 | +} |
654 | + |
655 | +inline GenericSaveHandler::Error& |
656 | +operator&=(GenericSaveHandler::Error& e1, GenericSaveHandler::Error e2) { |
657 | + return e1 = e1 & e2; |
658 | +} |
659 | + |
660 | +#endif // end of include guard: WL_LOGIC_GENERIC_SAVE_HANDLER_H |
661 | |
662 | === modified file 'src/logic/save_handler.cc' |
663 | --- src/logic/save_handler.cc 2018-08-26 18:22:27 +0000 |
664 | +++ src/logic/save_handler.cc 2019-02-20 09:56:48 +0000 |
665 | @@ -32,14 +32,13 @@ |
666 | #include "base/wexception.h" |
667 | #include "game_io/game_saver.h" |
668 | #include "io/filesystem/filesystem.h" |
669 | +#include "io/filesystem/filesystem_exceptions.h" |
670 | #include "logic/filesystem_constants.h" |
671 | #include "logic/game.h" |
672 | #include "logic/game_controller.h" |
673 | +#include "logic/generic_save_handler.h" |
674 | #include "wui/interactive_base.h" |
675 | |
676 | -// The actual work of saving is done by the GameSaver |
677 | -using Widelands::GameSaver; |
678 | - |
679 | SaveHandler::SaveHandler() |
680 | : next_save_realtime_(0), |
681 | initialized_(false), |
682 | @@ -52,40 +51,71 @@ |
683 | number_of_rolls_(5) { |
684 | } |
685 | |
686 | -void SaveHandler::roll_save_files(const std::string& filename) { |
687 | - |
688 | - int32_t rolls = number_of_rolls_; |
689 | - log("Autosave: Rolling savefiles (count): %d\n", rolls); |
690 | - rolls--; |
691 | - std::string filename_previous = |
692 | - create_file_name(kSaveDir, (boost::format("%s_%02d") % filename % rolls).str()); |
693 | - if (rolls > 0 && g_fs->file_exists(filename_previous)) { |
694 | - g_fs->fs_unlink(filename_previous); // Delete last of the rolling files |
695 | - log("Autosave: Deleted %s\n", filename_previous.c_str()); |
696 | - } |
697 | +bool SaveHandler::roll_save_files(const std::string& filename, |
698 | + std::string* const error) { |
699 | + int32_t rolls = 0; |
700 | + std::string filename_previous; |
701 | + |
702 | + // Only roll the smallest necessary number of files. |
703 | + while (rolls < number_of_rolls_) { |
704 | + filename_previous = create_file_name( |
705 | + kSaveDir, (boost::format("%s_%02d") % filename % rolls).str()); |
706 | + if (!g_fs->file_exists(filename_previous)) { |
707 | + break; |
708 | + } |
709 | + rolls++; |
710 | + } |
711 | + |
712 | + // If there is a file missing in the sequence; no need to delete any file. |
713 | + if (rolls < number_of_rolls_) { |
714 | + log("Autosave: Rolling savefiles (count): %d of %d\n", |
715 | + rolls, number_of_rolls_); |
716 | + } |
717 | + else { |
718 | + log("Autosave: Rolling savefiles (count): %d\n", rolls); |
719 | + rolls--; |
720 | + filename_previous = create_file_name( |
721 | + kSaveDir, (boost::format("%s_%02d") % filename % rolls).str()); |
722 | + if (rolls > 0) { |
723 | + try { |
724 | + g_fs->fs_unlink(filename_previous); // Delete last of the rolling files |
725 | + log("Autosave: Deleted %s\n", filename_previous.c_str()); |
726 | + } catch (const FileError& e) { |
727 | + log("Autosave: Unable to delete file %s: %s\n", |
728 | + filename_previous.c_str(), e.what()); |
729 | + if (error) { |
730 | + *error = |
731 | + (boost::format("Autosave: Unable to delete file %s: %s\n") |
732 | + % filename_previous.c_str() % e.what()).str(); |
733 | + } |
734 | + return false; |
735 | + } |
736 | + } |
737 | + } |
738 | + |
739 | rolls--; |
740 | while (rolls >= 0) { |
741 | - const std::string filename_next = |
742 | - create_file_name(kSaveDir, (boost::format("%s_%02d") % filename % rolls).str()); |
743 | - if (g_fs->file_exists(filename_next)) { |
744 | - try { |
745 | - g_fs->fs_rename( |
746 | - filename_next, filename_previous); // e.g. wl_autosave_08 -> wl_autosave_09 |
747 | - log("Autosave: Rolled %s to %s\n", filename_next.c_str(), filename_previous.c_str()); |
748 | - } catch (const std::exception& e) { |
749 | - log("Autosave: Unable to rename file %s to %s: %s\n", filename_previous.c_str(), |
750 | - filename_next.c_str(), e.what()); |
751 | - } |
752 | + const std::string filename_next = create_file_name( |
753 | + kSaveDir, (boost::format("%s_%02d") % filename % rolls).str()); |
754 | + try { |
755 | + g_fs->fs_rename(filename_next, filename_previous); // e.g. wl_autosave_08 -> wl_autosave_09 |
756 | + log("Autosave: Rolled %s to %s\n", |
757 | + filename_next.c_str(), filename_previous.c_str()); |
758 | + } catch (const FileError& e) { |
759 | + log("Autosave: Unable to roll file %s to %s: %s\n", |
760 | + filename_previous.c_str(), filename_next.c_str(), e.what()); |
761 | + return false; |
762 | } |
763 | filename_previous = filename_next; |
764 | rolls--; |
765 | } |
766 | + return true; |
767 | } |
768 | |
769 | /** |
770 | * Check if game should be saved at next tick / think. |
771 | * |
772 | - * @return true if game should be saved ad next think(). |
773 | + * @return true if game should be saved at next think(). |
774 | */ |
775 | bool SaveHandler::check_next_tick(Widelands::Game& game, uint32_t realtime) { |
776 | // Perhaps save is due now? |
777 | @@ -99,50 +129,14 @@ |
778 | } |
779 | |
780 | log("Autosave: %d ms interval elapsed, current gametime: %s, saving...\n", |
781 | - autosave_interval_in_ms_, gametimestring(game.get_gametime(), true).c_str()); |
782 | + autosave_interval_in_ms_, |
783 | + gametimestring(game.get_gametime(), true).c_str()); |
784 | |
785 | game.get_ibase()->log_message(_("Saving game…")); |
786 | return true; |
787 | } |
788 | |
789 | /** |
790 | - * If saving fails restore the backup file. |
791 | - * |
792 | - * @return true when save was a success. |
793 | - */ |
794 | -bool SaveHandler::save_and_handle_error(Widelands::Game& game, |
795 | - const std::string& complete_filename, |
796 | - const std::string& backup_filename) { |
797 | - std::string error; |
798 | - bool result = save_game(game, complete_filename, &error); |
799 | - if (!result) { |
800 | - log("Autosave: ERROR! - %s\n", error.c_str()); |
801 | - game.get_ibase()->log_message(_("Saving failed!")); |
802 | - |
803 | - // if backup file was created, move it back |
804 | - if (backup_filename.length() > 0) { |
805 | - if (g_fs->file_exists(complete_filename)) { |
806 | - g_fs->fs_unlink(complete_filename); |
807 | - } |
808 | - g_fs->fs_rename(backup_filename, complete_filename); |
809 | - } |
810 | - // Wait 30 seconds until next save try |
811 | - next_save_realtime_ = SDL_GetTicks() + 30000; |
812 | - } else { |
813 | - // if backup file was created, time to remove it |
814 | - if (backup_filename.length() > 0 && g_fs->file_exists(backup_filename)) { |
815 | - g_fs->fs_unlink(backup_filename); |
816 | - } |
817 | - |
818 | - // Count save interval from end of save. |
819 | - // This prevents us from going into endless autosave cycles if the save should take longer |
820 | - // than the autosave interval. |
821 | - next_save_realtime_ = SDL_GetTicks() + autosave_interval_in_ms_; |
822 | - } |
823 | - return result; |
824 | -} |
825 | - |
826 | -/** |
827 | * Check if autosave is needed and allowed or save was requested by user. |
828 | */ |
829 | void SaveHandler::think(Widelands::Game& game) { |
830 | @@ -155,6 +149,9 @@ |
831 | |
832 | // Are we saving now? |
833 | if (saving_next_tick_ || save_requested_) { |
834 | + saving_next_tick_ = false; |
835 | + bool save_success = true; |
836 | + std::string error; |
837 | std::string filename = autosave_filename_; |
838 | if (save_requested_) { |
839 | // Requested by user |
840 | @@ -166,59 +163,34 @@ |
841 | save_filename_ = ""; |
842 | } else { |
843 | // Autosave ... |
844 | - roll_save_files(filename); |
845 | - filename = (boost::format("%s_00") % autosave_filename_).str(); |
846 | - log("Autosave: saving as %s\n", filename.c_str()); |
847 | - } |
848 | - |
849 | - // Saving now |
850 | - std::string complete_filename = create_file_name(kSaveDir, filename); |
851 | - std::string backup_filename; |
852 | - |
853 | - // always overwrite a file |
854 | - if (g_fs->file_exists(complete_filename)) { |
855 | - filename += "2"; |
856 | - backup_filename = create_file_name(kSaveDir, filename); |
857 | - if (g_fs->file_exists(backup_filename)) { |
858 | - g_fs->fs_unlink(backup_filename); |
859 | - } |
860 | - if (save_requested_) { |
861 | - // Exceptions (e.g. no file permissions) will be handled in UI |
862 | - g_fs->fs_rename(complete_filename, backup_filename); |
863 | - } else { |
864 | - // We're autosaving, try to handle file permissions issues |
865 | - try { |
866 | - g_fs->fs_rename(complete_filename, backup_filename); |
867 | - } catch (const std::exception& e) { |
868 | - log("Autosave: Unable to rename file %s to %s: %s\n", complete_filename.c_str(), |
869 | - backup_filename.c_str(), e.what()); |
870 | - // See if we can find a file that doesn't exist and save to that |
871 | - int current_autosave = 0; |
872 | - do { |
873 | - filename = create_file_name( |
874 | - kSaveDir, |
875 | - (boost::format("%s_0%d") % autosave_filename_ % (++current_autosave)).str()); |
876 | - } while (current_autosave < 9 && g_fs->file_exists(filename)); |
877 | - complete_filename = filename; |
878 | - log("Autosave: saving as %s instead\n", complete_filename.c_str()); |
879 | - try { |
880 | - g_fs->fs_rename(complete_filename, backup_filename); |
881 | - } catch (const std::exception&) { |
882 | - log("Autosave failed, skipping this interval\n"); |
883 | - saving_next_tick_ = check_next_tick(game, realtime); |
884 | - return; |
885 | - } |
886 | - } |
887 | - } |
888 | - } |
889 | - |
890 | - if (!save_and_handle_error(game, complete_filename, backup_filename)) { |
891 | + save_success = roll_save_files(filename, &error); |
892 | + if (save_success) { |
893 | + filename = (boost::format("%s_00") % autosave_filename_).str(); |
894 | + log("Autosave: saving as %s\n", filename.c_str()); |
895 | + } |
896 | + } |
897 | + |
898 | + if (save_success) { |
899 | + // Saving now (always overwrite file) |
900 | + std::string complete_filename = create_file_name(kSaveDir, filename); |
901 | + save_success = save_game(game, complete_filename, &error); |
902 | + } |
903 | + if (!save_success) { |
904 | + log("Autosave: ERROR! - %s\n", error.c_str()); |
905 | + game.get_ibase()->log_message(_("Saving failed!")); |
906 | + |
907 | + // Wait 30 seconds until next save try |
908 | + next_save_realtime_ = SDL_GetTicks() + 30000; |
909 | return; |
910 | } |
911 | |
912 | + // Count save interval from end of save. |
913 | + // This prevents us from going into endless autosave cycles if the save |
914 | + // should take longer than the autosave interval. |
915 | + next_save_realtime_ = SDL_GetTicks() + autosave_interval_in_ms_; |
916 | + |
917 | log("Autosave: save took %d ms\n", SDL_GetTicks() - realtime); |
918 | game.get_ibase()->log_message(_("Game saved")); |
919 | - saving_next_tick_ = false; |
920 | } else { |
921 | saving_next_tick_ = check_next_tick(game, realtime); |
922 | } |
923 | @@ -262,34 +234,41 @@ |
924 | return complete_filename; |
925 | } |
926 | |
927 | + |
928 | /* |
929 | * Save the game using the GameSaver. |
930 | * |
931 | - * Will copy text of exceptions to error string. |
932 | - * |
933 | - * returns true if saved, false in case some error occured. |
934 | + * Overwrites file if it exists. |
935 | + * |
936 | + * Will copy text of errors to error string. |
937 | + * |
938 | + * Returns true if saved, false in case some error occured. |
939 | */ |
940 | bool SaveHandler::save_game(Widelands::Game& game, |
941 | const std::string& complete_filename, |
942 | - std::string* const error) { |
943 | + std::string* const error_str) { |
944 | ScopedTimer save_timer("SaveHandler::save_game() took %ums"); |
945 | |
946 | - // Make sure that the base directory exists |
947 | - g_fs->ensure_directory_exists(kSaveDir); |
948 | - |
949 | - // Make a filesystem out of this |
950 | - std::unique_ptr<FileSystem> fs; |
951 | - fs.reset(g_fs->create_sub_file_system(complete_filename, fs_type_)); |
952 | - |
953 | - bool result = true; |
954 | - GameSaver gs(*fs, game); |
955 | - try { |
956 | - gs.save(); |
957 | - } catch (const std::exception& e) { |
958 | - if (error) |
959 | - *error = e.what(); |
960 | - result = false; |
961 | - } |
962 | - |
963 | - return result; |
964 | + // save game via the GenericSaveHandler |
965 | + GenericSaveHandler gsh( |
966 | + [&game](FileSystem& fs) { |
967 | + Widelands::GameSaver gs(fs, game); |
968 | + gs.save(); |
969 | + }, |
970 | + complete_filename, |
971 | + fs_type_ |
972 | + ); |
973 | + gsh.save(); |
974 | + |
975 | + // Ignore it if only the temporary backup wasn't deleted |
976 | + // but save was successfull otherwise |
977 | + if (gsh.error() == GenericSaveHandler::Error::kSuccess || |
978 | + gsh.error() == GenericSaveHandler::Error::kDeletingBackupFailed) { |
979 | + return true; |
980 | + } |
981 | + |
982 | + if (error_str) { |
983 | + *error_str = gsh.error_message(); |
984 | + } |
985 | + return false; |
986 | } |
987 | |
988 | === modified file 'src/logic/save_handler.h' |
989 | --- src/logic/save_handler.h 2018-04-07 16:59:00 +0000 |
990 | +++ src/logic/save_handler.h 2019-02-20 09:56:48 +0000 |
991 | @@ -41,8 +41,13 @@ |
992 | SaveHandler(); |
993 | |
994 | void think(Widelands::Game&); |
995 | - std::string create_file_name(const std::string& dir, const std::string& filename) const; |
996 | - bool save_game(Widelands::Game&, const std::string& filename, std::string* error = nullptr); |
997 | + std::string create_file_name(const std::string& dir, |
998 | + const std::string& filename) const; |
999 | + |
1000 | + // Saves the game, overwrites file, handles errors |
1001 | + bool save_game(Widelands::Game&, |
1002 | + const std::string& filename, |
1003 | + std::string* error_str = nullptr); |
1004 | |
1005 | const std::string get_cur_filename() { |
1006 | return current_filename_; |
1007 | @@ -82,11 +87,8 @@ |
1008 | int32_t number_of_rolls_; // For rolling file update |
1009 | |
1010 | void initialize(uint32_t gametime); |
1011 | - void roll_save_files(const std::string& filename); |
1012 | + bool roll_save_files(const std::string& filename, std::string* error); |
1013 | bool check_next_tick(Widelands::Game& game, uint32_t realtime); |
1014 | - bool save_and_handle_error(Widelands::Game& game, |
1015 | - const std::string& complete_filename, |
1016 | - const std::string& backup_filename); |
1017 | }; |
1018 | |
1019 | #endif // end of include guard: WL_LOGIC_SAVE_HANDLER_H |
1020 | |
1021 | === modified file 'src/wlapplication.cc' |
1022 | --- src/wlapplication.cc 2019-02-12 08:02:22 +0000 |
1023 | +++ src/wlapplication.cc 2019-02-20 09:56:48 +0000 |
1024 | @@ -58,6 +58,7 @@ |
1025 | #include "graphic/text_constants.h" |
1026 | #include "helper.h" |
1027 | #include "io/filesystem/disk_filesystem.h" |
1028 | +#include "io/filesystem/filesystem_exceptions.h" |
1029 | #include "io/filesystem/layered_filesystem.h" |
1030 | #include "logic/ai_dna_handler.h" |
1031 | #include "logic/filesystem_constants.h" |
1032 | @@ -212,8 +213,8 @@ |
1033 | return tfile; |
1034 | } |
1035 | |
1036 | -// Returns true if 'filename' was autogenerated, i.e. if 'extract_creation_day' can return a date |
1037 | -// and it is old enough to be deleted. |
1038 | +// Returns true if 'filename' was autogenerated, i.e. if 'extract_creation_day' |
1039 | +// can return a date and it is old enough to be deleted. |
1040 | bool is_autogenerated_and_expired(const std::string& filename, |
1041 | const double keep_time = kReplayKeepAroundTime) { |
1042 | tm tfile; |
1043 | @@ -340,6 +341,7 @@ |
1044 | cleanup_replays(); |
1045 | cleanup_ai_files(); |
1046 | cleanup_temp_files(); |
1047 | + cleanup_temp_backups(); |
1048 | |
1049 | Section& config = g_options.pull_section("global"); |
1050 | |
1051 | @@ -1458,7 +1460,7 @@ |
1052 | } |
1053 | |
1054 | /** |
1055 | - * Delete the syncstream (.wss) files in the replay directory on startup |
1056 | + * Delete old syncstream (.wss) files in the replay directory on startup |
1057 | * Delete old replay files on startup |
1058 | */ |
1059 | void WLApplication::cleanup_replays() { |
1060 | @@ -1467,29 +1469,16 @@ |
1061 | return boost::ends_with( |
1062 | fn, (boost::format("%s%s") % kReplayExtension % kSyncstreamExtension).str()); |
1063 | })) { |
1064 | - if (is_autogenerated_and_expired(filename)) { |
1065 | - log("Delete syncstream %s\n", filename.c_str()); |
1066 | - g_fs->fs_unlink(filename); |
1067 | - assert(filename.length() > kSyncstreamExtension.length()); |
1068 | - std::string filename_wse = |
1069 | - filename.substr(0, filename.length() - kSyncstreamExtension.length()); |
1070 | - filename_wse += kSyncstreamExcerptExtension; |
1071 | - if (g_fs->file_exists(filename_wse)) { |
1072 | - log("Delete syncstream excerpt %s\n", filename_wse.c_str()); |
1073 | - g_fs->fs_unlink(filename_wse); |
1074 | + if (is_autogenerated_and_expired(filename, kReplayKeepAroundTime)) { |
1075 | + log("Delete syncstream or replay %s\n", filename.c_str()); |
1076 | + try { |
1077 | + g_fs->fs_unlink(filename); |
1078 | + } catch (const FileError& e) { |
1079 | + log("WLApplication::cleanup_replays: File %s couldn't be deleted.\n", |
1080 | + filename.c_str()); |
1081 | } |
1082 | } |
1083 | } |
1084 | - |
1085 | - for (const std::string& filename : |
1086 | - filter(g_fs->list_directory(kReplayDir), |
1087 | - [](const std::string& fn) { return boost::ends_with(fn, kReplayExtension); })) { |
1088 | - if (is_autogenerated_and_expired(filename)) { |
1089 | - log("Deleting replay %s\n", filename.c_str()); |
1090 | - g_fs->fs_unlink(filename); |
1091 | - g_fs->fs_unlink(filename + kSavegameExtension); |
1092 | - } |
1093 | - } |
1094 | } |
1095 | |
1096 | /** |
1097 | @@ -1502,7 +1491,12 @@ |
1098 | })) { |
1099 | if (is_autogenerated_and_expired(filename, kAIFilesKeepAroundTime)) { |
1100 | log("Deleting generated ai file: %s\n", filename.c_str()); |
1101 | - g_fs->fs_unlink(filename); |
1102 | + try { |
1103 | + g_fs->fs_unlink(filename); |
1104 | + } catch (const FileError& e) { |
1105 | + log("WLApplication::cleanup_ai_files: File %s couldn't be deleted.\n", |
1106 | + filename.c_str()); |
1107 | + } |
1108 | } |
1109 | } |
1110 | } |
1111 | @@ -1512,13 +1506,59 @@ |
1112 | */ |
1113 | void WLApplication::cleanup_temp_files() { |
1114 | for (const std::string& filename : |
1115 | - filter(g_fs->list_directory(kTempFileDir), |
1116 | - [](const std::string& fn) { return boost::ends_with(fn, kTempFileExtension); })) { |
1117 | + filter(g_fs->list_directory(kTempFileDir), [](const std::string& fn) { |
1118 | + return boost::ends_with(fn, kTempFileExtension); |
1119 | + })) { |
1120 | if (is_autogenerated_and_expired(filename, kTempFilesKeepAroundTime)) { |
1121 | log("Deleting old temp file: %s\n", filename.c_str()); |
1122 | - g_fs->fs_unlink(filename); |
1123 | - } |
1124 | - } |
1125 | + try { |
1126 | + g_fs->fs_unlink(filename); |
1127 | + } catch (const FileError& e) { |
1128 | + log("WLApplication::cleanup_temp_files: File %s couldn't be deleted.\n", |
1129 | + filename.c_str()); |
1130 | + } |
1131 | + } |
1132 | + } |
1133 | +} |
1134 | + |
1135 | +/** |
1136 | + * Recursively delete temporary backup files in a given directory |
1137 | + */ |
1138 | +void WLApplication::cleanup_temp_backups(std::string dir) { |
1139 | + for (const std::string& filename : |
1140 | + filter(g_fs->list_directory(dir), [](const std::string& fn) { |
1141 | + return boost::ends_with(fn, kTempBackupExtension); |
1142 | + })) { |
1143 | + if (is_autogenerated_and_expired(filename, kTempBackupsKeepAroundTime)) { |
1144 | + log("Deleting old temp backup file: %s\n", filename.c_str()); |
1145 | + try { |
1146 | + g_fs->fs_unlink(filename); |
1147 | + } catch (const FileError& e) { |
1148 | + log("WLApplication::cleanup_temp_backups: File %s couldn't be deleted.\n", |
1149 | + filename.c_str()); |
1150 | + } |
1151 | + } |
1152 | + } |
1153 | + // recursively delete in subdirs |
1154 | + for (const std::string& dirname : |
1155 | + filter(g_fs->list_directory(dir), [](const std::string& fn) { |
1156 | + return g_fs->is_directory(fn) && |
1157 | + // avoid searching within savegames/maps/backups that were created |
1158 | + // as directories instead of zipfiles |
1159 | + !boost::ends_with(fn, kSavegameExtension) && |
1160 | + !boost::ends_with(fn, kWidelandsMapExtension) && |
1161 | + !boost::ends_with(fn, kTempBackupExtension); |
1162 | + })) { |
1163 | + cleanup_temp_backups(dirname); |
1164 | + } |
1165 | +} |
1166 | + |
1167 | +/** |
1168 | + * Delete old temporary backup files that might still lurk around (game crashes etc.) |
1169 | + */ |
1170 | +void WLApplication::cleanup_temp_backups() { |
1171 | + cleanup_temp_backups(kSaveDir); |
1172 | + cleanup_temp_backups(kMapsDir); |
1173 | } |
1174 | |
1175 | bool WLApplication::redirect_output(std::string path) { |
1176 | |
1177 | === modified file 'src/wlapplication.h' |
1178 | --- src/wlapplication.h 2018-10-21 22:21:34 +0000 |
1179 | +++ src/wlapplication.h 2019-02-20 09:56:48 +0000 |
1180 | @@ -213,10 +213,10 @@ |
1181 | void setup_homedir(); |
1182 | |
1183 | void cleanup_replays(); |
1184 | - |
1185 | void cleanup_ai_files(); |
1186 | - |
1187 | void cleanup_temp_files(); |
1188 | + void cleanup_temp_backups(std::string dir); |
1189 | + void cleanup_temp_backups(); |
1190 | |
1191 | bool redirect_output(std::string path = ""); |
1192 | |
1193 | |
1194 | === modified file 'src/wui/CMakeLists.txt' |
1195 | --- src/wui/CMakeLists.txt 2018-09-12 21:24:39 +0000 |
1196 | +++ src/wui/CMakeLists.txt 2019-02-20 09:56:48 +0000 |
1197 | @@ -284,6 +284,7 @@ |
1198 | logic_filesystem_constants |
1199 | logic_game_controller |
1200 | logic_game_settings |
1201 | + logic_generic_save_handler |
1202 | logic_tribe_basic_info |
1203 | logic_widelands_geometry |
1204 | network |
1205 | |
1206 | === modified file 'src/wui/game_main_menu_save_game.cc' |
1207 | --- src/wui/game_main_menu_save_game.cc 2018-11-03 18:21:36 +0000 |
1208 | +++ src/wui/game_main_menu_save_game.cc 2019-02-20 09:56:48 +0000 |
1209 | @@ -21,6 +21,7 @@ |
1210 | |
1211 | #include <memory> |
1212 | |
1213 | +#include <boost/algorithm/string.hpp> |
1214 | #include <boost/format.hpp> |
1215 | |
1216 | #include "base/i18n.h" |
1217 | @@ -32,6 +33,7 @@ |
1218 | #include "logic/filesystem_constants.h" |
1219 | #include "logic/game.h" |
1220 | #include "logic/game_controller.h" |
1221 | +#include "logic/generic_save_handler.h" |
1222 | #include "logic/playersmanager.h" |
1223 | #include "ui_basic/messagebox.h" |
1224 | #include "wui/interactive_gamebase.h" |
1225 | @@ -154,63 +156,17 @@ |
1226 | } |
1227 | } |
1228 | |
1229 | -static void dosave(InteractiveGameBase& igbase, const std::string& complete_filename) { |
1230 | - Widelands::Game& game = igbase.game(); |
1231 | - |
1232 | - std::string error; |
1233 | - if (!game.save_handler().save_game(game, complete_filename, &error)) { |
1234 | - std::string s = _("Game Saving Error!\nSaved game file may be corrupt!\n\n" |
1235 | - "Reason given:\n"); |
1236 | - s += error; |
1237 | - UI::WLMessageBox mbox(&igbase, _("Save Game Error!"), s, UI::WLMessageBox::MBoxType::kOk); |
1238 | - mbox.run<UI::Panel::Returncodes>(); |
1239 | - } |
1240 | - game.save_handler().set_current_filename(complete_filename); |
1241 | -} |
1242 | - |
1243 | -struct SaveWarnMessageBox : public UI::WLMessageBox { |
1244 | - SaveWarnMessageBox(GameMainMenuSaveGame& parent, const std::string& filename) |
1245 | - : UI::WLMessageBox(&parent, |
1246 | - _("Save Game Error!"), |
1247 | - (boost::format(_("A file with the name ‘%s’ already exists. Overwrite?")) % |
1248 | - FileSystem::fs_filename(filename.c_str())) |
1249 | - .str(), |
1250 | - MBoxType::kOkCancel), |
1251 | - filename_(filename) { |
1252 | - } |
1253 | - |
1254 | - GameMainMenuSaveGame& menu_save_game() { |
1255 | - return dynamic_cast<GameMainMenuSaveGame&>(*get_parent()); |
1256 | - } |
1257 | - |
1258 | - void clicked_ok() override { |
1259 | - g_fs->fs_unlink(filename_); |
1260 | - dosave(menu_save_game().igbase(), filename_); |
1261 | - menu_save_game().die(); |
1262 | - } |
1263 | - |
1264 | - void clicked_back() override { |
1265 | - die(); |
1266 | - } |
1267 | - |
1268 | -private: |
1269 | - std::string const filename_; |
1270 | -}; |
1271 | - |
1272 | void GameMainMenuSaveGame::ok() { |
1273 | - if (filename_editbox_.text().empty()) { |
1274 | + if (!ok_.enabled()) { |
1275 | return; |
1276 | } |
1277 | |
1278 | - std::string const complete_filename = |
1279 | - igbase().game().save_handler().create_file_name(curdir_, filename_editbox_.text()); |
1280 | - |
1281 | - // Check if file exists. If it does, show a warning. |
1282 | - if (g_fs->file_exists(complete_filename)) { |
1283 | - new SaveWarnMessageBox(*this, complete_filename); |
1284 | + std::string filename = filename_editbox_.text(); |
1285 | + if (save_game(filename, |
1286 | + !g_options.pull_section("global").get_bool("nozip", false))) { |
1287 | + die(); |
1288 | } else { |
1289 | - dosave(igbase(), complete_filename); |
1290 | - die(); |
1291 | + load_or_save_.table_.focus(); |
1292 | } |
1293 | } |
1294 | |
1295 | @@ -245,3 +201,74 @@ |
1296 | } |
1297 | igbase().game().game_controller()->set_paused(paused); |
1298 | } |
1299 | + |
1300 | +/** |
1301 | + * Save the game in the Savegame directory with |
1302 | + * the given filename |
1303 | + * |
1304 | + * returns true if dialog should close, false if it |
1305 | + * should stay open |
1306 | + */ |
1307 | +bool GameMainMenuSaveGame::save_game(std::string filename, bool binary) { |
1308 | + // Trim it for preceding/trailing whitespaces in user input |
1309 | + boost::trim(filename); |
1310 | + |
1311 | + // OK, first check if the extension matches (ignoring case). |
1312 | + if (!boost::iends_with(filename, kSavegameExtension)) { |
1313 | + filename += kSavegameExtension; |
1314 | + } |
1315 | + |
1316 | + // Append directory name. |
1317 | + const std::string complete_filename = |
1318 | + curdir_ + g_fs->file_separator() + filename; |
1319 | + |
1320 | + // Check if file exists. If so, show a warning. |
1321 | + if (g_fs->file_exists(complete_filename)) { |
1322 | + const std::string s = |
1323 | + (boost::format(_("A file with the name ‘%s’ already exists. Overwrite?")) |
1324 | + % FileSystem::fs_filename(filename.c_str())).str(); |
1325 | + UI::WLMessageBox mbox(this, _("Error Saving Game!"), s, |
1326 | + UI::WLMessageBox::MBoxType::kOkCancel); |
1327 | + if (mbox.run<UI::Panel::Returncodes>() == UI::Panel::Returncodes::kBack) { |
1328 | + return false; |
1329 | + } |
1330 | + } |
1331 | + |
1332 | + // Try saving the game. |
1333 | + Widelands::Game& game = igbase().game(); |
1334 | + GenericSaveHandler gsh( |
1335 | + [&game](FileSystem& fs) { |
1336 | + Widelands::GameSaver gs(fs, game); |
1337 | + gs.save(); |
1338 | + }, |
1339 | + complete_filename, |
1340 | + binary ? FileSystem::ZIP : FileSystem::DIR |
1341 | + ); |
1342 | + GenericSaveHandler::Error error = gsh.save(); |
1343 | + |
1344 | + // If only the temporary backup couldn't be deleted, we still treat it as |
1345 | + // success. Automatic cleanup will deal with later. No need to bother the |
1346 | + // player with it. |
1347 | + if (error == GenericSaveHandler::Error::kSuccess || |
1348 | + error == GenericSaveHandler::Error::kDeletingBackupFailed) { |
1349 | + game.save_handler().set_current_filename(complete_filename); |
1350 | + igbase().log_message(_("Game saved")); |
1351 | + return true; |
1352 | + } |
1353 | + |
1354 | + // Show player an error message. |
1355 | + std::string msg = gsh.localized_formatted_result_message(); |
1356 | + UI::WLMessageBox mbox(this, _("Error Saving Game!"), msg, |
1357 | + UI::WLMessageBox::MBoxType::kOk); |
1358 | + mbox.run<UI::Panel::Returncodes>(); |
1359 | + |
1360 | + // If only the backup failed (likely just because of a file lock), |
1361 | + // then leave the dialog open for the player to try with a new filename. |
1362 | + if (error == GenericSaveHandler::Error::kBackupFailed) { |
1363 | + return false; |
1364 | + } |
1365 | + |
1366 | + // In the other error cases close the dialog. |
1367 | + igbase().log_message(_("Saving failed!")); |
1368 | + return true; |
1369 | +} |
1370 | |
1371 | === modified file 'src/wui/game_main_menu_save_game.h' |
1372 | --- src/wui/game_main_menu_save_game.h 2018-05-23 04:40:43 +0000 |
1373 | +++ src/wui/game_main_menu_save_game.h 2019-02-20 09:56:48 +0000 |
1374 | @@ -60,6 +60,8 @@ |
1375 | /// Called when the OK button is clicked or the Return key pressed in the edit box. |
1376 | void ok(); |
1377 | |
1378 | + bool save_game(std::string filename, bool binary); |
1379 | + |
1380 | /// Pause/unpause the game |
1381 | void pause_game(bool paused); |
1382 |
Continuous integration builds have changed state:
Travis build 4221. State: passed. Details: https:/ /travis- ci.org/ widelands/ widelands/ builds/ 454613234. /ci.appveyor. com/project/ widelands- dev/widelands/ build/_ widelands_ dev_widelands_ robust_ file_saving- 4017.
Appveyor build 4017. State: failed. Details: https:/