From 69e12f5bd10459ff7c239b82519107ae2a755bc0 Mon Sep 17 00:00:00 2001 From: "B. Stack" Date: Mon, 24 Jul 2023 15:08:16 -0400 Subject: add upstream 12.5 --- Changelog.txt | 14 + FreeFileSync/Build/Resources/Icons.zip | Bin 360204 -> 360707 bytes FreeFileSync/Build/Resources/Languages.zip | Bin 528587 -> 543736 bytes FreeFileSync/Source/Makefile | 4 +- FreeFileSync/Source/RealTimeSync/Makefile | 2 +- FreeFileSync/Source/RealTimeSync/application.cpp | 84 +- FreeFileSync/Source/RealTimeSync/config.cpp | 83 +- FreeFileSync/Source/RealTimeSync/config.h | 4 +- FreeFileSync/Source/RealTimeSync/main_dlg.cpp | 15 +- FreeFileSync/Source/RealTimeSync/tray_menu.cpp | 26 +- FreeFileSync/Source/RealTimeSync/tray_menu.h | 8 +- FreeFileSync/Source/afs/abstract.cpp | 13 +- FreeFileSync/Source/afs/abstract.h | 5 +- FreeFileSync/Source/afs/concrete.cpp | 5 +- FreeFileSync/Source/afs/concrete.h | 2 +- FreeFileSync/Source/afs/ftp.cpp | 107 +- FreeFileSync/Source/afs/gdrive.cpp | 15 +- FreeFileSync/Source/afs/gdrive.h | 2 +- FreeFileSync/Source/afs/init_curl_libssh2.cpp | 8 +- FreeFileSync/Source/afs/native.cpp | 8 +- FreeFileSync/Source/afs/sftp.cpp | 48 +- FreeFileSync/Source/application.cpp | 289 +- FreeFileSync/Source/application.h | 1 - FreeFileSync/Source/base/algorithm.cpp | 5 +- FreeFileSync/Source/base/comparison.cpp | 259 +- FreeFileSync/Source/base/db_file.cpp | 8 +- FreeFileSync/Source/base/db_file.h | 2 +- FreeFileSync/Source/base/dir_lock.cpp | 52 +- FreeFileSync/Source/base/dir_lock.h | 17 +- FreeFileSync/Source/base/lock_holder.h | 6 +- FreeFileSync/Source/base/multi_rename.cpp | 180 + FreeFileSync/Source/base/multi_rename.h | 23 + FreeFileSync/Source/base/process_callback.h | 6 +- FreeFileSync/Source/base/status_handler_impl.h | 2 +- FreeFileSync/Source/base/synchronization.cpp | 228 +- FreeFileSync/Source/base_tools.cpp | 2 +- FreeFileSync/Source/base_tools.h | 2 +- FreeFileSync/Source/config.cpp | 140 +- FreeFileSync/Source/config.h | 7 +- FreeFileSync/Source/ffs_paths.cpp | 10 +- FreeFileSync/Source/ffs_paths.h | 3 +- FreeFileSync/Source/icon_buffer.cpp | 13 +- FreeFileSync/Source/localization.cpp | 12 +- FreeFileSync/Source/log_file.cpp | 38 +- FreeFileSync/Source/return_codes.h | 22 +- FreeFileSync/Source/status_handler.cpp | 26 +- FreeFileSync/Source/status_handler.h | 55 +- FreeFileSync/Source/ui/batch_config.cpp | 14 +- FreeFileSync/Source/ui/batch_status_handler.cpp | 211 +- FreeFileSync/Source/ui/batch_status_handler.h | 25 +- FreeFileSync/Source/ui/cfg_grid.cpp | 36 +- FreeFileSync/Source/ui/cfg_grid.h | 4 +- FreeFileSync/Source/ui/file_grid.cpp | 33 +- FreeFileSync/Source/ui/gui_generated.cpp | 7183 +++++++++++----------- FreeFileSync/Source/ui/gui_generated.h | 2377 +++---- FreeFileSync/Source/ui/gui_status_handler.cpp | 242 +- FreeFileSync/Source/ui/gui_status_handler.h | 34 +- FreeFileSync/Source/ui/log_panel.cpp | 8 +- FreeFileSync/Source/ui/log_panel.h | 4 +- FreeFileSync/Source/ui/main_dlg.cpp | 694 ++- FreeFileSync/Source/ui/main_dlg.h | 15 +- FreeFileSync/Source/ui/progress_indicator.cpp | 147 +- FreeFileSync/Source/ui/progress_indicator.h | 10 +- FreeFileSync/Source/ui/rename_dlg.cpp | 224 + FreeFileSync/Source/ui/rename_dlg.h | 20 + FreeFileSync/Source/ui/small_dlgs.cpp | 16 +- FreeFileSync/Source/ui/small_dlgs.h | 1 - FreeFileSync/Source/ui/version_check.cpp | 30 +- FreeFileSync/Source/ui/version_check.h | 12 +- FreeFileSync/Source/version/version.h | 2 +- libcurl/curl_wrap.cpp | 88 +- libcurl/curl_wrap.h | 1 - wx+/bitmap_button.h | 4 +- wx+/grid.cpp | 18 +- wx+/no_flicker.h | 2 +- wx+/popup_dlg.cpp | 4 +- wx+/taskbar.h | 12 +- zen/file_access.cpp | 9 +- zen/file_io.cpp | 20 +- zen/file_path.cpp | 32 +- zen/format_unit.cpp | 13 +- zen/globals.h | 82 +- zen/http.cpp | 1 - zen/json.h | 4 +- zen/legacy_compiler.h | 2 +- zen/open_ssl.cpp | 80 +- zen/socket.h | 9 +- zen/stl_tools.h | 28 +- zen/symlink_target.h | 5 +- zen/sys_error.h | 3 +- zen/sys_version.cpp | 4 +- zen/zlib_wrap.cpp | 1 - zenXml/zenxml/cvrt_struc.h | 4 +- zenXml/zenxml/dom.h | 110 +- zenXml/zenxml/parser.h | 14 +- zenXml/zenxml/xml.h | 246 +- 96 files changed, 7432 insertions(+), 6567 deletions(-) create mode 100644 FreeFileSync/Source/base/multi_rename.cpp create mode 100644 FreeFileSync/Source/base/multi_rename.h create mode 100644 FreeFileSync/Source/ui/rename_dlg.cpp create mode 100644 FreeFileSync/Source/ui/rename_dlg.h diff --git a/Changelog.txt b/Changelog.txt index 9da83eaf..50d6e0af 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,17 @@ +FreeFileSync 12.5 [2023-07-21] +------------------------------ +Merge logs of individual steps (comparison, manual operation, sync) +Show total percentage in progress dialog header +Log and report errors during cleanup or exception handling +Skip folder traversal if existence check fails for other side of the pair +Automatically adapt batch options to prevent hanging a non-interactive process (Windows) +Support path lists for external applications: %item_paths%, %local_paths%, %item_names%, %parent_paths% +Create directory lock files with hidden attribute +Don't clear other side when right-clicking file selection +Fixed passive FTP when using different IP than control connection +Work around FTP servers silently renaming unsupported characters of temporary file + + FreeFileSync 12.4 [2023-06-20] ------------------------------ Show dynamic error and warning count in progress dialogs diff --git a/FreeFileSync/Build/Resources/Icons.zip b/FreeFileSync/Build/Resources/Icons.zip index 19dafa61..12822e10 100644 Binary files a/FreeFileSync/Build/Resources/Icons.zip and b/FreeFileSync/Build/Resources/Icons.zip differ diff --git a/FreeFileSync/Build/Resources/Languages.zip b/FreeFileSync/Build/Resources/Languages.zip index 3ff3de8f..8a828e86 100644 Binary files a/FreeFileSync/Build/Resources/Languages.zip and b/FreeFileSync/Build/Resources/Languages.zip differ diff --git a/FreeFileSync/Source/Makefile b/FreeFileSync/Source/Makefile index 5d253171..f49b9ff6 100644 --- a/FreeFileSync/Source/Makefile +++ b/FreeFileSync/Source/Makefile @@ -1,7 +1,7 @@ CXX ?= g++ exeName = FreeFileSync_$(shell arch) -CXXFLAGS += -std=c++2b -pipe -DWXINTL_NO_GETTEXT_MACRO -I../.. -I../../zenXml -include "zen/i18n.h" -include "zen/warn_static.h" \ +CXXFLAGS += -std=c++23 -pipe -DWXINTL_NO_GETTEXT_MACRO -I../.. -I../../zenXml -include "zen/i18n.h" -include "zen/warn_static.h" \ -Wall -Wfatal-errors -Wmissing-include-dirs -Wswitch-enum -Wcast-align -Wnon-virtual-dtor -Wno-unused-function -Wshadow -Wno-maybe-uninitialized \ -O3 -DNDEBUG `wx-config --cxxflags --debug=no` -pthread @@ -44,6 +44,7 @@ cppFiles+=base/db_file.cpp cppFiles+=base/dir_lock.cpp cppFiles+=base/file_hierarchy.cpp cppFiles+=base/icon_loader.cpp +cppFiles+=base/multi_rename.cpp cppFiles+=base/parallel_scan.cpp cppFiles+=base/path_filter.cpp cppFiles+=base/speed_test.cpp @@ -72,6 +73,7 @@ cppFiles+=ui/gui_generated.cpp cppFiles+=ui/gui_status_handler.cpp cppFiles+=ui/main_dlg.cpp cppFiles+=ui/progress_indicator.cpp +cppFiles+=ui/rename_dlg.cpp cppFiles+=ui/search_grid.cpp cppFiles+=ui/small_dlgs.cpp cppFiles+=ui/sync_cfg.cpp diff --git a/FreeFileSync/Source/RealTimeSync/Makefile b/FreeFileSync/Source/RealTimeSync/Makefile index 661b6b78..bac4de39 100644 --- a/FreeFileSync/Source/RealTimeSync/Makefile +++ b/FreeFileSync/Source/RealTimeSync/Makefile @@ -1,7 +1,7 @@ CXX ?= g++ exeName = RealTimeSync_$(shell arch) -CXXFLAGS += -std=c++2b -pipe -DWXINTL_NO_GETTEXT_MACRO -I../../.. -I../../../zenXml -include "zen/i18n.h" -include "zen/warn_static.h" \ +CXXFLAGS += -std=c++23 -pipe -DWXINTL_NO_GETTEXT_MACRO -I../../.. -I../../../zenXml -include "zen/i18n.h" -include "zen/warn_static.h" \ -Wall -Wfatal-errors -Wmissing-include-dirs -Wswitch-enum -Wcast-align -Wnon-virtual-dtor -Wno-unused-function -Wshadow -Wno-maybe-uninitialized \ -O3 -DNDEBUG `wx-config --cxxflags --debug=no` -pthread diff --git a/FreeFileSync/Source/RealTimeSync/application.cpp b/FreeFileSync/Source/RealTimeSync/application.cpp index 6add53b9..1558ea33 100644 --- a/FreeFileSync/Source/RealTimeSync/application.cpp +++ b/FreeFileSync/Source/RealTimeSync/application.cpp @@ -27,6 +27,7 @@ using namespace zen; using namespace rts; +using fff::FfsExitCode; #ifdef __WXGTK3__ //deprioritize Wayland: see FFS' application.cpp GLOBAL_RUN_ONCE(::gdk_set_allowed_backends("x11,*")); //call *before* gtk_init() @@ -37,29 +38,10 @@ IMPLEMENT_APP(Application) namespace { -using fff::FfsExitCode; - -void notifyAppError(const std::wstring& msg, FfsExitCode rc) +void notifyAppError(const std::wstring& msg) { - //raiseExitCode(exitCode_, rc); - - const std::wstring msgType = [&] - { - switch (rc) - { - //*INDENT-OFF* - case FfsExitCode::success: break; - case FfsExitCode::warning: return _("Warning"); - case FfsExitCode::error: return _("Error"); - case FfsExitCode::aborted: return _("Error"); - case FfsExitCode::exception: return _("An exception occurred"); - //*INDENT-ON* - } - assert(false); - return std::wstring{}; - }(); //error handling strategy unknown and no sync log output available at this point! - std::cerr << utfTo(msgType + L": " + msg) + '\n'; + std::cerr << utfTo(_("Error") + L": " + msg) + '\n'; //alternative0: std::wcerr: cannot display non-ASCII at all, so why does it exist??? //alternative1: wxSafeShowMessage => NO console output on Debian x86, WTF! //alternative2: wxMessageBox() => works, but we probably shouldn't block during command line usage @@ -71,16 +53,24 @@ bool Application::OnInit() { //do not call wxApp::OnInit() to avoid using wxWidgets command line parser + initExtraLog([](const ErrorLog& log) //don't call functions depending on global state (which might be destroyed already!) + { + std::wstring msg; + for (const LogEntry& e : log) + msg += utfTo(formatMessage(e)); + trim(msg); + notifyAppError(msg); + }); + try { imageResourcesInit(appendPath(fff::getResourceDirPath(), Zstr("Icons.zip"))); } - catch (const FileError& e) { notifyAppError(e.toString(), FfsExitCode::warning); } - //errors are not really critical in this context + catch (const FileError& e) { logExtraError(e.toString()); } //not critical in this context //GTK should already have been initialized by wxWidgets (see \src\gtk\app.cpp:wxApp::Initialize) #if GTK_MAJOR_VERSION == 2 ::gtk_rc_parse(appendPath(fff::getResourceDirPath(), "Gtk2Styles.rc").c_str()); //fix hang on Ubuntu 19.10 (see FFS's application.cpp) - g_vfs_get_default(); //returns unowned GVfs* + [[maybe_unused]] GVfs* defaultFs = ::g_vfs_get_default(); //not owned by us! #elif GTK_MAJOR_VERSION == 3 auto loadCSS = [&](const char* fileName) @@ -91,14 +81,14 @@ bool Application::OnInit() GError* error = nullptr; ZEN_ON_SCOPE_EXIT(if (error) ::g_error_free(error)); - ::gtk_css_provider_load_from_path(provider, //GtkCssProvider* css_provider, - appendPath(fff::getResourceDirPath(), fileName).c_str(), //const gchar* path, + ::gtk_css_provider_load_from_path(provider, //GtkCssProvider* css_provider + appendPath(fff::getResourceDirPath(), fileName).c_str(), //const gchar* path &error); //GError** error if (error) throw SysError(formatGlibError("gtk_css_provider_load_from_path", error)); - ::gtk_style_context_add_provider_for_screen(::gdk_screen_get_default(), //GdkScreen* screen, - GTK_STYLE_PROVIDER(provider), //GtkStyleProvider* provider, + ::gtk_style_context_add_provider_for_screen(::gdk_screen_get_default(), //GdkScreen* screen + GTK_STYLE_PROVIDER(provider), //GtkStyleProvider* provider GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); //guint priority }; try @@ -112,30 +102,26 @@ bool Application::OnInit() { loadCSS("Gtk3Styles.old.css"); //throw SysError } - catch (const SysError& e2) { notifyAppError(e2.toString(), FfsExitCode::warning); } + catch (const SysError& e3) { logExtraError(_("Error during process initialization.") + L"\n\n" + e3.toString()); } } #else #error unknown GTK version! #endif - try - { - /* we're a GUI app: ignore SIGHUP when the parent terminal quits! (or process is killed!) + /* we're a GUI app: ignore SIGHUP when the parent terminal quits! (or process is killed!) => the FFS launcher will still be killed => fine => macOS: apparently not needed! interestingly the FFS launcher does receive SIGHUP and *is* killed */ - if (sighandler_t oldHandler = ::signal(SIGHUP, SIG_IGN); - oldHandler == SIG_ERR) - THROW_LAST_SYS_ERROR("signal(SIGHUP)"); - else assert(!oldHandler); - } - catch (const SysError& e) { notifyAppError(e.toString(), FfsExitCode::warning); } + if (sighandler_t oldHandler = ::signal(SIGHUP, SIG_IGN); + oldHandler == SIG_ERR) + logExtraError(_("Error during process initialization.") + L"\n\n" + formatSystemError("signal(SIGHUP)", getLastError())); + else assert(!oldHandler); //Windows User Experience Interaction Guidelines: tool tips should have 5s timeout, info tips no timeout => compromise: wxToolTip::Enable(true); //wxWidgets screw-up: wxToolTip::SetAutoPop is no-op if global tooltip window is not yet constructed: wxToolTip::Enable creates it wxToolTip::SetAutoPop(15'000); //https://docs.microsoft.com/en-us/windows/win32/uxguide/ctrl-tooltips-and-infotips - SetAppName(L"RealTimeSync"); + SetAppName(L"RealTimeSync"); //if not set, defaults to executable name try @@ -143,25 +129,21 @@ bool Application::OnInit() fff::localizationInit(appendPath(fff::getResourceDirPath(), Zstr("Languages.zip"))); //throw FileError fff::setLanguage(getProgramLanguage()); //throw FileError } - catch (const FileError& e) { notifyAppError(e.toString(), FfsExitCode::warning); } + catch (const FileError& e) { logExtraError(e.toString()); } auto onSystemShutdown = [](int /*unused*/ = 0) { onSystemShutdownRunTasks(); //it's futile to try and clean up while the process is in full swing (CRASH!) => just terminate! - terminateProcess(static_cast(FfsExitCode::aborted)); + terminateProcess(static_cast(FfsExitCode::cancelled)); }; Bind(wxEVT_QUERY_END_SESSION, [onSystemShutdown](wxCloseEvent& event) { onSystemShutdown(); }); //can veto Bind(wxEVT_END_SESSION, [onSystemShutdown](wxCloseEvent& event) { onSystemShutdown(); }); //can *not* veto - try - { - if (auto /*sighandler_t n.a. on macOS*/ oldHandler = ::signal(SIGTERM, onSystemShutdown);//"graceful" exit requested, unlike SIGKILL - oldHandler == SIG_ERR) - THROW_LAST_SYS_ERROR("signal(SIGTERM)"); - else assert(!oldHandler); - } - catch (const SysError& e) { notifyAppError(e.toString(), FfsExitCode::warning); } + if (auto /*sighandler_t n.a. on macOS*/ oldHandler = ::signal(SIGTERM, onSystemShutdown);//"graceful" exit requested, unlike SIGKILL + oldHandler == SIG_ERR) + logExtraError(_("Error during process initialization.") + L"\n\n" + formatSystemError("signal(SIGTERM)", getLastError())); + else assert(!oldHandler); //Note: app start is deferred: -> see FreeFileSync CallAfter([&] { onEnterEventLoop(); }); @@ -207,7 +189,7 @@ void Application::onEnterEventLoop() } catch (const FileError& e) { - notifyAppError(e.toString(), FfsExitCode::exception); + notifyAppError(e.toString()); } } @@ -240,7 +222,7 @@ void Application::OnUnhandledException() //handles both wxApp::OnInit() + wxApp: } catch (const std::bad_alloc& e) //the only kind of exception we don't want crash dumps for { - notifyAppError(utfTo(e.what()), FfsExitCode::exception); + notifyAppError(utfTo(e.what())); terminateProcess(static_cast(FfsExitCode::exception)); } //catch (...) -> Windows: let it crash and create mini dump!!! Linux/macOS: std::exception::what() logged to console diff --git a/FreeFileSync/Source/RealTimeSync/config.cpp b/FreeFileSync/Source/RealTimeSync/config.cpp index 951aabd2..066bb3c5 100644 --- a/FreeFileSync/Source/RealTimeSync/config.cpp +++ b/FreeFileSync/Source/RealTimeSync/config.cpp @@ -73,7 +73,7 @@ void writeConfig(const XmlRealConfig& cfg, XmlOut& out) } -void rts::readConfig(const Zstring& filePath, XmlRealConfig& cfg, std::wstring& warningMsg) //throw FileError +std::pair rts::readConfig(const Zstring& filePath) //throw FileError { XmlDoc doc = loadXml(filePath); //throw FileError @@ -84,23 +84,23 @@ void rts::readConfig(const Zstring& filePath, XmlRealConfig& cfg, std::wstring& /*bool success =*/ doc.root().getAttribute("XmlFormat", formatVer); XmlIn in(doc); + XmlRealConfig cfg; ::readConfig(in, cfg, formatVer); - try - { - checkXmlMappingErrors(in); //throw FileError - - //(try to) migrate old configuration automatically + std::wstring warningMsg; + if (const std::wstring& errors = in.getErrors(); + !errors.empty()) + warningMsg = replaceCpy(_("Configuration file %x is incomplete. The missing elements have been set to their default values."), L"%x", fmtPath(filePath)) + L"\n\n" + + _("The following XML elements could not be read:") + L'\n' + errors; + else //(try to) migrate old configuration automatically if (formatVer < XML_FORMAT_RTS_CFG) - try { rts::writeConfig(cfg, filePath); /*throw FileError*/ } - catch (FileError&) { assert(false); } //don't bother user! - warn_static("at least log on failure!") - } - catch (const FileError& e) - { - warningMsg = replaceCpy(_("Configuration file %x is incomplete. The missing elements have been set to their default values."), L"%x", fmtPath(filePath)) + - L"\n\n" + e.toString(); - } + try + { + rts::writeConfig(cfg, filePath); //throw FileError + } + catch (const FileError& e) { warningMsg = e.toString(); } + + return {cfg, warningMsg}; } @@ -117,7 +117,7 @@ void rts::writeConfig(const XmlRealConfig& cfg, const Zstring& filePath) //throw } -void rts::readRealOrBatchConfig(const Zstring& filePath, XmlRealConfig& cfg, std::wstring& warningMsg) //throw FileError +std::pair rts::readRealOrBatchConfig(const Zstring& filePath) //throw FileError { XmlDoc doc = loadXml(filePath); //throw FileError //quick exit if file is not an FFS XML @@ -130,8 +130,10 @@ void rts::readRealOrBatchConfig(const Zstring& filePath, XmlRealConfig& cfg, std //read folder pairs std::set uniqueFolders; - for (XmlIn inPair = in["FolderPairs"]["Pair"]; inPair; inPair.next()) + in["FolderPairs"].visitChildren([&](const XmlIn& inPair) { + assert(*inPair.getName() == "Pair"); + Zstring folderPathPhraseLeft; Zstring folderPathPhraseRight; inPair["Left" ](folderPathPhraseLeft); @@ -139,21 +141,40 @@ void rts::readRealOrBatchConfig(const Zstring& filePath, XmlRealConfig& cfg, std uniqueFolders.insert(folderPathPhraseLeft); uniqueFolders.insert(folderPathPhraseRight); - } + }); + + if (const std::wstring& errors = in.getErrors(); + !errors.empty()) + throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath)), + _("The following XML elements could not be read:") + L'\n' + errors); - try - { - checkXmlMappingErrors(in); //throw FileError - } - catch (const FileError& e) { throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath)) + L"\n\n" + e.toString()); } //--------------------------------------------------------------------------------------- std::erase_if(uniqueFolders, [](const Zstring& str) { return trimCpy(str).empty(); }); - cfg.directories.assign(uniqueFolders.begin(), uniqueFolders.end()); - cfg.commandline = escapeCommandArg(fff::getFreeFileSyncLauncherPath()) + Zstr(' ') + escapeCommandArg(filePath); + + std::wstring warningMsg; + const Zstring ffsLaunchPath = [&]() -> Zstring + { + try + { + return fff::getFreeFileSyncLauncherPath(); //throw FileError + } + catch (const FileError& e) + { + warningMsg = e.toString(); + return Zstr("FreeFileSync"); //fallback: at least give some hint... + } + }(); + + XmlRealConfig cfg + { + .directories = {uniqueFolders.begin(), uniqueFolders.end()}, + .commandline = escapeCommandArg(ffsLaunchPath) + Zstr(' ') + escapeCommandArg(filePath), + }; + return {cfg, warningMsg}; } else - return readConfig(filePath, cfg, warningMsg); //throw FileError + return readConfig(filePath); //throw FileError } @@ -181,11 +202,11 @@ wxLanguage rts::getProgramLanguage() //throw FileError wxLanguage lng = wxLANGUAGE_UNKNOWN; in["Language"].attribute("Code", lng); - try - { - checkXmlMappingErrors(in); //throw FileError - } - catch (const FileError& e) { throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath)) + L"\n\n" + e.toString()); } + + if (const std::wstring& errors = in.getErrors(); + !errors.empty()) + throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath)), + _("The following XML elements could not be read:") + L'\n' + errors); return lng; } diff --git a/FreeFileSync/Source/RealTimeSync/config.h b/FreeFileSync/Source/RealTimeSync/config.h index b7b36514..3afc72f2 100644 --- a/FreeFileSync/Source/RealTimeSync/config.h +++ b/FreeFileSync/Source/RealTimeSync/config.h @@ -20,12 +20,12 @@ struct XmlRealConfig unsigned int delay = 10; }; -void readConfig(const Zstring& filePath, XmlRealConfig& config, std::wstring& warningMsg); //throw FileError +std::pair readConfig(const Zstring& filePath); //throw FileError void writeConfig(const XmlRealConfig& config, const Zstring& filePath); //throw FileError //reuse (some of) FreeFileSync's xml files -void readRealOrBatchConfig(const Zstring& filePath, XmlRealConfig& config, std::wstring& warningMsg); //throw FileError +std::pair readRealOrBatchConfig(const Zstring& filePath); //throw FileError wxLanguage getProgramLanguage(); //throw FileError } diff --git a/FreeFileSync/Source/RealTimeSync/main_dlg.cpp b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp index 796920e5..9eb80b51 100644 --- a/FreeFileSync/Source/RealTimeSync/main_dlg.cpp +++ b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp @@ -120,7 +120,7 @@ MainDialog::MainDialog(const Zstring& cfgFilePath) : try { std::wstring warningMsg; - readRealOrBatchConfig(currentConfigFile, newConfig, warningMsg); //throw FileError + std::tie(newConfig, warningMsg) = readRealOrBatchConfig(currentConfigFile); //throw FileError if (!warningMsg.empty()) showNotificationDialog(this, DialogInfoType::warning, PopupDialogCfg().setDetailInstructions(warningMsg)); @@ -176,9 +176,8 @@ MainDialog::~MainDialog() void MainDialog::onBeforeSystemShutdown() { - try { writeConfig(getConfiguration(), lastRunConfigPath_); } //throw FileError - catch (FileError&) { assert(false); } - warn_static("log, maybe?") + try { writeConfig(getConfiguration(), lastRunConfigPath_); } + catch (const FileError& e) { logExtraError(e.toString()); } } @@ -225,15 +224,15 @@ void MainDialog::onStart(wxCommandEvent& event) switch (runFolderMonitor(currentCfg, ::extractJobName(activeCfgFilePath))) { - case AbortReason::REQUEST_EXIT: + case CancelReason::requestExit: Close(); return; - case AbortReason::REQUEST_GUI: + case CancelReason::requestGui: break; } - Show(); //don't show for AbortReason::REQUEST_EXIT + Show(); //don't show for CancelReason::requestExit Raise(); m_buttonStart->SetFocus(); } @@ -283,7 +282,7 @@ void MainDialog::loadConfig(const Zstring& filepath) try { std::wstring warningMsg; - readRealOrBatchConfig(filepath, newConfig, warningMsg); //throw FileError + std::tie(newConfig, warningMsg) = readRealOrBatchConfig(filepath); //throw FileError if (!warningMsg.empty()) showNotificationDialog(this, DialogInfoType::warning, PopupDialogCfg().setDetailInstructions(warningMsg)); diff --git a/FreeFileSync/Source/RealTimeSync/tray_menu.cpp b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp index ad445b15..2209385c 100644 --- a/FreeFileSync/Source/RealTimeSync/tray_menu.cpp +++ b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp @@ -69,7 +69,7 @@ public: //require polling: bool resumeIsRequested() const { return resumeRequested_; } - bool abortIsRequested () const { return abortRequested_; } + bool abortIsRequested () const { return cancelRequested_; } //during TrayMode::error those two functions are available: void clearShowErrorRequested() { assert(mode_ == TrayMode::error); showErrorMsgRequested_ = false; } @@ -145,7 +145,7 @@ private: contextMenu->AppendSeparator(); wxMenuItem* itemAbort = contextMenu->Append(wxID_ANY, _("&Quit")); - contextMenu->Bind(wxEVT_COMMAND_MENU_SELECTED, [this](wxCommandEvent& event) { abortRequested_ = true; }, itemAbort->GetId()); + contextMenu->Bind(wxEVT_COMMAND_MENU_SELECTED, [this](wxCommandEvent& event) { cancelRequested_ = true; }, itemAbort->GetId()); return contextMenu; //ownership transferred to caller } @@ -165,7 +165,7 @@ private: } bool resumeRequested_ = false; - bool abortRequested_ = false; + bool cancelRequested_ = false; bool showErrorMsgRequested_ = false; TrayMode mode_ = TrayMode::waiting; @@ -182,8 +182,8 @@ private: struct AbortMonitoring //exception class { - AbortMonitoring(AbortReason reasonCode) : reasonCode_(reasonCode) {} - AbortReason reasonCode_; + AbortMonitoring(CancelReason reasonCode) : reasonCode_(reasonCode) {} + CancelReason reasonCode_; }; @@ -208,10 +208,10 @@ public: //advantage of polling vs callbacks: we can throw exceptions! if (trayObj_->resumeIsRequested()) - throw AbortMonitoring(AbortReason::REQUEST_GUI); + throw AbortMonitoring(CancelReason::requestGui); if (trayObj_->abortIsRequested()) - throw AbortMonitoring(AbortReason::REQUEST_EXIT); + throw AbortMonitoring(CancelReason::requestExit); } void setMode(TrayMode m, const Zstring& missingFolderPath) { trayObj_->setMode(m, missingFolderPath); } @@ -227,7 +227,7 @@ private: } -rts::AbortReason rts::runFolderMonitor(const XmlRealConfig& config, const wxString& jobname) +rts::CancelReason rts::runFolderMonitor(const XmlRealConfig& config, const wxString& jobname) { std::vector dirNamesNonFmt = config.directories; std::erase_if(dirNamesNonFmt, [](const Zstring& str) { return trimCpy(str).empty(); }); //remove empty entries WITHOUT formatting paths yet! @@ -235,7 +235,7 @@ rts::AbortReason rts::runFolderMonitor(const XmlRealConfig& config, const wxStri if (dirNamesNonFmt.empty()) { showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setMainInstructions(_("A folder input field is empty."))); - return AbortReason::REQUEST_GUI; + return CancelReason::requestGui; } const Zstring cmdLine = trimCpy(config.commandline); @@ -243,7 +243,7 @@ rts::AbortReason rts::runFolderMonitor(const XmlRealConfig& config, const wxStri if (cmdLine.empty()) { showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setMainInstructions(replaceCpy(_("Command %x failed."), L"%x", fmtPath(cmdLine)))); - return AbortReason::REQUEST_GUI; + return CancelReason::requestGui; } @@ -251,10 +251,8 @@ rts::AbortReason rts::runFolderMonitor(const XmlRealConfig& config, const wxStri auto executeExternalCommand = [&](const Zstring& changedItemPath, const std::wstring& actionName) //throw FileError { - warn_static("maybe not a good idea!? job for execve? https://rachelbythebay.com/w/2017/01/30/env/") ::wxSetEnv(L"change_path", utfTo(changedItemPath)); //crude way to report changed file ::wxSetEnv(L"change_action", actionName); // - warn_static("caveat: %change_path% is not subsituted 'thanks' to our *static* env variables! luckily there's a workaround") //https://freefilesync.org/forum/viewtopic.php?t=10160 auto cmdLineExp = expandMacros(cmdLine); try @@ -296,7 +294,7 @@ rts::AbortReason rts::runFolderMonitor(const XmlRealConfig& config, const wxStri return; case ConfirmationButton::cancel: - throw AbortMonitoring(AbortReason::REQUEST_GUI); + throw AbortMonitoring(CancelReason::requestGui); } std::this_thread::sleep_for(UI_UPDATE_INTERVAL); } @@ -310,7 +308,7 @@ rts::AbortReason rts::runFolderMonitor(const XmlRealConfig& config, const wxStri reportError, // UI_UPDATE_INTERVAL / 2); assert(false); - return AbortReason::REQUEST_GUI; + return CancelReason::requestGui; } catch (const AbortMonitoring& ab) { diff --git a/FreeFileSync/Source/RealTimeSync/tray_menu.h b/FreeFileSync/Source/RealTimeSync/tray_menu.h index 9c18fa08..cf8b4341 100644 --- a/FreeFileSync/Source/RealTimeSync/tray_menu.h +++ b/FreeFileSync/Source/RealTimeSync/tray_menu.h @@ -13,12 +13,12 @@ namespace rts { -enum class AbortReason +enum class CancelReason { - REQUEST_GUI, - REQUEST_EXIT + requestGui, + requestExit }; -AbortReason runFolderMonitor(const XmlRealConfig& config, const wxString& jobname); //jobname may be empty +CancelReason runFolderMonitor(const XmlRealConfig& config, const wxString& jobname); //jobname may be empty } #endif //TRAY_MENU_H_3967857420987534253245 diff --git a/FreeFileSync/Source/afs/abstract.cpp b/FreeFileSync/Source/afs/abstract.cpp index c7f20a92..11e5626d 100644 --- a/FreeFileSync/Source/afs/abstract.cpp +++ b/FreeFileSync/Source/afs/abstract.cpp @@ -139,9 +139,8 @@ AFS::FileCopyResult AFS::copyFileAsStream(const AfsPath& sourcePath, const Strea const FinalizeResult finResult = streamOut->finalize(notifyUnbufferedWrite); //throw FileError, X - ZEN_ON_SCOPE_FAIL(try { removeFilePlain(targetPath); /*throw FileError*/ } - catch (FileError&) {}); //after finalize(): not guarded by ~AFS::OutputStream() anymore! - warn_static("log it!") + ZEN_ON_SCOPE_FAIL(try { removeFilePlain(targetPath); } + catch (const FileError& e) { logExtraError(e.toString()); }); //after finalize(): not guarded by ~AFS::OutputStream() anymore! //catch file I/O bugs + read/write conflicts: (note: different check than inside AFS::OutputStream::finalize() => checks notifyUnbufferedIO()!) if (totalBytesWritten != totalBytesRead) @@ -155,7 +154,7 @@ AFS::FileCopyResult AFS::copyFileAsStream(const AfsPath& sourcePath, const Strea .sourceFilePrint = attrSourceNew.filePrint, .targetFilePrint = finResult.filePrint, .errorModTime = finResult.errorModTime, - /* Failing to set modification time is not a serious problem from synchronization perspective (treat like external update) + /* Failing to set modification time is not a fatal error from synchronization perspective (treat like external update) => Support additional scenarios: - GVFS failing to set modTime for FTP: https://freefilesync.org/forum/viewtopic.php?t=2372 - GVFS failing to set modTime for MTP: https://freefilesync.org/forum/viewtopic.php?t=2803 @@ -207,15 +206,15 @@ AFS::FileCopyResult AFS::copyFileTransactional(const AbstractPath& sourcePath, c const Zstring& shortGuid = printNumber(Zstr("%04x"), static_cast(getCrc16(generateGUID()))); - const AbstractPath targetPathTmp = appendRelPath(*parentPath, tmpName + Zstr('~') + shortGuid + TEMP_FILE_ENDING); + const AbstractPath targetPathTmp = appendRelPath(*parentPath, tmpName + Zstr('-') + //don't use '~': some FTP servers *silently* replace it with '_'! + shortGuid + TEMP_FILE_ENDING); //------------------------------------------------------------------------------------------- const FileCopyResult result = copyFilePlain(targetPathTmp); //throw FileError, ErrorFileLocked //transactional behavior: ensure cleanup; not needed before copyFilePlain() which is already transactional ZEN_ON_SCOPE_FAIL( try { removeFilePlain(targetPathTmp); } - catch (FileError&) {}); - warn_static("log it!") + catch (const FileError& e) { logExtraError(e.toString()); }); //have target file deleted (after read access on source and target has been confirmed) => allow for almost transactional overwrite if (onDeleteTargetFile) diff --git a/FreeFileSync/Source/afs/abstract.h b/FreeFileSync/Source/afs/abstract.h index 72161695..4a94d1cf 100644 --- a/FreeFileSync/Source/afs/abstract.h +++ b/FreeFileSync/Source/afs/abstract.h @@ -263,7 +263,7 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t //Note: it MAY happen that copyFileTransactional() leaves temp files behind, e.g. temporary network drop. // => clean them up at an appropriate time (automatically set sync directions to delete them). They have the following ending: - static inline const ZstringView TEMP_FILE_ENDING = Zstr(".ffs_tmp"); //don't use Zstring as global constant: avoid static initialization order problem in global namespace! + static inline constexpr ZstringView TEMP_FILE_ENDING = Zstr(".ffs_tmp"); //don't use Zstring as global constant: avoid static initialization order problem in global namespace! // caveat: ending is hard-coded by RealTimeSync struct FileCopyResult @@ -474,8 +474,7 @@ AbstractFileSystem::OutputStream::~OutputStream() //- needed for Google Drive: e.g. user might cancel during OutputStreamImpl::finalize(), just after file was written transactionally //- also for Native: setFileTime() may fail *after* FileOutput::finalize() try { AbstractFileSystem::removeFilePlain(filePath_); /*throw FileError*/ } - catch (zen::FileError&) {} - warn_static("log on error") + catch (const zen::FileError& e) { zen::logExtraError(e.toString()); } } diff --git a/FreeFileSync/Source/afs/concrete.cpp b/FreeFileSync/Source/afs/concrete.cpp index ff2fd6be..b7001832 100644 --- a/FreeFileSync/Source/afs/concrete.cpp +++ b/FreeFileSync/Source/afs/concrete.cpp @@ -23,12 +23,11 @@ void fff::initAfs(const AfsConfig& cfg) } -std::wstring /*warningMsg*/ fff::teardownAfs() +void fff::teardownAfs() { - std::wstring warningMsg = gdriveTeardown(); + gdriveTeardown(); sftpTeardown(); ftpTeardown(); - return warningMsg; } diff --git a/FreeFileSync/Source/afs/concrete.h b/FreeFileSync/Source/afs/concrete.h index c627e58b..81e29103 100644 --- a/FreeFileSync/Source/afs/concrete.h +++ b/FreeFileSync/Source/afs/concrete.h @@ -17,7 +17,7 @@ struct AfsConfig Zstring configDirPath; //directory to store AFS-specific files }; void initAfs(const AfsConfig& cfg); -[[nodiscard]] std::wstring /*warningMsg*/ teardownAfs(); +void teardownAfs(); AbstractPath getNullPath(); AbstractPath createAbstractPath(const Zstring& itemPathPhrase); //noexcept diff --git a/FreeFileSync/Source/afs/ftp.cpp b/FreeFileSync/Source/afs/ftp.cpp index bf21cf4d..283ca3ec 100644 --- a/FreeFileSync/Source/afs/ftp.cpp +++ b/FreeFileSync/Source/afs/ftp.cpp @@ -35,7 +35,7 @@ const size_t FTP_BLOCK_SIZE_UPLOAD = 64 * 1024; //libcurl requests blocks of 6 const size_t FTP_STREAM_BUFFER_SIZE = 1024 * 1024; //unit: [byte] //stream buffer should be big enough to facilitate prefetching during alternating read/write operations => e.g. see serialize.h::unbufferedStreamCopy() -const ZstringView ftpPrefix = Zstr("ftp:"); +constexpr ZstringView ftpPrefix = Zstr("ftp:"); enum class ServerEncoding @@ -331,10 +331,16 @@ public: else ::curl_easy_reset(easyHandle_); - std::vector options; + auto setCurlOption = [easyHandle = easyHandle_](const CurlOption& curlOpt) //throw SysError + { + if (const CURLcode rc = ::curl_easy_setopt(easyHandle, curlOpt.option, curlOpt.value); + rc != CURLE_OK) + throw SysError(formatSystemError("curl_easy_setopt(" + numberTo(static_cast(curlOpt.option)) + ")", + formatCurlStatusCode(rc), utfTo(::curl_easy_strerror(rc)))); + }; char curlErrorBuf[CURL_ERROR_SIZE] = {}; - options.emplace_back(CURLOPT_ERRORBUFFER, curlErrorBuf); + setCurlOption({CURLOPT_ERRORBUFFER, curlErrorBuf}); //throw SysError std::string headerData; curl_write_callback onHeaderReceived = [](/*const*/ char* buffer, size_t size, size_t nitems, void* callbackData) @@ -343,50 +349,59 @@ public: output.append(buffer, size * nitems); return size * nitems; }; - options.emplace_back(CURLOPT_HEADERDATA, &headerData); - options.emplace_back(CURLOPT_HEADERFUNCTION, onHeaderReceived); + setCurlOption({CURLOPT_HEADERDATA, &headerData}); //throw SysError + setCurlOption({CURLOPT_HEADERFUNCTION, onHeaderReceived}); //throw SysError - //lifetime: keep alive until after curl_easy_setopt() below - const std::string curlPath = getCurlUrlPath(itemPath, isDir); //throw SysError - options.emplace_back(CURLOPT_URL, curlPath.c_str()); + setCurlOption({CURLOPT_URL, getCurlUrlPath(itemPath, isDir).c_str()}); //throw SysError assert(pathMethod != CURLFTPMETHOD_MULTICWD); //too slow! - options.emplace_back(CURLOPT_FTP_FILEMETHOD, pathMethod); - - //ANSI or UTF encoding? - // "modern" FTP servers (implementing RFC 2640) have UTF8 enabled by default => pray and hope for the best. - // What about ANSI-FTP servers and "Microsoft FTP Service" which requires "OPTS UTF8 ON"? => *psh* - // CURLOPT_PREQUOTE to the rescue? Nope, issued long after USER/PASS - const auto& username = utfTo(sessionCfg_.deviceId.username); - const auto& password = utfTo(sessionCfg_.password); - if (!username.empty()) //else: libcurl will default to CURL_DEFAULT_USER("anonymous") and CURL_DEFAULT_PASSWORD("ftp@example.com") - { - options.emplace_back(CURLOPT_USERNAME, username.c_str()); - options.emplace_back(CURLOPT_PASSWORD, password.c_str()); //curious: libcurl will *not* default to CURL_DEFAULT_USER when setting password but no username + setCurlOption({CURLOPT_FTP_FILEMETHOD, pathMethod}); //throw SysError + + if (!sessionCfg_.deviceId.username.empty()) //else: libcurl will default to CURL_DEFAULT_USER("anonymous") and CURL_DEFAULT_PASSWORD("ftp@example.com") + { + //ANSI or UTF encoding? + // "modern" FTP servers (implementing RFC 2640) have UTF8 enabled by default => pray and hope for the best. + // What about ANSI-FTP servers and "Microsoft FTP Service" which requires "OPTS UTF8 ON"? => *psh* + // CURLOPT_PREQUOTE to the rescue? Nope, issued long after USER/PASS + setCurlOption({CURLOPT_USERNAME, utfTo(sessionCfg_.deviceId.username).c_str()}); //throw SysError + setCurlOption({CURLOPT_PASSWORD, utfTo(sessionCfg_.password ).c_str()}); //throw SysError + //curious: libcurl will *not* default to CURL_DEFAULT_USER when setting password but no username } - options.emplace_back(CURLOPT_PORT, sessionCfg_.deviceId.port); + setCurlOption({CURLOPT_PORT, sessionCfg_.deviceId.port}); //throw SysError + + //thread-safety: https://curl.haxx.se/libcurl/c/threadsafe.html + setCurlOption({CURLOPT_NOSIGNAL, 1}); //throw SysError + + //allow PASV IP: some FTP servers really use IP different from control connection + setCurlOption({CURLOPT_FTP_SKIP_PASV_IP, 0}); //throw SysError + //let's not hold our breath until Curl adds a reasonable PASV handling => patch libcurl accordingly! + //https://github.com/curl/curl/issues/1455 + //https://github.com/curl/curl/pull/1470 + //support broken servers like this one: https://freefilesync.org/forum/viewtopic.php?t=4301 - options.emplace_back(CURLOPT_NOSIGNAL, 1); //thread-safety: https://curl.haxx.se/libcurl/c/threadsafe.html const std::shared_ptr timeoutSec = timeoutSec_.lock(); assert(timeoutSec); if (!timeoutSec) throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] FtpSession: Timeout duration was not set."); - options.emplace_back(CURLOPT_CONNECTTIMEOUT, *timeoutSec); + setCurlOption({CURLOPT_CONNECTTIMEOUT, *timeoutSec}); //throw SysError //CURLOPT_TIMEOUT: "Since this puts a hard limit for how long time a request is allowed to take, it has limited use in dynamic use cases with varying transfer times." - options.emplace_back(CURLOPT_LOW_SPEED_TIME, *timeoutSec); - options.emplace_back(CURLOPT_LOW_SPEED_LIMIT, 1); //[bytes], can't use "0" which means "inactive", so use some low number + setCurlOption({CURLOPT_LOW_SPEED_TIME, *timeoutSec}); //throw SysError + setCurlOption({CURLOPT_LOW_SPEED_LIMIT, 1}); //throw SysError + ; //[bytes], can't use "0" which means "inactive", so use some low number //unlike CURLOPT_TIMEOUT, this one is NOT a limit on the total transfer time - options.emplace_back(CURLOPT_SERVER_RESPONSE_TIMEOUT, *timeoutSec); //== alias of CURLOPT_SERVER_RESPONSE_TIMEOUT + setCurlOption({CURLOPT_SERVER_RESPONSE_TIMEOUT, *timeoutSec}); //throw SysError + //== alias of CURLOPT_SERVER_RESPONSE_TIMEOUT //CURLOPT_ACCEPTTIMEOUT_MS? => only relevant for "active" FTP connections //long-running file uploads require us to send keep-alives for the TCP control connection: https://freefilesync.org/forum/viewtopic.php?t=6928 - options.emplace_back(CURLOPT_TCP_KEEPALIVE, 1); //=> CURLOPT_TCP_KEEPIDLE (=delay) and CURLOPT_TCP_KEEPINTVL both default to 60 sec + setCurlOption({CURLOPT_TCP_KEEPALIVE, 1}); //throw SysError + //=> CURLOPT_TCP_KEEPIDLE (=delay) and CURLOPT_TCP_KEEPINTVL both default to 60 sec std::optional socketException; @@ -409,8 +424,8 @@ public: return (*clientp)(curlfd, purpose); //free this poor little C-API from its shackles and redirect to a proper lambda }; - options.emplace_back(CURLOPT_SOCKOPTFUNCTION, onSocketCreateWrapper); - options.emplace_back(CURLOPT_SOCKOPTDATA, &onSocketCreate); + setCurlOption({CURLOPT_SOCKOPTFUNCTION, onSocketCreateWrapper}); //throw SysError + setCurlOption({CURLOPT_SOCKOPTDATA, &onSocketCreate}); //throw SysError //Use share interface? https://curl.haxx.se/libcurl/c/libcurl-share.html //perf test, 4 and 8 parallel threads: @@ -475,37 +490,35 @@ public: return cs; }(); //CURLSHcode ::curl_share_cleanup(curlShare); - options.emplace_back(CURLOPT_SHARE, curlShare); + setCurlOption({CURLOPT_SHARE, curlShare}); //throw SysError #endif //TODO: FTP option to require certificate checking? #if 0 - options.emplace_back(CURLOPT_CAINFO, "cacert.pem"); //hopefully latest version from https://curl.haxx.se/docs/caextract.html + setCurlOption({CURLOPT_CAINFO, "cacert.pem"}); //throw SysError + //hopefully latest version from https://curl.haxx.se/docs/caextract.html //libcurl forwards this char-string to OpenSSL as is, which (thank god) accepts UTF8 #else - options.emplace_back(CURLOPT_CAINFO, 0); //be explicit: "even when [CURLOPT_SSL_VERIFYPEER] is disabled [...] curl may still load the certificate file specified in CURLOPT_CAINFO." + setCurlOption({CURLOPT_CAINFO, 0}); //throw SysError + //be explicit: "even when [CURLOPT_SSL_VERIFYPEER] is disabled [...] curl may still load the certificate file specified in CURLOPT_CAINFO." //check if server certificate can be trusted? (Default: 1L) // => may fail with: "CURLE_PEER_FAILED_VERIFICATION: SSL certificate problem: certificate has expired" - options.emplace_back(CURLOPT_SSL_VERIFYPEER, 0); + setCurlOption({CURLOPT_SSL_VERIFYPEER, 0}); //throw SysError //check that server name matches the name in the certificate? (Default: 2L) // => may fail with: "CURLE_PEER_FAILED_VERIFICATION: SSL: no alternative certificate subject name matches target host name 'freefilesync.org'" - options.emplace_back(CURLOPT_SSL_VERIFYHOST, 0); + setCurlOption({CURLOPT_SSL_VERIFYHOST, 0}); //throw SysError #endif if (sessionCfg_.useTls) //https://tools.ietf.org/html/rfc4217 { - options.emplace_back(CURLOPT_USE_SSL, CURLUSESSL_ALL); //require SSL for both control and data - options.emplace_back(CURLOPT_FTPSSLAUTH, CURLFTPAUTH_TLS); //try TLS first, then SSL (currently: CURLFTPAUTH_DEFAULT == CURLFTPAUTH_SSL) + //require SSL for both control and data: + setCurlOption({CURLOPT_USE_SSL, CURLUSESSL_ALL}); //throw SysError + //try TLS first, then SSL (currently: CURLFTPAUTH_DEFAULT == CURLFTPAUTH_SSL): + setCurlOption({CURLOPT_FTPSSLAUTH, CURLFTPAUTH_TLS}); //throw SysError } - //let's not hold our breath until Curl adds a reasonable PASV handling => patch libcurl accordingly! - //https://github.com/curl/curl/issues/1455 - //https://github.com/curl/curl/pull/1470 - //support broken servers like this one: https://freefilesync.org/forum/viewtopic.php?t=4301 - - append(options, extraOptions); - - applyCurlOptions(easyHandle_, options); //throw SysError + for (const CurlOption& option : extraOptions) + setCurlOption(option); //throw SysError //======================================================================================================= const CURLcode rcPerf = ::curl_easy_perform(easyHandle_); @@ -857,11 +870,11 @@ private: if (!featureCache_) { static constinit FunStatGlobal> globalServerFeatures; - globalServerFeatures.initOnce([] { return std::make_unique>(); }); + globalServerFeatures.setOnce([] { return std::make_unique>(); }); const auto sf = globalServerFeatures.get(); if (!sf) - throw SysError(formatSystemError("FtpSession::getFeatureSupport", L"", L"Function call not allowed during init/shutdown.")); + throw SysError(formatSystemError("FtpSession::getFeatureSupport", L"", L"Function call not allowed during application shutdown.")); sf->access([&](const FeatureList& featList) { @@ -2210,7 +2223,7 @@ private: const Zstring itemName = getItemName(itemPath); assert(!itemName.empty()); //is the underlying file system case-sensitive? we don't know => assume "case-sensitive" - //all path components (except the base folder part!) can be expected to have the right case anyway after traversal + //all path components (except the base folder part!) can be expected to have the right case anyway after directory traversal for (const FtpItem& item : items) if (item.itemName == itemName) return item.type; diff --git a/FreeFileSync/Source/afs/gdrive.cpp b/FreeFileSync/Source/afs/gdrive.cpp index 668a45db..25c0255b 100644 --- a/FreeFileSync/Source/afs/gdrive.cpp +++ b/FreeFileSync/Source/afs/gdrive.cpp @@ -82,7 +82,7 @@ const size_t GDRIVE_BLOCK_SIZE_UPLOAD = 64 * 1024; //libcurl requests blocks const size_t GDRIVE_STREAM_BUFFER_SIZE = 1024 * 1024; //unit: [byte] //stream buffer should be big enough to facilitate prefetching during alternating read/write operations => e.g. see serialize.h::unbufferedStreamCopy() -const Zchar gdrivePrefix[] = Zstr("gdrive:"); +constexpr ZstringView gdrivePrefix = Zstr("gdrive:"); const char gdriveFolderMimeType [] = "application/vnd.google-apps.folder"; const char gdriveShortcutMimeType[] = "application/vnd.google-apps.shortcut"; //= symbolic link! @@ -488,7 +488,6 @@ GdriveAccessInfo gdriveAuthorizeAccess(const std::string& gdriveLoginHint, const if (testSocket == invalidSocket) THROW_LAST_SYS_ERROR_WSA("socket"); ZEN_ON_SCOPE_FAIL(closeSocket(testSocket)); - warn_static("log on error!") if (::bind(testSocket, ai.ai_addr, static_cast(ai.ai_addrlen)) != 0) THROW_LAST_SYS_ERROR_WSA("bind"); @@ -893,7 +892,7 @@ std::vector getStarredFolders(const GdriveAccess& access) if (!itemId || itemId->empty() || !itemName || itemName->empty()) throw SysError(formatGdriveErrorRaw(serializeJson(childVal))); - starredFolders.push_back({std::move(*itemId), + starredFolders.push_back({*itemId, utfTo(*itemName), driveId ? *driveId : ""}); } @@ -2884,8 +2883,7 @@ private: { try //let's not lose Google Drive data due to unexpected system shutdown: { saveActiveSessions(); } //throw FileError - catch (FileError&) { assert(false); } - warn_static("at least log on failure!") + catch (const FileError& e) { logExtraError(e.toString()); } }); }; //========================================================================================== @@ -3949,23 +3947,20 @@ void fff::gdriveInit(const Zstring& configDirPath, const Zstring& caCertFilePath } -std::wstring /*warningMsg*/ fff::gdriveTeardown() +void fff::gdriveTeardown() { - std::wstring warningMsg; try //don't use ~GdrivePersistentSessions() to save! Might never happen, e.g. detached thread waiting for Google Drive authentication; terminated on exit! { if (const std::shared_ptr gps = globalGdriveSessions.get()) gps->saveActiveSessions(); //throw FileError } - catch (const FileError& e) { warningMsg = e.toString(); } + catch (const FileError& e) { logExtraError(e.toString()); } assert(globalGdriveSessions.get()); globalGdriveSessions.set(nullptr); assert(globalHttpSessionManager.get()); globalHttpSessionManager.set(nullptr); - - return warningMsg; } diff --git a/FreeFileSync/Source/afs/gdrive.h b/FreeFileSync/Source/afs/gdrive.h index a5ebbba8..78f7d31c 100644 --- a/FreeFileSync/Source/afs/gdrive.h +++ b/FreeFileSync/Source/afs/gdrive.h @@ -16,7 +16,7 @@ AbstractPath createItemPathGdrive (const Zstring& itemPathPhrase); //noexc void gdriveInit(const Zstring& configDirPath, //directory to store Google-Drive-specific files const Zstring& caCertFilePath); //cacert.pem -[[nodiscard]] std::wstring /*warningMsg*/ gdriveTeardown(); +void gdriveTeardown(); //------------------------------------------------------- diff --git a/FreeFileSync/Source/afs/init_curl_libssh2.cpp b/FreeFileSync/Source/afs/init_curl_libssh2.cpp index c1cbf754..40026a9a 100644 --- a/FreeFileSync/Source/afs/init_curl_libssh2.cpp +++ b/FreeFileSync/Source/afs/init_curl_libssh2.cpp @@ -26,12 +26,8 @@ void libsshCurlUnifiedInit() libcurlInit(); //includes WSAStartup() also needed by libssh2 - [[maybe_unused]] const int rc2 = ::libssh2_init(0); - assert(rc2 == 0); //libssh2 unconditionally returns 0 => why then have a return value in first place??? - /* we need libssh2's crypto init: - - there is other OpenSSL-related initialization which might be needed (and hopefully won't hurt...) */ - - warn_static("log on error") + [[maybe_unused]] const int rc = ::libssh2_init(0); //includes OpenSSL-related initialization which might be needed (and hopefully won't hurt...) + assert(rc == 0); //libssh2 unconditionally returns 0 => why then have a return value in first place??? } diff --git a/FreeFileSync/Source/afs/native.cpp b/FreeFileSync/Source/afs/native.cpp index 1c23d688..46b2bb4d 100644 --- a/FreeFileSync/Source/afs/native.cpp +++ b/FreeFileSync/Source/afs/native.cpp @@ -566,8 +566,7 @@ private: //at this point we know we created a new file, so it's fine to delete it for cleanup! ZEN_ON_SCOPE_FAIL(try { zen::removeFilePlain(nativePathTarget); } - catch (FileError&) {}); - warn_static("log it!") + catch (const FileError& e) { logExtraError(e.toString()); }); if (copyFilePermissions) copyItemPermissions(getNativePath(sourcePath), nativePathTarget, ProcSymlink::follow); //throw FileError @@ -613,9 +612,8 @@ private: initComForThread(); //throw FileError zen::copySymlink(getNativePath(sourcePath), targetPathNative); //throw FileError - ZEN_ON_SCOPE_FAIL(try { zen::removeSymlinkPlain(targetPathNative); /*throw FileError*/ } - catch (FileError&) {}); - warn_static("log it!") + ZEN_ON_SCOPE_FAIL(try { zen::removeSymlinkPlain(targetPathNative); } + catch (const FileError& e) { logExtraError(e.toString()); }); if (copyFilePermissions) copyItemPermissions(getNativePath(sourcePath), targetPathNative, ProcSymlink::asLink); //throw FileError diff --git a/FreeFileSync/Source/afs/sftp.cpp b/FreeFileSync/Source/afs/sftp.cpp index 1221db24..4787b668 100644 --- a/FreeFileSync/Source/afs/sftp.cpp +++ b/FreeFileSync/Source/afs/sftp.cpp @@ -47,7 +47,7 @@ OpenSSL supports the same ciphers like WinCNG plus the following: cast128-cbc blowfish-cbc */ -const ZstringView sftpPrefix = Zstr("sftp:"); +constexpr ZstringView sftpPrefix = Zstr("sftp:"); constexpr std::chrono::seconds SFTP_SESSION_MAX_IDLE_TIME (20); constexpr std::chrono::seconds SFTP_SESSION_CLEANUP_INTERVAL (4); //facilitate default of 5-seconds delay for error retry @@ -658,19 +658,23 @@ private: { for (SftpChannelInfo& ci : sftpChannels_) //ci.nbInfo.commandPending? => may "legitimately" happen when an SFTP command times out - ::libssh2_sftp_shutdown(ci.sftpChannel); + if (::libssh2_sftp_shutdown(ci.sftpChannel) != LIBSSH2_ERROR_NONE) + assert(false); if (sshSession_) { + //*INDENT-OFF* if (!nbInfo_.commandPending && std::all_of(sftpChannels_.begin(), sftpChannels_.end(), - [](const SftpChannelInfo& ci) { return !ci.nbInfo.commandPending; })) - ::libssh2_session_disconnect(sshSession_, "FreeFileSync says \"bye\"!"); //= server notification only! no local cleanup apparently + [](const SftpChannelInfo& ci) { return !ci.nbInfo.commandPending; })) + if (::libssh2_session_disconnect(sshSession_, "FreeFileSync says \"bye\"!") != LIBSSH2_ERROR_NONE) //= server notification only! no local cleanup apparently + assert(false); //else: avoid further stress on the broken SSH session and take French leave //nbInfo_.commandPending? => have to clean up, no matter what! - ::libssh2_session_free(sshSession_); + if (::libssh2_session_free(sshSession_) != LIBSSH2_ERROR_NONE) + assert(false); + //*INDENT-ON* } - warn_static("log on error!") } std::wstring formatLastSshError(const char* functionName, LIBSSH2_SFTP* sftpChannel /*optional*/) const @@ -787,19 +791,9 @@ public: return session_->tryNonBlocking(channelNo, commandStartTime, functionName, sftpCommand, timeoutSec_); //throw SysError, SysErrorSftpProtocol } - void finishBlocking(size_t channelNo, std::chrono::steady_clock::time_point commandStartTime, const char* functionName, - const std::function& sftpCommand /*noexcept!*/) + void waitForTraffic() //throw SysError { - for (;;) - try - { - if (session_->tryNonBlocking(channelNo, commandStartTime, functionName, sftpCommand, timeoutSec_)) //throw SysError, SysErrorSftpProtocol - return; - else //pending - SshSession::waitForTraffic({session_.get()}, timeoutSec_); //throw SysError - } - catch (SysError&) { return; } - warn_static("log on error!") + SshSession::waitForTraffic({session_.get()}, timeoutSec_); //throw SysError } size_t getSftpChannelCount() const { return session_->getSftpChannelCount(); } @@ -1148,8 +1142,7 @@ std::vector getDirContentFlat(const SftpLogin& login, const AfsPath& d runSftpCommand(login, "libssh2_sftp_closedir", //throw SysError, SysErrorSftpProtocol [&](const SshSession::Details& sd) { return ::libssh2_sftp_closedir(dirHandle); }); //noexcept! } - catch (SysError&) {}); - warn_static("log on error!") + catch (const SysError& e) { logExtraError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(getSftpDisplayPath(login, dirPath))) + L"\n\n" + e.toString()); }); std::vector output; for (;;) @@ -1340,8 +1333,7 @@ struct InputStreamSftp : public AFS::InputStream session_->executeBlocking("libssh2_sftp_close", //throw SysError, SysErrorSftpProtocol [&](const SshSession::Details& sd) { return ::libssh2_sftp_close(fileHandle_); }); //noexcept! } - catch (const SysError&) {} - warn_static("log on error?") + catch (const SysError& e) { logExtraError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(displayPath_)) + L"\n\n" + e.toString()); } } size_t getBlockSize() override { return SFTP_OPTIMAL_BLOCK_SIZE_READ; } //throw (FileError); non-zero block size is AFS contract! @@ -1364,8 +1356,7 @@ struct InputStreamSftp : public AFS::InputStream return static_cast(bytesRead); }); - if (makeUnsigned(bytesRead) > bytesToRead) //better safe than sorry - throw SysError(formatSystemError("libssh2_sftp_read", L"", L"Buffer overflow.")); //user should never see this + ASSERT_SYSERROR(makeUnsigned(bytesRead) <= bytesToRead); //better safe than sorry (user should never see this) } catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(displayPath_)), e.toString()); } @@ -1424,8 +1415,7 @@ struct OutputStreamSftp : public AFS::OutputStreamImpl { close(); //throw FileError } - catch (FileError&) {} - warn_static("log!?") + catch (const FileError& e) { logExtraError(e.toString()); } } size_t getBlockSize() override { return SFTP_OPTIMAL_BLOCK_SIZE_WRITE; } //throw (FileError) @@ -1450,8 +1440,7 @@ struct OutputStreamSftp : public AFS::OutputStreamImpl return static_cast(bytesWritten); }); - if (makeUnsigned(bytesWritten) > bytesToWrite) //better safe than sorry - throw SysError(formatSystemError("libssh2_sftp_write", L"", L"Buffer overflow.")); + ASSERT_SYSERROR(makeUnsigned(bytesWritten) <= bytesToWrite); //better safe than sorry } catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(displayPath_)), e.toString()); } @@ -1755,8 +1744,7 @@ private: runSftpCommand(sftpFs.login_, "libssh2_sftp_readlink", //throw SysError, SysErrorSftpProtocol [&](const SshSession::Details& sd) { return rc = ::libssh2_sftp_readlink(sd.sftpChannel, getLibssh2Path(linkPath), buf.data(), buf.size()); }); //noexcept! - if (makeUnsigned(rc) > buf.size()) //better safe than sorry - throw SysError(formatSystemError("libssh2_sftp_readlink", L"", L"Buffer overflow.")); //user should never see this + ASSERT_SYSERROR(makeUnsigned(rc) <= buf.size()); //better safe than sorry } catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(sftpFs.getDisplayPath(linkPath))), e.toString()); } diff --git a/FreeFileSync/Source/application.cpp b/FreeFileSync/Source/application.cpp index 1dad6e07..272fed83 100644 --- a/FreeFileSync/Source/application.cpp +++ b/FreeFileSync/Source/application.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -88,35 +89,15 @@ void showSyntaxHelp() _("global config file:") + L'\n' + _("Path to an alternate GlobalSettings.xml file."))); } -} -void Application::notifyAppError(const std::wstring& msg, FfsExitCode rc) +void notifyAppError(const std::wstring& msg) { - raiseExitCode(exitCode_, rc); - - const std::wstring msgType = [&] - { - switch (rc) - { - //*INDENT-OFF* - case FfsExitCode::success: break; - case FfsExitCode::warning: return _("Warning"); - case FfsExitCode::error: return _("Error"); - case FfsExitCode::aborted: return _("Error"); - case FfsExitCode::exception: return _("An exception occurred"); - //*INDENT-ON* - } - assert(false); - return std::wstring{}; - }(); - //error handling strategy unknown and no sync log output available at this point! - std::cerr << utfTo(msgType + L": " + msg) + '\n'; + std::cerr << utfTo(_("Error") + L": " + msg) + '\n'; //alternative0: std::wcerr: cannot display non-ASCII at all, so why does it exist??? //alternative1: wxSafeShowMessage => NO console output on Debian x86, WTF! //alternative2: wxMessageBox() => works, but we probably shouldn't block during command line usage - - warn_static(" show message box on linux/macos, too!?") +} } //################################################################################################################## @@ -125,10 +106,23 @@ bool Application::OnInit() { //do not call wxApp::OnInit() to avoid using wxWidgets command line parser + const auto now = std::chrono::system_clock::now(); //e.g. "ErrorLog 2023-07-05 105207.073.xml" + initExtraLog([logFilePath = appendPath(getConfigDirPath(), Zstr("ErrorLog ") + + formatTime(Zstr("%Y-%m-%d %H%M%S"), getLocalTime(std::chrono::system_clock::to_time_t(now))) + Zstr('.') + + printNumber(Zstr("%03d"), //[ms] should yield a fairly unique name + static_cast(std::chrono::duration_cast(now.time_since_epoch()).count() % 1000)) + + Zstr(".xml"))](const ErrorLog& log) + { + try //don't call functions depending on global state (which might be destroyed already!) + { + saveErrorLog(log, logFilePath); //throw FileError + } + catch (const FileError& e) { assert(false); notifyAppError(e.toString()); } + }); + //parallel xBRZ-scaling! => run as early as possible try { imageResourcesInit(appendPath(getResourceDirPath(), Zstr("Icons.zip"))); } - catch (const FileError& e) { notifyAppError(e.toString(), FfsExitCode::warning); } - //errors are not really critical in this context + catch (const FileError& e) { logExtraError(e.toString()); } //not critical in this context //GTK should already have been initialized by wxWidgets (see \src\gtk\app.cpp:wxApp::Initialize) #if GTK_MAJOR_VERSION == 2 @@ -141,7 +135,7 @@ bool Application::OnInit() // std::cerr << utfTo(formatSystemError("setenv(GIO_USE_VFS)", errno)) + '\n'; // //=> work around 2: - g_vfs_get_default(); //returns unowned GVfs* + [[maybe_unused]] GVfs* defaultFs = ::g_vfs_get_default(); //not owned by us! //no such issue on GTK3! #elif GTK_MAJOR_VERSION == 3 @@ -153,8 +147,8 @@ bool Application::OnInit() GError* error = nullptr; ZEN_ON_SCOPE_EXIT(if (error) ::g_error_free(error)); - ::gtk_css_provider_load_from_path(provider, //GtkCssProvider* css_provider, - appendPath(getResourceDirPath(), fileName).c_str(), //const gchar* path, + ::gtk_css_provider_load_from_path(provider, //GtkCssProvider* css_provider + appendPath(getResourceDirPath(), fileName).c_str(), //const gchar* path &error); //GError** error if (error) throw SysError(formatGlibError("gtk_css_provider_load_from_path", error)); @@ -174,35 +168,31 @@ bool Application::OnInit() { loadCSS("Gtk3Styles.old.css"); //throw SysError } - catch (const SysError& e2) { notifyAppError(e2.toString(), FfsExitCode::warning); } + catch (const SysError& e2) { logExtraError(_("Error during process initialization.") + L"\n\n" + e2.toString()); } } #else #error unknown GTK version! #endif - try - { - /* we're a GUI app: ignore SIGHUP when the parent terminal quits! (or process is killed!) - => the FFS launcher will still be killed => fine - => macOS: apparently not needed! interestingly the FFS launcher does receive SIGHUP and *is* killed */ - if (sighandler_t oldHandler = ::signal(SIGHUP, SIG_IGN); - oldHandler == SIG_ERR) - THROW_LAST_SYS_ERROR("signal(SIGHUP)"); - else assert(!oldHandler); - } - catch (const SysError& e) { notifyAppError(e.toString(), FfsExitCode::warning); } + /* we're a GUI app: ignore SIGHUP when the parent terminal quits! (or process is killed!) + => the FFS launcher will still be killed => fine + => macOS: apparently not needed! interestingly the FFS launcher does receive SIGHUP and *is* killed */ + if (sighandler_t oldHandler = ::signal(SIGHUP, SIG_IGN); + oldHandler == SIG_ERR) + logExtraError(_("Error during process initialization.") + L"\n\n" + formatSystemError("signal(SIGHUP)", getLastError())); + else assert(!oldHandler); //Windows User Experience Interaction Guidelines: tool tips should have 5s timeout, info tips no timeout => compromise: wxToolTip::Enable(true); //wxWidgets screw-up: wxToolTip::SetAutoPop is no-op if global tooltip window is not yet constructed: wxToolTip::Enable creates it wxToolTip::SetAutoPop(15'000); //https://docs.microsoft.com/en-us/windows/win32/uxguide/ctrl-tooltips-and-infotips - SetAppName(L"FreeFileSync"); //if not set, the default is the executable's name! + SetAppName(L"FreeFileSync"); //if not set, defaults to executable name //tentatively set program language to OS default until GlobalSettings.xml is read later try { localizationInit(appendPath(getResourceDirPath(), Zstr("Languages.zip"))); } //throw FileError - catch (const FileError& e) { notifyAppError(e.toString(), FfsExitCode::warning); } + catch (const FileError& e) { logExtraError(e.toString()); } initAfs({getResourceDirPath(), getConfigDirPath()}); //bonus: using FTP Gdrive implicitly inits OpenSSL (used in runSanityChecks() on Linux) already during globals init @@ -214,7 +204,7 @@ bool Application::OnInit() //- it's futile to try and clean up while the process is in full swing (CRASH!) => just terminate! //- system sends close events to all open dialogs: If one of these calls wxCloseEvent::Veto(), // e.g. user clicking cancel on save prompt, this would cancel the shutdown - terminateProcess(static_cast(FfsExitCode::aborted)); + terminateProcess(static_cast(FfsExitCode::cancelled)); }; Bind(wxEVT_QUERY_END_SESSION, [onSystemShutdown](wxCloseEvent& event) { onSystemShutdown(); }); //can veto Bind(wxEVT_END_SESSION, [onSystemShutdown](wxCloseEvent& event) { onSystemShutdown(); }); //can *not* veto @@ -223,14 +213,10 @@ bool Application::OnInit() //- Windows sends WM_QUERYENDSESSION, WM_ENDSESSION during log off, *not* WM_CLOSE https://devblogs.microsoft.com/oldnewthing/20080421-00/?p=22663 // => taskkill sending WM_CLOSE (without /f) is a misguided app simulating a button-click on X // -> should send WM_QUERYENDSESSION instead! - try - { - if (auto /*sighandler_t n.a. on macOS*/ oldHandler = ::signal(SIGTERM, onSystemShutdown);//"graceful" exit requested, unlike SIGKILL - oldHandler == SIG_ERR) - THROW_LAST_SYS_ERROR("signal(SIGTERM)"); - else assert(!oldHandler); - } - catch (const SysError& e) { notifyAppError(e.toString(), FfsExitCode::warning); } + if (auto /*sighandler_t n.a. on macOS*/ oldHandler = ::signal(SIGTERM, onSystemShutdown);//"graceful" exit requested, unlike SIGKILL + oldHandler == SIG_ERR) + logExtraError(_("Error during process initialization.") + L"\n\n" + formatSystemError("signal(SIGTERM)", getLastError())); + else assert(!oldHandler); //Note: app start is deferred: batch mode requires the wxApp eventhandler to be established for UI update events. This is not the case at the time of OnInit()! CallAfter([&] { onEnterEventLoop(); }); @@ -245,10 +231,7 @@ int Application::OnExit() //assert(rv); -> fails if clipboard wasn't used localizationCleanup(); imageResourcesCleanup(); - - const std::wstring& warningMsg = teardownAfs(); - if (!warningMsg.empty()) - notifyAppError(warningMsg, FfsExitCode::warning); + teardownAfs(); return wxApp::OnExit(); } @@ -272,7 +255,7 @@ void Application::OnUnhandledException() //handles both wxApp::OnInit() + wxApp: } catch (const std::bad_alloc& e) //the only kind of exception we don't want crash dumps for { - notifyAppError(utfTo(e.what()), FfsExitCode::exception); + notifyAppError(utfTo(e.what())); terminateProcess(static_cast(FfsExitCode::exception)); } //catch (...) -> Windows: let it crash and create mini dump!!! Linux/macOS: std::exception::what() logged to console @@ -488,7 +471,8 @@ void Application::onEnterEventLoop() } catch (const FileError& e) { - notifyAppError(e.toString(), FfsExitCode::exception); + raiseExitCode(exitCode_, FfsExitCode::exception); + notifyAppError(e.toString()); } } @@ -507,6 +491,9 @@ void Application::runGuiMode(const Zstring& globalConfigFilePath, void Application::runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& batchCfg, const Zstring& cfgFilePath) { + const bool allowUserInteraction = !batchCfg.batchExCfg.autoCloseSummary || + (!batchCfg.guiCfg.mainCfg.ignoreErrors && batchCfg.batchExCfg.batchErrorHandling == BatchErrorHandling::showPopup); + XmlGlobalSettings globalCfg; try { @@ -527,7 +514,14 @@ void Application::runBatchMode(const Zstring& globalConfigFilePath, const XmlBat } catch (const FileError& e3) { - return notifyAppError(e3.toString(), FfsExitCode::exception); + raiseExitCode(exitCode_, FfsExitCode::exception); + + if (allowUserInteraction) + showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e3.toString())); + else + logExtraError(e3.toString()); + + return; } } @@ -535,11 +529,7 @@ void Application::runBatchMode(const Zstring& globalConfigFilePath, const XmlBat { setLanguage(globalCfg.programLanguage); //throw FileError } - catch (const FileError& e) - { - notifyAppError(e.toString(), FfsExitCode::warning); - //continue! - } + catch (const FileError& e) { logExtraError(e.toString()); } //all settings have been read successfully... @@ -573,19 +563,16 @@ void Application::runBatchMode(const Zstring& globalConfigFilePath, const XmlBat globalCfg.soundFileAlertPending, progressDim, batchCfg.batchExCfg.autoCloseSummary, - batchCfg.batchExCfg.postSyncAction, + batchCfg.batchExCfg.postBatchAction, batchCfg.batchExCfg.batchErrorHandling); - const bool allowUserInteraction = !batchCfg.batchExCfg.autoCloseSummary || - (!batchCfg.guiCfg.mainCfg.ignoreErrors && batchCfg.batchExCfg.batchErrorHandling == BatchErrorHandling::showPopup); - - AFS::RequestPasswordFun requestPassword; //throw AbortProcess + AFS::RequestPasswordFun requestPassword; //throw CancelProcess if (allowUserInteraction) requestPassword = [&, password = Zstring()](const std::wstring& msg, const std::wstring& lastErrorMsg) mutable { assert(runningOnMainThread()); if (showPasswordPrompt(statusHandler.getWindowIfVisible(), msg, lastErrorMsg, password) != ConfirmationButton::accept) - statusHandler.abortProcessNow(AbortTrigger::user); //throw AbortProcess + statusHandler.cancelProcessNow(CancelReason::user); //throw CancelProcess return password; }; @@ -593,12 +580,11 @@ void Application::runBatchMode(const Zstring& globalConfigFilePath, const XmlBat try { //inform about (important) non-default global settings - logNonDefaultSettings(globalCfg, statusHandler); //throw AbortProcess + logNonDefaultSettings(globalCfg, statusHandler); //throw CancelProcess //batch mode: place directory locks on directories during both comparison AND synchronization std::unique_ptr dirLocks; - //COMPARE DIRECTORIES FolderComparison cmpResult = compare(globalCfg.warnDlgs, globalCfg.fileTimeTolerance, requestPassword, @@ -606,8 +592,7 @@ void Application::runBatchMode(const Zstring& globalConfigFilePath, const XmlBat globalCfg.createLockFile, dirLocks, extractCompareCfg(batchCfg.guiCfg.mainCfg), - statusHandler); //throw AbortProcess - //START SYNCHRONIZATION + statusHandler); //throw CancelProcess if (!cmpResult.empty()) synchronize(syncStartTime, globalCfg.verifyFileCopy, @@ -618,9 +603,13 @@ void Application::runBatchMode(const Zstring& globalConfigFilePath, const XmlBat extractSyncCfg(batchCfg.guiCfg.mainCfg), cmpResult, globalCfg.warnDlgs, - statusHandler); //throw AbortProcess + statusHandler); //throw CancelProcess } - catch (AbortProcess&) {} //exit used by statusHandler + catch (CancelProcess&) {} + + //------------------------------------------------------------------- + BatchStatusHandler::Result r = statusHandler.prepareResult(); + AbstractPath logFolderPath = createAbstractPath(batchCfg.guiCfg.mainCfg.altLogFolderPathPhrase); //optional if (AFS::isNullPath(logFolderPath)) @@ -629,51 +618,128 @@ void Application::runBatchMode(const Zstring& globalConfigFilePath, const XmlBat if (AFS::isNullPath(logFolderPath)) logFolderPath = createAbstractPath(getLogFolderDefaultPath()); - BatchStatusHandler::Result r = statusHandler.reportResults(batchCfg.guiCfg.mainCfg.postSyncCommand, batchCfg.guiCfg.mainCfg.postSyncCondition, - logFolderPath, globalCfg.logfilesMaxAgeDays, globalCfg.logFormat, logFilePathsToKeep, - batchCfg.guiCfg.mainCfg.emailNotifyAddress, batchCfg.guiCfg.mainCfg.emailNotifyCondition); //noexcept - //---------------------------------------------------------------------- - switch (r.summary.syncResult) + AbstractPath logFilePath = AFS::appendRelPath(logFolderPath, generateLogFileName(globalCfg.logFormat, r.summary)); + //e.g. %AppData%\FreeFileSync\Logs\Backup FreeFileSync 2013-09-15 015052.123 [Error].log + + auto notifyStatusNoThrow = [&](std::wstring&& msg) { try { statusHandler.updateStatus(std::move(msg)); /*throw CancelProcess*/ } catch (CancelProcess&) {} }; + + + if (statusHandler.taskCancelled() && *statusHandler.taskCancelled() == CancelReason::user) + ; /* user cancelled => don't run post sync command + => don't send email notification + => don't play sound notification + => don't run post sync action */ + else { - //*INDENT-OFF* - case SyncResult::finishedSuccess: raiseExitCode(exitCode_, FfsExitCode::success); break; - case SyncResult::finishedWarning: raiseExitCode(exitCode_, FfsExitCode::warning); break; - case SyncResult::finishedError: raiseExitCode(exitCode_, FfsExitCode::error ); break; - case SyncResult::aborted: raiseExitCode(exitCode_, FfsExitCode::aborted); break; - //*INDENT-ON* + //--------------------- post sync command ---------------------- + if (const Zstring cmdLine = trimCpy(expandMacros(batchCfg.guiCfg.mainCfg.postSyncCommand)); + !cmdLine.empty()) + if (batchCfg.guiCfg.mainCfg.postSyncCondition == PostSyncCondition::completion || + (batchCfg.guiCfg.mainCfg.postSyncCondition == PostSyncCondition::errors) == (r.summary.result == TaskResult::cancelled || + r.summary.result == TaskResult::error)) + try + { + //give consoleExecute() some "time to fail", but not too long to hang our process + const int DEFAULT_APP_TIMEOUT_MS = 100; + + if (const auto& [exitCode, output] = consoleExecute(cmdLine, DEFAULT_APP_TIMEOUT_MS); //throw SysError, SysErrorTimeOut + exitCode != 0) + throw SysError(formatSystemError("", replaceCpy(_("Exit code %x"), L"%x", numberTo(exitCode)), utfTo(output))); + + logMsg(r.errorLog.ref(), _("Executing command:") + L' ' + utfTo(cmdLine) + L" [" + replaceCpy(_("Exit code %x"), L"%x", L"0") + L']', MSG_TYPE_INFO); + } + catch (SysErrorTimeOut&) //child process not failed yet => probably fine :> + { + logMsg(r.errorLog.ref(), _("Executing command:") + L' ' + utfTo(cmdLine), MSG_TYPE_INFO); + } + catch (const SysError& e) + { + logMsg(r.errorLog.ref(), replaceCpy(_("Command %x failed."), L"%x", fmtPath(cmdLine)) + L"\n\n" + e.toString(), MSG_TYPE_ERROR); + } + + //--------------------- email notification ---------------------- + if (const std::string notifyEmail = trimCpy(batchCfg.guiCfg.mainCfg.emailNotifyAddress); + !notifyEmail.empty()) + if (batchCfg.guiCfg.mainCfg.emailNotifyCondition == ResultsNotification::always || + (batchCfg.guiCfg.mainCfg.emailNotifyCondition == ResultsNotification::errorWarning && (r.summary.result == TaskResult::cancelled || + r.summary.result == TaskResult::error || + r.summary.result == TaskResult::warning)) || + (batchCfg.guiCfg.mainCfg.emailNotifyCondition == ResultsNotification::errorOnly && (r.summary.result == TaskResult::cancelled || + r.summary.result == TaskResult::error))) + try + { + logMsg(r.errorLog.ref(), replaceCpy(_("Sending email notification to %x"), L"%x", utfTo(notifyEmail)), MSG_TYPE_INFO); + sendLogAsEmail(notifyEmail, r.summary, r.errorLog.ref(), logFilePath, notifyStatusNoThrow); //throw FileError + } + catch (const FileError& e) { logMsg(r.errorLog.ref(), e.toString(), MSG_TYPE_ERROR); } } - globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.size = r.dlgDim.size; //=> ignore r.dim.pos - globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized = r.dlgDim.isMaximized; + //--------------------- save log file ---------------------- + try //create not before destruction: 1. avoid issues with FFS trying to sync open log file 2. include status in log file name without extra rename + { + //do NOT use tryReportingError()! saving log files should not be cancellable! + saveLogFile(logFilePath, r.summary, r.errorLog.ref(), globalCfg.logfilesMaxAgeDays, globalCfg.logFormat, logFilePathsToKeep, notifyStatusNoThrow); //throw FileError + } + catch (const FileError& e) + { + logMsg(r.errorLog.ref(), e.toString(), MSG_TYPE_ERROR); - //email sending, or saving log file failed? at least this should affect the exit code: - if (r.logStats.error > 0) - raiseExitCode(exitCode_, FfsExitCode::error); - else if (r.logStats.warning > 0) - raiseExitCode(exitCode_, FfsExitCode::warning); + const AbstractPath logFileDefaultPath = AFS::appendRelPath(createAbstractPath(getLogFolderDefaultPath()), generateLogFileName(globalCfg.logFormat, r.summary)); + if (logFilePath != logFileDefaultPath) //fallback: log file *must* be saved no matter what! + try + { + logFilePath = logFileDefaultPath; + saveLogFile(logFileDefaultPath, r.summary, r.errorLog.ref(), globalCfg.logfilesMaxAgeDays, globalCfg.logFormat, logFilePathsToKeep, notifyStatusNoThrow); //throw FileError + } + catch (const FileError& e2) { logMsg(r.errorLog.ref(), e2.toString(), MSG_TYPE_ERROR); assert(false); } //should never happen!!! + } + //--------- update last sync stats for the selected cfg files --------- + const ErrorLogStats& logStats = getStats(r.errorLog.ref()); - //update last sync stats for the selected cfg file for (ConfigFileItem& cfi : globalCfg.mainDlg.config.fileHistory) if (equalNativePath(cfi.cfgFilePath, cfgFilePath)) { - assert(!AFS::isNullPath(r.logFilePath)); + assert(!AFS::isNullPath(logFilePath)); assert(r.summary.startTime == syncStartTime); cfi.lastRunStats = { - r.logFilePath, + logFilePath, std::chrono::system_clock::to_time_t(r.summary.startTime), - r.summary.syncResult, + r.summary.result, r.summary.statsProcessed.items, r.summary.statsProcessed.bytes, r.summary.totalTime, - r.logStats.error, - r.logStats.warning, + logStats.error, + logStats.warning, }; break; } + //--------------------------------------------------------------------------- + const BatchStatusHandler::DlgOptions dlgOpt = statusHandler.showResult(); + + globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.size = dlgOpt.dim.size; //=> ignore dim.pos + globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized = dlgOpt.dim.isMaximized; + + //---------------------------------------------------------------------- + switch (r.summary.result) + { + //*INDENT-OFF* + case TaskResult::success: raiseExitCode(exitCode_, FfsExitCode::success); break; + case TaskResult::warning: raiseExitCode(exitCode_, FfsExitCode::warning); break; + case TaskResult::error: raiseExitCode(exitCode_, FfsExitCode::error ); break; + case TaskResult::cancelled: raiseExitCode(exitCode_, FfsExitCode::cancelled); break; + //*INDENT-ON* + } + + //email sending, or saving log file failed? at least this should affect the exit code: + if (logStats.error > 0) + raiseExitCode(exitCode_, FfsExitCode::error); + else if (logStats.warning > 0) + raiseExitCode(exitCode_, FfsExitCode::warning); + //--------------------------------------------------------------------------- try //save global settings to XML: e.g. ignored warnings, last sync stats { @@ -681,24 +747,39 @@ void Application::runBatchMode(const Zstring& globalConfigFilePath, const XmlBat } catch (const FileError& e) { - notifyAppError(e.toString(), FfsExitCode::warning); + //raiseExitCode(exitCode_, FfsExitCode::error); -> sync successful + if (allowUserInteraction) + showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + else + logExtraError(e.toString()); } + //--------------------------------------------------------------------------- + //run shutdown *after* saving global config! https://freefilesync.org/forum/viewtopic.php?t=5761 using FinalRequest = BatchStatusHandler::FinalRequest; - switch (r.finalRequest) + switch (dlgOpt.finalRequest) { case FinalRequest::none: break; + case FinalRequest::switchGui: //open new top-level window *after* progress dialog is gone => run on main event loop MainDialog::create(globalConfigFilePath, &globalCfg, batchCfg.guiCfg, {cfgFilePath}, true /*startComparison*/); break; - case FinalRequest::shutdown: //run *after* last sync stats were updated and saved! https://freefilesync.org/forum/viewtopic.php?t=5761 + + case FinalRequest::shutdown: try { shutdownSystem(); //throw FileError - terminateProcess(static_cast(exitCode_)); //no point in continuing and saving cfg again in onSystemShutdown() while the OS will kill us anytime! + terminateProcess(static_cast(exitCode_)); //better exit in a controlled manner rather than letting the OS kill us any time! + } + catch (const FileError& e) + { + //raiseExitCode(exitCode_, FfsExitCode::error); -> no! sync was successful + if (allowUserInteraction) + showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + else + logExtraError(e.toString()); } - catch (const FileError& e) { notifyAppError(e.toString(), FfsExitCode::error); } break; } } diff --git a/FreeFileSync/Source/application.h b/FreeFileSync/Source/application.h index 1eaf8fe1..aa3b570f 100644 --- a/FreeFileSync/Source/application.h +++ b/FreeFileSync/Source/application.h @@ -24,7 +24,6 @@ private: int OnExit() override; bool OnExceptionInMainLoop() override { throw; } //just re-throw and avoid display of additional messagebox: it will be caught in OnUnhandledException() void OnUnhandledException () override; - void notifyAppError(const std::wstring& msg, FfsExitCode rc); wxLayoutDirection GetLayoutDirection() const override; void onEnterEventLoop(); diff --git a/FreeFileSync/Source/base/algorithm.cpp b/FreeFileSync/Source/base/algorithm.cpp index b22c7802..5ff1cfbc 100644 --- a/FreeFileSync/Source/base/algorithm.cpp +++ b/FreeFileSync/Source/base/algorithm.cpp @@ -1468,7 +1468,7 @@ void fff::deleteFromGridAndHD(const std::vector& rowsToDelete //ensure cleanup: redetermination of sync-directions and removal of invalid rows auto updateDirection = [&] { - //update sync direction: we cannot do a full redetermination since the user may already have entered manual changes + //update sync direction: we cannot do a full redetermination since the user may have manual changes applied already std::vector rowsToDelete; append(rowsToDelete, deleteLeft); append(rowsToDelete, deleteRight); @@ -1566,8 +1566,7 @@ TempFileBuffer::~TempFileBuffer() { removeDirectoryPlainRecursion(tempFolderPath_); //throw FileError } - catch (FileError&) { assert(false); } - warn_static("log, maybe?") + catch (const FileError& e) { logExtraError(e.toString()); } } diff --git a/FreeFileSync/Source/base/comparison.cpp b/FreeFileSync/Source/base/comparison.cpp index 2b5273ba..8c1c9544 100644 --- a/FreeFileSync/Source/base/comparison.cpp +++ b/FreeFileSync/Source/base/comparison.cpp @@ -186,44 +186,61 @@ ResolvedBaseFolders initializeBaseFolders(const std::vector& fpCf class ComparisonBuffer { public: - ComparisonBuffer(const std::set& folderKeys, - const FolderStatus& baseFolderStatus, + ComparisonBuffer(const FolderStatus& folderStatus, int fileTimeTolerance, - ProcessCallback& callback); + ProcessCallback& callback) : + fileTimeTolerance_(fileTimeTolerance), + folderStatus_(folderStatus), + cb_(callback) {} - //create comparison result table and fill category except for files existing on both sides: undefinedFiles and undefinedSymlinks are appended! - SharedRef compareByTimeSize(const ResolvedFolderPair& fp, const FolderPairCfg& fpConfig) const; - SharedRef compareBySize (const ResolvedFolderPair& fp, const FolderPairCfg& fpConfig) const; - std::vector> compareByContent(const std::vector>& workLoad) const; + FolderComparison execute(const std::vector>& workLoad); private: ComparisonBuffer (const ComparisonBuffer&) = delete; ComparisonBuffer& operator=(const ComparisonBuffer&) = delete; + //create comparison result table and fill category except for files existing on both sides: undefinedFiles and undefinedSymlinks are appended! + SharedRef compareByTimeSize(const ResolvedFolderPair& fp, const FolderPairCfg& fpConfig) const; + SharedRef compareBySize (const ResolvedFolderPair& fp, const FolderPairCfg& fpConfig) const; + std::vector> compareByContent(const std::vector>& workLoad) const; + SharedRef performComparison(const ResolvedFolderPair& fp, - const FolderPairCfg& fpCfg, - std::vector& undefinedFiles, - std::vector& undefinedSymlinks) const; + const FolderPairCfg& fpCfg, + std::vector& undefinedFiles, + std::vector& undefinedSymlinks) const; + + BaseFolderStatus getBaseFolderStatus(const AbstractPath& folderPath) const + { + if (folderStatus_.existing.contains(folderPath)) + return BaseFolderStatus::existing; + if (folderStatus_.notExisting.contains(folderPath)) + return BaseFolderStatus::notExisting; + if (folderStatus_.failedChecks.contains(folderPath)) + return BaseFolderStatus::failure; + assert(AFS::isNullPath(folderPath)); + return BaseFolderStatus::notExisting; + }; - std::map folderBuffer_; //contains entries for *all* scanned folders! const int fileTimeTolerance_; const FolderStatus& folderStatus_; + std::map folderBuffer_; //contains entries for *all* scanned folders! ProcessCallback& cb_; }; -ComparisonBuffer::ComparisonBuffer(const std::set& folderKeys, - const FolderStatus& folderStatus, - int fileTimeTolerance, - ProcessCallback& callback) : - fileTimeTolerance_(fileTimeTolerance), - folderStatus_(folderStatus), - cb_(callback) +FolderComparison ComparisonBuffer::execute(const std::vector>& workLoad) { std::set foldersToRead; - for (const DirectoryKey& folderKey : folderKeys) - if (folderStatus.existing.contains(folderKey.folderPath)) - foldersToRead.insert(folderKey); //only traverse *existing* folders + for (const auto& [folderPair, fpCfg] : workLoad) + if (getBaseFolderStatus(folderPair.folderPathLeft ) != BaseFolderStatus::failure && //no need to list or display one-sided results if + getBaseFolderStatus(folderPair.folderPathRight) != BaseFolderStatus::failure) //*either* folder existence check fails + { + //+ only traverse *existing* folders + if (getBaseFolderStatus(folderPair.folderPathLeft) == BaseFolderStatus::existing) + foldersToRead.emplace(DirectoryKey({folderPair.folderPathLeft, fpCfg.filter.nameFilter, fpCfg.handleSymlinks})); + if (getBaseFolderStatus(folderPair.folderPathRight) == BaseFolderStatus::existing) + foldersToRead.emplace(DirectoryKey({folderPair.folderPathRight, fpCfg.filter.nameFilter, fpCfg.handleSymlinks})); + } //------------------------------------------------------------------ const std::chrono::steady_clock::time_point compareStartTime = std::chrono::steady_clock::now(); @@ -231,37 +248,54 @@ ComparisonBuffer::ComparisonBuffer(const std::set& folderKeys, auto onStatusUpdate = [&, textScanning = _("Scanning:") + L' '](const std::wstring& statusLine, int itemsTotal) { - callback.updateDataProcessed(itemsTotal - itemsReported, 0); //noexcept + cb_.updateDataProcessed(itemsTotal - itemsReported, 0); //noexcept itemsReported = itemsTotal; - callback.updateStatus(textScanning + statusLine); //throw X + cb_.updateStatus(textScanning + statusLine); //throw X }; + //PERF_START; folderBuffer_ = parallelDeviceTraversal(foldersToRead, - [&](const PhaseCallback::ErrorInfo& errorInfo) { return callback.reportError(errorInfo); }, //throw X + [&](const PhaseCallback::ErrorInfo& errorInfo) { return cb_.reportError(errorInfo); }, //throw X onStatusUpdate, //throw X UI_UPDATE_INTERVAL / 2); //every ~50 ms + //PERF_STOP; const int64_t totalTimeSec = std::chrono::duration_cast(std::chrono::steady_clock::now() - compareStartTime).count(); - callback.logMessage(_("Comparison finished:") + L' ' + - _P("1 item found", "%x items found", itemsReported) + SPACED_DASH + - _("Time elapsed:") + L' ' + utfTo(formatTimeSpan(totalTimeSec)), - PhaseCallback::MsgType::info); //throw X + cb_.logMessage(_("Comparison finished:") + L' ' + + _P("1 item found", "%x items found", itemsReported) + SPACED_DASH + + _("Time elapsed:") + L' ' + utfTo(formatTimeSpan(totalTimeSec)), + PhaseCallback::MsgType::info); //throw X //------------------------------------------------------------------ - //folderStatus_.existing already in buffer, now create entries for the rest: - for (const DirectoryKey& folderKey : folderKeys) - if (auto it = folderStatus_.failedChecks.find(folderKey.folderPath); - it != folderStatus_.failedChecks.end()) - //make sure all items are disabled => avoid user panicking: https://freefilesync.org/forum/viewtopic.php?t=7582 - folderBuffer_[folderKey].failedFolderReads[Zstring() /*empty string for root*/] = utfTo(it->second.toString()); - else + //process binary comparison as one junk + std::vector> workLoadByContent; + for (const auto& [folderPair, fpCfg] : workLoad) + if (fpCfg.compareVar == CompareVariant::content) + workLoadByContent.push_back({folderPair, fpCfg}); + + std::vector> outputByContent = compareByContent(workLoadByContent); + auto itOByC = outputByContent.begin(); + + FolderComparison output; + + //write output in expected order + for (const auto& [folderPair, fpCfg] : workLoad) + switch (fpCfg.compareVar) { - folderBuffer_[folderKey]; - assert(folderStatus_.existing .contains(folderKey.folderPath) || - folderStatus_.notExisting.contains(folderKey.folderPath) || - AFS::isNullPath(folderKey.folderPath)); + case CompareVariant::timeSize: + output.push_back(compareByTimeSize(folderPair, fpCfg)); + break; + case CompareVariant::size: + output.push_back(compareBySize(folderPair, fpCfg)); + break; + case CompareVariant::content: + assert(itOByC != outputByContent.end()); + if (itOByC != outputByContent.end()) + output.push_back(*itOByC++); + break; } + return output; } @@ -617,7 +651,7 @@ std::vector> ComparisonBuffer::compareByContent(const } //finish categorization: compare files (that have same size) bytewise... - if (!fpWorkload.empty()) //run ProcessPhase::comparingContent only when needed + if (!fpWorkload.empty()) //run ProcessPhase::binaryCompare only when needed { int itemsTotal = 0; uint64_t bytesTotal = 0; @@ -628,7 +662,7 @@ std::vector> ComparisonBuffer::compareByContent(const for (const FilePair* file : bwl.filesToCompareBytewise) bytesTotal += file->getFileSize(); //left and right file sizes are equal } - cb_.initNewPhase(itemsTotal, bytesTotal, ProcessPhase::comparingContent); //throw X + cb_.initNewPhase(itemsTotal, bytesTotal, ProcessPhase::binaryCompare); //throw X //PERF_START; @@ -733,7 +767,7 @@ const Zstringc* MergeSides::checkFailedRead(FileSystemObject& fsObj, const Zstri it != errorsByRelPath_.end()) errorMsg = &it->second; - if (errorMsg) + if (errorMsg) //make sure all items are disabled => avoid user panicking: https://freefilesync.org/forum/viewtopic.php?t=7582 { fsObj.setActive(false); fsObj.setCategoryConflict(*errorMsg); //peak memory: Zstringc is ref-counted, unlike std::string! @@ -866,7 +900,7 @@ void MergeSides::mergeTwoSides(const FolderContainer& lhs, const FolderContainer matchFolders(lhs.files, rhs.files, [&](const FileData& fileLeft, const Zstringc* conflictMsg) { - FilePair& newItem = output.addFile(fileLeft .first, fileLeft .second); + FilePair& newItem = output.addFile(fileLeft.first, fileLeft.second); checkFailedRead(newItem, conflictMsg ? conflictMsg : errorMsg); }, [&](const FileData& fileRight, const Zstringc* conflictMsg) @@ -891,7 +925,7 @@ void MergeSides::mergeTwoSides(const FolderContainer& lhs, const FolderContainer matchFolders(lhs.symlinks, rhs.symlinks, [&](const SymlinkData& symlinkLeft, const Zstringc* conflictMsg) { - SymlinkPair& newItem = output.addLink(symlinkLeft .first, symlinkLeft .second); + SymlinkPair& newItem = output.addLink(symlinkLeft.first, symlinkLeft.second); checkFailedRead(newItem, conflictMsg ? conflictMsg : errorMsg); }, [&](const SymlinkData& symlinkRight, const Zstringc* conflictMsg) @@ -969,38 +1003,64 @@ void stripExcludedDirectories(ContainerObject& hierObj, const PathFilter& filter //create comparison result table and fill category except for files existing on both sides: undefinedFiles and undefinedSymlinks are appended! SharedRef ComparisonBuffer::performComparison(const ResolvedFolderPair& fp, - const FolderPairCfg& fpCfg, - std::vector& undefinedFiles, - std::vector& undefinedSymlinks) const + const FolderPairCfg& fpCfg, + std::vector& undefinedFiles, + std::vector& undefinedSymlinks) const { cb_.updateStatus(_("Generating file list...")); //throw X cb_.requestUiUpdate(true /*force*/); //throw X + const BaseFolderStatus folderStatusL = getBaseFolderStatus(fp.folderPathLeft); + const BaseFolderStatus folderStatusR = getBaseFolderStatus(fp.folderPathRight); + + std::unordered_map failedReads; //base-relative paths or empty if read-error for whole base directory + const FolderContainer* folderContL = nullptr; + const FolderContainer* folderContR = nullptr; + - auto evalFolderContent = [&](const AbstractPath& folderPath) -> const FolderContainer& + const FolderContainer empty; + if (folderStatusL == BaseFolderStatus::failure || + folderStatusR == BaseFolderStatus::failure) { - const DirectoryValue& dirVal = folderBuffer_.find({folderPath, fpCfg.filter.nameFilter, fpCfg.handleSymlinks})->second; - //contract: folderBuffer_ has entries for *all* folders (existing or not) + auto it = folderStatus_.failedChecks.find(fp.folderPathLeft); + if (it == folderStatus_.failedChecks.end()) + it = folderStatus_.failedChecks.find(fp.folderPathRight); + + failedReads[Zstring() /*empty string for root*/] = utfTo(it->second.toString()); - //mix failedFolderReads with failedItemReads: - //associate folder traversing errors with folder (instead of child items only) to show on GUI! See "MergeSides" - //=> minor pessimization for "excludefilterFailedRead" which needlessly excludes parent folders, too - auto append = [&](const std::unordered_map& c) + folderContL = ∅ //no need to list or display one-sided results if + folderContR = ∅ //*any* folder existence check fails (even if other side would have been in folderBuffer_!) + } + else + { + auto evalBuffer = [&](const AbstractPath& folderPath, const FolderContainer*& folderCont) { - for (const auto& [relPath, errorMsg] : c) - failedReads.emplace(relPath, errorMsg); - }; - append(dirVal.failedFolderReads); - append(dirVal.failedItemReads); + auto it = folderBuffer_.find({folderPath, fpCfg.filter.nameFilter, fpCfg.handleSymlinks}); + if (it != folderBuffer_.end()) + { + const DirectoryValue& dirVal = it->second; - return dirVal.folderCont; - }; + //mix failedFolderReads with failedItemReads: + //associate folder traversing errors with folder (instead of child items only) to show on GUI! See "MergeSides" + //=> minor pessimization for "excludefilterFailedRead" which needlessly excludes parent folders, too + failedReads.insert(dirVal.failedFolderReads.begin(), dirVal.failedFolderReads.end()); + failedReads.insert(dirVal.failedItemReads .begin(), dirVal.failedItemReads .end()); + + assert(getBaseFolderStatus(folderPath) == BaseFolderStatus::existing); + folderCont = &dirVal.folderCont; + } + else + { + assert(getBaseFolderStatus(folderPath) == BaseFolderStatus::notExisting); //including AFS::isNullPath() + folderCont = ∅ + } + }; + evalBuffer(fp.folderPathLeft, folderContL); + evalBuffer(fp.folderPathRight, folderContR); + } - const FolderContainer& folderContL = evalFolderContent(fp.folderPathLeft); - const FolderContainer& folderContR = evalFolderContent(fp.folderPathRight); - //*after* evalFolderContent(): Zstring excludefilterFailedRead; if (failedReads.contains(Zstring())) //empty path if read-error for whole base directory excludefilterFailedRead += Zstr("*\n"); @@ -1014,28 +1074,16 @@ SharedRef ComparisonBuffer::performComparison(const ResolvedFold if constexpr (FILE_NAME_SEPARATOR != Zstr('\\')) replace(excludefilterFailedRead, Zstr('\\'), Zstr('?')); - auto getBaseFolderStatus = [&](const AbstractPath& folderPath) - { - if (folderStatus_.existing.contains(folderPath)) - return BaseFolderStatus::existing; - if (folderStatus_.notExisting.contains(folderPath)) - return BaseFolderStatus::notExisting; - if (folderStatus_.failedChecks.contains(folderPath)) - return BaseFolderStatus::failure; - assert(AFS::isNullPath(folderPath)); - return BaseFolderStatus::notExisting; - }; - SharedRef output = makeSharedRef(fp.folderPathLeft, - getBaseFolderStatus(fp.folderPathLeft), //dir existence must be checked only once! - fp.folderPathRight, - getBaseFolderStatus(fp.folderPathRight), - fpCfg.filter.nameFilter.ref().copyFilterAddingExclusion(excludefilterFailedRead), - fpCfg.compareVar, - fileTimeTolerance_, - fpCfg.ignoreTimeShiftMinutes); + folderStatusL, //check folder existence only once! + fp.folderPathRight, + folderStatusR, // + fpCfg.filter.nameFilter.ref().copyFilterAddingExclusion(excludefilterFailedRead), + fpCfg.compareVar, + fileTimeTolerance_, + fpCfg.ignoreTimeShiftMinutes); //PERF_START; - MergeSides(failedReads, undefinedFiles, undefinedSymlinks).execute(folderContL, folderContR, output.ref()); + MergeSides(failedReads, undefinedFiles, undefinedSymlinks).execute(*folderContL, *folderContR, output.ref()); //PERF_STOP; //##################### in/exclude rows according to filtering ##################### @@ -1063,11 +1111,9 @@ FolderComparison fff::compare(WarningDialogs& warnings, const std::vector& fpCfgList, ProcessCallback& callback /*throw X*/) //throw X { - //PERF_START; - //indicator at the very beginning of the log to make sense of "total time" //init process: keep at beginning so that all gui elements are initialized properly - callback.initNewPhase(-1, -1, ProcessPhase::scanning); //throw X; it's unknown how many files will be scanned => -1 objects + callback.initNewPhase(-1, -1, ProcessPhase::scan); //throw X; it's unknown how many files will be scanned => -1 objects //callback.logInfo(Comparison started")); -> still useful? //------------------------------------------------------------------------------- @@ -1165,44 +1211,11 @@ FolderComparison fff::compare(WarningDialogs& warnings, //reduce peak memory by restricting lifetime of ComparisonBuffer to have ended when loading potentially huge InSyncFolder instance in redetermineSyncDirection() { //------------------- fill directory buffer: traverse/read folders -------------------------- - std::set folderKeys; - for (const auto& [folderPair, fpCfg] : workLoad) - { - folderKeys.emplace(DirectoryKey({folderPair.folderPathLeft, fpCfg.filter.nameFilter, fpCfg.handleSymlinks})); - folderKeys.emplace(DirectoryKey({folderPair.folderPathRight, fpCfg.filter.nameFilter, fpCfg.handleSymlinks})); - } - + ComparisonBuffer cmpBuf(resInfo.baseFolderStatus, + fileTimeTolerance, callback); //PERF_START; - ComparisonBuffer cmpBuff(folderKeys, - resInfo.baseFolderStatus, - fileTimeTolerance, callback); + output = cmpBuf.execute(workLoad); //PERF_STOP; - - //process binary comparison as one junk - std::vector> workLoadByContent; - for (const auto& [folderPair, fpCfg] : workLoad) - if (fpCfg.compareVar == CompareVariant::content) - workLoadByContent.push_back({folderPair, fpCfg}); - - std::vector> outputByContent = cmpBuff.compareByContent(workLoadByContent); - auto itOByC = outputByContent.begin(); - - //write output in expected order - for (const auto& [folderPair, fpCfg] : workLoad) - switch (fpCfg.compareVar) - { - case CompareVariant::timeSize: - output.push_back(cmpBuff.compareByTimeSize(folderPair, fpCfg)); - break; - case CompareVariant::size: - output.push_back(cmpBuff.compareBySize(folderPair, fpCfg)); - break; - case CompareVariant::content: - assert(itOByC != outputByContent.end()); - if (itOByC != outputByContent.end()) - output.push_back(*itOByC++); - break; - } } assert(output.size() == fpCfgList.size()); diff --git a/FreeFileSync/Source/base/db_file.cpp b/FreeFileSync/Source/base/db_file.cpp index d0037dec..193abe7a 100644 --- a/FreeFileSync/Source/base/db_file.cpp +++ b/FreeFileSync/Source/base/db_file.cpp @@ -980,11 +980,10 @@ void fff::saveLastSynchronousState(const BaseFolderPair& baseFolder, bool transa ZEN_ON_SCOPE_EXIT ( //*INDENT-OFF* - if (dbPathTmpL) try { AFS::removeFilePlain(*dbPathTmpL); } catch (FileError&) {} - if (dbPathTmpR) try { AFS::removeFilePlain(*dbPathTmpR); } catch (FileError&) {} + if (dbPathTmpL) try { AFS::removeFilePlain(*dbPathTmpL); } catch (const FileError& e) { logExtraError(e.toString()); } + if (dbPathTmpR) try { AFS::removeFilePlain(*dbPathTmpR); } catch (const FileError& e) { logExtraError(e.toString()); } //*INDENT-ON* ) - warn_static("log it!") std::vector> parallelWorkloadSave, parallelWorkloadMove; @@ -1037,9 +1036,8 @@ void fff::saveLastSynchronousState(const BaseFolderPair& baseFolder, bool transa massParallelExecute(parallelWorkloadSave, Zstr("Save sync.ffs_db"), callback /*throw X*/); //throw X - + //---------------------------------------------------------------- if (saveSuccessL && saveSuccessR) massParallelExecute(parallelWorkloadMove, Zstr("Move sync.ffs_db"), callback /*throw X*/); //throw X - //---------------------------------------------------------------- } diff --git a/FreeFileSync/Source/base/db_file.h b/FreeFileSync/Source/base/db_file.h index 98d681b8..67be33fa 100644 --- a/FreeFileSync/Source/base/db_file.h +++ b/FreeFileSync/Source/base/db_file.h @@ -15,7 +15,7 @@ namespace fff { -const ZstringView SYNC_DB_FILE_ENDING = Zstr(".ffs_db"); //don't use Zstring as global constant: avoid static initialization order problem in global namespace! +constexpr ZstringView SYNC_DB_FILE_ENDING = Zstr(".ffs_db"); //don't use Zstring as global constant: avoid static initialization order problem in global namespace! struct InSyncDescrFile //subset of FileAttributes { diff --git a/FreeFileSync/Source/base/dir_lock.cpp b/FreeFileSync/Source/base/dir_lock.cpp index 5c6450ef..0486ccd3 100644 --- a/FreeFileSync/Source/base/dir_lock.cpp +++ b/FreeFileSync/Source/base/dir_lock.cpp @@ -37,23 +37,15 @@ const int ABANDONED_LOCK_LEVEL_MAX = 10; } -Zstring fff::impl::getLockFilePathForAbandonedLock(const Zstring& lockFilePath) //throw FileError +Zstring fff::impl::getAbandonedLockFileName(const Zstring& lockFileName) //throw SysError { - auto it = zen::findLast(lockFilePath.begin(), lockFilePath.end(), FILE_NAME_SEPARATOR); - if (it == lockFilePath.end()) - it = lockFilePath.begin(); - else - ++it; - - const Zstring prefix (lockFilePath.begin(), it); - /**/ Zstring fileName( it, lockFilePath.end()); + Zstring fileName = lockFileName; int level = 0; //recursive abandoned locks!? (almost) impossible, except for file system bugs: https://freefilesync.org/forum/viewtopic.php?t=6568 - if (startsWith(fileName, Zstr("Delete."))) //e.g. Delete.1.sync.ffs_lock + const Zstring tmp = afterFirst(fileName, Zstr("Delete."), IfNotFoundReturn::none); //e.g. Delete.1.sync.ffs_lock + if (!tmp.empty()) { - const Zstring tmp = afterFirst(fileName, Zstr('.'), IfNotFoundReturn::none); - const Zstring levelStr = beforeFirst(tmp, Zstr('.'), IfNotFoundReturn::none); if (!levelStr.empty() && std::all_of(levelStr.begin(), levelStr.end(), [](Zchar c) { return zen::isDigit(c); })) { @@ -61,11 +53,11 @@ Zstring fff::impl::getLockFilePathForAbandonedLock(const Zstring& lockFilePath) level = stringTo(levelStr) + 1; if (level >= ABANDONED_LOCK_LEVEL_MAX) - throw FileError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(lockFilePath)), L"Endless recursion."); + throw SysError(L"Endless recursion."); } } - return prefix + Zstr("Delete.") + numberTo(level) + Zstr(".") + fileName; //preserve lock file extension! + return Zstr("Delete.") + numberTo(level) + Zstr(".") + fileName; //preserve lock file extension! } @@ -113,22 +105,18 @@ private: offset == -1) THROW_LAST_SYS_ERROR("lseek"); #endif - if (const ssize_t bytesWritten = ::write(fdLockFile, " ", 1); //writes *up to* count bytes - bytesWritten <= 0) + const ssize_t bytesWritten = ::write(fdLockFile, " ", 1); //writes *up to* count bytes + if (bytesWritten <= 0) { if (bytesWritten == 0) //comment in safe-read.c suggests to treat this as an error due to buggy drivers errno = ENOSPC; THROW_LAST_SYS_ERROR("write"); } - else if (bytesWritten > 1) //better safe than sorry - throw SysError(formatSystemError("write", L"", L"Buffer overflow.")); + ASSERT_SYSERROR(bytesWritten == 1); //better safe than sorry } catch (const SysError& e) { - const std::wstring logMsg = replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(lockFilePath_)) + L' ' + e.toString(); - std::cerr << utfTo(logMsg) + '\n'; - - warn_static("log on failure!") + logExtraError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(lockFilePath_)) + L"\n\n" + e.toString()); } } @@ -373,7 +361,16 @@ void waitOnDirLock(const Zstring& lockFilePath, const DirLockCallback& notifySta if (lockOwnderDead || //no need to wait any longer... lastCheckTime >= lastLifeSign + DETECT_ABANDONED_INTERVAL) { - DirLock guardDeletion(fff::impl::getLockFilePathForAbandonedLock(lockFilePath), notifyStatus, cbInterval); //throw FileError + const Zstring lockFileName = [&] + { + try + { + return fff::impl::getAbandonedLockFileName(getItemName(lockFilePath)); //throw SysError + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(lockFilePath)), e.toString()); } + }(); + + DirLock guardDeletion(*getParentFolderPath(lockFilePath), lockFileName, notifyStatus, cbInterval); //throw FileError //now that the lock is in place check existence again: meanwhile another process may have deleted and created a new lock! std::string currentLockId; @@ -388,7 +385,7 @@ void waitOnDirLock(const Zstring& lockFilePath, const DirLockCallback& notifySta if (getLockFileSize(lockFilePath) != fileSizeOld) //throw FileError, ErrorFileNotExisting return; //late life sign (or maybe even a different lock if retrieveLockId() failed!) } - catch (ErrorFileNotExisting&) { return; } //what we are waiting for... + catch (ErrorFileNotExisting&) { return; } //what we are waiting for anyway... removeFilePlain(lockFilePath); //throw FileError return; @@ -480,8 +477,7 @@ public: { ::releaseLock(lockFilePath_); //throw FileError } - catch (FileError&) {} - warn_static("log!!! at the very least") //https://freefilesync.org/forum/viewtopic.php?t=7655 + catch (const FileError& e) { logExtraError(e.toString()); } //inform user about remnant lock files *somehow*! } private: @@ -560,7 +556,7 @@ private: }; -DirLock::DirLock(const Zstring& lockFilePath, const DirLockCallback& notifyStatus, std::chrono::milliseconds cbInterval) //throw FileError +DirLock::DirLock(const Zstring& folderPath, const Zstring& fileName, const DirLockCallback& notifyStatus, std::chrono::milliseconds cbInterval) //throw FileError { - sharedLock_ = LockAdmin::instance().retrieve(lockFilePath, notifyStatus, cbInterval); //throw FileError + sharedLock_ = LockAdmin::instance().retrieve(appendPath(folderPath, fileName), notifyStatus, cbInterval); //throw FileError } diff --git a/FreeFileSync/Source/base/dir_lock.h b/FreeFileSync/Source/base/dir_lock.h index 87b3a7e6..31da3dbc 100644 --- a/FreeFileSync/Source/base/dir_lock.h +++ b/FreeFileSync/Source/base/dir_lock.h @@ -24,15 +24,24 @@ namespace fff - race-free (Windows, almost on Linux(NFS)) - NOT thread-safe! (1. global LockAdmin 2. locks for directory aliases should be created sequentially to detect duplicate locks!) */ +//intermediate locks created by DirLock use this extension, too: +constexpr ZstringView LOCK_FILE_ENDING = Zstr(".ffs_lock"); //don't use Zstring as global constant: avoid static initialization order problem in global namespace! + //while waiting for the lock using DirLockCallback = std::function; //throw X class DirLock { public: - DirLock(const Zstring& lockFilePath, //throw FileError - const DirLockCallback& notifyStatus, //callback only used during construction - std::chrono::milliseconds cbInterval); // + DirLock(const Zstring& folderPath, + const DirLockCallback& notifyStatus, //callback only used during construction + std::chrono::milliseconds cbInterval) : + DirLock(folderPath, Zstring(Zstr("sync")) + LOCK_FILE_ENDING, notifyStatus, cbInterval) {} //throw FileError + + DirLock(const Zstring& folderPath, + const Zstring& fileName, + const DirLockCallback& notifyStatus, + std::chrono::milliseconds cbInterval); //throw FileError private: class LockAdmin; @@ -43,7 +52,7 @@ private: namespace impl //declare for unit tests: { -Zstring getLockFilePathForAbandonedLock(const Zstring& lockFilePath); //throw FileError +Zstring getAbandonedLockFileName(const Zstring& lockFilePath); //throw FileError } } diff --git a/FreeFileSync/Source/base/lock_holder.h b/FreeFileSync/Source/base/lock_holder.h index e7d28c6a..e6a9eedc 100644 --- a/FreeFileSync/Source/base/lock_holder.h +++ b/FreeFileSync/Source/base/lock_holder.h @@ -9,8 +9,6 @@ namespace fff { -//intermediate locks created by DirLock use this extension, too: -const ZstringView LOCK_FILE_ENDING = Zstr(".ffs_lock"); //don't use Zstring as global constant: avoid static initialization order problem in global namespace! //Attention: 1. call after having checked directory existence! // 2. perf: remove folder aliases (e.g. case differences) *before* calling this function!!! @@ -28,8 +26,8 @@ public: for (const Zstring& folderPath : folderPaths) try { - //lock file creation is synchronous and may block noticeably for very slow devices (USB sticks, mapped cloud storage) - lockHolder_.emplace_back(appendPath(folderPath, Zstring(Zstr("sync")) + LOCK_FILE_ENDING), + //lock file creation is synchronous and may block noticeably for slow devices (USB sticks, mapped cloud storage) + lockHolder_.emplace_back(folderPath, [&](std::wstring&& msg) { pcb.updateStatus(std::move(msg)); /*throw X*/ }, UI_UPDATE_INTERVAL / 2); //throw FileError } diff --git a/FreeFileSync/Source/base/multi_rename.cpp b/FreeFileSync/Source/base/multi_rename.cpp new file mode 100644 index 00000000..bc1e48ad --- /dev/null +++ b/FreeFileSync/Source/base/multi_rename.cpp @@ -0,0 +1,180 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#include "multi_rename.h" +#include + +using namespace zen; +using namespace fff; + + +namespace +{ +std::wstring_view findLongestSubstring(const std::vector& strings) +{ + if (strings.empty()) + return {}; + + const std::wstring_view strMin = *std::min_element(strings.begin(), strings.end(), + /**/[](const std::wstring_view lhs, const std::wstring_view rhs) { return lhs.size() < rhs.size(); }); + + for (size_t sz = strMin.size(); sz > 0; --sz) //iterate over size, descending + for (size_t i = 0; i + sz <= strMin.size(); ++i) + { + const std::wstring_view substr(strMin.data() + i, sz); + //perf: duplicate substrings, especially für size = 1? + + const bool isCommon = [&] + { + for (const std::wstring_view str : strings) + if (str.data() != strMin.data()) //sufficient check: an extension of strMin wouldn't prune anyway + if (!contains(str, substr)) + return false; + return true; + }(); + + if (isCommon) + return substr; //*first* occuring substring of maximum size + } + + return {}; +} + + +struct StringPart +{ + std::vector diff; //may be empty, but only at beginning + std::wstring_view common; //may be empty, but only at end +}; + +std::vector getStringParts(std::vector&& strings) +{ + std::wstring_view substr = findLongestSubstring(strings); + if (!substr.empty()) + { + std::vector head; + std::vector tail; + + for (const std::wstring_view str : strings) + { + head.push_back(beforeFirst(str, substr, IfNotFoundReturn::none)); + tail.push_back(afterFirst (str, substr, IfNotFoundReturn::none)); + } + + std::vector np = getStringParts(std::move(head)); + assert(np.empty() || np.back().common.empty()); //otherwise we could construct an even longer substring! + + if (np.empty()) + np.push_back({{}, substr}); + else + np.back().common = substr; + + const std::vector npTail = getStringParts(std::move(tail)); + assert(npTail.empty() || !npTail.front().diff.empty()); //otherwise we could construct an even longer substring! + + append(np, npTail); + return np; + } + else + { + if (std::all_of(strings.begin(), strings.end(), [](const std::wstring_view str) { return str.empty(); })) + /**/return {}; + + return {{std::move(strings), {}}}; + } +} + + +constexpr wchar_t placeholders[] = //http://xahlee.info/comp/unicode_circled_numbers.html +{ + //L'\u24FF', //⓿ <- rendered bigger than the rest (same for ⓫) on Centos Linux + L'\u2776', //❶ + L'\u2777', //❷ + L'\u2778', //❸ + L'\u2779', //❹ + L'\u277A', //❺ + L'\u277B', //❻ + L'\u277C', //❼ + L'\u277D', //❽ + L'\u277E', //❾ + L'\u277F', //❿ -> last one is special: represents "all the rest" +}; + + +inline +size_t getPlaceholderIndex(wchar_t c) +{ + static_assert(std::size(placeholders) == 10); + if (placeholders[0] <= c && c <= placeholders[9]) + return static_cast(c - placeholders[0]); + + return static_cast(-1); +} +} + +struct fff::RenameBuf +{ + explicit RenameBuf(const std::vector& s) : strings(s) {} + + std::vector strings; + std::vector parts = getStringParts({strings.begin(), strings.end()}); +}; + + +//e.g. "Season ❶, Episode ❷ - ❸.avi" +std::pair> fff::getPlaceholderPhrase(const std::vector& strings) +{ + auto renameBuf = makeSharedRef(strings); + + std::wstring phrase; + size_t placeIdx = 0; + + for (const StringPart& p : renameBuf.ref().parts) + { + if (!p.diff.empty()) + { + phrase += placeholders[placeIdx++]; + + if (placeIdx >= std::size(placeholders)) + break; //represent "all the rest" with last placeholder + } + phrase += p.common; //TODO: what if common part already contains placeholder character!? + } + return {phrase, renameBuf}; +} + + +const std::vector fff::resolvePlaceholderPhrase(const std::wstring_view phrase, const RenameBuf& buf) +{ + std::vector> diffByIdx; + + for (const StringPart& p : buf.parts) + if (!p.diff.empty()) + diffByIdx.push_back(std::move(p.diff)), assert(diffByIdx.back().size() == buf.strings.size()); + + std::vector output; + + for (size_t i = 0; i < buf.strings.size(); ++i) + { + std::wstring resolved; + + for (const wchar_t c : phrase) + if (const size_t placeIdx = getPlaceholderIndex(c); + placeIdx < diffByIdx.size()) + { + if (placeIdx == std::size(placeholders) - 1) //last placeholder represents "all the rest" + resolved.append(diffByIdx[placeIdx][i].data(), buf.strings[i].data() + buf.strings[i].size()); + else + resolved += diffByIdx[placeIdx][i]; + } + else + resolved += c; + + output.push_back(std::move(resolved)); + } + + return output; +} diff --git a/FreeFileSync/Source/base/multi_rename.h b/FreeFileSync/Source/base/multi_rename.h new file mode 100644 index 00000000..40139049 --- /dev/null +++ b/FreeFileSync/Source/base/multi_rename.h @@ -0,0 +1,23 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#ifndef MULTI_RENAME_H_489572039485723453425 +#define MULTI_RENAME_H_489572039485723453425 + +//#include +//#include +//#include +#include + +namespace fff +{ +struct RenameBuf; + +std::pair> getPlaceholderPhrase(const std::vector& strings); +const std::vector resolvePlaceholderPhrase(const std::wstring_view phrase, const RenameBuf& buf); +} + +#endif //MULTI_RENAME_H_489572039485723453425 diff --git a/FreeFileSync/Source/base/process_callback.h b/FreeFileSync/Source/base/process_callback.h index 5bd75439..6d57b2d2 100644 --- a/FreeFileSync/Source/base/process_callback.h +++ b/FreeFileSync/Source/base/process_callback.h @@ -76,9 +76,9 @@ constexpr std::chrono::milliseconds UI_UPDATE_INTERVAL(100); //perform ui update enum class ProcessPhase { none, //initial status - scanning, - comparingContent, - synchronizing + scan, + binaryCompare, + sync }; //report status during comparison and synchronization diff --git a/FreeFileSync/Source/base/status_handler_impl.h b/FreeFileSync/Source/base/status_handler_impl.h index 0504e649..e0bf44ad 100644 --- a/FreeFileSync/Source/base/status_handler_impl.h +++ b/FreeFileSync/Source/base/status_handler_impl.h @@ -459,7 +459,7 @@ private: /**/ return 4; }(); //const int decPlaces = expectedSteps <= 100 ? 0 : static_cast(std::ceil(std::log10(expectedSteps))) - 2; -> overkill? - return zen::formatProgressPercent(fraction, decPlaces); + return zen::formatProgressPercent(fraction, decPlaces); } bool showPercent_ = false; diff --git a/FreeFileSync/Source/base/synchronization.cpp b/FreeFileSync/Source/base/synchronization.cpp index c36da571..1d686900 100644 --- a/FreeFileSync/Source/base/synchronization.cpp +++ b/FreeFileSync/Source/base/synchronization.cpp @@ -598,72 +598,69 @@ void checkPathRaceCondition(const BaseFolderPair& baseFolderP, const BaseFolderP const AbstractPath basePathP = baseFolderP.getAbstractPath(); //parent/child notion is tentative at this point const AbstractPath basePathC = baseFolderC.getAbstractPath(); //=> will be swapped if necessary - if (!AFS::isNullPath(basePathP) && !AFS::isNullPath(basePathC)) - if (basePathP.afsDevice == basePathC.afsDevice) - { - if (basePathP.afsPath.value.size() > basePathC.afsPath.value.size()) - return checkPathRaceCondition(baseFolderC, baseFolderP, pathRaceItems); + assert(!AFS::isNullPath(basePathP) && !AFS::isNullPath(basePathC)); + if (basePathP.afsDevice == basePathC.afsDevice) + { + if (basePathP.afsPath.value.size() > basePathC.afsPath.value.size()) + return checkPathRaceCondition(baseFolderC, baseFolderP, pathRaceItems); + + const std::vector relPathP = splitCpy(basePathP.afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip); + const std::vector relPathC = splitCpy(basePathC.afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip); - const std::vector relPathP = splitCpy(basePathP.afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip); - const std::vector relPathC = splitCpy(basePathC.afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip); + if (relPathP.size() <= relPathC.size() && + /**/std::equal(relPathP.begin(), relPathP.end(), relPathC.begin(), [](const Zstring& lhs, const Zstring& rhs) { return equalNoCase(lhs, rhs); })) + { + //=> at this point parent/child folders are confirmed + //now find child folder match inside baseFolderP + //e.g. C:\folder <-> C:\folder\sub => find "sub" inside C:\folder + std::vector childFolderP{&baseFolderP}; - if (relPathP.size() <= relPathC.size() && - /**/std::equal(relPathP.begin(), relPathP.end(), relPathC.begin(), [](const Zstring& lhs, const Zstring& rhs) { return equalNoCase(lhs, rhs); })) + std::for_each(relPathC.begin() + relPathP.size(), relPathC.end(), [&](const Zstring& itemName) { - //=> at this point parent/child folders are confirmed - //now find child folder match inside baseFolderP - //e.g. C:\folder <-> C:\folder\sub => find "sub" inside C:\folder - std::vector childFolderP{&baseFolderP}; + std::vector childFolderP2; - std::for_each(relPathC.begin() + relPathP.size(), relPathC.end(), [&](const Zstring& itemName) - { - std::vector childFolderP2; + for (const ContainerObject* childFolder : childFolderP) + for (const FolderPair& folder : childFolder->refSubFolders()) + if (equalNoCase(folder.getItemName(), itemName)) + childFolderP2.push_back(&folder); + //no "break": yes, weird, but there could be more than one (for case-sensitive file system) - for (const ContainerObject* childFolder : childFolderP) - for (const FolderPair& folder : childFolder->refSubFolders()) - if (equalNoCase(folder.getItemName(), itemName)) - childFolderP2.push_back(&folder); - //no "break": yes, weird, but there could be more than one (for case-sensitive file system) + childFolderP = std::move(childFolderP2); + }); - childFolderP = std::move(childFolderP2); - }); + std::vector pathRefsP; + for (const ContainerObject* childFolder : childFolderP) + append(pathRefsP, GetChildItemsHashed::execute(*childFolder)); - std::vector pathRefsP; - for (const ContainerObject* childFolder : childFolderP) - append(pathRefsP, GetChildItemsHashed::execute(*childFolder)); - - std::vector pathRefsC = GetChildItemsHashed::execute(baseFolderC); - - //--------------------------------------------------------------------------------------------------- - //case-sensitive comparison because items were scanned by FFS (=> no messy user input)? - //not good enough! E.g. not-yet-existing files are set to be created with different case! - // + (weird) a file and a folder are set to be created with same name - // => (throw hands in the air) fine, check path only and don't consider case - sortAndRemoveDuplicates(pathRefsP); - sortAndRemoveDuplicates(pathRefsC); - - mergeTraversal(pathRefsP.begin(), pathRefsP.end(), - pathRefsC.begin(), pathRefsC.end(), - [](const ChildPathRef&) {} /*left only*/, - [&](const ChildPathRef& lhs, const ChildPathRef& rhs) + std::vector pathRefsC = GetChildItemsHashed::execute(baseFolderC); + + //--------------------------------------------------------------------------------------------------- + //case-sensitive comparison because items were scanned by FFS (=> no messy user input)? + //not good enough! E.g. not-yet-existing files are set to be created with different case! + // + (weird) a file and a folder are set to be created with same name + // => (throw hands in the air) fine, check path only and don't consider case + sortAndRemoveDuplicates(pathRefsP); + sortAndRemoveDuplicates(pathRefsC); + + mergeTraversal(pathRefsP.begin(), pathRefsP.end(), + pathRefsC.begin(), pathRefsC.end(), + [](const ChildPathRef&) {} /*left only*/, + [&](const ChildPathRef& lhs, const ChildPathRef& rhs) + { + if (plannedWriteAccess(*lhs.fsObj) || + plannedWriteAccess(*rhs.fsObj)) { - if (plannedWriteAccess(*lhs.fsObj) || - plannedWriteAccess(*rhs.fsObj)) - { - pathRaceItems.push_back({lhs.fsObj, sideP}); - pathRaceItems.push_back({rhs.fsObj, sideC}); - } - }, - [](const ChildPathRef&) {} /*right only*/, compareHashedPathNoCase); - } + pathRaceItems.push_back({lhs.fsObj, sideP}); + pathRaceItems.push_back({rhs.fsObj, sideC}); + } + }, + [](const ChildPathRef&) {} /*right only*/, compareHashedPathNoCase); } + } } //################################################################################################################# -warn_static("review: does flushFileBuffers() make sense?") -//https://devblogs.microsoft.com/oldnewthing/20221007-00/?p=107261 - //--------------------- data verification ------------------------- void flushFileBuffers(const Zstring& nativeFilePath) //throw FileError { @@ -2485,7 +2482,7 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime //keep at beginning so that all gui elements are initialized properly callback.initNewPhase(itemsTotal, //throw X bytesTotal, - ProcessPhase::synchronizing); + ProcessPhase::sync); } //------------------------------------------------------------------------------- @@ -2532,6 +2529,9 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime const FolderPairSyncCfg& folderPairCfg = syncConfig[folderIndex]; const SyncStatistics& folderPairStat = folderPairStats[folderIndex]; + //=============== start with checks that may SKIP folder pairs =============== + //============================================================================ + //exclude a few pathological cases, e.g. empty folder pair if (baseFolder.getAbstractPath() == baseFolder.getAbstractPath()) @@ -2540,54 +2540,19 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime continue; } - //prepare conflict preview: - if (folderPairStat.conflictCount() > 0) - checkUnresolvedConflicts.emplace_back(&baseFolder, folderPairStat.conflictCount(), folderPairStat.getConflictsPreview()); - - //consider *all* paths that might be used during versioning limit at some time - const AbstractPath versioningFolderPath = createAbstractPath(folderPairCfg.versioningFolderPhrase); - - if (folderPairCfg.handleDeletion == DeletionVariant::versioning && - folderPairCfg.versioningStyle != VersioningStyle::replace) - if (folderPairCfg.versionMaxAgeDays > 0 || folderPairCfg.versionCountMax > 0) //same check as in applyVersioningLimit() - checkVersioningLimitPaths.insert(versioningFolderPath); - - const bool writeLeft = folderPairStat.createCount() + - folderPairStat.updateCount() + - folderPairStat.deleteCount() > 0; - - const bool writeRight = folderPairStat.createCount() + - folderPairStat.updateCount() + - folderPairStat.deleteCount() > 0; - - //prepare: check if some files are used by multiple pairs in read/write access - checkBaseFolderRaceCondition.emplace_back(&baseFolder, SelectSide::left, writeLeft); - checkBaseFolderRaceCondition.emplace_back(&baseFolder, SelectSide::right, writeRight); - - //prepare: check if versioning path itself will be synchronized (and was not excluded via filter) - if (folderPairCfg.handleDeletion == DeletionVariant::versioning) - checkVersioningPaths.insert(versioningFolderPath); - - checkVersioningBasePaths.emplace_back(baseFolder.getAbstractPath(), &baseFolder.getFilter()); - checkVersioningBasePaths.emplace_back(baseFolder.getAbstractPath(), &baseFolder.getFilter()); - - //=============================================================================== - //================ begin of checks that may SKIP folder pairs =================== - //=============================================================================== - - //skip folder pair if there is nothing to do (except when DB files need to be updated for two-way mode and move-detection) - //=> avoid redundant errors in checkBaseFolderStatus() if base folder existence test failed during comparison - if (getCUD(folderPairStat) == 0 && !folderPairCfg.saveSyncDB) + //synchronization with only one folder selected, doesn't make sense => don't support "deletion via empty source folder" + if (AFS::isNullPath(baseFolder.getAbstractPath()) || + AFS::isNullPath(baseFolder.getAbstractPath())) { + callback.reportFatalError(_("A folder input field is empty.")); skipFolderPair[folderIndex] = true; continue; } - //check for empty target folder paths: this only makes sense if empty field is source (and no DB files need to be created) - if ((AFS::isNullPath(baseFolder.getAbstractPath()) && (writeLeft || folderPairCfg.saveSyncDB)) || - (AFS::isNullPath(baseFolder.getAbstractPath()) && (writeRight || folderPairCfg.saveSyncDB))) + //skip folder pair if there is nothing to do (except when DB files need to be updated for two-way mode and move-detection) + //=> avoid redundant errors in checkBaseFolderStatus() if base folder existence test failed during comparison + if (getCUD(folderPairStat) == 0 && !folderPairCfg.saveSyncDB) { - callback.reportFatalError(_("Target folder input field must not be empty.")); skipFolderPair[folderIndex] = true; continue; } @@ -2605,16 +2570,15 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime //allow propagation of deletions only from *empty* or *existing* source folder: auto sourceFolderMissing = [&](const AbstractPath& baseFolderPath, BaseFolderStatus folderStatus) //we need to evaluate existence status from time of comparison! { - if (!AFS::isNullPath(baseFolderPath)) - //PERMANENT network drop: avoid data loss when source directory is not found AND user chose to ignore errors (else we wouldn't arrive here) - if (folderPairStat.deleteCount() > 0) //check deletions only... (respect filtered items!) - //folderPairStat.conflictCount() == 0 && -> there COULD be conflicts for variant if directory existence check fails, but loading sync.ffs_db succeeds - //https://sourceforge.net/tracker/?func=detail&atid=1093080&aid=3531351&group_id=234430 -> fixed, but still better not consider conflicts! - if (folderStatus != BaseFolderStatus::existing) //avoid race-condition: we need to evaluate existence status from time of comparison! - { - callback.reportFatalError(replaceCpy(_("Source folder %x not found."), L"%x", fmtPath(AFS::getDisplayPath(baseFolderPath)))); - return true; - } + //PERMANENT network drop: avoid data loss when source directory is not found AND user chose to ignore errors (else we wouldn't arrive here) + if (folderPairStat.deleteCount() > 0) //check deletions only... (respect filtered items!) + //folderPairStat.conflictCount() == 0 && -> there COULD be conflicts for variant if directory existence check fails, but loading sync.ffs_db succeeds + //https://sourceforge.net/tracker/?func=detail&atid=1093080&aid=3531351&group_id=234430 -> fixed, but still better not consider conflicts! + if (folderStatus != BaseFolderStatus::existing) //avoid race-condition: we need to evaluate existence status from time of comparison! + { + callback.reportFatalError(replaceCpy(_("Source folder %x not found."), L"%x", fmtPath(AFS::getDisplayPath(baseFolderPath)))); + return true; + } return false; }; if (sourceFolderMissing(baseFolder.getAbstractPath(), baseFolder.getFolderStatus()) || @@ -2625,6 +2589,8 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime } //check if user-defined directory for deletion was specified + const AbstractPath versioningFolderPath = createAbstractPath(folderPairCfg.versioningFolderPhrase); + if (folderPairCfg.handleDeletion == DeletionVariant::versioning) if (AFS::isNullPath(versioningFolderPath)) { @@ -2633,17 +2599,47 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime continue; } + //============ Warnings (*after* potential folder pair skips) ============ + //======================================================================== + + //prepare conflict preview: + if (folderPairStat.conflictCount() > 0) + checkUnresolvedConflicts.emplace_back(&baseFolder, folderPairStat.conflictCount(), folderPairStat.getConflictsPreview()); + + //prepare: check if some files are used by multiple pairs in read/write access + const bool writeLeft = folderPairStat.createCount() + + folderPairStat.updateCount() + + folderPairStat.deleteCount() > 0; + + const bool writeRight = folderPairStat.createCount() + + folderPairStat.updateCount() + + folderPairStat.deleteCount() > 0; + + checkBaseFolderRaceCondition.emplace_back(&baseFolder, SelectSide::left, writeLeft); + checkBaseFolderRaceCondition.emplace_back(&baseFolder, SelectSide::right, writeRight); + + //prepare: check if versioning path itself will be synchronized (and was not excluded via filter) + if (folderPairCfg.handleDeletion == DeletionVariant::versioning) + checkVersioningPaths.insert(versioningFolderPath); + + checkVersioningBasePaths.emplace_back(baseFolder.getAbstractPath(), &baseFolder.getFilter()); + checkVersioningBasePaths.emplace_back(baseFolder.getAbstractPath(), &baseFolder.getFilter()); + + //prepare: versioning folder paths differing only in case + if (folderPairCfg.handleDeletion == DeletionVariant::versioning && + folderPairCfg.versioningStyle != VersioningStyle::replace) + if (folderPairCfg.versionMaxAgeDays > 0 || folderPairCfg.versionCountMax > 0) //same check as in applyVersioningLimit() + checkVersioningLimitPaths.insert(versioningFolderPath); + //check if more than 50% of total number of files/dirs are to be created/overwritten/deleted - if (!AFS::isNullPath(baseFolder.getAbstractPath()) && - !AFS::isNullPath(baseFolder.getAbstractPath())) - if (significantDifferenceDetected(folderPairStat)) - checkSignificantDiffPairs.emplace_back(baseFolder.getAbstractPath(), - baseFolder.getAbstractPath()); + if (significantDifferenceDetected(folderPairStat)) + checkSignificantDiffPairs.emplace_back(baseFolder.getAbstractPath(), + baseFolder.getAbstractPath()); //check for sufficient free diskspace (folderPath might not yet exist!) auto checkSpace = [&](const AbstractPath& baseFolderPath, int64_t minSpaceNeeded) { - if (!AFS::isNullPath(baseFolderPath) && minSpaceNeeded > 0) + if (minSpaceNeeded > 0) try { const int64_t freeSpace = AFS::getFreeDiskSpace(baseFolderPath); //throw FileError, returns < 0 if not available @@ -2823,14 +2819,14 @@ break2: msg += L"\n\n" + _("Selected folder:") + L" \t" + AFS::getDisplayPath(folderPath) + L'\n' + _("Versioning folder:") + L" \t" + AFS::getDisplayPath(versioningFolderPath); - if (pd->folderPathParent == folderPath) //else: probably fine? :> - if (!pd->relPath.empty()) + + if (pd->folderPathParent == folderPath) //if versioning folder is a subfolder of a base folder + if (!pd->relPath.empty()) //this can be fixed via an exclude filter { shouldExclude = true; msg += std::wstring() + L'\n' + L"⇒ " + _("Exclude:") + L" \t" + utfTo(FILE_NAME_SEPARATOR + pd->relPath + FILE_NAME_SEPARATOR); } - warn_static("else: ???") } } if (!msg.empty()) @@ -2935,8 +2931,6 @@ break2: tryReportingError([&] { copyPermissionsFp = copyFilePermissions && //copy permissions only if asked for and supported by *both* sides! - !AFS::isNullPath(baseFolder.getAbstractPath()) && //scenario: directory selected on one side only - !AFS::isNullPath(baseFolder.getAbstractPath()) && // AFS::supportPermissionCopy(baseFolder.getAbstractPath(), baseFolder.getAbstractPath()); //throw FileError }, callback); //throw X diff --git a/FreeFileSync/Source/base_tools.cpp b/FreeFileSync/Source/base_tools.cpp index 8ab91efd..179ecdab 100644 --- a/FreeFileSync/Source/base_tools.cpp +++ b/FreeFileSync/Source/base_tools.cpp @@ -12,7 +12,7 @@ using namespace zen; using namespace fff; -std::vector fff::fromTimeShiftPhrase(const std::wstring& timeShiftPhrase) +std::vector fff::fromTimeShiftPhrase(const std::wstring_view timeShiftPhrase) { std::vector minutes; diff --git a/FreeFileSync/Source/base_tools.h b/FreeFileSync/Source/base_tools.h index f67a1940..1ea3e65e 100644 --- a/FreeFileSync/Source/base_tools.h +++ b/FreeFileSync/Source/base_tools.h @@ -15,7 +15,7 @@ namespace fff { //convert "ignoreTimeShiftMinutes" into compact format: -std::vector fromTimeShiftPhrase(const std::wstring& timeShiftPhrase); +std::vector fromTimeShiftPhrase(const std::wstring_view timeShiftPhrase); std::wstring toTimeShiftPhrase (const std::vector& ignoreTimeShiftMinutes); //inform about (important) non-default global settings related to comparison and synchronization diff --git a/FreeFileSync/Source/config.cpp b/FreeFileSync/Source/config.cpp index 36ce5cf2..45c20daa 100644 --- a/FreeFileSync/Source/config.cpp +++ b/FreeFileSync/Source/config.cpp @@ -266,32 +266,32 @@ bool readText(const std::string& input, PostSyncCondition& value) template <> inline -void writeText(const PostSyncAction& value, std::string& output) +void writeText(const PostBatchAction& value, std::string& output) { switch (value) { - case PostSyncAction::none: + case PostBatchAction::none: output = "None"; break; - case PostSyncAction::sleep: + case PostBatchAction::sleep: output = "Sleep"; break; - case PostSyncAction::shutdown: + case PostBatchAction::shutdown: output = "Shutdown"; break; } } template <> inline -bool readText(const std::string& input, PostSyncAction& value) +bool readText(const std::string& input, PostBatchAction& value) { const std::string tmp = trimCpy(input); if (tmp == "None") - value = PostSyncAction::none; + value = PostBatchAction::none; else if (tmp == "Sleep") - value = PostSyncAction::sleep; + value = PostBatchAction::sleep; else if (tmp == "Shutdown") - value = PostSyncAction::shutdown; + value = PostBatchAction::shutdown; else return false; return true; @@ -820,37 +820,37 @@ bool readStruc(const XmlElement& input, ExternalApp& value) template <> inline -void writeText(const SyncResult& value, std::string& output) +void writeText(const TaskResult& value, std::string& output) { switch (value) { - case SyncResult::finishedSuccess: + case TaskResult::success: output = "Success"; break; - case SyncResult::finishedWarning: + case TaskResult::warning: output = "Warning"; break; - case SyncResult::finishedError: + case TaskResult::error: output = "Error"; break; - case SyncResult::aborted: + case TaskResult::cancelled: output = "Stopped"; break; } } template <> inline -bool readText(const std::string& input, SyncResult& value) +bool readText(const std::string& input, TaskResult& value) { const std::string tmp = trimCpy(input); if (tmp == "Success") - value = SyncResult::finishedSuccess; + value = TaskResult::success; else if (tmp == "Warning") - value = SyncResult::finishedWarning; + value = TaskResult::warning; else if (tmp == "Error") - value = SyncResult::finishedError; + value = TaskResult::error; else if (tmp == "Stopped") - value = SyncResult::aborted; + value = TaskResult::cancelled; else return false; return true; @@ -915,7 +915,7 @@ template <> inline bool readStruc(const XmlElement& input, ConfigFileItem& value) { bool success = true; - success = input.getAttribute("LastSync", value.lastRunStats.syncTime) && success; + success = input.getAttribute("LastSync", value.lastRunStats.startTime) && success; success = input.getAttribute("Result", value.lastRunStats.syncResult) && success; if (input.hasAttribute("CfgPath")) //TODO: remove after migration! 2020-02-09 @@ -971,18 +971,18 @@ bool readStruc(const XmlElement& input, ConfigFileItem& value) template <> inline void writeStruc(const ConfigFileItem& value, XmlElement& output) { - output.setAttribute("LastSync", value.lastRunStats.syncTime); + output.setAttribute("LastSync", value.lastRunStats.startTime); output.setAttribute("Result", value.lastRunStats.syncResult); output.setAttribute("Config", makePortablePath(value.cfgFilePath)); - output.setAttribute("Log", makePortablePath(AFS::getInitPathPhrase(value.lastRunStats.logFilePath))); + output.setAttribute("Log", makePortablePath(AFS::getInitPathPhrase(value.lastRunStats.logFilePath))); output.setAttribute("Items", value.lastRunStats.itemsProcessed); output.setAttribute("Bytes", value.lastRunStats.bytesProcessed); output.setAttribute("TotalTime", value.lastRunStats.totalTime); - output.setAttribute("Errors", value.lastRunStats.errors); + output.setAttribute("Errors", value.lastRunStats.errors); output.setAttribute("Warnings", value.lastRunStats.warnings); if (value.backColor.IsOk()) @@ -1151,8 +1151,10 @@ void readConfig(const XmlIn& in, MainConfiguration& mainCfg, int formatVer) //########################################################### //read folder pairs bool firstItem = true; - for (XmlIn inPair = in["FolderPairs"]["Pair"]; inPair; inPair.next()) + in["FolderPairs"].visitChildren([&](const XmlIn& inPair) { + assert(*inPair.getName() == "Pair"); + LocalPairConfig lpc; readConfig(inPair, lpc, mainCfg.deviceParallelOps, formatVer); @@ -1164,7 +1166,7 @@ void readConfig(const XmlIn& in, MainConfiguration& mainCfg, int formatVer) } else mainCfg.additionalPairs.push_back(lpc); - } + }); in["Errors"].attribute("Ignore", mainCfg.ignoreErrors); in["Errors"].attribute("Retry", mainCfg.autoRetryCount); @@ -1230,7 +1232,7 @@ void readConfig(const XmlIn& in, XmlBatchConfig& cfg, int formatVer) inBatch["ProgressDialog"].attribute("Minimized", cfg.batchExCfg.runMinimized); inBatch["ProgressDialog"].attribute("AutoClose", cfg.batchExCfg.autoCloseSummary); inBatch["ErrorDialog"](cfg.batchExCfg.batchErrorHandling); - inBatch["PostSyncAction"](cfg.batchExCfg.postSyncAction); + inBatch["PostSyncAction"](cfg.batchExCfg.postBatchAction); } @@ -1669,7 +1671,9 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) //cfg.dpiLayouts.clear(); -> NO: honor migration code above! - for (XmlIn inLayout = in["DpiLayouts"]["Layout"]; inLayout; inLayout.next()) + in["DpiLayouts"].visitChildren([&](const XmlIn& inLayout) + { + assert(*inLayout.getName() == "Layout"); if (std::string scaleTxt; inLayout.attribute("Scale", scaleTxt)) { @@ -1741,6 +1745,7 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) cfg.dpiLayouts.emplace(scalePercent, std::move(layout)); } + }); } @@ -1755,21 +1760,18 @@ std::pair parseConfig(const XmlDoc& doc readConfig(in, cfg, formatVer); std::wstring warningMsg; - try - { - checkXmlMappingErrors(in); //throw FileError - //(try to) migrate old configuration if needed + if (const std::wstring& errors = in.getErrors(); + !errors.empty()) + warningMsg = replaceCpy(_("Configuration file %x is incomplete. The missing elements have been set to their default values."), L"%x", fmtPath(filePath)) + L"\n\n" + + _("The following XML elements could not be read:") + L'\n' + errors; + else //(try to) migrate old configuration if needed if (formatVer < currentXmlFormatVer) - try { fff::writeConfig(cfg, filePath); /*throw FileError*/ } - catch (FileError&) { assert(false); } //don't bother user! - warn_static("at least log on failure!") - } - catch (const FileError& e) - { - warningMsg = replaceCpy(_("Configuration file %x is incomplete. The missing elements have been set to their default values."), L"%x", fmtPath(filePath)) + - L"\n\n" + e.toString(); - } + try + { + fff::writeConfig(cfg, filePath); //throw FileError + } + catch (const FileError& e) { warningMsg = e.toString(); } return {cfg, warningMsg}; } @@ -2022,7 +2024,7 @@ void writeConfig(const XmlBatchConfig& cfg, XmlOut& out) outBatch["ProgressDialog"].attribute("Minimized", cfg.batchExCfg.runMinimized); outBatch["ProgressDialog"].attribute("AutoClose", cfg.batchExCfg.autoCloseSummary); outBatch["ErrorDialog" ](cfg.batchExCfg.batchErrorHandling); - outBatch["PostSyncAction"](cfg.batchExCfg.postSyncAction); + outBatch["PostSyncAction"](cfg.batchExCfg.postBatchAction); } @@ -2258,17 +2260,63 @@ std::optional fff::parseFilterBuf(const std::string& filterBuf) try { XmlDoc doc = parseXml(filterBuf); //throw XmlParsingError - XmlIn in(doc); + FilterConfig filterCfg; ::readConfig(in, filterCfg); - - checkXmlMappingErrors(in); //throw FileError - - return filterCfg; + if (in.getErrors().empty()) + return filterCfg; } catch (XmlParsingError&) {} - catch (FileError&) {} return std::nullopt; } + + +void fff::saveErrorLog(const ErrorLog& log, const Zstring& filePath) //throw FileError +{ + XmlDoc doc("Log"); + doc.setEncoding(""); + + XmlOut out(doc); + + for (const LogEntry& e : log) + { + XmlOut outMsg = out.addChild(e.type == MessageType::MSG_TYPE_ERROR ? "Error" : (e.type == MessageType::MSG_TYPE_WARNING ? "Warning" : "Info")); + outMsg.attribute("Time", formatTime(formatIsoDateTimeTag, getLocalTime(e.time))); + outMsg(e.message); + } + + saveXml(doc, filePath); //throw FileError +} + + +ErrorLog fff::loadErrorLog(const Zstring& filePath) //throw FileError +{ + XmlDoc doc = loadXml(filePath); //throw FileError + + XmlIn in(doc); + ErrorLog log; + + in.visitChildren([&](const XmlIn& inMsg) + { + Zstring timeStr; + inMsg.attribute("Time", timeStr); + + Zstringc msg; + inMsg(msg); + + log.push_back( + { + .time = localToTimeT(parseTime(formatIsoDateTimeTag, timeStr)).first, + .type = *inMsg.getName() == "Error" ? MessageType::MSG_TYPE_ERROR : (*inMsg.getName() == "Warning" ? MessageType::MSG_TYPE_WARNING : MessageType::MSG_TYPE_INFO), + .message = std::move(msg), + }); + }); + + if (const std::wstring& errors = in.getErrors(); + !errors.empty()) + throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(filePath)), + _("The following XML elements could not be read:") + L'\n' + errors); + return log; +} diff --git a/FreeFileSync/Source/config.h b/FreeFileSync/Source/config.h index d7c65d72..6193effd 100644 --- a/FreeFileSync/Source/config.h +++ b/FreeFileSync/Source/config.h @@ -27,7 +27,7 @@ enum class BatchErrorHandling }; -enum class PostSyncAction +enum class PostBatchAction { none, sleep, @@ -58,7 +58,7 @@ struct BatchExclusiveConfig bool runMinimized = false; bool autoCloseSummary = false; BatchErrorHandling batchErrorHandling = BatchErrorHandling::showPopup; - PostSyncAction postSyncAction = PostSyncAction::none; + PostBatchAction postBatchAction = PostBatchAction::none; }; @@ -271,6 +271,9 @@ std::wstring extractJobName(const Zstring& cfgFilePath); //human-readable/editable format suitable for clipboard std::string serializeFilter(const FilterConfig& filterCfg); std::optional parseFilterBuf(const std::string& filterBuf); + +void saveErrorLog(const zen::ErrorLog& log, const Zstring& filePath); //throw FileError +zen::ErrorLog loadErrorLog(const Zstring& filePath); //throw FileError } #endif //PROCESS_XML_H_28345825704254262435 diff --git a/FreeFileSync/Source/ffs_paths.cpp b/FreeFileSync/Source/ffs_paths.cpp index 751a3739..f1f6cd00 100644 --- a/FreeFileSync/Source/ffs_paths.cpp +++ b/FreeFileSync/Source/ffs_paths.cpp @@ -83,12 +83,8 @@ Zstring fff::getConfigDirPath() { createDirectoryIfMissingRecursion(configPath); //throw FileError } - catch (const FileError& e) - { - assert(false); - std::cerr << utfTo(e.toString()) + '\n'; - warn_static("at least log on failure!") - } + catch (const FileError& e) { logExtraError(e.toString()); } + return configPath; }(); return ffsConfigPath; @@ -96,7 +92,7 @@ Zstring fff::getConfigDirPath() //this function is called by RealTimeSync!!! -Zstring fff::getFreeFileSyncLauncherPath() +Zstring fff::getFreeFileSyncLauncherPath() //throw FileError { return appendPath(getInstallDirPath(), Zstr("FreeFileSync")); diff --git a/FreeFileSync/Source/ffs_paths.h b/FreeFileSync/Source/ffs_paths.h index c2d63252..6cf122f1 100644 --- a/FreeFileSync/Source/ffs_paths.h +++ b/FreeFileSync/Source/ffs_paths.h @@ -22,7 +22,8 @@ Zstring getConfigDirPath(); Zstring getInstallDirPath(); -Zstring getFreeFileSyncLauncherPath(); //full path to application launcher C:\...\FreeFileSync.exe +Zstring getFreeFileSyncLauncherPath(); //throw FileError +//full path to application launcher C:\...\FreeFileSync.exe } #endif //FFS_PATHS_H_842759083425342534253 diff --git a/FreeFileSync/Source/icon_buffer.cpp b/FreeFileSync/Source/icon_buffer.cpp index baaa31f1..f4851b06 100644 --- a/FreeFileSync/Source/icon_buffer.cpp +++ b/FreeFileSync/Source/icon_buffer.cpp @@ -46,6 +46,7 @@ std::variant getDisplayIcon(const AbstractPath& ite return ih; } catch (FileError&) {} + //else: fallback to non-thumbnail icon break; } @@ -141,7 +142,7 @@ public: { if (*ih) //if not yet converted... { - idata.iconFmt = std::make_unique(extractWxImage(std::move(*ih))); //convert in main thread! + idata.iconImg = std::make_unique(extractWxImage(std::move(*ih))); //convert in main thread! assert(!*ih); } } @@ -149,13 +150,13 @@ public: { if (FileIconHolder& fih = std::get(idata.iconHolder)) //if not yet converted... { - idata.iconFmt = std::make_unique(extractWxImage(std::move(fih))); //convert in main thread! + idata.iconImg = std::make_unique(extractWxImage(std::move(fih))); //convert in main thread! assert(!fih); - //!idata.iconFmt->IsOk(): extractWxImage() might fail if icon theme is missing a MIME type! + //!idata.iconImg->IsOk(): extractWxImage() might fail if icon theme is missing a MIME type! } } - return idata.iconFmt ? *idata.iconFmt : wxNullImage; //idata.iconHolder may be inserted as empty from worker thread! + return idata.iconImg ? *idata.iconImg : wxNullImage; //idata.iconHolder may be inserted as empty from worker thread! } //called by main and worker thread: @@ -253,11 +254,11 @@ private: struct IconData { IconData() {} - IconData(IconData&& tmp) noexcept : iconHolder(std::move(tmp.iconHolder)), iconFmt(std::move(tmp.iconFmt)), prev(tmp.prev), next(tmp.next) {} + IconData(IconData&& tmp) noexcept : iconHolder(std::move(tmp.iconHolder)), iconImg(std::move(tmp.iconImg)), prev(tmp.prev), next(tmp.next) {} std::variant iconHolder; //native icon representation: may be used by any thread - std::unique_ptr iconFmt; //use ONLY from main thread! + std::unique_ptr iconImg; //use ONLY from main thread! //wxImage is NOT thread-safe: non-atomic ref-count just to begin with... //- prohibit implicit calls to wxImage() //- prohibit calls to ~wxImage() and transitively ~IconData() diff --git a/FreeFileSync/Source/localization.cpp b/FreeFileSync/Source/localization.cpp index 6bb2f376..7c6fa18a 100644 --- a/FreeFileSync/Source/localization.cpp +++ b/FreeFileSync/Source/localization.cpp @@ -156,8 +156,14 @@ std::vector loadTranslations(const Zstring& zipPath) //throw Fi .lngStream = std::move(stream), }); } - catch (lng::ParsingError&) { assert(false); } - warn_static("at least log on failure!") + catch (const lng::ParsingError& e) + { + throw FileError(replaceCpy(replaceCpy(replaceCpy(_("Error parsing file %x, row %y, column %z."), + L"%x", fmtPath(fileName)), + L"%y", formatNumber(e.row + 1)), + L"%z", formatNumber(e.col + 1)) + + L"\n\n" + e.msg); + } std::sort(translations.begin(), translations.end(), [](const TranslationInfo& lhs, const TranslationInfo& rhs) { @@ -376,7 +382,7 @@ void fff::setLanguage(wxLanguage lng) //throw FileError { setTranslator(std::make_unique(lngStream)); //throw lng::ParsingError, plural::ParsingError } - catch (lng::ParsingError& e) + catch (const lng::ParsingError& e) { throw FileError(replaceCpy(replaceCpy(replaceCpy(_("Error parsing file %x, row %y, column %z."), L"%x", fmtPath(lngFileName)), diff --git a/FreeFileSync/Source/log_file.cpp b/FreeFileSync/Source/log_file.cpp index d5231ca6..b8f4021b 100644 --- a/FreeFileSync/Source/log_file.cpp +++ b/FreeFileSync/Source/log_file.cpp @@ -46,7 +46,7 @@ std::string generateLogHeaderTxt(const ProcessSummary& s, const ErrorLog& log, i //assemble summary box std::vector summary; summary.emplace_back(); - summary.push_back(tabSpace + utfTo(getSyncResultLabel(s.syncResult))); + summary.push_back(tabSpace + utfTo(getSyncResultLabel(s.result))); summary.emplace_back(); const ErrorLogStats logCount = getStats(log); @@ -192,12 +192,12 @@ std::wstring generateLogTitle(const ProcessSummary& s) if (!jobNamesFmt.empty()) title += jobNamesFmt + L' '; - switch (s.syncResult) + switch (s.result) { - case SyncResult::finishedSuccess: title += utfTo("\xe2\x9c\x94" "\xef\xb8\x8f"); break; //✔️ - case SyncResult::finishedWarning: title += utfTo("\xe2\x9a\xa0" "\xef\xb8\x8f"); break; //⚠️ - case SyncResult::finishedError: //efb88f (U+FE0F): variation selector-16 to prefer emoji over text rendering - case SyncResult::aborted: title += utfTo("\xe2\x9d\x8c" "\xef\xb8\x8f"); break; //❌️ + case TaskResult::success: title += utfTo("\xe2\x9c\x94" "\xef\xb8\x8f"); break; //✔️ + case TaskResult::warning: title += utfTo("\xe2\x9a\xa0" "\xef\xb8\x8f"); break; //⚠️ + case TaskResult::error: //efb88f (U+FE0F): variation selector-16 to prefer emoji over text rendering + case TaskResult::cancelled: title += utfTo("\xe2\x9d\x8c" "\xef\xb8\x8f"); break; //❌️ } return title; } @@ -205,6 +205,7 @@ std::wstring generateLogTitle(const ProcessSummary& s) std::string generateLogHeaderHtml(const ProcessSummary& s, const ErrorLog& log, int logPreviewMax) { + //caveat: non-inline CSS is often ignored by email clients! std::string output = R"( @@ -212,7 +213,6 @@ std::string generateLogHeaderHtml(const ProcessSummary& s, const ErrorLog& log, )" + htmlTxt(generateLogTitle(s)) + R"(