Files
2025-12-10 14:38:26 -08:00

769 lines
38 KiB
C++

// *****************************************************************************
// * 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 "application.h"
#include <memory>
#include <zen/file_access.h>
#include <zen/json.h>
#include <zen/shutdown.h>
#include <zen/process_exec.h>
#include <zen/resolve_path.h>
#include <zen/sys_info.h>
#include <wx/clipbrd.h>
#include <wx/tooltip.h>
#include <wx+/darkmode.h>
#include <wx+/popup_dlg.h>
#include <wx+/image_resources.h>
#include "afs/concrete.h"
#include "base/comparison.h"
#include "base/synchronization.h"
#include "ui/batch_status_handler.h"
#include "ui/main_dlg.h"
#include "ui/small_dlgs.h"
#include "base_tools.h"
#include "ffs_paths.h"
#include "return_codes.h"
#include <gtk/gtk.h>
using namespace zen;
using namespace fff;
#ifdef __WXGTK3__
/* Wayland backend used by GTK3 does not allow to move windows! (no such issue on GTK2)
"I'd really like to know if there is some deep technical reason for it or
if this is really as bloody stupid as it seems?" - vadz https://github.com/wxWidgets/wxWidgets/issues/18733#issuecomment-1011235902
Show all available GTK backends: run FreeFileSync with env variable: GDK_BACKEND=help
=> workaround: https://docs.gtk.org/gdk3/func.set_allowed_backends.html */
GLOBAL_RUN_ONCE(::gdk_set_allowed_backends("x11,*")); //call *before* gtk_init()
#endif
IMPLEMENT_APP(Application)
namespace
{
std::vector<Zstring> getCommandlineArgs(const wxApp& app)
{
std::vector<Zstring> args;
for (const wxString& arg : app.argv.GetArguments())
args.push_back(utfTo<Zstring>(arg));
//remove first argument which is exe path by convention: https://devblogs.microsoft.com/oldnewthing/20060515-07/?p=31203
if (!args.empty())
args.erase(args.begin());
return args;
}
void showSyntaxHelp()
{
showNotificationDialog(nullptr, DialogInfoType::info, PopupDialogCfg().
setTitle(_("Command line")).
setDetailInstructions(_("Syntax:") + L"\n\n" +
L"FreeFileSync" + L'\n' +
TAB_SPACE + L"[" + _("config files:") + L" *.ffs_gui/*.ffs_batch]" + L'\n' +
TAB_SPACE + L"[-DirPair " + _("directory") + L' ' + _("directory") + L"]" L"\n" +
TAB_SPACE + L"[-Edit]" + L'\n' +
TAB_SPACE + L"[" + _("global config file:") + L" GlobalSettings.xml]" + L"\n\n" +
_("config files:") + L'\n' +
_("Any number of FreeFileSync \"ffs_gui\" and/or \"ffs_batch\" configuration files.") + L"\n\n" +
L"-DirPair " + _("directory") + L' ' + _("directory") + L'\n' +
_("Any number of alternative directory pairs for at most one config file.") + L"\n\n" +
L"-Edit" + L'\n' +
_("Open the selected configuration for editing only, without executing it.") + L"\n\n" +
_("global config file:") + L'\n' +
_("Path to an alternate GlobalSettings.xml file.")));
}
void notifyAppError(const std::wstring& msg)
{
std::cerr << utfTo<std::string>(_("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
}
}
//##################################################################################################################
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<Zstring>(Zstr("%03d"), //[ms] should yield a fairly unique name
static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(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()); }
});
//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) { logExtraError(e.toString()); }
//parallel xBRZ-scaling! => run as early as possible
try { imageResourcesInit(appendPath(getResourceDirPath(), Zstr("Icons.zip"))); }
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(getResourceDirPath(), "Gtk2Styles.rc").c_str());
//hang on Ubuntu 19.10 (GLib 2.62) caused by ibus initialization: https://freefilesync.org/forum/viewtopic.php?t=6704
//=> work around 1: bonus: avoid needless DBus calls: https://developer.gnome.org/gio/stable/running-gio-apps.html
// drawback: missing MTP and network links in folder picker: https://freefilesync.org/forum/viewtopic.php?t=6871
//if (::setenv("GIO_USE_VFS", "local", true /*overwrite*/) != 0)
// std::cerr << utfTo<std::string>(formatSystemError("setenv(GIO_USE_VFS)", errno)) + '\n';
// //BUGZ!?: "Modifications of environment variables are not allowed in multi-threaded programs" - https://rachelbythebay.com/w/2017/01/30/env/
//=> work around 2:
[[maybe_unused]] GVfs* defaultFs = ::g_vfs_get_default(); //not owned by us!
//no such issue on GTK3!
#elif GTK_MAJOR_VERSION == 3
auto loadCSS = [&](const char* fileName)
{
GtkCssProvider* provider = ::gtk_css_provider_new();
ZEN_ON_SCOPE_EXIT(::g_object_unref(provider));
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
&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_PROVIDER_PRIORITY_APPLICATION); //guint priority
};
try
{
loadCSS("Gtk3Styles.css"); //throw SysError
}
catch (const SysError& e)
{
std::cerr << "[FreeFileSync] " + utfTo<std::string>(e.toString()) + "\n" "Loading GTK3\'s old CSS format instead..." "\n";
try
{
loadCSS("Gtk3Styles.old.css"); //throw SysError
}
catch (const SysError& e2) { logExtraError(_("Failed to update the color theme.") + L"\n\n" + e2.toString()); }
}
#else
#error unknown GTK version!
#endif
/* 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, defaults to executable name
initAfs({getResourceDirPath(), getConfigDirPath()}); //bonus: using FTP Gdrive implicitly inits OpenSSL (used in runSanityChecks() on Linux) already during globals init
auto onSystemShutdown = [](int /*unused*/ = 0)
{
onSystemShutdownRunTasks();
//- 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<int>(FfsExitCode::cancelled));
};
Bind(wxEVT_QUERY_END_SESSION, [onSystemShutdown](wxCloseEvent& event) { onSystemShutdown(); }); //can veto
Bind(wxEVT_END_SESSION, [onSystemShutdown](wxCloseEvent& event) { onSystemShutdown(); }); //can *not* veto
//- log off: Windows/macOS generates wxEVT_QUERY_END_SESSION/wxEVT_END_SESSION
// Linux/macOS generates SIGTERM, which we handle below
//- 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!
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(); });
return true; //true: continue processing; false: exit immediately
}
int Application::OnExit()
{
[[maybe_unused]] const bool rv = wxClipboard::Get()->Flush(); //see wx+/context_menu.h
//assert(rv); -> fails if clipboard wasn't used
localizationCleanup();
imageResourcesCleanup();
teardownAfs();
colorThemeCleanup();
return wxApp::OnExit();
}
wxLayoutDirection Application::GetLayoutDirection() const { return languageLayoutIsRtl() ? wxLayout_RightToLeft : wxLayout_LeftToRight; }
int Application::OnRun()
{
#if wxUSE_EXCEPTIONS
#error why is wxWidgets uncaught exception handling enabled!?
#endif
//exception => Windows: let it crash and create mini dump!!! Linux/macOS: std::exception::what() logged to console
[[maybe_unused]] const int rc = wxApp::OnRun();
return static_cast<int>(exitCode_);
}
void Application::onEnterEventLoop()
{
const std::vector<Zstring>& commandArgs = getCommandlineArgs(*this);
//wxWidgets app exit handling is weird... we want to exit only if the logical main window is closed, not just *any* window!
wxTheApp->SetExitOnFrameDelete(false); //prevent popup-windows from becoming temporary top windows leading to program exit after closure
ZEN_ON_SCOPE_EXIT(if (!wxTheApp->GetExitOnFrameDelete()) wxTheApp->ExitMainLoop()); //quit application, if no main window was set (batch silent mode)
try
{
//parse command line arguments
std::vector<std::pair<Zstring, Zstring>> dirPathPhrasePairs;
std::vector<Zstring> cfgFilePaths;
Zstring globalCfgPathAlt;
bool openForEdit = false;
{
const char* optionEdit = "-edit";
const char* optionDirPair = "-dirpair";
const char* optionSendTo = "-sendto"; //remaining arguments are unspecified number of folder paths; wonky syntax; let's keep it undocumented
auto isHelpRequest = [](const Zstring& arg)
{
auto it = std::find_if(arg.begin(), arg.end(), [](Zchar c) { return c != Zstr('/') && c != Zstr('-'); });
if (it == arg.begin()) return false; //require at least one prefix character
const Zstring argTmp(it, arg.end());
return equalAsciiNoCase(argTmp, "help") ||
equalAsciiNoCase(argTmp, "h") ||
argTmp == Zstr("?");
};
auto isCommandLineOption = [&](const Zstring& arg)
{
return equalAsciiNoCase(arg, optionEdit ) ||
equalAsciiNoCase(arg, optionDirPair) ||
equalAsciiNoCase(arg, optionSendTo ) ||
isHelpRequest(arg);
};
for (auto it = commandArgs.begin(); it != commandArgs.end(); ++it)
if (isHelpRequest(*it))
return showSyntaxHelp();
else if (equalAsciiNoCase(*it, optionEdit))
openForEdit = true;
else if (equalAsciiNoCase(*it, optionDirPair))
{
if (++it == commandArgs.end() || isCommandLineOption(*it))
throw FileError(replaceCpy(_("A left and a right directory path are expected after %x."), L"%x", utfTo<std::wstring>(optionDirPair)));
dirPathPhrasePairs.emplace_back(*it, Zstring());
if (++it == commandArgs.end() || isCommandLineOption(*it))
throw FileError(replaceCpy(_("A left and a right directory path are expected after %x."), L"%x", utfTo<std::wstring>(optionDirPair)));
dirPathPhrasePairs.back().second = *it;
}
else if (equalAsciiNoCase(*it, optionSendTo))
{
for (size_t i = 0; ; ++i)
{
if (++it == commandArgs.end() || isCommandLineOption(*it))
{
--it;
break;
}
if (i < 2) //else: -SendTo with more than 2 paths? Doesn't make any sense, does it!?
{
//for -SendTo we expect a list of full native paths, not "phrases" that need to be resolved!
auto getFolderPath = [](Zstring itemPath)
{
try
{
if (getItemType(itemPath) == ItemType::file) //throw FileError
if (const std::optional<Zstring>& parentPath = getParentFolderPath(itemPath))
return *parentPath;
}
catch (FileError&) {}
return itemPath;
};
if (i % 2 == 0)
dirPathPhrasePairs.emplace_back(getFolderPath(*it), Zstring());
else
{
const Zstring folderPath = getFolderPath(*it);
if (dirPathPhrasePairs.back().first != folderPath) //else: user accidentally sending to two files, which each time yield the same parent folder
dirPathPhrasePairs.back().second = folderPath;
}
}
}
}
else
{
const Zstring& filePath = getResolvedFilePath(*it);
#if 0
if (!fileAvailable(filePath)) //...be a little tolerant
for (const Zchar* ext : {Zstr(".ffs_gui"), Zstr(".ffs_batch"), Zstr(".xml")})
if (fileAvailable(filePath + ext))
filePath += ext;
#endif
if (endsWithAsciiNoCase(filePath, Zstr(".ffs_gui")) ||
endsWithAsciiNoCase(filePath, Zstr(".ffs_batch")))
cfgFilePaths.push_back(filePath);
else if (endsWithAsciiNoCase(filePath, Zstr(".xml")))
globalCfgPathAlt = filePath;
else
throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(filePath)),
_("Unexpected file extension:") + L' ' + fmtPath(getFileExtension(filePath)) + L'\n' +
_("Expected:") + L" ffs_gui, ffs_batch, xml");
}
}
//----------------------------------------------------------------------------------------------------
auto hasNonDefaultConfig = [](const LocalPairConfig& lpc)
{
return lpc != LocalPairConfig{lpc.folderPathPhraseLeft,
lpc.folderPathPhraseRight,
std::nullopt, std::nullopt, FilterConfig()};
};
auto replaceDirectories = [&](MainConfiguration& mainCfg) //throw FileError
{
if (!dirPathPhrasePairs.empty())
{
if (cfgFilePaths.size() > 1)
throw FileError(_("Directories cannot be set for more than one configuration file."));
//check if config at folder-pair level is present: this probably doesn't make sense when replacing/adding the user-specified directories
if (hasNonDefaultConfig(mainCfg.firstPair) || std::any_of(mainCfg.additionalPairs.begin(), mainCfg.additionalPairs.end(), hasNonDefaultConfig))
throw FileError(_("The config file must not contain settings at directory pair level when directories are set via command line."));
mainCfg.additionalPairs.clear();
for (size_t i = 0; i < dirPathPhrasePairs.size(); ++i)
if (i == 0)
{
mainCfg.firstPair.folderPathPhraseLeft = dirPathPhrasePairs[0].first;
mainCfg.firstPair.folderPathPhraseRight = dirPathPhrasePairs[0].second;
}
else
mainCfg.additionalPairs.push_back({dirPathPhrasePairs[i].first, dirPathPhrasePairs[i].second,
std::nullopt, std::nullopt, FilterConfig()});
}
};
const Zstring globalCfgFilePath = !globalCfgPathAlt.empty() ? globalCfgPathAlt : getGlobalConfigDefaultPath();
GlobalConfig globalCfg;
try
{
std::wstring warningMsg;
std::tie(globalCfg, warningMsg) = readGlobalConfig(globalCfgFilePath); //throw FileError
assert(warningMsg.empty()); //ignore parsing errors: should be migration problems only *cross-fingers*
}
catch (const FileError& e)
{
try
{
bool cfgFileExists = true;
try { cfgFileExists = itemExists(globalCfgFilePath); /*throw FileError*/ } //=> unclear which exception is more relevant/useless:
catch (const FileError& e2) { throw FileError(replaceCpy(e.toString(), L"\n\n", L'\n'), replaceCpy(e2.toString(), L"\n\n", L'\n')); }
if (cfgFileExists)
throw;
}
catch (const FileError& e3) { logExtraError(e3.toString()); }
}
//late GlobalSettings.xml-dependent app initialization:
try { setLanguage(globalCfg.programLanguage); } //throw FileError
catch (const FileError& e) { logExtraError(e.toString()); }
try { colorThemeInit(*this, globalCfg.appColorTheme); } //throw FileError
catch (const FileError& e) { logExtraError(e.toString()); } //not critical in this context
//-----------------------------------------------------------
//distinguish sync scenarios:
//-----------------------------------------------------------
if (cfgFilePaths.empty())
{
//gui mode: default startup
if (dirPathPhrasePairs.empty())
MainDialog::create(globalCfg, globalCfgFilePath);
//gui mode: default config with given directories
else
{
FfsGuiConfig guiCfg;
guiCfg.mainCfg.syncCfg.directionCfg = getDefaultSyncCfg(SyncVariant::mirror);
replaceDirectories(guiCfg.mainCfg); //throw FileError
MainDialog::create(guiCfg, {} /*cfgFilePaths*/, globalCfg, globalCfgFilePath, !openForEdit /*startComparison*/);
}
}
else if (const Zstring filePath0 = cfgFilePaths[0];
//batch mode (single config)
cfgFilePaths.size() == 1 && endsWithAsciiNoCase(filePath0, Zstr(".ffs_batch")) && !openForEdit)
{
auto [batchCfg, warningMsg] = readBatchConfig(filePath0); //throw FileError
if (!warningMsg.empty())
throw FileError(warningMsg); //batch mode: break on errors AND even warnings!
replaceDirectories(batchCfg.guiCfg.mainCfg); //throw FileError
runBatchMode(batchCfg, filePath0, globalCfg, globalCfgFilePath);
}
else //GUI mode: (ffs_gui *or* ffs_batch)
{
auto [guiCfg, warningMsg] = readAnyConfig(cfgFilePaths); //throw FileError
if (!warningMsg.empty())
showNotificationDialog(nullptr, DialogInfoType::warning, PopupDialogCfg().setDetailInstructions(warningMsg));
//what about simulating changed config on parsing errors?
replaceDirectories(guiCfg.mainCfg); //throw FileError
//what about simulating changed config due to directory replacement?
//-> propably fine to not show as changed on GUI and not ask user to save on exit!
MainDialog::create(guiCfg, cfgFilePaths, globalCfg, globalCfgFilePath, !openForEdit /*startComparison*/);
}
}
catch (const FileError& e)
{
raiseExitCode(exitCode_, FfsExitCode::exception);
notifyAppError(e.toString());
}
}
void Application::runBatchMode(const FfsBatchConfig& batchCfg, const Zstring& cfgFilePath, GlobalConfig globalCfg, const Zstring& globalCfgFilePath)
{
const bool allowUserInteraction = !batchCfg.batchExCfg.autoCloseSummary ||
(!batchCfg.guiCfg.mainCfg.ignoreErrors && batchCfg.batchExCfg.batchErrorHandling == BatchErrorHandling::showPopup);
/* regular check for software updates -> disabled for batch
if (batchCfg.showProgress && manualProgramUpdateRequired())
checkForUpdatePeriodically(globalCfg.lastUpdateCheck);
-> WinInet not working when FFS is running as a service!!! https://support.microsoft.com/en-us/help/238425/info-wininet-not-supported-for-use-in-services */
const std::chrono::system_clock::time_point syncStartTime = std::chrono::system_clock::now();
const WindowLayout::Dimensions progressDim
{
globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.size,
std::nullopt /*pos*/,
globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized
};
//class handling status updates and error messages
BatchStatusHandler statusHandler(!batchCfg.batchExCfg.runMinimized,
extractJobName(cfgFilePath),
syncStartTime,
batchCfg.guiCfg.mainCfg.ignoreErrors,
batchCfg.guiCfg.mainCfg.autoRetryCount,
batchCfg.guiCfg.mainCfg.autoRetryDelay,
globalCfg.soundFileSyncFinished,
globalCfg.soundFileAlertPending,
progressDim,
batchCfg.batchExCfg.autoCloseSummary,
batchCfg.batchExCfg.postBatchAction,
batchCfg.batchExCfg.batchErrorHandling);
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.cancelProcessNow(CancelReason::user); //throw CancelProcess
return password;
};
try
{
//inform about (important) non-default global settings
logNonDefaultSettings(globalCfg, statusHandler); //throw CancelProcess
//batch mode: place directory locks on directories during both comparison AND synchronization
std::unique_ptr<LockHolder> dirLocks;
FolderComparison cmpResult = compare(globalCfg.warnDlgs,
globalCfg.fileTimeTolerance,
requestPassword,
globalCfg.runWithBackgroundPriority,
globalCfg.createLockFile,
dirLocks,
extractCompareCfg(batchCfg.guiCfg.mainCfg),
statusHandler); //throw CancelProcess
if (!cmpResult.empty())
synchronize(syncStartTime,
globalCfg.verifyFileCopy,
globalCfg.copyLockedFiles,
globalCfg.copyFilePermissions,
globalCfg.failSafeFileCopy,
globalCfg.runWithBackgroundPriority,
extractSyncCfg(batchCfg.guiCfg.mainCfg),
cmpResult,
globalCfg.warnDlgs,
statusHandler); //throw CancelProcess
}
catch (CancelProcess&) {}
//-------------------------------------------------------------------
BatchStatusHandler::Result r = statusHandler.prepareResult();
AbstractPath logFolderPath = createAbstractPath(batchCfg.guiCfg.mainCfg.altLogFolderPathPhrase); //optional
if (AFS::isNullPath(logFolderPath))
logFolderPath = createAbstractPath(globalCfg.logFolderPhrase);
assert(!AFS::isNullPath(logFolderPath)); //mandatory! but still: let's include fall back
if (AFS::isNullPath(logFolderPath))
logFolderPath = createAbstractPath(getLogFolderDefaultPath());
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 run post sync action
=> don't send email notification
=> don't play sound notification */
else
{
//--------------------- 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<std::wstring>(exitCode)), utfTo<std::wstring>(output)));
logMsg(r.errorLog.ref(), _("Executing command:") + L' ' + utfTo<std::wstring>(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<std::wstring>(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<std::wstring>(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); }
}
//--------------------- save log file ----------------------
std::set<AbstractPath> logsToKeepPaths;
for (const ConfigFileItem& cfi : globalCfg.mainDlg.config.fileHistory)
if (!equalNativePath(cfi.cfgFilePath, cfgFilePath)) //exception: don't keep old log for the selected cfg file!
logsToKeepPaths.insert(cfi.lastRunStats.logFilePath);
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, logsToKeepPaths, notifyStatusNoThrow); //throw FileError
}
catch (const FileError& e)
{
try //fallback: log file *must* be saved no matter what!
{
const AbstractPath logFileDefaultPath = AFS::appendRelPath(createAbstractPath(getLogFolderDefaultPath()), generateLogFileName(globalCfg.logFormat, r.summary));
if (logFilePath == logFileDefaultPath)
throw;
logMsg(r.errorLog.ref(), e.toString(), MSG_TYPE_ERROR);
logFilePath = logFileDefaultPath;
saveLogFile(logFileDefaultPath, r.summary, r.errorLog.ref(), globalCfg.logfilesMaxAgeDays, globalCfg.logFormat, logsToKeepPaths, notifyStatusNoThrow); //throw FileError
}
catch (const FileError& e2) { logMsg(r.errorLog.ref(), e2.toString(), MSG_TYPE_ERROR); logExtraError(e2.toString()); } //should never happen!!!
}
//--------- update last sync stats for the selected cfg file ---------
const ErrorLogStats& logStats = getStats(r.errorLog.ref());
for (ConfigFileItem& cfi : globalCfg.mainDlg.config.fileHistory)
if (equalNativePath(cfi.cfgFilePath, cfgFilePath))
{
assert(r.summary.startTime == syncStartTime);
assert(!AFS::isNullPath(logFilePath));
cfi.lastRunStats =
{
std::chrono::system_clock::to_time_t(r.summary.startTime),
logFilePath,
r.summary.result,
r.summary.statsProcessed.items,
r.summary.statsProcessed.bytes,
r.summary.totalTime,
logStats.errors,
logStats.warnings,
};
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)
{
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;
}
//email sending, or saving log file failed? at least this should affect the exit code:
if (logStats.errors > 0)
raiseExitCode(exitCode_, FfsExitCode::error);
else if (logStats.warnings > 0)
raiseExitCode(exitCode_, FfsExitCode::warning);
//---------------------------------------------------------------------------
//stream sync stats to STDOUT as JSON
JsonValue syncStats(JsonValue::Type::object);
switch (r.summary.result)
{
case TaskResult::success: syncStats.objectVal.set("syncResult", "success"); break;
case TaskResult::warning: syncStats.objectVal.set("syncResult", "warning"); break;
case TaskResult::error: syncStats.objectVal.set("syncResult", "error"); break;
case TaskResult::cancelled: syncStats.objectVal.set("syncResult", "cancelled"); break;
}
std::string startTimeStr = utfTo<std::string>(formatTime(Zstr("%Y-%m-%dT%H:%M:%S%z"), getLocalTime(std::chrono::system_clock::to_time_t(r.summary.startTime))));
syncStats.objectVal.set("startTime", std::move(startTimeStr.insert(startTimeStr.size() - 2, ":"))); //ISO 8601 date/time with offset e.g. 2001-08-23T14:55:02+02:00
syncStats.objectVal.set("totalTimeSec", std::chrono::duration_cast<std::chrono::seconds>(r.summary.totalTime).count());
syncStats.objectVal.set("errors", logStats.errors);
syncStats.objectVal.set("warnings", logStats.warnings);
syncStats.objectVal.set("totalItems", r.summary.statsTotal.items);
syncStats.objectVal.set("totalBytes", r.summary.statsTotal.bytes);
syncStats.objectVal.set("processedItems", r.summary.statsProcessed.items);
syncStats.objectVal.set("processedBytes", r.summary.statsProcessed.bytes);
syncStats.objectVal.set("logFile", utfTo<std::string>(AFS::getDisplayPath(logFilePath)));
std::cout << serializeJson(syncStats);
//---------------------------------------------------------------------------
try //save global settings to XML: e.g. ignored warnings, last sync stats
{
writeConfig(globalCfg, globalCfgFilePath); //FileError
}
catch (const FileError& e)
{
//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 (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(batchCfg.guiCfg, {cfgFilePath}, globalCfg, globalCfgFilePath, true /*startComparison*/);
break;
case FinalRequest::shutdown:
try
{
shutdownSystem(); //throw FileError
terminateProcess(static_cast<int>(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());
}
break;
}
}