Merge lp:~widelands-dev/widelands/robust-file-saving into lp:widelands

Proposed by Arty
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
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.

To post a comment you must log in.
Revision history for this message
bunnybot (widelandsofficial) wrote :

Continuous integration builds have changed state:

Travis build 4221. State: passed. Details: https://travis-ci.org/widelands/widelands/builds/454613234.
Appveyor build 4017. State: failed. Details: https://ci.appveyor.com/project/widelands-dev/widelands/build/_widelands_dev_widelands_robust_file_saving-4017.

Revision history for this message
GunChleoc (gunchleoc) wrote :

I have added 2 comments. I'll do another review round when you have looked at them.

Revision history for this message
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.

Revision history for this message
Arty (artydent) wrote :

Switched to a type safe error type.

Revision history for this message
bunnybot (widelandsofficial) wrote :

Continuous integration builds have changed state:

Travis build 4242. State: failed. Details: https://travis-ci.org/widelands/widelands/builds/456239294.
Appveyor build 4037. State: failed. Details: https://ci.appveyor.com/project/widelands-dev/widelands/build/_widelands_dev_widelands_robust_file_saving-4037.

Revision history for this message
Arty (artydent) wrote :

Oops, forgot to enable codecheck again. Will be fixed in the next round.

Revision history for this message
bunnybot (widelandsofficial) wrote :

Continuous integration builds have changed state:

Travis build 4287. State: failed. Details: https://travis-ci.org/widelands/widelands/builds/459370561.
Appveyor build 4081. State: success. Details: https://ci.appveyor.com/project/widelands-dev/widelands/build/_widelands_dev_widelands_robust_file_saving-4081.

Revision history for this message
GunChleoc (gunchleoc) wrote :

Error from Travis log:

/home/travis/build/widelands/widelands/src/logic/generic_save_handler.cc:44:71: error: declaration of ‘error’ shadows a member of 'this' [-Werror=shadow]
 uint32_t GenericSaveHandler::get_index(GenericSaveHandler::Error error) {

Revision history for this message
bunnybot (widelandsofficial) wrote :

Continuous integration builds have changed state:

Travis build 4296. State: passed. Details: https://travis-ci.org/widelands/widelands/builds/459708047.
Appveyor build 4089. State: success. Details: https://ci.appveyor.com/project/widelands-dev/widelands/build/_widelands_dev_widelands_robust_file_saving-4089.

Revision history for this message
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?

Revision history for this message
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 :-)

review: Approve (compile, test, debug)
Revision history for this message
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.)

Revision history for this message
Klaus Halfmann (klaus-halfmann) wrote :

I think should get this in now,
I dont think it will break anything.

@bunnybot merge

review: Approve (reaprove)
Revision history for this message
bunnybot (widelandsofficial) wrote :

Error merging this proposal:

Output:
stdout:

stderr:
+N src/logic/generic_save_handler.cc
+N src/logic/generic_save_handler.h
 M src/editor/CMakeLists.txt
 M src/editor/ui_menus/main_menu_save_map.cc
 M src/logic/CMakeLists.txt
 M src/logic/filesystem_constants.h
 M src/logic/save_handler.cc
 M src/logic/save_handler.h
 M src/wlapplication.cc
 M src/wlapplication.h
 M src/wui/CMakeLists.txt
 M src/wui/game_main_menu_save_game.cc
 M src/wui/game_main_menu_save_game.h
Text conflict in src/wlapplication.cc
1 conflicts encountered.

Revision history for this message
Klaus Halfmann (klaus-halfmann) wrote :

Hello Arty I try to fix these conflicts, but maybe I will need you help?

Revision history for this message
Klaus Halfmann (klaus-halfmann) wrote :

Arty can, you take another look? Maybe my merge broke it ....

lets try to compile this at leasst

review: Needs Resubmitting (merge)
Revision history for this message
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.

Revision history for this message
GunChleoc (gunchleoc) wrote :

I have had a look at the diff for the trunk merge and the resolution of the merge conflict looks fine.

Revision history for this message
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.

review: Needs Resubmitting
Revision history for this message
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.

Revision history for this message
bunnybot (widelandsofficial) wrote :

Continuous integration builds have changed state:

Travis build 4498. State: errored. Details: https://travis-ci.org/widelands/widelands/builds/495915762.
Appveyor build 4285. State: success. Details: https://ci.appveyor.com/project/widelands-dev/widelands/build/_widelands_dev_widelands_robust_file_saving-4285.

Revision history for this message
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

review: Approve
Revision history for this message
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://travis-ci.org/widelands/widelands/builds/495915762.

Revision history for this message
GunChleoc (gunchleoc) wrote :

@bunnybot merge force

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches

to status/vote changes: