diff options
90 files changed, 1619 insertions, 1314 deletions
diff --git a/Changelog.txt b/Changelog.txt index 8717bc73..31963c7d 100755 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,29 @@ +FreeFileSync 10.23 [2020-04-17] +------------------------------- +Run "on completion" commands on console (no need for "cmd.exe /c") +Check exit code and report errors for external applications +Report stream output of failed command line calls (macOs, Linux) +Use Unicode symbols compatible with older macOS +RealTimeSync: invoke command using cmd.exe instead of ShellExecute (Windows) +Avoid hitting log file length limitations for aggregated jobs +Fix OpenSSL failing on HTTP 1.0 response without Content-Length +Don't allow creating folder names ending with space or dot +Support base folders with trailing blanks +Show system error descriptions on volume shadow copy errors +Raise exit code if saving log file or sending email failed +Report all documented MTP error descriptions +Updated default exclude filter (macOS/Linux) +Added image outlines for improved dark mode support +Work around WBEM_E_INVALID_CLASS error during installation +Align file path rendering with app layout direction +Play sound notification also when "cancel on first error" is set +Cleaner file path formatting (macOs, Linux) +Added instructions when failing to start due to missing GTK2 (Ubuntu) +RealTimeSync: distinguish drive unmount from folder change notification +Avoid blocking command scripts waiting for user input +Updated translation files + + FreeFileSync 10.22 [2020-03-18] ------------------------------- Fixed upper-case conversion bug for non-ASCII strings diff --git a/FreeFileSync/Build/Resources/Icons.zip b/FreeFileSync/Build/Resources/Icons.zip Binary files differindex 0a393bbc..5448052f 100644 --- a/FreeFileSync/Build/Resources/Icons.zip +++ b/FreeFileSync/Build/Resources/Icons.zip diff --git a/FreeFileSync/Build/Resources/Languages.zip b/FreeFileSync/Build/Resources/Languages.zip Binary files differindex 3f00d604..39d9b8e7 100644 --- a/FreeFileSync/Build/Resources/Languages.zip +++ b/FreeFileSync/Build/Resources/Languages.zip diff --git a/FreeFileSync/Source/Makefile b/FreeFileSync/Source/Makefile index 5d4f6b20..ce88fe85 100755 --- a/FreeFileSync/Source/Makefile +++ b/FreeFileSync/Source/Makefile @@ -90,6 +90,7 @@ CPP_FILES+=../../zen/format_unit.cpp CPP_FILES+=../../zen/legacy_compiler.cpp CPP_FILES+=../../zen/open_ssl.cpp CPP_FILES+=../../zen/process_priority.cpp +CPP_FILES+=../../zen/shell_execute.cpp CPP_FILES+=../../zen/shutdown.cpp CPP_FILES+=../../zen/sys_error.cpp CPP_FILES+=../../zen/system.cpp diff --git a/FreeFileSync/Source/RealTimeSync/Makefile b/FreeFileSync/Source/RealTimeSync/Makefile index 9c7f88e0..ed6e23ac 100755 --- a/FreeFileSync/Source/RealTimeSync/Makefile +++ b/FreeFileSync/Source/RealTimeSync/Makefile @@ -31,6 +31,7 @@ CPP_FILES+=../../../zen/file_io.cpp CPP_FILES+=../../../zen/file_traverser.cpp CPP_FILES+=../../../zen/format_unit.cpp CPP_FILES+=../../../zen/legacy_compiler.cpp +CPP_FILES+=../../../zen/shell_execute.cpp CPP_FILES+=../../../zen/shutdown.cpp CPP_FILES+=../../../zen/sys_error.cpp CPP_FILES+=../../../zen/thread.cpp diff --git a/FreeFileSync/Source/RealTimeSync/application.cpp b/FreeFileSync/Source/RealTimeSync/application.cpp index 037d7059..9c1fa138 100644 --- a/FreeFileSync/Source/RealTimeSync/application.cpp +++ b/FreeFileSync/Source/RealTimeSync/application.cpp @@ -61,8 +61,8 @@ bool Application::OnInit() (fff::getResourceDirPf() + "Gtk3Styles.css").c_str(), //const gchar* path, &error); //GError** error if (error) - throw SysError(formatSystemError(L"gtk_css_provider_load_from_data", replaceCpy(_("Error Code %x"), L"%x", - numberTo<std::wstring>(error->code)), + throw SysError(formatSystemError("gtk_css_provider_load_from_data", + replaceCpy(_("Error code %x"), L"%x", numberTo<std::wstring>(error->code)), utfTo<std::wstring>(error->message))); ::gtk_style_context_add_provider_for_screen(::gdk_screen_get_default(), //GdkScreen* screen, @@ -74,9 +74,10 @@ bool Application::OnInit() #error unknown GTK version! #endif + //Windows User Experience Interaction Guidelines: tool tips should have 5s timeout, info tips no timeout => compromise: wxToolTip::Enable(true); //yawn, a wxWidgets screw-up: wxToolTip::SetAutoPop is no-op if global tooltip window is not yet constructed: wxToolTip::Enable creates it - wxToolTip::SetAutoPop(10000); //https://msdn.microsoft.com/en-us/library/windows/desktop/aa511495 + wxToolTip::SetAutoPop(10000); //https://docs.microsoft.com/en-us/windows/win32/uxguide/ctrl-tooltips-and-infotips SetAppName(L"RealTimeSync"); @@ -161,11 +162,11 @@ int Application::OnRun() const auto titleFmt = copyStringTo<std::wstring>(wxTheApp->GetAppDisplayName()) + SPACED_DASH + _("An exception occurred"); std::cerr << utfTo<std::string>(titleFmt + SPACED_DASH) << e.what() << '\n'; - return fff::FFS_RC_EXCEPTION; + return fff::FFS_EXIT_EXCEPTION; } //catch (...) -> let it crash and create mini dump!!! - return fff::FFS_RC_SUCCESS; //program's return code + return fff::FFS_EXIT_SUCCESS; //program's return code } @@ -175,5 +176,5 @@ void Application::onQueryEndSession(wxEvent& event) if (auto mainWin = dynamic_cast<MainDialog*>(GetTopWindow())) mainWin->onQueryEndSession(); //it's futile to try and clean up while the process is in full swing (CRASH!) => just terminate! - terminateProcess(fff::FFS_RC_ABORTED); + terminateProcess(fff::FFS_EXIT_ABORTED); } diff --git a/FreeFileSync/Source/RealTimeSync/config.cpp b/FreeFileSync/Source/RealTimeSync/config.cpp index 7051c6d1..2454b941 100644 --- a/FreeFileSync/Source/RealTimeSync/config.cpp +++ b/FreeFileSync/Source/RealTimeSync/config.cpp @@ -15,7 +15,7 @@ using namespace zen; using namespace rts; //------------------------------------------------------------------------------------------------------------------------------- -const int XML_FORMAT_RTS_CFG = 1; //2019-05-10 +const int XML_FORMAT_RTS_CFG = 2; //2020-04-14 //------------------------------------------------------------------------------------------------------------------------------- @@ -68,11 +68,11 @@ void readConfig(const XmlIn& in, XmlRealConfig& cfg, int formatVer) in["Delay" ](cfg.delay); in["Commandline"](cfg.commandline); - //TODO: remove if clause after migration! 2019-05-10 - if (formatVer < 1) - ; - else - in["Commandline"].attribute("HideConsole", cfg.hideConsoleWindow); + //TODO: remove if clause after migration! 2020-04-14 + if (formatVer < 2) + if (startsWithAsciiNoCase(cfg.commandline, "cmd /c ") || + startsWithAsciiNoCase(cfg.commandline, "cmd.exe /c ")) + cfg.commandline = afterFirst(cfg.commandline, Zstr("/c "), IF_MISSING_RETURN_ALL); } @@ -81,7 +81,6 @@ void writeConfig(const XmlRealConfig& cfg, XmlOut& out) out["Directories"](cfg.directories); out["Delay" ](cfg.delay); out["Commandline"](cfg.commandline); - out["Commandline"].attribute("HideConsole", cfg.hideConsoleWindow); } } diff --git a/FreeFileSync/Source/RealTimeSync/config.h b/FreeFileSync/Source/RealTimeSync/config.h index 75d88aa5..b7b36514 100644 --- a/FreeFileSync/Source/RealTimeSync/config.h +++ b/FreeFileSync/Source/RealTimeSync/config.h @@ -17,7 +17,6 @@ struct XmlRealConfig { std::vector<Zstring> directories; Zstring commandline; - bool hideConsoleWindow = false; unsigned int delay = 10; }; diff --git a/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp b/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp index 7900b5c5..e4b4a451 100644 --- a/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp +++ b/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp @@ -111,6 +111,9 @@ void FolderSelector2::onFilesDropped(FileDropEvent& event) } catch (FileError&) {} //e.g. good for inactive mapped network shares, not so nice for C:\pagefile.sys + if (endsWith(itemPath, Zstr(' '))) //prevent getResolvedFilePath() from trimming legit trailing blank! + itemPath += FILE_NAME_SEPARATOR; + setPath(itemPath); //event.Skip(); @@ -141,12 +144,16 @@ void FolderSelector2::onSelectDir(wxCommandEvent& event) } } + Zstring newFolderPath; wxDirDialog dirPicker(parent_, _("Select a folder"), utfTo<wxString>(defaultFolderPath)); //put modal wxWidgets dialogs on stack: creating on freestore leads to memleak! if (dirPicker.ShowModal() != wxID_OK) return; - const Zstring newFolderPath = utfTo<Zstring>(dirPicker.GetPath()); + newFolderPath = utfTo<Zstring>(dirPicker.GetPath()); + + if (endsWith(newFolderPath, Zstr(' '))) //prevent getResolvedFilePath() from trimming legit trailing blank! + newFolderPath += FILE_NAME_SEPARATOR; - setFolderPath(newFolderPath, &folderPathCtrl_, folderPathCtrl_, staticText_); + setPath(newFolderPath); } diff --git a/FreeFileSync/Source/RealTimeSync/gui_generated.cpp b/FreeFileSync/Source/RealTimeSync/gui_generated.cpp index dccc3495..318c8f1b 100644 --- a/FreeFileSync/Source/RealTimeSync/gui_generated.cpp +++ b/FreeFileSync/Source/RealTimeSync/gui_generated.cpp @@ -251,21 +251,15 @@ MainDlgGenerated::MainDlgGenerated( wxWindow* parent, wxWindowID id, const wxStr wxBoxSizer* bSizer13; bSizer13 = new wxBoxSizer( wxHORIZONTAL ); - m_bitmapCommand = new wxStaticBitmap( m_panelMain, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); - bSizer13->Add( m_bitmapCommand, 0, wxTOP|wxBOTTOM|wxLEFT|wxALIGN_CENTER_VERTICAL, 5 ); + m_bitmapConsole = new wxStaticBitmap( m_panelMain, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer13->Add( m_bitmapConsole, 0, wxTOP|wxBOTTOM|wxLEFT|wxALIGN_CENTER_VERTICAL, 5 ); m_staticText6 = new wxStaticText( m_panelMain, wxID_ANY, _("Command line:"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticText6->Wrap( -1 ); bSizer13->Add( m_staticText6, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); - bSizer13->Add( 0, 0, 1, wxEXPAND, 5 ); - - m_checkBoxHideConsole = new wxCheckBox( m_panelMain, wxID_ANY, _("&Hide console window"), wxDefaultPosition, wxDefaultSize, 0 ); - bSizer13->Add( m_checkBoxHideConsole, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); - - - bSizer141->Add( bSizer13, 0, wxEXPAND, 5 ); + bSizer141->Add( bSizer13, 0, 0, 5 ); m_textCtrlCommand = new wxTextCtrl( m_panelMain, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); m_textCtrlCommand->SetToolTip( _("The command is triggered if:\n- files or subfolders change\n- new folders arrive (e.g. USB stick insert)") ); diff --git a/FreeFileSync/Source/RealTimeSync/gui_generated.h b/FreeFileSync/Source/RealTimeSync/gui_generated.h index 5cc0f0ca..5ecd49d3 100644 --- a/FreeFileSync/Source/RealTimeSync/gui_generated.h +++ b/FreeFileSync/Source/RealTimeSync/gui_generated.h @@ -32,7 +32,6 @@ namespace zen { class BitmapTextButton; } #include <wx/panel.h> #include <wx/scrolwin.h> #include <wx/spinctrl.h> -#include <wx/checkbox.h> #include <wx/frame.h> #include "zen/i18n.h" @@ -82,9 +81,8 @@ protected: wxStaticText* m_staticText8; wxSpinCtrl* m_spinCtrlDelay; wxStaticLine* m_staticline211; - wxStaticBitmap* m_bitmapCommand; + wxStaticBitmap* m_bitmapConsole; wxStaticText* m_staticText6; - wxCheckBox* m_checkBoxHideConsole; wxTextCtrl* m_textCtrlCommand; wxStaticLine* m_staticline5; zen::BitmapTextButton* m_buttonStart; diff --git a/FreeFileSync/Source/RealTimeSync/main_dlg.cpp b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp index 694d6e79..7e6418f4 100644 --- a/FreeFileSync/Source/RealTimeSync/main_dlg.cpp +++ b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp @@ -80,13 +80,12 @@ MainDialog::MainDialog(const Zstring& cfgFileName) : m_txtCtrlDirectoryMain->SetMinSize({fastFromDIP(300), -1}); m_spinCtrlDelay ->SetMinSize({fastFromDIP( 70), -1}); //Hack: set size (why does wxWindow::Size() not work?) - m_checkBoxHideConsole->Hide(); //only relevant on Windows m_bpButtonRemoveTopFolder->Hide(); m_panelMainFolder->Layout(); m_bitmapBatch ->SetBitmap(getResourceImage(L"file_batch_sicon")); m_bitmapFolders->SetBitmap(fff::IconBuffer::genericDirIcon(fff::IconBuffer::SIZE_SMALL)); - m_bitmapCommand->SetBitmap(shrinkImage(getResourceImage(L"command_line").ConvertToImage(), fastFromDIP(20))); + m_bitmapConsole->SetBitmap(shrinkImage(getResourceImage(L"command_line").ConvertToImage(), fastFromDIP(20))); m_bpButtonAddFolder ->SetBitmapLabel(getResourceImage(L"item_add")); m_bpButtonRemoveTopFolder->SetBitmapLabel(getResourceImage(L"item_remove")); @@ -358,9 +357,8 @@ void MainDialog::setConfiguration(const XmlRealConfig& cfg) insertAddFolder(addFolderPaths, 0); - m_textCtrlCommand ->SetValue(utfTo<wxString>(cfg.commandline)); - m_checkBoxHideConsole->SetValue(cfg.hideConsoleWindow); - m_spinCtrlDelay ->SetValue(static_cast<int>(cfg.delay)); + m_textCtrlCommand->SetValue(utfTo<wxString>(cfg.commandline)); + m_spinCtrlDelay ->SetValue(static_cast<int>(cfg.delay)); } @@ -373,9 +371,8 @@ XmlRealConfig MainDialog::getConfiguration() for (const DirectoryPanel* dp : additionalFolderPanels_) output.directories.push_back(dp->getPath()); - output.commandline = utfTo<Zstring>(m_textCtrlCommand->GetValue()); - output.hideConsoleWindow = m_checkBoxHideConsole->GetValue(); - output.delay = m_spinCtrlDelay->GetValue(); + output.commandline = utfTo<Zstring>(m_textCtrlCommand->GetValue()); + output.delay = m_spinCtrlDelay->GetValue(); return output; } diff --git a/FreeFileSync/Source/RealTimeSync/monitor.cpp b/FreeFileSync/Source/RealTimeSync/monitor.cpp index 66a83f3c..dc79609c 100644 --- a/FreeFileSync/Source/RealTimeSync/monitor.cpp +++ b/FreeFileSync/Source/RealTimeSync/monitor.cpp @@ -101,25 +101,8 @@ std::set<Zstring, LessNativePath> waitForMissingDirs(const std::vector<Zstring>& //wait until changes are detected or if a directory is not available (anymore) -struct WaitResult -{ - enum ChangeType - { - ITEM_CHANGED, - FOLDER_UNAVAILABLE //1. not existing or 2. can't access - }; - - explicit WaitResult(const DirWatcher::Entry& changeEntry) : type(ITEM_CHANGED), changedItem(changeEntry) {} - explicit WaitResult(const Zstring& folderPath) : type(FOLDER_UNAVAILABLE), missingFolderPath(folderPath) {} - - ChangeType type; - DirWatcher::Entry changedItem; //for type == ITEM_CHANGED: file or directory - Zstring missingFolderPath; //for type == FOLDER_UNAVAILABLE -}; - - -WaitResult waitForChanges(const std::set<Zstring, LessNativePath>& folderPaths, //throw FileError - const std::function<void(bool readyForSync)>& requestUiUpdate, std::chrono::milliseconds cbInterval) +DirWatcher::Change waitForChanges(const std::set<Zstring, LessNativePath>& folderPaths, //throw FileError + const std::function<void(bool readyForSync)>& requestUiUpdate, std::chrono::milliseconds cbInterval) { assert(std::all_of(folderPaths.begin(), folderPaths.end(), [](const Zstring& folderPath) { return dirAvailable(folderPath); })); if (folderPaths.empty()) //pathological case, but we have to check else this function will wait endlessly @@ -135,7 +118,7 @@ WaitResult waitForChanges(const std::set<Zstring, LessNativePath>& folderPaths, catch (FileError&) { if (!dirAvailable(folderPath)) //folder not existing or can't access - return WaitResult(folderPath); + return { DirWatcher::ChangeType::baseFolderUnavailable, folderPath }; throw; } @@ -158,12 +141,18 @@ WaitResult waitForChanges(const std::set<Zstring, LessNativePath>& folderPaths, //IMPORTANT CHECK: DirWatcher has problems detecting removal of top watched directories! if (checkDirNow) if (!dirAvailable(folderPath)) //catch errors related to directory removal, e.g. ERROR_NETNAME_DELETED - return WaitResult(folderPath); + return { DirWatcher::ChangeType::baseFolderUnavailable, folderPath }; try { - std::vector<DirWatcher::Entry> changedItems = watcher->getChanges([&] { requestUiUpdate(false /*readyForSync*/); /*throw X*/ }, - cbInterval); //throw FileError - std::erase_if(changedItems, [](const DirWatcher::Entry& e) + std::vector<DirWatcher::Change> changes = watcher->fetchChanges([&] { requestUiUpdate(false /*readyForSync*/); /*throw X*/ }, + cbInterval); //throw FileError + + //give precedence to ChangeType::baseFolderUnavailable + for (const DirWatcher::Change& change : changes) + if (change.type == DirWatcher::ChangeType::baseFolderUnavailable) + return change; + + std::erase_if(changes, [](const DirWatcher::Change& e) { return endsWith(e.itemPath, Zstr(".ffs_tmp")) || //sync.8ea2.ffs_tmp @@ -172,13 +161,13 @@ WaitResult waitForChanges(const std::set<Zstring, LessNativePath>& folderPaths, //no need to ignore temporary recycle bin directory: this must be caused by a file deletion anyway }); - if (!changedItems.empty()) - return WaitResult(changedItems[0]); //directory change detected + if (!changes.empty()) + return changes[0]; } catch (FileError&) { if (!dirAvailable(folderPath)) //a benign(?) race condition with FileError - return WaitResult(folderPath); + return { DirWatcher::ChangeType::baseFolderUnavailable, folderPath }; throw; } } @@ -189,20 +178,21 @@ WaitResult waitForChanges(const std::set<Zstring, LessNativePath>& folderPaths, } -inline -std::wstring getActionName(DirWatcher::ActionType type) +std::wstring getChangeTypeName(DirWatcher::ChangeType type) { switch (type) { - case DirWatcher::ACTION_CREATE: - return L"CREATE"; - case DirWatcher::ACTION_UPDATE: - return L"UPDATE"; - case DirWatcher::ACTION_DELETE: - return L"DELETE"; + case DirWatcher::ChangeType::create: + return L"Create"; + case DirWatcher::ChangeType::update: + return L"Update"; + case DirWatcher::ChangeType::remove: + return L"Delete"; + case DirWatcher::ChangeType::baseFolderUnavailable: + return L"Base Folder Unavailable"; } assert(false); - return L"ERROR"; + return L"Error"; } struct ExecCommandNowException {}; @@ -229,35 +219,29 @@ void rts::monitorDirectories(const std::vector<Zstring>& folderPathPhrases, std: for (;;) //command executions { - DirWatcher::Entry lastChangeDetected; + DirWatcher::Change lastChangeDetected; try { for (;;) //detected changes { - const WaitResult res = waitForChanges(folderPaths, [&](bool readyForSync) //throw FileError, ExecCommandNowException + lastChangeDetected = waitForChanges(folderPaths, [&](bool readyForSync) //throw FileError, ExecCommandNowException { requestUiUpdate(nullptr); if (readyForSync && std::chrono::steady_clock::now() >= nextExecTime) throw ExecCommandNowException(); //abort wait and start sync }, cbInterval); - switch (res.type) - { - case WaitResult::ITEM_CHANGED: - lastChangeDetected = res.changedItem; - break; - - case WaitResult::FOLDER_UNAVAILABLE: //don't execute the command before all directories are available! - lastChangeDetected = DirWatcher::Entry{ DirWatcher::ACTION_UPDATE, res.missingFolderPath}; - folderPaths = waitForMissingDirs(folderPathPhrases, [&](const Zstring& folderPath) { requestUiUpdate(&folderPath); }, cbInterval); //throw FileError - break; - } + + if (lastChangeDetected.type == DirWatcher::ChangeType::baseFolderUnavailable) + //don't execute the command before all directories are available! + folderPaths = waitForMissingDirs(folderPathPhrases, [&](const Zstring& folderPath) { requestUiUpdate(&folderPath); }, cbInterval); //throw FileError + nextExecTime = std::chrono::steady_clock::now() + delay; } } catch (ExecCommandNowException&) {} - executeExternalCommand(lastChangeDetected.itemPath, getActionName(lastChangeDetected.action)); + executeExternalCommand(lastChangeDetected.itemPath, getChangeTypeName(lastChangeDetected.type)); nextExecTime = std::chrono::steady_clock::time_point::max(); } } diff --git a/FreeFileSync/Source/RealTimeSync/tray_menu.cpp b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp index 09928566..c2b614ad 100644 --- a/FreeFileSync/Source/RealTimeSync/tray_menu.cpp +++ b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp @@ -259,7 +259,7 @@ rts::AbortReason rts::runFolderMonitor(const XmlRealConfig& config, const wxStri if (cmdLine.empty()) { - showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setMainInstructions(_("Incorrect command line:") + L" \"\"")); + showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setMainInstructions(replaceCpy(_("Command %x failed."), L"%x", fmtPath(cmdLine)))); return AbortReason::REQUEST_GUI; } @@ -274,12 +274,15 @@ rts::AbortReason rts::runFolderMonitor(const XmlRealConfig& config, const wxStri auto cmdLineExp = fff::expandMacros(cmdLine); try { - shellExecute(cmdLineExp, ExecutionType::sync, config.hideConsoleWindow); //throw FileError + if (const auto [exitCode, output] = consoleExecute(cmdLineExp, std::nullopt /*timeoutMs*/); //throw SysError, (SysErrorTimeOut) + exitCode != 0) + throw SysError(formatSystemError("", replaceCpy(_("Exit code %x"), L"%x", numberTo<std::wstring>(exitCode)), output)); } - catch (const FileError& e) + catch (const SysError& e) { //blocks! however, we *expect* this to be a persistent error condition... - showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); + showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg(). + setDetailInstructions(replaceCpy(_("Command %x failed."), L"%x", fmtPath(cmdLineExp)) + L"\n\n" + e.toString())); } }; diff --git a/FreeFileSync/Source/afs/abstract.cpp b/FreeFileSync/Source/afs/abstract.cpp index 10cb7995..c775133e 100644 --- a/FreeFileSync/Source/afs/abstract.cpp +++ b/FreeFileSync/Source/afs/abstract.cpp @@ -25,6 +25,15 @@ bool fff::isValidRelPath(const Zstring& relPath) } +AfsPath fff::sanitizeDeviceRelativePath(Zstring relPath) +{ + if constexpr (FILE_NAME_SEPARATOR != Zstr('/' )) replace(relPath, Zstr('/'), FILE_NAME_SEPARATOR); + if constexpr (FILE_NAME_SEPARATOR != Zstr('\\')) replace(relPath, Zstr('\\'), FILE_NAME_SEPARATOR); + trim(relPath, true, true, [](Zchar c) { return c == FILE_NAME_SEPARATOR; }); + return AfsPath(relPath); +} + + int AFS::compareDevice(const AbstractFileSystem& lhs, const AbstractFileSystem& rhs) { //note: in worst case, order is guaranteed to be stable only during each program run @@ -224,24 +233,21 @@ AFS::FileCopyResult AFS::copyFileTransactional(const AbstractPath& apSource, con //perf: this call is REALLY expensive on unbuffered volumes! ~40% performance decrease on FAT USB stick! moveAndRenameItem(apTargetTmp, apTarget); //throw FileError, (ErrorMoveUnsupported) - /* - CAVEAT on FAT/FAT32: the sequence of deleting the target file and renaming "file.txt.ffs_tmp" to "file.txt" does + /* CAVEAT on FAT/FAT32: the sequence of deleting the target file and renaming "file.txt.ffs_tmp" to "file.txt" does NOT PRESERVE the creation time of the .ffs_tmp file, but SILENTLY "reuses" whatever creation time the old "file.txt" had! This "feature" is called "File System Tunneling": https://devblogs.microsoft.com/oldnewthing/?p=34923 - https://support.microsoft.com/kb/172190/en-us - */ + https://support.microsoft.com/kb/172190/en-us */ + return result; } else { - /* - Note: non-transactional file copy solves at least four problems: + /* Note: non-transactional file copy solves at least four problems: -> skydrive - doesn't allow for .ffs_tmp extension and returns ERROR_INVALID_PARAMETER -> network renaming issues -> allow for true delete before copy to handle low disk space problems - -> higher performance on unbuffered drives (e.g. USB-sticks) - */ + -> higher performance on unbuffered drives (e.g. USB-sticks) */ if (onDeleteTargetFile) onDeleteTargetFile(); diff --git a/FreeFileSync/Source/afs/abstract.h b/FreeFileSync/Source/afs/abstract.h index d48c5f87..f4f58310 100644 --- a/FreeFileSync/Source/afs/abstract.h +++ b/FreeFileSync/Source/afs/abstract.h @@ -16,7 +16,9 @@ namespace fff { +struct AfsPath; bool isValidRelPath(const Zstring& relPath); +AfsPath sanitizeDeviceRelativePath(Zstring relPath); struct AbstractFileSystem; diff --git a/FreeFileSync/Source/afs/abstract_impl.h b/FreeFileSync/Source/afs/abstract_impl.h index ceabb6b6..d6bec8d2 100644 --- a/FreeFileSync/Source/afs/abstract_impl.h +++ b/FreeFileSync/Source/afs/abstract_impl.h @@ -14,16 +14,6 @@ namespace fff { -inline -AfsPath sanitizeRootRelativePath(Zstring relPath) -{ - if constexpr (FILE_NAME_SEPARATOR != Zstr('/' )) replace(relPath, Zstr('/'), FILE_NAME_SEPARATOR); - if constexpr (FILE_NAME_SEPARATOR != Zstr('\\')) replace(relPath, Zstr('\\'), FILE_NAME_SEPARATOR); - trim(relPath, true, true, [](Zchar c) { return c == FILE_NAME_SEPARATOR; }); - return AfsPath(std::move(relPath)); -} - - template <class Function> inline //return ignored error message if available std::wstring tryReportingDirError(Function cmd /*throw FileError*/, AbstractFileSystem::TraverserCallback& cb /*throw X*/) { diff --git a/FreeFileSync/Source/afs/ftp.cpp b/FreeFileSync/Source/afs/ftp.cpp index 795692db..33c0884d 100644 --- a/FreeFileSync/Source/afs/ftp.cpp +++ b/FreeFileSync/Source/afs/ftp.cpp @@ -65,7 +65,7 @@ struct FtpSessionId bool operator<(const FtpSessionId& lhs, const FtpSessionId& rhs) { //exactly the type of case insensitive comparison we need for server names! - int rv = compareAsciiNoCase(lhs.server, rhs.server); //https://msdn.microsoft.com/en-us/library/windows/desktop/ms738519#IDNs + int rv = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs if (rv != 0) return rv < 0; @@ -100,13 +100,9 @@ Zstring ansiToUtfEncoding(const std::string& str) //throw SysError &bytesWritten, //gsize* bytes_written, &error); //GError** error if (!utfStr) - { - if (!error) - throw SysError(L"g_convert: unknown error. (" + utfTo<std::wstring>(str) + L')'); //user should never see this - - throw SysError(formatSystemError(L"g_convert(" + utfTo<std::wstring>(str) + L')', - replaceCpy(_("Error Code %x"), L"%x", numberTo<std::wstring>(error->code)), utfTo<std::wstring>(error->message)) ); - } + throw SysError(formatSystemError("g_convert(" + utfTo<std::string>(str) + ')', + error ? replaceCpy(_("Error code %x"), L"%x", numberTo<std::wstring>(error->code)) : L"", + error ? utfTo<std::wstring>(error->message) : L"Unknown error.")); ZEN_ON_SCOPE_EXIT(::g_free(utfStr)); return { utfStr, bytesWritten }; @@ -130,13 +126,9 @@ std::string utfToAnsiEncoding(const Zstring& str) //throw SysError &bytesWritten, //gsize* bytes_written, &error); //GError** error if (!ansiStr) - { - if (!error) - throw SysError(L"g_convert: unknown error. (" + utfTo<std::wstring>(str) + L')'); //user should never see this - - throw SysError(formatSystemError(L"g_convert(" + utfTo<std::wstring>(str) + L')', - replaceCpy(_("Error Code %x"), L"%x", numberTo<std::wstring>(error->code)), utfTo<std::wstring>(error->message))); - } + throw SysError(formatSystemError("g_convert(" + utfTo<std::string>(str) + ')', + error ? replaceCpy(_("Error code %x"), L"%x", numberTo<std::wstring>(error->code)) : L"", + error ? utfTo<std::wstring>(error->message) : L"Unknown error.")); ZEN_ON_SCOPE_EXIT(::g_free(ansiStr)); return { ansiStr, bytesWritten }; @@ -318,7 +310,7 @@ public: { easyHandle_ = ::curl_easy_init(); if (!easyHandle_) - throw SysError(formatSystemError(L"curl_easy_init", formatCurlStatusCode(CURLE_OUT_OF_MEMORY), std::wstring())); + throw SysError(formatSystemError("curl_easy_init", formatCurlStatusCode(CURLE_OUT_OF_MEMORY), L"")); } else ::curl_easy_reset(easyHandle_); @@ -504,7 +496,7 @@ public: if (nativeErrorCode != 0) errorMsg += (errorMsg.empty() ? L"" : L"\n") + std::wstring(L"Native error code: ") + numberTo<std::wstring>(nativeErrorCode); #endif - throw SysError(formatSystemError(L"curl_easy_perform", formatCurlStatusCode(rcPerf), errorMsg)); + throw SysError(formatSystemError("curl_easy_perform", formatCurlStatusCode(rcPerf), errorMsg)); } lastSuccessfulUseTime_ = std::chrono::steady_clock::now(); @@ -550,7 +542,7 @@ public: /*CURLcode rc =*/ ::curl_easy_getinfo(easyHandle_, CURLINFO_FTP_ENTRY_PATH, &homePathCurl); if (homePathCurl && isAsciiString(homePathCurl)) - return sanitizeRootRelativePath(utfTo<Zstring>(homePathCurl)); + return sanitizeDeviceRelativePath(utfTo<Zstring>(homePathCurl)); //home path with non-ASCII chars: libcurl issues PWD right after login *before* server was set up for UTF8 //=> CURLINFO_FTP_ENTRY_PATH could be in any encoding => useless! @@ -579,7 +571,7 @@ public: const std::string homePathRaw = replaceCpy<std::string>({ itBegin, it }, "\"\"", '"'); const ServerEncoding enc = getServerEncoding(timeoutSec); //throw SysError const Zstring homePathUtf = serverToUtfEncoding(homePathRaw, enc); //throw SysError - return sanitizeRootRelativePath(homePathUtf); + return sanitizeDeviceRelativePath(homePathUtf); } } } @@ -627,7 +619,7 @@ private: { char* compFmt = ::curl_easy_escape(easyHandle_, comp.c_str(), static_cast<int>(comp.size())); if (!compFmt) - throw SysError(replaceCpy<std::wstring>(L"curl_easy_escape: conversion failure (%x)", L"%x", utfTo<std::wstring>(comp))); + throw SysError(formatSystemError("curl_easy_escape(" + comp + ')', L"", L"Conversion failure")); ZEN_ON_SCOPE_EXIT(::curl_free(compFmt)); if (!curlRelPath.empty()) @@ -686,7 +678,7 @@ private: curl_socket_t currentSocket = 0; const CURLcode rc = ::curl_easy_getinfo(easyHandle_, CURLINFO_ACTIVESOCKET, ¤tSocket); if (rc != CURLE_OK) - throw SysError(formatSystemError(L"curl_easy_getinfo(CURLINFO_ACTIVESOCKET)", formatCurlStatusCode(rc), utfTo<std::wstring>(::curl_easy_strerror(rc)))); + throw SysError(formatSystemError("curl_easy_getinfo(CURLINFO_ACTIVESOCKET)", formatCurlStatusCode(rc), utfTo<std::wstring>(::curl_easy_strerror(rc)))); if (currentSocket != CURL_SOCKET_BAD) return currentSocket; } @@ -712,7 +704,7 @@ private: const auto sf = globalServerFeatures.get(); if (!sf) - throw SysError(L"FtpSession::getFeatureSupport() function call not allowed during init/shutdown."); + throw SysError(formatSystemError("FtpSession::getFeatureSupport", L"", L"Function call not allowed during init/shutdown.")); sf->access([&](FeatureList& feat) { featureCache_ = feat[sessionId_.server]; }); @@ -902,7 +894,7 @@ void accessFtpSession(const FtpLoginInfo& login, const std::function<void(FtpSes if (const std::shared_ptr<FtpSessionManager> mgr = globalFtpSessionManager.get()) mgr->access(login, useFtpSession); //throw SysError, X else - throw SysError(L"accessFtpSession() function call not allowed during init/shutdown."); + throw SysError(formatSystemError("accessFtpSession", L"", L"Function call not allowed during init/shutdown.")); } //=========================================================================================================================== @@ -1806,6 +1798,8 @@ class FtpFileSystem : public AbstractFileSystem public: FtpFileSystem(const FtpLoginInfo& login) : login_(login) {} + const FtpLoginInfo& getLogin() const { return login_; } + private: Zstring getInitPathPhrase(const AfsPath& afsPath) const override { return concatenateFtpFolderPathPhrase(login_, afsPath); } @@ -1819,7 +1813,7 @@ private: const FtpLoginInfo& rhs = static_cast<const FtpFileSystem&>(afsRhs).login_; //exactly the type of case insensitive comparison we need for server names! - const int rv = compareAsciiNoCase(lhs.server, rhs.server); //https://msdn.microsoft.com/en-us/library/windows/desktop/ms738519#IDNs + const int rv = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs if (rv != 0) return rv; @@ -2116,7 +2110,7 @@ private: //=========================================================================================================================== -//expects "clean" login data, see condenseToFtpFolderPathPhrase() +//expects "clean" login data Zstring concatenateFtpFolderPathPhrase(const FtpLoginInfo& login, const AfsPath& afsPath) //noexcept { Zstring port; @@ -2172,11 +2166,10 @@ AfsPath fff::getFtpHomePath(const FtpLoginInfo& login) //throw FileError } -Zstring fff::condenseToFtpFolderPathPhrase(const FtpLoginInfo& login, const Zstring& relPath) //noexcept +AfsDevice fff::condenseToFtpDevice(const FtpLoginInfo& login) //noexcept { + //clean up input: FtpLoginInfo loginTmp = login; - - //clean-up input: trim(loginTmp.server); trim(loginTmp.username); @@ -2188,9 +2181,27 @@ Zstring fff::condenseToFtpFolderPathPhrase(const FtpLoginInfo& login, const Zstr startsWithAsciiNoCase(loginTmp.server, "ftps:" ) || startsWithAsciiNoCase(loginTmp.server, "sftp:" )) loginTmp.server = afterFirst(loginTmp.server, Zstr(':'), IF_MISSING_RETURN_NONE); - trim(loginTmp.server, true, false, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); }); + trim(loginTmp.server, true, true, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); }); + + return makeSharedRef<FtpFileSystem>(loginTmp); +} + + +FtpLoginInfo fff::extractFtpLogin(const AfsDevice& afsDevice) //noexcept +{ + if (const auto ftpDevice = dynamic_cast<const FtpFileSystem*>(&afsDevice.ref())) + return ftpDevice->getLogin(); + + assert(false); + return {}; +} + - return concatenateFtpFolderPathPhrase(loginTmp, sanitizeRootRelativePath(relPath)); +bool fff::acceptsItemPathPhraseFtp(const Zstring& itemPathPhrase) //noexcept +{ + Zstring path = expandMacros(itemPathPhrase); //expand before trimming! + trim(path); + return startsWithAsciiNoCase(path, ftpPrefix); //check for explicit FTP path } @@ -2198,9 +2209,9 @@ Zstring fff::condenseToFtpFolderPathPhrase(const FtpLoginInfo& login, const Zstr // // e.g. ftp://user001:secretpassword@private.example.com:222/mydirectory/ // ftp://user001@private.example.com/mydirectory|pass64=c2VjcmV0cGFzc3dvcmQ -FtpPathInfo fff::getResolvedFtpPath(const Zstring& folderPathPhrase) //noexcept +AbstractPath fff::createItemPathFtp(const Zstring& itemPathPhrase) //noexcept { - Zstring pathPhrase = expandMacros(folderPathPhrase); //expand before trimming! + Zstring pathPhrase = expandMacros(itemPathPhrase); //expand before trimming! trim(pathPhrase); if (startsWithAsciiNoCase(pathPhrase, ftpPrefix)) @@ -2212,14 +2223,14 @@ FtpPathInfo fff::getResolvedFtpPath(const Zstring& folderPathPhrase) //noexcept FtpLoginInfo login; login.username = decodeFtpUsername(beforeFirst(credentials, Zstr(':'), IF_MISSING_RETURN_ALL)); //support standard FTP syntax, even though ':' - login.password = afterFirst(credentials, Zstr(':'), IF_MISSING_RETURN_NONE); //is not used by our concatenateSftpFolderPathPhrase()! + login.password = afterFirst(credentials, Zstr(':'), IF_MISSING_RETURN_NONE); //is not used by concatenateFtpFolderPathPhrase()! const Zstring fullPath = beforeFirst(fullPathOpt, Zstr('|'), IF_MISSING_RETURN_ALL); const Zstring options = afterFirst(fullPathOpt, Zstr('|'), IF_MISSING_RETURN_NONE); auto it = std::find_if(fullPath.begin(), fullPath.end(), [](Zchar c) { return c == '/' || c == '\\'; }); const Zstring serverPort(fullPath.begin(), it); - const AfsPath serverRelPath = sanitizeRootRelativePath({ it, fullPath.end() }); + const AfsPath serverRelPath = sanitizeDeviceRelativePath({ it, fullPath.end() }); login.server = beforeLast(serverPort, Zstr(':'), IF_MISSING_RETURN_ALL); const Zstring port = afterLast(serverPort, Zstr(':'), IF_MISSING_RETURN_NONE); @@ -2237,20 +2248,6 @@ FtpPathInfo fff::getResolvedFtpPath(const Zstring& folderPathPhrase) //noexcept else assert(false); } //fix "-Wdangling-else" - return { login, serverRelPath }; -} - - -bool fff::acceptsItemPathPhraseFtp(const Zstring& itemPathPhrase) //noexcept -{ - Zstring path = expandMacros(itemPathPhrase); //expand before trimming! - trim(path); - return startsWithAsciiNoCase(path, ftpPrefix); //check for explicit FTP path -} - -AbstractPath fff::createItemPathFtp(const Zstring& itemPathPhrase) //noexcept -{ - const FtpPathInfo& pi = getResolvedFtpPath(itemPathPhrase); //noexcept - return AbstractPath(makeSharedRef<FtpFileSystem>(pi.login), pi.afsPath); + return AbstractPath(makeSharedRef<FtpFileSystem>(login), serverRelPath); } diff --git a/FreeFileSync/Source/afs/ftp.h b/FreeFileSync/Source/afs/ftp.h index 12f8fd1a..2978cdec 100644 --- a/FreeFileSync/Source/afs/ftp.h +++ b/FreeFileSync/Source/afs/ftp.h @@ -15,11 +15,11 @@ namespace fff bool acceptsItemPathPhraseFtp(const Zstring& itemPathPhrase); //noexcept AbstractPath createItemPathFtp(const Zstring& itemPathPhrase); //noexcept -//------------------------------------------------------- - void ftpInit(); void ftpTeardown(); +//------------------------------------------------------- + struct FtpLoginInfo { Zstring server; @@ -31,16 +31,8 @@ struct FtpLoginInfo //other settings not specific to FTP session: int timeoutSec = 15; }; - -struct FtpPathInfo -{ - FtpLoginInfo login; - AfsPath afsPath; //server-relative path -}; -FtpPathInfo getResolvedFtpPath(const Zstring& folderPathPhrase); //noexcept - -//expects (potentially messy) user input: -Zstring condenseToFtpFolderPathPhrase(const FtpLoginInfo& login, const Zstring& relPath); //noexcept +AfsDevice condenseToFtpDevice(const FtpLoginInfo& login); //noexcept; potentially messy user input +FtpLoginInfo extractFtpLogin(const AfsDevice& afsDevice); //noexcept AfsPath getFtpHomePath(const FtpLoginInfo& login); //throw FileError } diff --git a/FreeFileSync/Source/afs/gdrive.cpp b/FreeFileSync/Source/afs/gdrive.cpp index d653a030..d47f780d 100644 --- a/FreeFileSync/Source/afs/gdrive.cpp +++ b/FreeFileSync/Source/afs/gdrive.cpp @@ -34,6 +34,11 @@ using AFS = AbstractFileSystem; namespace fff { +struct GdrivePath +{ + Zstring userEmail; + AfsPath itemPath; //path relative to Google Drive root +}; bool operator<(const GdrivePath& lhs, const GdrivePath& rhs) { const int rv = compareAsciiNoCase(lhs.userEmail, rhs.userEmail); @@ -86,11 +91,11 @@ struct HttpSessionId bool operator<(const HttpSessionId& lhs, const HttpSessionId& rhs) { //exactly the type of case insensitive comparison we need for server names! - return compareAsciiNoCase(lhs.server, rhs.server) < 0; //https://msdn.microsoft.com/en-us/library/windows/desktop/ms738519#IDNs + return compareAsciiNoCase(lhs.server, rhs.server) < 0; //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs } -//expects "clean" input data, see condenseToGoogleFolderPathPhrase() +//expects "clean" input data Zstring concatenateGoogleFolderPathPhrase(const GdrivePath& gdrivePath) //noexcept { Zstring pathPhrase = Zstring(googleDrivePrefix) + FILE_NAME_SEPARATOR + gdrivePath.userEmail; @@ -284,7 +289,7 @@ HttpSession::Result googleHttpsRequest(const std::string& serverRelPath, //throw { const std::shared_ptr<HttpSessionManager> mgr = globalHttpSessionManager.get(); if (!mgr) - throw SysError(L"googleHttpsRequest() function call not allowed during init/shutdown."); + throw SysError(formatSystemError("googleHttpsRequest", L"", L"Function call not allowed during init/shutdown.")); HttpSession::Result httpResult; @@ -433,7 +438,7 @@ GoogleAccessInfo authorizeAccessToGoogleDrive(const Zstring& googleLoginHint, co &hints, //_In_opt_ const ADDRINFOA* pHints, &servinfo); //_Outptr_ PADDRINFOA* ppResult if (rcGai != 0) - throw SysError(formatSystemError(L"getaddrinfo", replaceCpy(_("Error Code %x"), L"%x", numberTo<std::wstring>(rcGai)), utfTo<std::wstring>(::gai_strerror(rcGai)))); + throw SysError(formatSystemError("getaddrinfo", replaceCpy(_("Error code %x"), L"%x", numberTo<std::wstring>(rcGai)), utfTo<std::wstring>(::gai_strerror(rcGai)))); if (!servinfo) throw SysError(L"getaddrinfo: empty server info"); @@ -441,11 +446,11 @@ GoogleAccessInfo authorizeAccessToGoogleDrive(const Zstring& googleLoginHint, co { SocketType testSocket = ::socket(ai.ai_family, ai.ai_socktype, ai.ai_protocol); if (testSocket == invalidSocket) - THROW_LAST_SYS_ERROR_WSA(L"socket"); + THROW_LAST_SYS_ERROR_WSA("socket"); ZEN_ON_SCOPE_FAIL(closeSocket(testSocket)); if (::bind(testSocket, ai.ai_addr, static_cast<int>(ai.ai_addrlen)) != 0) - THROW_LAST_SYS_ERROR_WSA(L"bind"); + THROW_LAST_SYS_ERROR_WSA("bind"); return testSocket; }; @@ -470,18 +475,18 @@ GoogleAccessInfo authorizeAccessToGoogleDrive(const Zstring& googleLoginHint, co sockaddr_storage addr = {}; //"sufficiently large to store address information for IPv4 or IPv6" => sockaddr_in and sockaddr_in6 socklen_t addrLen = sizeof(addr); if (::getsockname(socket, reinterpret_cast<sockaddr*>(&addr), &addrLen) != 0) - THROW_LAST_SYS_ERROR_WSA(L"getsockname"); + THROW_LAST_SYS_ERROR_WSA("getsockname"); if (addr.ss_family != AF_INET && addr.ss_family != AF_INET6) - throw SysError(L"getsockname: unknown protocol family (" + numberTo<std::wstring>(addr.ss_family) + L')'); + throw SysError(formatSystemError("getsockname", L"", L"Unknown protocol family: " + numberTo<std::wstring>(addr.ss_family))); const int port = ntohs(reinterpret_cast<const sockaddr_in&>(addr).sin_port); //the socket is not bound to a specific local IP => inet_ntoa(reinterpret_cast<const sockaddr_in&>(addr).sin_addr) == "0.0.0.0" const std::string redirectUrl = "http://127.0.0.1:" + numberTo<std::string>(port); if (::listen(socket, SOMAXCONN) != 0) - THROW_LAST_SYS_ERROR_WSA(L"listen"); + THROW_LAST_SYS_ERROR_WSA("listen"); //"A code_verifier is a high-entropy cryptographic random string using the unreserved characters:" @@ -505,7 +510,7 @@ if (::listen(socket, SOMAXCONN) != 0) }); try { - openWithDefaultApplication(utfTo<Zstring>(oauthUrl)); //throw FileError + openWithDefaultApp(utfTo<Zstring>(oauthUrl)); //throw FileError } catch (const FileError& e) { throw SysError(e.toString()); } //errors should be further enriched by context info => SysError @@ -528,7 +533,7 @@ for (;;) //::accept() blocks forever if no client connects (e.g. user just close //perf: no significant difference compared to ::WSAPoll() const int rc = ::select(socket + 1, readfds, nullptr /*writefds*/, nullptr /*errorfds*/, &tv); if (rc < 0) - THROW_LAST_SYS_ERROR_WSA(L"select"); + THROW_LAST_SYS_ERROR_WSA("select"); if (rc != 0) break; //else: time-out! @@ -538,7 +543,7 @@ for (;;) //::accept() blocks forever if no client connects (e.g. user just close nullptr, //sockaddr *addr, nullptr); //int *addrlen if (clientSocket == invalidSocket) - THROW_LAST_SYS_ERROR_WSA(L"accept"); + THROW_LAST_SYS_ERROR_WSA("accept"); //receive first line of HTTP request std::string reqLine; @@ -586,7 +591,7 @@ for (;;) //::accept() blocks forever if no client connects (e.g. user just close try { if (!error.empty()) - throw SysError(replaceCpy(_("Error Code %x"), L"%x", + L"\"" + utfTo<std::wstring>(error) + L"\"")); + throw SysError(replaceCpy(_("Error code %x"), L"%x", + L"\"" + utfTo<std::wstring>(error) + L"\"")); //do as many login-related tasks as possible while we have the browser as an error output device! //see AFS::connectNetworkFolder() => errors will be lost after time out in dir_exist_async.h! @@ -655,7 +660,7 @@ void revokeAccessToGoogleDrive(const std::string& accessToken, const Zstring& go //https://developers.google.com/identity/protocols/OAuth2InstalledApp#tokenrevoke const std::shared_ptr<HttpSessionManager> mgr = globalHttpSessionManager.get(); if (!mgr) - throw SysError(L"revokeAccessToGoogleDrive() Function call not allowed during process init/shutdown."); + throw SysError(formatSystemError("revokeAccessToGoogleDrive", L"", L"Function call not allowed during init/shutdown.")); HttpSession::Result httpResult; std::string response; @@ -730,84 +735,84 @@ std::vector<GoogleFileItem> readFolderContent(const std::string& folderId, const { warn_static("perf: trashed=false and ('114231411234' in parents or '123123' in parents)") - //https://developers.google.com/drive/api/v3/reference/files/list - std::vector<GoogleFileItem> childItems; +//https://developers.google.com/drive/api/v3/reference/files/list +std::vector<GoogleFileItem> childItems; +{ + std::optional<std::string> nextPageToken; + do { - std::optional<std::string> nextPageToken; - do + std::string queryParams = xWwwFormUrlEncode( { - std::string queryParams = xWwwFormUrlEncode( - { - { "spaces", "drive" }, // - { "corpora", "user" }, //"The 'user' corpus includes all files in "My Drive" and "Shared with me" https://developers.google.com/drive/api/v3/reference/files/list - { "pageSize", "1000" }, //"[1, 1000] Default: 100" - { "fields", "nextPageToken,incompleteSearch,files(name,id,mimeType,shared,size,modifiedTime,parents)" }, //https://developers.google.com/drive/api/v3/reference/files - { "q", "trashed=false and '" + folderId + "' in parents" }, - }); - if (nextPageToken) - queryParams += '&' + xWwwFormUrlEncode({ { "pageToken", *nextPageToken } }); + { "spaces", "drive" }, // + { "corpora", "user" }, //"The 'user' corpus includes all files in "My Drive" and "Shared with me" https://developers.google.com/drive/api/v3/reference/files/list + { "pageSize", "1000" }, //"[1, 1000] Default: 100" + { "fields", "nextPageToken,incompleteSearch,files(name,id,mimeType,shared,size,modifiedTime,parents)" }, //https://developers.google.com/drive/api/v3/reference/files + { "q", "trashed=false and '" + folderId + "' in parents" }, + }); + if (nextPageToken) + queryParams += '&' + xWwwFormUrlEncode({ { "pageToken", *nextPageToken } }); - std::string response; - googleHttpsRequest("/drive/v3/files?" + queryParams, { "Authorization: Bearer " + accessToken }, {} /*extraOptions*/, //throw SysError - [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast<const char*>(buffer), bytesToWrite); }, nullptr /*readRequest*/); + std::string response; + googleHttpsRequest("/drive/v3/files?" + queryParams, { "Authorization: Bearer " + accessToken }, {} /*extraOptions*/, //throw SysError + [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast<const char*>(buffer), bytesToWrite); }, nullptr /*readRequest*/); - JsonValue jresponse; - try { jresponse = parseJson(response); } - catch (JsonParsingError&) {} + JsonValue jresponse; + try { jresponse = parseJson(response); } + catch (JsonParsingError&) {} - /**/ nextPageToken = getPrimitiveFromJsonObject(jresponse, "nextPageToken"); - const std::optional<std::string> incompleteSearch = getPrimitiveFromJsonObject(jresponse, "incompleteSearch"); - const JsonValue* files = getChildFromJsonObject (jresponse, "files"); - if (!incompleteSearch || *incompleteSearch != "false" || !files || files->type != JsonValue::Type::array) - throw SysError(formatGoogleErrorRaw(response)); + /**/ nextPageToken = getPrimitiveFromJsonObject(jresponse, "nextPageToken"); + const std::optional<std::string> incompleteSearch = getPrimitiveFromJsonObject(jresponse, "incompleteSearch"); + const JsonValue* files = getChildFromJsonObject (jresponse, "files"); + if (!incompleteSearch || *incompleteSearch != "false" || !files || files->type != JsonValue::Type::array) + throw SysError(formatGoogleErrorRaw(response)); - for (const auto& childVal : files->arrayVal) - { - const std::optional<std::string> itemId = getPrimitiveFromJsonObject(childVal, "id"); - const std::optional<std::string> itemName = getPrimitiveFromJsonObject(childVal, "name"); - const std::optional<std::string> mimeType = getPrimitiveFromJsonObject(childVal, "mimeType"); - const std::optional<std::string> shared = getPrimitiveFromJsonObject(childVal, "shared"); - const std::optional<std::string> size = getPrimitiveFromJsonObject(childVal, "size"); - const std::optional<std::string> modifiedTime = getPrimitiveFromJsonObject(childVal, "modifiedTime"); - const JsonValue* parents = getChildFromJsonObject (childVal, "parents"); - - if (!itemId || !itemName || !mimeType || !modifiedTime || !parents) - throw SysError(formatGoogleErrorRaw(response)); + for (const auto& childVal : files->arrayVal) + { + const std::optional<std::string> itemId = getPrimitiveFromJsonObject(childVal, "id"); + const std::optional<std::string> itemName = getPrimitiveFromJsonObject(childVal, "name"); + const std::optional<std::string> mimeType = getPrimitiveFromJsonObject(childVal, "mimeType"); + const std::optional<std::string> shared = getPrimitiveFromJsonObject(childVal, "shared"); + const std::optional<std::string> size = getPrimitiveFromJsonObject(childVal, "size"); + const std::optional<std::string> modifiedTime = getPrimitiveFromJsonObject(childVal, "modifiedTime"); + const JsonValue* parents = getChildFromJsonObject (childVal, "parents"); - const bool isFolder = *mimeType == googleFolderMimeType; - const bool isShared = shared && *shared == "true"; //"Not populated for items in shared drives" - const uint64_t fileSize = size ? stringTo<uint64_t>(*size) : 0; //not available for folders + if (!itemId || !itemName || !mimeType || !modifiedTime || !parents) + throw SysError(formatGoogleErrorRaw(response)); - //RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z" - const TimeComp tc = parseTime("%Y-%m-%dT%H:%M:%S", beforeLast(*modifiedTime, '.', IF_MISSING_RETURN_ALL)); - if (tc == TimeComp() || !endsWith(*modifiedTime, 'Z')) //'Z' means "UTC" => it seems Google doesn't use the time-zone offset postfix - throw SysError(L"Modification time could not be parsed. (" + utfTo<std::wstring>(*modifiedTime) + L')'); + const bool isFolder = *mimeType == googleFolderMimeType; + const bool isShared = shared && *shared == "true"; //"Not populated for items in shared drives" + const uint64_t fileSize = size ? stringTo<uint64_t>(*size) : 0; //not available for folders - time_t modTime = utcToTimeT(tc); //returns -1 on error - if (modTime == -1) - { - if (tc.year == 1600 || //zero-initialized FILETIME equals "December 31, 1600" or "January 1, 1601" - tc.year == 1601) // => yes, possible even on Google Drive: https://freefilesync.org/forum/viewtopic.php?t=6602 - modTime = 0; - else - throw SysError(L"Modification time could not be parsed. (" + utfTo<std::wstring>(*modifiedTime) + L')'); - } + //RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z" + const TimeComp tc = parseTime("%Y-%m-%dT%H:%M:%S", beforeLast(*modifiedTime, '.', IF_MISSING_RETURN_ALL)); + if (tc == TimeComp() || !endsWith(*modifiedTime, 'Z')) //'Z' means "UTC" => it seems Google doesn't use the time-zone offset postfix + throw SysError(L"Modification time could not be parsed. (" + utfTo<std::wstring>(*modifiedTime) + L')'); - std::vector<std::string> parentIds; - for (const auto& parentVal : parents->arrayVal) - { - if (parentVal.type != JsonValue::Type::string) - throw SysError(formatGoogleErrorRaw(response)); - parentIds.push_back(parentVal.primVal); - } - assert(std::find(parentIds.begin(), parentIds.end(), folderId) != parentIds.end()); + time_t modTime = utcToTimeT(tc); //returns -1 on error + if (modTime == -1) + { + if (tc.year == 1600 || //zero-initialized FILETIME equals "December 31, 1600" or "January 1, 1601" + tc.year == 1601) // => yes, possible even on Google Drive: https://freefilesync.org/forum/viewtopic.php?t=6602 + modTime = 0; + else + throw SysError(L"Modification time could not be parsed. (" + utfTo<std::wstring>(*modifiedTime) + L')'); + } - childItems.push_back({ *itemId, { *itemName, isFolder, isShared, fileSize, modTime, std::move(parentIds) } }); + std::vector<std::string> parentIds; + for (const auto& parentVal : parents->arrayVal) + { + if (parentVal.type != JsonValue::Type::string) + throw SysError(formatGoogleErrorRaw(response)); + parentIds.push_back(parentVal.primVal); } + assert(std::find(parentIds.begin(), parentIds.end(), folderId) != parentIds.end()); + + childItems.push_back({ *itemId, { *itemName, isFolder, isShared, fileSize, modTime, std::move(parentIds) } }); } - while (nextPageToken); } - return childItems; + while (nextPageToken); +} +return childItems; } @@ -1085,7 +1090,7 @@ void gdriveMoveAndRenameItem(const std::string& itemId, const std::string& paren if (!std::any_of(parents->arrayVal.begin(), parents->arrayVal.end(), [&](const JsonValue& jval) { return jval.type == JsonValue::Type::string && jval.primVal == parentIdTo; })) - throw SysError(L"gdriveMoveAndRenameItem: Google Drive internal failure"); //user should never see this... + throw SysError(formatSystemError("gdriveMoveAndRenameItem", L"", L"Google Drive internal failure.")); //user should never see this... } @@ -2069,7 +2074,7 @@ GooglePersistentSessions::AsyncAccessInfo accessGlobalFileState(const Zstring& g if (const std::shared_ptr<GooglePersistentSessions> gps = globalGoogleSessions.get()) return gps->accessGlobalFileState(googleUserEmail, useFileState); //throw SysError, X - throw SysError(L"accessGlobalFileState() function call not allowed during init/shutdown."); + throw SysError(formatSystemError("accessGlobalFileState", L"", L"Function call not allowed during init/shutdown.")); } //========================================================================================== @@ -2399,10 +2404,18 @@ class GdriveFileSystem : public AbstractFileSystem public: GdriveFileSystem(const Zstring& googleUserEmail) : googleUserEmail_(googleUserEmail) {} + const Zstring& getEmail() const { return googleUserEmail_; } + private: GdrivePath getGdrivePath(const AfsPath& afsPath) const { return { googleUserEmail_, afsPath }; } - Zstring getInitPathPhrase(const AfsPath& afsPath) const override { return concatenateGoogleFolderPathPhrase(getGdrivePath(afsPath)); } + Zstring getInitPathPhrase(const AfsPath& afsPath) const override + { + Zstring initPathPhrase = concatenateGoogleFolderPathPhrase(getGdrivePath(afsPath)); + if (endsWith(initPathPhrase, Zstr(' '))) //path prase concept must survive trimming! + initPathPhrase += FILE_NAME_SEPARATOR; + return initPathPhrase; + } std::wstring getDisplayPath(const AfsPath& afsPath) const override { return getGoogleDisplayPath(getGdrivePath(afsPath)); } @@ -2707,7 +2720,7 @@ private: { const std::shared_ptr<GooglePersistentSessions> gps = globalGoogleSessions.get(); if (!gps) - throw SysError(L"GdriveFileSystem::authenticateAccess() function call not allowed during init/shutdown."); + throw SysError(formatSystemError("GdriveFileSystem::authenticateAccess", L"", L"Function call not allowed during init/shutdown.")); for (const Zstring& email : gps->listUserSessions()) //throw SysError if (equalAsciiNoCase(email, googleUserEmail_)) @@ -2800,7 +2813,7 @@ Zstring fff::googleAddUser(const std::function<void()>& updateGui /*throw X*/) / if (const std::shared_ptr<GooglePersistentSessions> gps = globalGoogleSessions.get()) return gps->addUserSession(Zstr("") /*googleLoginHint*/, updateGui); //throw SysError, X - throw SysError(L"googleAddUser() function call not allowed during init/shutdown."); + throw SysError(formatSystemError("googleAddUser", L"", L"Function call not allowed during init/shutdown.")); } catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to connect to %x."), L"%x", L"Google Drive"), e.toString()); } } @@ -2813,7 +2826,7 @@ void fff::googleRemoveUser(const Zstring& googleUserEmail) //throw FileError if (const std::shared_ptr<GooglePersistentSessions> gps = globalGoogleSessions.get()) return gps->removeUserSession(googleUserEmail); //throw SysError - throw SysError(L"googleRemoveUser() function call not allowed during init/shutdown."); + throw SysError(formatSystemError("googleRemoveUser", L"", L"Function call not allowed during init/shutdown.")); } catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to disconnect from %x."), L"%x", fmtPath(getGoogleDisplayPath({ googleUserEmail, AfsPath() }))), e.toString()); } } @@ -2826,34 +2839,25 @@ std::vector<Zstring> /*Google user email*/ fff::googleListConnectedUsers() //thr if (const std::shared_ptr<GooglePersistentSessions> gps = globalGoogleSessions.get()) return gps->listUserSessions(); //throw SysError - throw SysError(L"googleListConnectedUsers() function call not allowed during init/shutdown."); + throw SysError(formatSystemError("googleListConnectedUsers", L"", L"Function call not allowed during init/shutdown.")); } catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to access %x."), L"%x", L"Google Drive"), e.toString()); } } -Zstring fff::condenseToGoogleFolderPathPhrase(const Zstring& userEmail, const Zstring& relPath) //noexcept +AfsDevice fff::condenseToGdriveDevice(const Zstring& userEmail) //noexcept { - return concatenateGoogleFolderPathPhrase({ trimCpy(userEmail), sanitizeRootRelativePath(relPath) }); + return makeSharedRef<GdriveFileSystem>(trimCpy(userEmail)); } -//e.g.: gdrive:/john@gmail.com/folder/file.txt -GdrivePath fff::getResolvedGooglePath(const Zstring& folderPathPhrase) //noexcept +Zstring fff::extractGdriveEmail(const AfsDevice& afsDevice) //noexcept { - Zstring path = folderPathPhrase; - path = expandMacros(path); //expand before trimming! - trim(path); + if (const auto gdriveDevice = dynamic_cast<const GdriveFileSystem*>(&afsDevice.ref())) + return gdriveDevice ->getEmail(); - if (startsWithAsciiNoCase(path, googleDrivePrefix)) - path = path.c_str() + strLength(googleDrivePrefix); - - const AfsPath& sanPath = sanitizeRootRelativePath(path); //Win/macOS compatibility: let's ignore slash/backslash differences - - const Zstring& userEmail = beforeFirst(sanPath.value, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL); - const AfsPath afsPath (afterFirst(sanPath.value, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE)); - - return { userEmail, afsPath }; + assert(false); + return {}; } @@ -2865,8 +2869,20 @@ bool fff::acceptsItemPathPhraseGdrive(const Zstring& itemPathPhrase) //noexcept } +//e.g.: gdrive:/john@gmail.com/folder/file.txt AbstractPath fff::createItemPathGdrive(const Zstring& itemPathPhrase) //noexcept { - const GdrivePath& gdrivePath = getResolvedGooglePath(itemPathPhrase); //noexcept - return AbstractPath(makeSharedRef<GdriveFileSystem>(gdrivePath.userEmail), gdrivePath.itemPath); + Zstring path = itemPathPhrase; + path = expandMacros(path); //expand before trimming! + trim(path); + + if (startsWithAsciiNoCase(path, googleDrivePrefix)) + path = path.c_str() + strLength(googleDrivePrefix); + + const AfsPath& sanPath = sanitizeDeviceRelativePath(path); //Win/macOS compatibility: let's ignore slash/backslash differences + + const Zstring& userEmail = beforeFirst(sanPath.value, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL); + const AfsPath afsPath (afterFirst(sanPath.value, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE)); + + return AbstractPath(makeSharedRef<GdriveFileSystem>(userEmail), afsPath); } diff --git a/FreeFileSync/Source/afs/gdrive.h b/FreeFileSync/Source/afs/gdrive.h index d81620a3..fb90a2ab 100644 --- a/FreeFileSync/Source/afs/gdrive.h +++ b/FreeFileSync/Source/afs/gdrive.h @@ -14,26 +14,18 @@ namespace fff bool acceptsItemPathPhraseGdrive(const Zstring& itemPathPhrase); //noexcept AbstractPath createItemPathGdrive(const Zstring& itemPathPhrase); //noexcept -//------------------------------------------------------- - void googleDriveInit(const Zstring& configDirPath, //directory to store Google-Drive-specific files const Zstring& caCertFilePath); //cacert.pem void googleDriveTeardown(); +//------------------------------------------------------- + Zstring /*Google user email*/ googleAddUser(const std::function<void()>& updateGui /*throw X*/); //throw FileError, X void googleRemoveUser(const Zstring& googleUserEmail); //throw FileError std::vector<Zstring> /*Google user email*/ googleListConnectedUsers(); //throw FileError - -struct GdrivePath -{ - Zstring userEmail; - AfsPath itemPath; //path relative to Google Drive root => no leading or trailing backslash! -}; -GdrivePath getResolvedGooglePath(const Zstring& folderPathPhrase); //noexcept - -//expects (potentially messy) user input: -Zstring condenseToGoogleFolderPathPhrase(const Zstring& userEmail, const Zstring& relPath); //noexcept +AfsDevice condenseToGdriveDevice(const Zstring& userEmail); //noexcept; potentially messy user input +Zstring extractGdriveEmail(const AfsDevice& afsDevice); //noexcept } #endif //FS_GDRIVE_9238425018342701356 diff --git a/FreeFileSync/Source/afs/init_curl_libssh2.cpp b/FreeFileSync/Source/afs/init_curl_libssh2.cpp index 57cbfa95..d1645ee1 100644 --- a/FreeFileSync/Source/afs/init_curl_libssh2.cpp +++ b/FreeFileSync/Source/afs/init_curl_libssh2.cpp @@ -73,7 +73,7 @@ public: assert(sessionCount_ >= 0); if (!newSessionsAllowed_) - throw SysError(L"UniSessionCounter::inc() function call not allowed during init/shutdown."); + throw SysError(formatSystemError("UniSessionCounter::inc", L"", L"Function call not allowed during init/shutdown.")); ++sessionCount_; } @@ -148,8 +148,8 @@ std::shared_ptr<UniCounterCookie> zen::getLibsshCurlUnifiedInitCookie(Global<Uni { std::shared_ptr<UniSessionCounter> sessionCounter = globalSftpSessionCount.get(); if (!sessionCounter) - throw SysError(L"getLibsshCurlUnifiedInitCookie() function call not allowed during init/shutdown."); //=> ~UniCounterCookie() *not* called! - sessionCounter->pimpl->inc(); //throw SysError // + throw SysError(formatSystemError("getLibsshCurlUnifiedInitCookie", L"", L"Function call not allowed during init/shutdown.")); //=> ~UniCounterCookie() *not* called! + sessionCounter->pimpl->inc(); //throw SysError // //pass "ownership" of having to call UniSessionCounter::dec() return std::make_shared<UniCounterCookie>(sessionCounter); //throw SysError diff --git a/FreeFileSync/Source/afs/native.cpp b/FreeFileSync/Source/afs/native.cpp index 6e1c96fc..78ced1be 100644 --- a/FreeFileSync/Source/afs/native.cpp +++ b/FreeFileSync/Source/afs/native.cpp @@ -60,7 +60,7 @@ NativeFileInfo getFileAttributes(FileBase::FileHandle fh) //throw SysError { struct ::stat fileAttr = {}; if (::fstat(fh, &fileAttr) != 0) - THROW_LAST_SYS_ERROR(L"fstat"); + THROW_LAST_SYS_ERROR("fstat"); return { @@ -84,7 +84,7 @@ std::vector<FsItemRaw> getDirContentFlat(const Zstring& dirPath) //throw FileErr DIR* folder = ::opendir(dirPath.c_str()); //directory must NOT end with path separator, except "/" if (!folder) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot open directory %x."), L"%x", fmtPath(dirPath)), L"opendir"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot open directory %x."), L"%x", fmtPath(dirPath)), "opendir"); ZEN_ON_SCOPE_EXIT(::closedir(folder)); //never close nullptr handles! -> crash std::vector<FsItemRaw> output; @@ -107,7 +107,7 @@ std::vector<FsItemRaw> getDirContentFlat(const Zstring& dirPath) //throw FileErr if (errno == 0) //errno left unchanged => no more items return output; - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(dirPath)), L"readdir"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(dirPath)), "readdir"); //don't retry but restart dir traversal on error! https://devblogs.microsoft.com/oldnewthing/20140612-00/?p=753 } @@ -163,7 +163,7 @@ ItemDetailsRaw getItemDetails(const Zstring& itemPath) //throw FileError { struct ::stat statData = {}; if (::lstat(itemPath.c_str(), &statData) != 0) //lstat() does not resolve symlinks - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(itemPath)), L"lstat"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(itemPath)), "lstat"); return { S_ISLNK(statData.st_mode) ? ItemType::SYMLINK : //on Linux there is no distinction between file and directory symlinks! (S_ISDIR(statData.st_mode) ? ItemType::FOLDER : @@ -175,7 +175,7 @@ ItemDetailsRaw getSymlinkTargetDetails(const Zstring& linkPath) //throw FileErro { struct ::stat statData = {}; if (::stat(linkPath.c_str(), &statData) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(linkPath)), L"stat"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(linkPath)), "stat"); return { S_ISDIR(statData.st_mode) ? ItemType::FOLDER : ItemType::FILE, statData.st_mtime, makeUnsigned(statData.st_size), generateFileId(statData) }; } @@ -371,7 +371,13 @@ private: std::optional<Zstring> getNativeItemPath(const AfsPath& afsPath) const override { return getNativePath(afsPath); } - Zstring getInitPathPhrase(const AfsPath& afsPath) const override { return getNativePath(afsPath); } + Zstring getInitPathPhrase(const AfsPath& afsPath) const override + { + Zstring initPathPhrase = getNativePath(afsPath); + if (endsWith(initPathPhrase, Zstr(' '))) //path prase concept must survive trimming! + initPathPhrase += FILE_NAME_SEPARATOR; + return initPathPhrase; + } std::wstring getDisplayPath(const AfsPath& afsPath) const override { return utfTo<std::wstring>(getNativePath(afsPath)); } diff --git a/FreeFileSync/Source/afs/sftp.cpp b/FreeFileSync/Source/afs/sftp.cpp index ed0ff13e..c19b8974 100644 --- a/FreeFileSync/Source/afs/sftp.cpp +++ b/FreeFileSync/Source/afs/sftp.cpp @@ -123,7 +123,7 @@ struct SshSessionId bool operator<(const SshSessionId& lhs, const SshSessionId& rhs) { //exactly the type of case insensitive comparison we need for server names! - int rv = compareAsciiNoCase(lhs.server, rhs.server); //https://msdn.microsoft.com/en-us/library/windows/desktop/ms738519#IDNs + int rv = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs if (rv != 0) return rv < 0; @@ -207,11 +207,11 @@ public: sshSession_ = ::libssh2_session_init(); if (!sshSession_) //does not set ssh last error; source: only memory allocation may fail - throw SysError(formatSystemError(L"libssh2_session_init", formatSshStatusCode(LIBSSH2_ERROR_ALLOC), std::wstring())); + throw SysError(formatSystemError("libssh2_session_init", formatSshStatusCode(LIBSSH2_ERROR_ALLOC), std::wstring())); //if zlib compression causes trouble, make it a user setting: https://freefilesync.org/forum/viewtopic.php?t=6663 if (const int rc = ::libssh2_session_flag(sshSession_, LIBSSH2_FLAG_COMPRESS, 1); rc != 0) //does not set ssh last error - throw SysError(formatSystemError(L"libssh2_session_flag", formatSshStatusCode(rc), std::wstring())); + throw SysError(formatSystemError("libssh2_session_flag", formatSshStatusCode(rc), std::wstring())); ::libssh2_session_set_blocking(sshSession_, 1); @@ -220,7 +220,7 @@ public: if (::libssh2_session_handshake(sshSession_, socket_->get()) != 0) - throw SysError(formatLastSshError(L"libssh2_session_handshake", nullptr)); + throw SysError(formatLastSshError("libssh2_session_handshake", nullptr)); //evaluate fingerprint = libssh2_hostkey_hash(sshSession_, LIBSSH2_HOSTKEY_HASH_SHA1) ??? @@ -231,7 +231,7 @@ public: if (!authList) { if (::libssh2_userauth_authenticated(sshSession_) == 0) - throw SysError(formatLastSshError(L"libssh2_userauth_list", nullptr)); + throw SysError(formatLastSshError("libssh2_userauth_list", nullptr)); //else: SSH_USERAUTH_NONE has authenticated successfully => we're already done } else @@ -257,7 +257,7 @@ public: if (supportAuthPassword) { if (::libssh2_userauth_password(sshSession_, usernameUtf8, passwordUtf8) != 0) - throw SysError(formatLastSshError(L"libssh2_userauth_password", nullptr)); + throw SysError(formatLastSshError("libssh2_userauth_password", nullptr)); } else if (supportAuthInteractive) //some servers, e.g. web.sourceforge.net, support "keyboard-interactive", but not "password" { @@ -297,7 +297,7 @@ public: ZEN_ON_SCOPE_EXIT(*::libssh2_session_abstract(sshSession_) = nullptr); if (::libssh2_userauth_keyboard_interactive(sshSession_, usernameUtf8, authCallbackWrapper) != 0) - throw SysError(formatLastSshError(L"libssh2_userauth_keyboard_interactive", nullptr) + + throw SysError(formatLastSshError("libssh2_userauth_keyboard_interactive", nullptr) + (unexpectedPrompts.empty() ? L"" : L"\nUnexpected prompts: " + unexpectedPrompts)); } else @@ -369,7 +369,7 @@ public: replaceCpy<std::wstring>(L"%x is not an OpenSSH or PuTTY private key file.", L"%x", fmtPath(sessionId_.privateKeyFilePath) + L" [" + invalidKeyFormat + L']')); - throw SysError(formatLastSshError(L"libssh2_userauth_publickey_frommemory", nullptr)); + throw SysError(formatLastSshError("libssh2_userauth_publickey_frommemory", nullptr)); } } break; @@ -378,15 +378,15 @@ public: { LIBSSH2_AGENT* sshAgent = ::libssh2_agent_init(sshSession_); if (!sshAgent) - throw SysError(formatLastSshError(L"libssh2_agent_init", nullptr)); + throw SysError(formatLastSshError("libssh2_agent_init", nullptr)); ZEN_ON_SCOPE_EXIT(::libssh2_agent_free(sshAgent)); if (::libssh2_agent_connect(sshAgent) != 0) - throw SysError(formatLastSshError(L"libssh2_agent_connect", nullptr)); + throw SysError(formatLastSshError("libssh2_agent_connect", nullptr)); ZEN_ON_SCOPE_EXIT(::libssh2_agent_disconnect(sshAgent)); if (::libssh2_agent_list_identities(sshAgent) != 0) - throw SysError(formatLastSshError(L"libssh2_agent_list_identities", nullptr)); + throw SysError(formatLastSshError("libssh2_agent_list_identities", nullptr)); for (libssh2_agent_publickey* prev = nullptr;;) { @@ -397,7 +397,7 @@ public: else if (rc == 1) //no more public keys throw SysError(L"SSH agent contains no matching public key."); else - throw SysError(formatLastSshError(L"libssh2_agent_get_identity", nullptr)); + throw SysError(formatLastSshError("libssh2_agent_get_identity", nullptr)); if (::libssh2_agent_userauth(sshAgent, usernameUtf8.c_str(), identity) == 0) break; //authentication successful @@ -446,7 +446,7 @@ public: size_t getSftpChannelCount() const { return sftpChannels_.size(); } //return "false" if pending - bool tryNonBlocking(size_t channelNo, std::chrono::steady_clock::time_point commandStartTime, const std::wstring& functionName, + bool tryNonBlocking(size_t channelNo, std::chrono::steady_clock::time_point commandStartTime, const char* functionName, const std::function<int(const SshSession::Details& sd)>& sftpCommand /*noexcept!*/, int timeoutSec) //throw SysError, FatalSshError { assert(::libssh2_session_get_blocking(sshSession_)); @@ -514,10 +514,8 @@ public: throw FatalSshError(_P("Cannot wait on more than 1 connection at a time.", "Cannot wait on more than %x connections at a time.", FD_SETSIZE) + L' ' + replaceCpy(_("Active connections: %x"), L"%x", numberTo<std::wstring>(sshSessions.size()))); SocketType nfds = 0; - fd_set rfd = {}; + fd_set rfd = {}; //includes FD_ZERO fd_set wfd = {}; - FD_ZERO(&wfd); - FD_ZERO(&rfd); fd_set* writefds = nullptr; fd_set* readfds = nullptr; @@ -568,15 +566,16 @@ public: //WSAPoll broken, even ::poll() on OS X? https://daniel.haxx.se/blog/2012/10/10/wsapoll-is-broken/ //perf: no significant difference compared to ::WSAPoll() - const int rc = ::select(nfds + 1, readfds, writefds, nullptr /*errorfds*/, &tv); + const int rc = ::select(nfds + 1, //int nfds, + readfds, //fd_set* readfds, + writefds, //fd_set* writefds, + nullptr, //fd_set* exceptfds, + &tv); //struct timeval* timeout if (rc == 0) return; //time-out! => let next tryNonBlocking() call fail with detailed error! if (rc < 0) - { //consider SSH sessions corrupted! => isHealthy() will see pending commands - ErrorCode ec = getLastError(); //copy before directly/indirectly making other system calls! - throw FatalSshError(formatSystemError(L"select", ec)); - } + throw FatalSshError(formatSystemError("select", getLastError())); } static void addSftpChannel(const std::vector<SshSession*>& sshSessions, int timeoutSec) //throw SysError, FatalSshError @@ -601,7 +600,7 @@ public: for (size_t pos = pendingSessions.size(); pos-- > 0 ; ) //CAREFUL WITH THESE ERASEs (invalidate positions!!!) try { - if (pendingSessions[pos]->tryNonBlocking(static_cast<size_t>(-1), sftpCommandStartTime, L"libssh2_sftp_init", + if (pendingSessions[pos]->tryNonBlocking(static_cast<size_t>(-1), sftpCommandStartTime, "libssh2_sftp_init", [&](const SshSession::Details& sd) //noexcept! { LIBSSH2_SFTP* sftpChannelNew = ::libssh2_sftp_init(sd.sshSession); @@ -661,7 +660,7 @@ private: } } - std::wstring formatLastSshError(const std::wstring& functionName, LIBSSH2_SFTP* sftpChannel /*optional*/) const + std::wstring formatLastSshError(const char* functionName, LIBSSH2_SFTP* sftpChannel /*optional*/) const { char* lastErrorMsg = nullptr; //owned by "sshSession" const int sshStatusCode = ::libssh2_session_last_error(sshSession_, &lastErrorMsg, nullptr, false /*want_buf*/); @@ -681,7 +680,7 @@ private: { bool commandPending = false; std::chrono::steady_clock::time_point commandStartTime; //specified by client, try to detect libssh2 usage errors - std::wstring functionName; + std::string functionName; }; struct SftpChannelInfo @@ -744,7 +743,7 @@ public: //bool isHealthy() const { return session_->isHealthy(); } - void executeBlocking(const std::wstring& functionName, const std::function<int(const SshSession::Details& sd)>& sftpCommand /*noexcept!*/) //throw SysError, FatalSshError + void executeBlocking(const char* functionName, const std::function<int(const SshSession::Details& sd)>& sftpCommand /*noexcept!*/) //throw SysError, FatalSshError { assert(threadId_ == getThreadId()); assert(session_->getSftpChannelCount() > 0); @@ -769,13 +768,13 @@ public: SshSessionExclusive(std::unique_ptr<SshSession, ReUseOnDelete>&& idleSession, int timeoutSec) : session_(std::move(idleSession)) /*bound!*/, timeoutSec_(timeoutSec) { /*assert(session_->isHealthy());*/ } - bool tryNonBlocking(size_t channelNo, std::chrono::steady_clock::time_point commandStartTime, const std::wstring& functionName, //throw SysError, FatalSshError + bool tryNonBlocking(size_t channelNo, std::chrono::steady_clock::time_point commandStartTime, const char* functionName, //throw SysError, FatalSshError const std::function<int(const SshSession::Details& sd)>& sftpCommand /*noexcept!*/) { return session_->tryNonBlocking(channelNo, commandStartTime, functionName, sftpCommand, timeoutSec_); //throw SysError, FatalSshError } - void finishBlocking(size_t channelNo, std::chrono::steady_clock::time_point commandStartTime, const std::wstring& functionName, + void finishBlocking(size_t channelNo, std::chrono::steady_clock::time_point commandStartTime, const char* functionName, const std::function<int(const SshSession::Details& sd)>& sftpCommand /*noexcept!*/) { for (;;) @@ -998,7 +997,7 @@ std::shared_ptr<SftpSessionManager::SshSessionShared> getSharedSftpSession(const if (const std::shared_ptr<SftpSessionManager> mgr = globalSftpSessionManager.get()) return mgr->getSharedSession(login); //throw SysError - throw SysError(L"getSharedSftpSession() function call not allowed during init/shutdown."); + throw SysError(formatSystemError("getSharedSftpSession", L"", L"Function call not allowed during init/shutdown.")); } @@ -1007,11 +1006,11 @@ std::unique_ptr<SftpSessionManager::SshSessionExclusive> getExclusiveSftpSession if (const std::shared_ptr<SftpSessionManager> mgr = globalSftpSessionManager.get()) return mgr->getExclusiveSession(login); //throw SysError - throw SysError(L"getExclusiveSftpSession() function call not allowed during init/shutdown."); + throw SysError(formatSystemError("getExclusiveSftpSession", L"", L"Function call not allowed during init/shutdown.")); } -void runSftpCommand(const SftpLoginInfo& login, const std::wstring& functionName, +void runSftpCommand(const SftpLoginInfo& login, const char* functionName, const std::function<int(const SshSession::Details& sd)>& sftpCommand /*noexcept!*/) //throw SysError { std::shared_ptr<SftpSessionManager::SshSessionShared> asyncSession = getSharedSftpSession(login); //throw SysError @@ -1041,7 +1040,7 @@ std::vector<SftpItem> getDirContentFlat(const SftpLoginInfo& login, const AfsPat LIBSSH2_SFTP_HANDLE* dirHandle = nullptr; try { - runSftpCommand(login, L"libssh2_sftp_opendir", //throw SysError + runSftpCommand(login, "libssh2_sftp_opendir", //throw SysError [&](const SshSession::Details& sd) //noexcept! { dirHandle = ::libssh2_sftp_opendir(sd.sftpChannel, getLibssh2Path(dirPath)); @@ -1054,7 +1053,7 @@ std::vector<SftpItem> getDirContentFlat(const SftpLoginInfo& login, const AfsPat ZEN_ON_SCOPE_EXIT(try { - runSftpCommand(login, L"libssh2_sftp_closedir", //throw SysError + runSftpCommand(login, "libssh2_sftp_closedir", //throw SysError [&](const SshSession::Details& sd) { return ::libssh2_sftp_closedir(dirHandle); }); //noexcept! } catch (SysError&) {}); @@ -1067,7 +1066,7 @@ std::vector<SftpItem> getDirContentFlat(const SftpLoginInfo& login, const AfsPat int rc = 0; try { - runSftpCommand(login, L"libssh2_sftp_readdir", //throw SysError + runSftpCommand(login, "libssh2_sftp_readdir", //throw SysError [&](const SshSession::Details& sd) { return rc = ::libssh2_sftp_readdir(dirHandle, &buffer[0], buffer.size(), &attribs); }); //noexcept! } catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(getSftpDisplayPath(login.server, dirPath))), e.toString()); } @@ -1111,7 +1110,7 @@ SftpItemDetails getSymlinkTargetDetails(const SftpLoginInfo& login, const AfsPat LIBSSH2_SFTP_ATTRIBUTES attribsTrg = {}; try { - runSftpCommand(login, L"libssh2_sftp_stat", //throw SysError + runSftpCommand(login, "libssh2_sftp_stat", //throw SysError [&](const SshSession::Details& sd) { return ::libssh2_sftp_stat(sd.sftpChannel, getLibssh2Path(linkPath), &attribsTrg); }); //noexcept! } catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(getSftpDisplayPath(login.server, linkPath))), e.toString()); } @@ -1227,7 +1226,7 @@ struct InputStreamSftp : public AbstractFileSystem::InputStream { session_ = getSharedSftpSession(login); //throw SysError - session_->executeBlocking(L"libssh2_sftp_open", //throw SysError, FatalSshError + session_->executeBlocking("libssh2_sftp_open", //throw SysError, FatalSshError [&](const SshSession::Details& sd) //noexcept! { fileHandle_ = ::libssh2_sftp_open(sd.sftpChannel, getLibssh2Path(filePath), LIBSSH2_FXF_READ, 0); @@ -1244,7 +1243,7 @@ struct InputStreamSftp : public AbstractFileSystem::InputStream { try { - session_->executeBlocking(L"libssh2_sftp_close", //throw SysError, FatalSshError + session_->executeBlocking("libssh2_sftp_close", //throw SysError, FatalSshError [&](const SshSession::Details& sd) { return ::libssh2_sftp_close(fileHandle_); }); //noexcept! } catch (const SysError&) {} @@ -1300,7 +1299,7 @@ private: ssize_t bytesRead = 0; try { - session_->executeBlocking(L"libssh2_sftp_read", //throw SysError, FatalSshError + session_->executeBlocking("libssh2_sftp_read", //throw SysError, FatalSshError [&](const SshSession::Details& sd) //noexcept! { bytesRead = ::libssh2_sftp_read(fileHandle_, static_cast<char*>(buffer), bytesToRead); @@ -1308,7 +1307,7 @@ private: }); if (static_cast<size_t>(bytesRead) > bytesToRead) //better safe than sorry - throw SysError(L"libssh2_sftp_read: buffer overflow."); //user should never see this + throw SysError(formatSystemError("libssh2_sftp_read", L"", L"Buffer overflow.")); //user should never see this } catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(displayPath_)), e.toString()); } catch (const FatalSshError& e) { throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(displayPath_)), e.toString()); } //SSH session corrupted! => caller (will/should) stop using session @@ -1344,7 +1343,7 @@ struct OutputStreamSftp : public AbstractFileSystem::OutputStreamImpl { session_ = getSharedSftpSession(login); //throw SysError - session_->executeBlocking(L"libssh2_sftp_open", //throw SysError, FatalSshError + session_->executeBlocking("libssh2_sftp_open", //throw SysError, FatalSshError [&](const SshSession::Details& sd) //noexcept! { fileHandle_ = ::libssh2_sftp_open(sd.sftpChannel, getLibssh2Path(filePath), @@ -1445,7 +1444,7 @@ private: throw SysError(L"Contract error: close() called more than once."); ZEN_ON_SCOPE_EXIT(fileHandle_ = nullptr); - session_->executeBlocking(L"libssh2_sftp_close", //throw SysError, FatalSshError + session_->executeBlocking("libssh2_sftp_close", //throw SysError, FatalSshError [&](const SshSession::Details& sd) { return ::libssh2_sftp_close(fileHandle_); }); //noexcept! } catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(displayPath_)), e.toString()); } @@ -1461,7 +1460,7 @@ private: ssize_t bytesWritten = 0; try { - session_->executeBlocking(L"libssh2_sftp_write", //throw SysError, FatalSshError + session_->executeBlocking("libssh2_sftp_write", //throw SysError, FatalSshError [&](const SshSession::Details& sd) //noexcept! { bytesWritten = ::libssh2_sftp_write(fileHandle_, static_cast<const char*>(buffer), bytesToWrite); @@ -1469,7 +1468,7 @@ private: }); if (bytesWritten > static_cast<ssize_t>(bytesToWrite)) //better safe than sorry - throw SysError(L"libssh2_sftp_write: buffer overflow."); + throw SysError(formatSystemError("libssh2_sftp_write", L"", L"Buffer overflow.")); } catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(displayPath_)), e.toString()); } catch (const FatalSshError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(displayPath_)), e.toString()); } //SSH session corrupted! => caller (will/should) stop using session @@ -1490,7 +1489,7 @@ private: try { - session_->executeBlocking(L"libssh2_sftp_setstat", //throw SysError, FatalSshError + session_->executeBlocking("libssh2_sftp_setstat", //throw SysError, FatalSshError [&](const SshSession::Details& sd) { return ::libssh2_sftp_setstat(sd.sftpChannel, getLibssh2Path(filePath_), &attribNew); }); //noexcept! } catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write modification time of %x."), L"%x", fmtPath(displayPath_)), e.toString()); } @@ -1517,6 +1516,8 @@ class SftpFileSystem : public AbstractFileSystem public: SftpFileSystem(const SftpLoginInfo& login) : login_(login) {} + const SftpLoginInfo& getLogin() const { return login_; } + AfsPath getHomePath() const //throw FileError { try @@ -1541,7 +1542,7 @@ private: const SftpLoginInfo& rhs = static_cast<const SftpFileSystem&>(afsRhs).login_; //exactly the type of case insensitive comparison we need for server names! - const int rv = compareAsciiNoCase(lhs.server, rhs.server); //https://msdn.microsoft.com/en-us/library/windows/desktop/ms738519#IDNs + const int rv = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs if (rv != 0) return rv; @@ -1558,11 +1559,11 @@ private: try { LIBSSH2_SFTP_ATTRIBUTES attr = {}; - runSftpCommand(login_, L"libssh2_sftp_lstat", //throw SysError + runSftpCommand(login_, "libssh2_sftp_lstat", //throw SysError [&](const SshSession::Details& sd) { return ::libssh2_sftp_lstat(sd.sftpChannel, getLibssh2Path(afsPath), &attr); }); //noexcept! if ((attr.flags & LIBSSH2_SFTP_ATTR_PERMISSIONS) == 0) - throw SysError(L"File attributes not available."); + throw SysError(formatSystemError("libssh2_sftp_lstat", L"", L"File attributes not available.")); if (LIBSSH2_SFTP_S_ISLNK(attr.permissions)) return ItemType::SYMLINK; @@ -1590,7 +1591,7 @@ private: try { //fails if folder is already existing: - runSftpCommand(login_, L"libssh2_sftp_mkdir", //throw SysError + runSftpCommand(login_, "libssh2_sftp_mkdir", //throw SysError [&](const SshSession::Details& sd) //noexcept! { return ::libssh2_sftp_mkdir(sd.sftpChannel, getLibssh2Path(afsPath), SFTP_DEFAULT_PERMISSION_FOLDER); @@ -1607,7 +1608,7 @@ private: { try { - runSftpCommand(login_, L"libssh2_sftp_unlink", //throw SysError + runSftpCommand(login_, "libssh2_sftp_unlink", //throw SysError [&](const SshSession::Details& sd) { return ::libssh2_sftp_unlink(sd.sftpChannel, getLibssh2Path(afsPath)); }); //noexcept! } catch (const SysError& e) @@ -1626,7 +1627,7 @@ private: int delResult = LIBSSH2_ERROR_NONE; try { - runSftpCommand(login_, L"libssh2_sftp_rmdir", //throw SysError + runSftpCommand(login_, "libssh2_sftp_rmdir", //throw SysError [&](const SshSession::Details& sd) { return delResult = ::libssh2_sftp_rmdir(sd.sftpChannel, getLibssh2Path(afsPath)); }); //noexcept! } catch (const SysError& e) @@ -1659,14 +1660,14 @@ private: const size_t bufSize = 10000; std::vector<char> buf(bufSize + 1); //ensure buffer is always null-terminated since we don't evaluate the byte count returned by libssh2_sftp_realpath()! - runSftpCommand(login_, L"libssh2_sftp_realpath", //throw SysError + runSftpCommand(login_, "libssh2_sftp_realpath", //throw SysError [&](const SshSession::Details& sd) { return ::libssh2_sftp_realpath(sd.sftpChannel, sftpPath, &buf[0], bufSize); }); //noexcept! const std::string sftpPathTrg = &buf[0]; if (!startsWith(sftpPathTrg, '/')) throw SysError(replaceCpy<std::wstring>(L"Invalid path %x.", L"%x", fmtPath(utfTo<std::wstring>(sftpPathTrg)))); - return sanitizeRootRelativePath(utfTo<Zstring>(sftpPathTrg)); //code-reuse! but the sanitize part isn't really needed here... + return sanitizeDeviceRelativePath(utfTo<Zstring>(sftpPathTrg)); //code-reuse! but the sanitize part isn't really needed here... } AbstractPath getSymlinkResolvedPath(const AfsPath& afsPath) const override //throw FileError @@ -1685,7 +1686,7 @@ private: std::string buf(bufSize + 1, '\0'); //ensure buffer is always null-terminated since we don't evaluate the byte count returned by libssh2_sftp_readlink()! try { - runSftpCommand(login_, L"libssh2_sftp_readlink", //throw SysError + runSftpCommand(login_, "libssh2_sftp_readlink", //throw SysError [&](const SshSession::Details& sd) { return ::libssh2_sftp_readlink(sd.sftpChannel, getLibssh2Path(afsPath), &buf[0], bufSize); }); //noexcept! } catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(getDisplayPath(afsPath))), e.toString()); } @@ -1761,7 +1762,7 @@ private: try { - runSftpCommand(login_, L"libssh2_sftp_rename", //throw SysError + runSftpCommand(login_, "libssh2_sftp_rename", //throw SysError [&](const SshSession::Details& sd) //noexcept! { /* @@ -1816,7 +1817,7 @@ private: LIBSSH2_SFTP_STATVFS fsStats = {}; try { - runSftpCommand(login_, L"libssh2_sftp_statvfs", //throw SysError + runSftpCommand(login_, "libssh2_sftp_statvfs", //throw SysError [&](const SshSession::Details& sd) { return ::libssh2_sftp_statvfs(sd.sftpChannel, sftpPath.c_str(), sftpPath.size(), &fsStats); }); //noexcept! } catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot determine free disk space for %x."), L"%x", fmtPath(getDisplayPath(L"/"))), e.toString()); } @@ -1845,7 +1846,7 @@ private: //=========================================================================================================================== -//expects "clean" login data, see condenseToSftpFolderPathPhrase() +//expects "clean" login data Zstring concatenateSftpFolderPathPhrase(const SftpLoginInfo& login, const AfsPath& afsPath) //noexcept { Zstring port; @@ -1904,11 +1905,10 @@ AfsPath fff::getSftpHomePath(const SftpLoginInfo& login) //throw FileError } -Zstring fff::condenseToSftpFolderPathPhrase(const SftpLoginInfo& login, const Zstring& relPath) //noexcept +AfsDevice fff::condenseToSftpDevice(const SftpLoginInfo& login) //noexcept { + //clean up input: SftpLoginInfo loginTmp = login; - - //clean-up input: trim(loginTmp.server); trim(loginTmp.username); trim(loginTmp.privateKeyFilePath); @@ -1922,9 +1922,19 @@ Zstring fff::condenseToSftpFolderPathPhrase(const SftpLoginInfo& login, const Zs startsWithAsciiNoCase(loginTmp.server, "ftps:" ) || startsWithAsciiNoCase(loginTmp.server, "sftp:" )) loginTmp.server = afterFirst(loginTmp.server, Zstr(':'), IF_MISSING_RETURN_NONE); - trim(loginTmp.server, true, false, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); }); + trim(loginTmp.server, true, true, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); }); + + return makeSharedRef<SftpFileSystem>(loginTmp); +} + + +SftpLoginInfo fff::extractSftpLogin(const AfsDevice& afsDevice) //noexcept +{ + if (const auto sftpDevice = dynamic_cast<const SftpFileSystem*>(&afsDevice.ref())) + return sftpDevice->getLogin(); - return concatenateSftpFolderPathPhrase(loginTmp, sanitizeRootRelativePath(relPath)); + assert(false); + return {}; } @@ -1960,13 +1970,21 @@ int fff::getServerMaxChannelsPerConnection(const SftpLoginInfo& login) //throw F } +bool fff::acceptsItemPathPhraseSftp(const Zstring& itemPathPhrase) //noexcept +{ + Zstring path = expandMacros(itemPathPhrase); //expand before trimming! + trim(path); + return startsWithAsciiNoCase(path, sftpPrefix); //check for explicit SFTP path +} + + //syntax: sftp://[<user>[:<password>]@]<server>[:port]/<relative-path>[|option_name=value] // // e.g. sftp://user001:secretpassword@private.example.com:222/mydirectory/ // sftp://user001@private.example.com/mydirectory|con=2|cpc=10|keyfile=%AppData%\id_rsa|pass64=c2VjcmV0cGFzc3dvcmQ -SftpPathInfo fff::getResolvedSftpPath(const Zstring& folderPathPhrase) //noexcept +AbstractPath fff::createItemPathSftp(const Zstring& itemPathPhrase) //noexcept { - Zstring pathPhrase = expandMacros(folderPathPhrase); //expand before trimming! + Zstring pathPhrase = expandMacros(itemPathPhrase); //expand before trimming! trim(pathPhrase); if (startsWithAsciiNoCase(pathPhrase, sftpPrefix)) @@ -1985,7 +2003,7 @@ SftpPathInfo fff::getResolvedSftpPath(const Zstring& folderPathPhrase) //noexcep auto it = std::find_if(fullPath.begin(), fullPath.end(), [](Zchar c) { return c == '/' || c == '\\'; }); const Zstring serverPort(fullPath.begin(), it); - const AfsPath serverRelPath = sanitizeRootRelativePath({ it, fullPath.end() }); + const AfsPath serverRelPath = sanitizeDeviceRelativePath({ it, fullPath.end() }); login.server = beforeLast(serverPort, Zstr(':'), IF_MISSING_RETURN_ALL); const Zstring port = afterLast(serverPort, Zstr(':'), IF_MISSING_RETURN_NONE); @@ -2010,20 +2028,6 @@ SftpPathInfo fff::getResolvedSftpPath(const Zstring& folderPathPhrase) //noexcep else assert(false); } //fix "-Wdangling-else" - return { login, serverRelPath }; -} - -bool fff::acceptsItemPathPhraseSftp(const Zstring& itemPathPhrase) //noexcept -{ - Zstring path = expandMacros(itemPathPhrase); //expand before trimming! - trim(path); - return startsWithAsciiNoCase(path, sftpPrefix); //check for explicit SFTP path -} - - -AbstractPath fff::createItemPathSftp(const Zstring& itemPathPhrase) //noexcept -{ - const SftpPathInfo& pi = getResolvedSftpPath(itemPathPhrase); //noexcept - return AbstractPath(makeSharedRef<SftpFileSystem>(pi.login), pi.afsPath); + return AbstractPath(makeSharedRef<SftpFileSystem>(login), serverRelPath); } diff --git a/FreeFileSync/Source/afs/sftp.h b/FreeFileSync/Source/afs/sftp.h index bdfcda6f..a400a57f 100644 --- a/FreeFileSync/Source/afs/sftp.h +++ b/FreeFileSync/Source/afs/sftp.h @@ -15,17 +15,17 @@ namespace fff bool acceptsItemPathPhraseSftp(const Zstring& itemPathPhrase); //noexcept AbstractPath createItemPathSftp(const Zstring& itemPathPhrase); //noexcept +void sftpInit(); +void sftpTeardown(); + //------------------------------------------------------- + enum class SftpAuthType { password, keyFile, agent, }; -//------------------------------------------------------- - -void sftpInit(); -void sftpTeardown(); struct SftpLoginInfo { @@ -41,17 +41,8 @@ struct SftpLoginInfo int timeoutSec = 15; //valid range: [1, inf) int traverserChannelsPerConnection = 1; //valid range: [1, inf) }; - - -struct SftpPathInfo -{ - SftpLoginInfo login; - AfsPath afsPath; //server-relative path -}; -SftpPathInfo getResolvedSftpPath(const Zstring& folderPathPhrase); //noexcept - -//expects (potentially messy) user input: -Zstring condenseToSftpFolderPathPhrase(const SftpLoginInfo& login, const Zstring& relPath); //noexcept +AfsDevice condenseToSftpDevice(const SftpLoginInfo& login); //noexcept; potentially messy user input +SftpLoginInfo extractSftpLogin(const AfsDevice& afsDevice); //noexcept int getServerMaxChannelsPerConnection(const SftpLoginInfo& login); //throw FileError diff --git a/FreeFileSync/Source/application.cpp b/FreeFileSync/Source/application.cpp index bc78b74e..bb865e27 100644 --- a/FreeFileSync/Source/application.cpp +++ b/FreeFileSync/Source/application.cpp @@ -9,6 +9,7 @@ #include <zen/file_access.h> #include <zen/perf.h> #include <zen/shutdown.h> +#include <zen/shell_execute.h> #include <wx/tooltip.h> #include <wx/log.h> #include <wx+/app_main.h> @@ -66,7 +67,7 @@ bool Application::OnInit() //=> 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(L"setenv(GIO_USE_VFS)", errno)) << "\n"; + // std::cerr << utfTo<std::string>(formatSystemError("setenv(GIO_USE_VFS)", errno)) << "\n"; // //=> work around 2: g_vfs_get_default(); //returns unowned GVfs* @@ -85,8 +86,8 @@ bool Application::OnInit() (getResourceDirPf() + "Gtk3Styles.css").c_str(), //const gchar* path, &error); //GError** error if (error) - throw SysError(formatSystemError(L"gtk_css_provider_load_from_data", replaceCpy(_("Error Code %x"), L"%x", - numberTo<std::wstring>(error->code)), + throw SysError(formatSystemError("gtk_css_provider_load_from_data", + replaceCpy(_("Error code %x"), L"%x", numberTo<std::wstring>(error->code)), utfTo<std::wstring>(error->message))); ::gtk_style_context_add_provider_for_screen(::gdk_screen_get_default(), //GdkScreen* screen, @@ -101,7 +102,7 @@ bool Application::OnInit() //Windows User Experience Interaction Guidelines: tool tips should have 5s timeout, info tips no timeout => compromise: wxToolTip::Enable(true); //yawn, a wxWidgets screw-up: wxToolTip::SetAutoPop is no-op if global tooltip window is not yet constructed: wxToolTip::Enable creates it - wxToolTip::SetAutoPop(10000); //https://msdn.microsoft.com/en-us/library/windows/desktop/aa511495 + wxToolTip::SetAutoPop(10000); //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! @@ -143,9 +144,7 @@ void Application::onEnterEventLoop(wxEvent& event) { Disconnect(EVENT_ENTER_EVENT_LOOP, wxEventHandler(Application::onEnterEventLoop), nullptr, this); - //determine FFS mode of operation - std::vector<Zstring> commandArgs = getCommandlineArgs(*this); - launch(commandArgs); + launch(getCommandlineArgs(*this)); //determine FFS mode of operation } @@ -163,11 +162,11 @@ int Application::OnRun() const auto titleFmt = copyStringTo<std::wstring>(wxTheApp->GetAppDisplayName()) + SPACED_DASH + _("An exception occurred"); std::cerr << utfTo<std::string>(titleFmt + SPACED_DASH) << e.what() << '\n'; - return FFS_RC_EXCEPTION; + return FFS_EXIT_EXCEPTION; } //catch (...) -> let it crash and create mini dump!!! - return returnCode_; + return exitCode_; } @@ -177,13 +176,13 @@ void Application::onQueryEndSession(wxEvent& event) mainWin->onQueryEndSession(); //it's futile to try and clean up while the process is in full swing (CRASH!) => just terminate! //also: avoid wxCloseEvent::Veto() cancelling shutdown when some dialogs receive a close event from the system - terminateProcess(FFS_RC_ABORTED); + terminateProcess(FFS_EXIT_ABORTED); } void runGuiMode (const Zstring& globalConfigFile); void runGuiMode (const Zstring& globalConfigFile, const XmlGuiConfig& guiCfg, const std::vector<Zstring>& cfgFilePaths, bool startComparison); -void runBatchMode(const Zstring& globalConfigFile, const XmlBatchConfig& batchCfg, const Zstring& cfgFilePath, FfsReturnCode& returnCode); +void runBatchMode(const Zstring& globalConfigFile, const XmlBatchConfig& batchCfg, const Zstring& cfgFilePath, FfsExitCode& exitCode); void showSyntaxHelp(); @@ -203,7 +202,7 @@ void Application::launch(const std::vector<Zstring>& commandArgs) //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 - raiseReturnCode(returnCode_, FFS_RC_ABORTED); + raiseExitCode(exitCode_, FFS_EXIT_ABORTED); }; //parse command line arguments @@ -402,7 +401,7 @@ void Application::launch(const std::vector<Zstring>& commandArgs) } if (!replaceDirectories(batchCfg.mainCfg)) return; - runBatchMode(globalConfigFilePath, batchCfg, filepath, returnCode_); + runBatchMode(globalConfigFilePath, batchCfg, filepath, exitCode_); } //GUI mode: single config (ffs_gui *or* ffs_batch) else @@ -495,18 +494,18 @@ void showSyntaxHelp() } -void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& batchCfg, const Zstring& cfgFilePath, FfsReturnCode& returnCode) +void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& batchCfg, const Zstring& cfgFilePath, FfsExitCode& exitCode) { const bool showPopupAllowed = !batchCfg.mainCfg.ignoreErrors && batchCfg.batchExCfg.batchErrorHandling == BatchErrorHandling::showPopup; - auto notifyError = [&](const std::wstring& msg, FfsReturnCode rc) + auto notifyError = [&](const std::wstring& msg, FfsExitCode rc) { if (showPopupAllowed) showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(msg)); else //"exit" or "ignore" logFatalError(utfTo<std::string>(msg)); - raiseReturnCode(returnCode, rc); + raiseExitCode(exitCode, rc); }; XmlGlobalSettings globalCfg; @@ -525,7 +524,7 @@ void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& bat } catch (const FileError& e) { - return notifyError(e.toString(), FFS_RC_ABORTED); //abort sync! + return notifyError(e.toString(), FFS_EXIT_ABORTED); //abort sync! } } @@ -535,7 +534,7 @@ void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& bat } catch (const FileError& e) { - notifyError(e.toString(), FFS_RC_WARNING); + notifyError(e.toString(), FFS_EXIT_WARNING); //continue! } @@ -544,7 +543,7 @@ void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& bat //regular check for program 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/kb/238425 + //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 std::set<AbstractPath> logFilePathsToKeep; @@ -555,15 +554,15 @@ void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& bat //class handling status updates and error messages BatchStatusHandler statusHandler(!batchCfg.batchExCfg.runMinimized, - batchCfg.batchExCfg.autoCloseSummary, extractJobName(cfgFilePath), - globalCfg.soundFileSyncFinished, syncStartTime, batchCfg.mainCfg.ignoreErrors, - batchCfg.batchExCfg.batchErrorHandling, batchCfg.mainCfg.automaticRetryCount, batchCfg.mainCfg.automaticRetryDelay, - batchCfg.batchExCfg.postSyncAction); + globalCfg.soundFileSyncFinished, + batchCfg.batchExCfg.autoCloseSummary, + batchCfg.batchExCfg.postSyncAction, + batchCfg.batchExCfg.batchErrorHandling); try { //inform about (important) non-default global settings @@ -600,20 +599,34 @@ void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& bat batchCfg.mainCfg.altLogFolderPathPhrase, globalCfg.logfilesMaxAgeDays, globalCfg.logFormat, logFilePathsToKeep, batchCfg.mainCfg.emailNotifyAddress, batchCfg.mainCfg.emailNotifyCondition); //noexcept //---------------------------------------------------------------------- + switch (r.syncResult) + { + //*INDENT-OFF* + case SyncResult::finishedSuccess: raiseExitCode(exitCode, FFS_EXIT_SUCCESS); break; + case SyncResult::finishedWarning: raiseExitCode(exitCode, FFS_EXIT_WARNING); break; + case SyncResult::finishedError: raiseExitCode(exitCode, FFS_EXIT_ERROR ); break; + case SyncResult::aborted: raiseExitCode(exitCode, FFS_EXIT_ABORTED); break; + //*INDENT-ON* + } + + //email sending, or saving log file failed? at the very least this should affect the exit code: + if (r.logStats.fatal > 0 || r.logStats.error > 0) + raiseExitCode(exitCode, FFS_EXIT_ERROR); + else if (r.logStats.warning > 0) + raiseExitCode(exitCode, FFS_EXIT_WARNING); - raiseReturnCode(returnCode, mapToReturnCode(r.resultStatus)); //update last sync stats for the selected cfg file for (ConfigFileItem& cfi : globalCfg.gui.mainDlg.cfgFileHistory) if (equalNativePath(cfi.cfgFilePath, cfgFilePath)) { - if (r.resultStatus != SyncResult::aborted) + if (r.syncResult != SyncResult::aborted) cfi.lastSyncTime = std::chrono::system_clock::to_time_t(syncStartTime); assert(!AFS::isNullPath(r.logFilePath)); if (!AFS::isNullPath(r.logFilePath)) { cfi.logFilePath = r.logFilePath; - cfi.logResult = r.resultStatus; + cfi.logResult = r.syncResult; } break; } @@ -625,7 +638,7 @@ void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& bat } catch (const FileError& e) { - notifyError(e.toString(), FFS_RC_WARNING); + notifyError(e.toString(), FFS_EXIT_WARNING); } using FinalRequest = BatchStatusHandler::FinalRequest; @@ -640,9 +653,9 @@ void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& bat try { shutdownSystem(); //throw FileError - terminateProcess(0 /*exitCode*/); //no point in continuing and saving cfg again in onQueryEndSession() while the OS will kill us anytime! + terminateProcess(exitCode); //no point in continuing and saving cfg again in onQueryEndSession() while the OS will kill us anytime! } - catch (const FileError& e) { notifyError(e.toString(), FFS_RC_WARNING); } + catch (const FileError& e) { notifyError(e.toString(), FFS_EXIT_ERROR); } break; } } diff --git a/FreeFileSync/Source/application.h b/FreeFileSync/Source/application.h index a52e6617..0b8c4d79 100644 --- a/FreeFileSync/Source/application.h +++ b/FreeFileSync/Source/application.h @@ -28,7 +28,7 @@ private: void onQueryEndSession(wxEvent& event); void launch(const std::vector<Zstring>& commandArgs); - FfsReturnCode returnCode_ = FFS_RC_SUCCESS; + FfsExitCode exitCode_ = FFS_EXIT_SUCCESS; }; } diff --git a/FreeFileSync/Source/base/dir_lock.cpp b/FreeFileSync/Source/base/dir_lock.cpp index 5ba444c7..1d087955 100644 --- a/FreeFileSync/Source/base/dir_lock.cpp +++ b/FreeFileSync/Source/base/dir_lock.cpp @@ -117,7 +117,7 @@ std::optional<SessionId> getSessionId(ProcessId processId) //throw FileError const pid_t procSid = ::getsid(processId); //NOT to be confused with "login session", e.g. not stable on OS X!!! if (procSid < 0) //pids are never negative, empiric proof: https://linux.die.net/man/2/wait - THROW_LAST_FILE_ERROR(_("Cannot get process information."), L"getsid"); + THROW_LAST_FILE_ERROR(_("Cannot get process information."), "getsid"); return procSid; } @@ -148,11 +148,11 @@ LockInformation getLockInfoFromCurrentProcess() //throw FileError //wxGetFullHostName() is a performance killer and can hang for some users, so don't touch! std::vector<char> buffer(10000); if (::gethostname(&buffer[0], buffer.size()) != 0) - THROW_LAST_FILE_ERROR(_("Cannot get process information."), L"gethostname"); + THROW_LAST_FILE_ERROR(_("Cannot get process information."), "gethostname"); lockInfo.computerName = osName + ' ' + &buffer[0] + '.'; if (::getdomainname(&buffer[0], buffer.size()) != 0) - THROW_LAST_FILE_ERROR(_("Cannot get process information."), L"getdomainname"); + THROW_LAST_FILE_ERROR(_("Cannot get process information."), "getdomainname"); lockInfo.computerName += &buffer[0]; lockInfo.processId = ::getpid(); //never fails @@ -284,14 +284,14 @@ uint64_t getLockFileSize(const Zstring& filePath) //throw FileError, ErrorFileNo return fileInfo.st_size; if (errno == ENOENT) - throw ErrorFileNotExisting(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(filePath)), formatSystemError(L"stat", errno)); - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(filePath)), L"stat"); + throw ErrorFileNotExisting(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(filePath)), formatSystemError("stat", errno)); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(filePath)), "stat"); } void waitOnDirLock(const Zstring& lockFilePath, const DirLockCallback& notifyStatus /*throw X*/, std::chrono::milliseconds cbInterval) //throw FileError { - std::wstring infoMsg = _("Waiting while directory is locked:") + L' ' + fmtPath(lockFilePath); + std::wstring infoMsg = _("Waiting while directory is in use:") + L' ' + fmtPath(lockFilePath); if (notifyStatus) notifyStatus(infoMsg); //throw X @@ -399,7 +399,7 @@ bool tryLock(const Zstring& lockFilePath) //throw FileError const mode_t oldMask = ::umask(0); //important: we want the lock file to have exactly the permissions specified ZEN_ON_SCOPE_EXIT(::umask(oldMask)); - //O_EXCL contains a race condition on NFS file systems: http://linux.die.net/man/2/open + //O_EXCL contains a race condition on NFS file systems: https://linux.die.net/man/2/open const int hFile = ::open(lockFilePath.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); //0666 if (hFile == -1) @@ -407,7 +407,7 @@ bool tryLock(const Zstring& lockFilePath) //throw FileError if (errno == EEXIST) return false; - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(lockFilePath)), L"open"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(lockFilePath)), "open"); } ZEN_ON_SCOPE_FAIL(try { removeFilePlain(lockFilePath); } catch (FileError&) {}); diff --git a/FreeFileSync/Source/base/resolve_path.cpp b/FreeFileSync/Source/base/resolve_path.cpp index f2737069..6c7105b2 100644 --- a/FreeFileSync/Source/base/resolve_path.cpp +++ b/FreeFileSync/Source/base/resolve_path.cpp @@ -47,38 +47,52 @@ std::optional<Zstring> getEnvironmentVar(const Zstring& name) Zstring resolveRelativePath(const Zstring& relativePath) { assert(runningMainThread()); //GetFullPathName() is documented to NOT be thread-safe! + /* MSDN: "Multithreaded applications and shared library code should not use the GetFullPathName function + and should avoid using relative path names. + The current directory state written by the SetCurrentDirectory function is stored as a global variable in each process, */ - //http://linux.die.net/man/2/path_resolution - if (!startsWith(relativePath, FILE_NAME_SEPARATOR)) //absolute names are exactly those starting with a '/' + if (relativePath.empty()) + return relativePath; + + Zstring pathTmp = relativePath; + //https://linux.die.net/man/2/path_resolution + if (!startsWith(pathTmp, FILE_NAME_SEPARATOR)) //absolute names are exactly those starting with a '/' { - /* - basic support for '~': strictly speaking this is a shell-layer feature, so "realpath()" won't handle it - http://www.gnu.org/software/bash/manual/html_node/Tilde-Expansion.html - - http://linux.die.net/man/3/getpwuid: An application that wants to determine its user's home directory - should inspect the value of HOME (rather than the value getpwuid(getuid())->pw_dir) since this allows - the user to modify their notion of "the home directory" during a login session. - */ - if (startsWith(relativePath, "~/") || relativePath == "~") + /* basic support for '~': strictly speaking this is a shell-layer feature, so "realpath()" won't handle it + https://www.gnu.org/software/bash/manual/html_node/Tilde-Expansion.html + + https://linux.die.net/man/3/getpwuid: An application that wants to determine its user's home directory + should inspect the value of HOME (rather than the value getpwuid(getuid())->pw_dir) since this allows + the user to modify their notion of "the home directory" during a login session. */ + if (startsWith(pathTmp, "~/") || pathTmp == "~") { - std::optional<Zstring> homeDir = getEnvironmentVar("HOME"); - if (!homeDir) - return relativePath; //error! no further processing! - - if (startsWith(relativePath, "~/")) - return appendSeparator(*homeDir) + afterFirst(relativePath, '/', IF_MISSING_RETURN_NONE); - else //relativePath == "~" - return *homeDir; + if (const std::optional<Zstring> homeDir = getEnvironmentVar("HOME")) + { + if (startsWith(pathTmp, "~/")) + pathTmp = appendSeparator(*homeDir) + afterFirst(pathTmp, '/', IF_MISSING_RETURN_NONE); + else //pathTmp == "~" + pathTmp = *homeDir; + } + //else: error! no further processing! } - - //we cannot use ::realpath() since it resolves *existing* relative paths only! - if (char* dirPath = ::getcwd(nullptr, 0)) + else { - ZEN_ON_SCOPE_EXIT(::free(dirPath)); - return appendSeparator(dirPath) + relativePath; + //we cannot use ::realpath() since it resolves *existing* relative paths only! + if (char* dirPath = ::getcwd(nullptr, 0)) + { + ZEN_ON_SCOPE_EXIT(::free(dirPath)); + pathTmp = appendSeparator(dirPath) + pathTmp; + } } } - return relativePath; + //get rid of some cruft (just like GetFullPathName()) + replace(pathTmp, "/./", '/'); + if (endsWith(pathTmp, "/.")) + pathTmp.pop_back(); //keep the "/" => consider pathTmp == "/." + + //what about "/../"? might be relative to symlinks => preserve! + + return pathTmp; } @@ -245,18 +259,14 @@ Zstring fff::getResolvedFilePath(const Zstring& pathPhrase) //noexcept path = expandVolumeName(path); //may block for slow USB sticks and idle HDDs! - if (path.empty()) //an empty string would later be resolved as "\"; this is not desired - return Zstring(); - /* - need to resolve relative paths: - WINDOWS: - - \\?\-prefix requires absolute names - - Volume Shadow Copy: volume name needs to be part of each file path - - file icon buffer (at least for extensions that are actually read from disk, like "exe") - - Use of relative path names is not thread safe! (e.g. SHFileOperation) - WINDOWS/LINUX: - - detection of dependent directories, e.g. "\" and "C:\test" - */ + /* need to resolve relative paths: + WINDOWS: + - \\?\-prefix requires absolute names + - Volume Shadow Copy: volume name needs to be part of each file path + - file icon buffer (at least for extensions that are actually read from disk, like "exe") + - Use of relative path names is not thread safe! (e.g. SHFileOperation) + WINDOWS/LINUX: + - detection of dependent directories, e.g. "\" and "C:\test" */ path = resolveRelativePath(path); //remove trailing slash, unless volume root: diff --git a/FreeFileSync/Source/base/structures.cpp b/FreeFileSync/Source/base/structures.cpp index 1e6758ac..8452f3f2 100644 --- a/FreeFileSync/Source/base/structures.cpp +++ b/FreeFileSync/Source/base/structures.cpp @@ -17,16 +17,17 @@ using namespace zen; using namespace fff; -std::wstring fff::getVariantNameImpl(DirectionConfig::Variant var, const wchar_t* arrowLeft, const wchar_t* arrowRight, const wchar_t* angleRight) +//use in sync log files where users expect ANSI: https://freefilesync.org/forum/viewtopic.php?t=4647 +std::wstring fff::getVariantNameForLog(DirectionConfig::Variant var) { switch (var) { case DirectionConfig::TWO_WAY: - return arrowLeft + _("Two way") + arrowRight; + return _("Two way") + L" <->"; case DirectionConfig::MIRROR: - return _("Mirror") + arrowRight; + return _("Mirror") + L" ->"; case DirectionConfig::UPDATE: - return _("Update") + angleRight; + return _("Update") + L" >"; case DirectionConfig::CUSTOM: return _("Custom"); } @@ -35,13 +36,6 @@ std::wstring fff::getVariantNameImpl(DirectionConfig::Variant var, const wchar_t } -//use in sync log files where users expect ANSI: https://freefilesync.org/forum/viewtopic.php?t=4647 -std::wstring fff::getVariantNameForLog(DirectionConfig::Variant var) -{ - return getVariantNameImpl(var, L"<-", L"->", L">"); -} - - DirectionSet fff::extractDirections(const DirectionConfig& cfg) { DirectionSet output; diff --git a/FreeFileSync/Source/base/structures.h b/FreeFileSync/Source/base/structures.h index 11e98948..88d7eb54 100644 --- a/FreeFileSync/Source/base/structures.h +++ b/FreeFileSync/Source/base/structures.h @@ -148,7 +148,6 @@ bool detectMovedFilesEnabled (const DirectionConfig& cfg); DirectionSet extractDirections(const DirectionConfig& cfg); //get sync directions: DON'T call for DirectionConfig::TWO_WAY! -std::wstring getVariantNameImpl(DirectionConfig::Variant var, const wchar_t* arrowLeft, const wchar_t* arrowRight, const wchar_t* angleRight); std::wstring getVariantNameForLog(DirectionConfig::Variant var); inline diff --git a/FreeFileSync/Source/base/synchronization.cpp b/FreeFileSync/Source/base/synchronization.cpp index 38ad1771..c64915a9 100644 --- a/FreeFileSync/Source/base/synchronization.cpp +++ b/FreeFileSync/Source/base/synchronization.cpp @@ -451,11 +451,11 @@ void flushFileBuffers(const Zstring& nativeFilePath) //throw FileError { const int fileHandle = ::open(nativeFilePath.c_str(), O_WRONLY | O_APPEND | O_CLOEXEC); if (fileHandle == -1) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(nativeFilePath)), L"open"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(nativeFilePath)), "open"); ZEN_ON_SCOPE_EXIT(::close(fileHandle)); if (::fsync(fileHandle) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(nativeFilePath)), L"fsync"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(nativeFilePath)), "fsync"); } @@ -2398,7 +2398,7 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime msg += L'\n' + utfTo<std::wstring>(item.relPath) + L": " + item.msg; if (makeUnsigned(conflictCount) > conflictPreview.size()) - msg += L"\n [...] " + replaceCpy(_P("Showing %y of 1 row", "Showing %y of %x rows", conflictCount), //%x used as plural form placeholder! + msg += L"\n [...] " + replaceCpy(_P("Showing %y of 1 item", "Showing %y of %x items", conflictCount), //%x used as plural form placeholder! L"%y", formatNumber(conflictPreview.size())); } } diff --git a/FreeFileSync/Source/base_tools.cpp b/FreeFileSync/Source/base_tools.cpp index 3a7dabbd..97f6cc65 100644 --- a/FreeFileSync/Source/base_tools.cpp +++ b/FreeFileSync/Source/base_tools.cpp @@ -30,18 +30,28 @@ std::wstring fff::getVariantName(CompareVariant var) std::wstring fff::getVariantName(DirectionConfig::Variant var) { - const wchar_t* arrowLeft = L"\u25C4 "; //black triangle pointer - const wchar_t* arrowRight = L" \u25BA"; // - const wchar_t* angleRight = L" \uFF1E"; //fullwidth greater-than - //const wchar_t arrowLeft [] = L"\u2190 "; //unicode arrows -> too small - //const wchar_t arrowRight[] = L" \u2192"; // + //https://www.key-shortcut.com/en/writing-systems/35-symbols/arrows/ + //*INDENT-OFF* + const wchar_t* arrowLeft = L"\u25C4 "; //◄ + const wchar_t* arrowRight = L" \u25BA"; //► + const wchar_t* angleRight = L" \uFF1E"; //> + //alternatives: ←, → (too small) ⬅, ⮕ (right one not generally available) if (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft) { arrowLeft = L"\u25BA "; //not mirrored automatically: Windows/Linux Unicode bug!? arrowRight = L" \u25C4"; // } - return getVariantNameImpl(var, arrowLeft, arrowRight, angleRight); + switch (var) + { + case DirectionConfig::TWO_WAY: return arrowLeft + _("Two way") + arrowRight; + case DirectionConfig::MIRROR: return _("Mirror") + arrowRight; + case DirectionConfig::UPDATE: return _("Update") + angleRight; + case DirectionConfig::CUSTOM: return _("Custom"); + } + //*INDENT-ON* + assert(false); + return _("Error"); } diff --git a/FreeFileSync/Source/config.cpp b/FreeFileSync/Source/config.cpp index 120ad466..4bbb7120 100644 --- a/FreeFileSync/Source/config.cpp +++ b/FreeFileSync/Source/config.cpp @@ -21,7 +21,7 @@ using namespace fff; //functionally needed for correct overload resolution!!! namespace { //------------------------------------------------------------------------------------------------------------------------------- -const int XML_FORMAT_GLOBAL_CFG = 16; //2020-01-30 +const int XML_FORMAT_GLOBAL_CFG = 17; //2020-04-15 const int XML_FORMAT_SYNC_CFG = 15; //2020-01-30 //------------------------------------------------------------------------------------------------------------------------------- } @@ -1840,58 +1840,13 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) if (cfg.gui.commandHistoryMax <= 8) cfg.gui.commandHistoryMax = XmlGlobalSettings().gui.commandHistoryMax; - //external applications - //TODO: remove old parameter after migration! 2016-05-28 - if (inGui["ExternalApplications"]) - { - inGui["ExternalApplications"](cfg.gui.externalApps); - if (cfg.gui.externalApps.empty()) //who knows, let's repair some old failed data migrations - cfg.gui.externalApps = XmlGlobalSettings().gui.externalApps; - else - { - } - } + + //TODO: remove old parameter after migration! 2018-01-16 + if (formatVer < 7) + ; //reset this old crap else - { - //TODO: remove old parameter after migration! 2018-01-16 - if (formatVer < 7) - { - std::vector<std::pair<std::wstring, Zstring>> extApps; - if (inGui["ExternalApps"](extApps)) - { - cfg.gui.externalApps.clear(); - for (const auto& [description, cmdLine] : extApps) - cfg.gui.externalApps.push_back({ description, cmdLine }); - } - } - else - inGui["ExternalApps"](cfg.gui.externalApps); - } + inGui["ExternalApps"](cfg.gui.externalApps); - //TODO: remove macro migration after some time! 2016-06-30 - if (formatVer < 3) - for (ExternalApp& item : cfg.gui.externalApps) - { - replace(item.cmdLine, Zstr("%item2_path%"), Zstr("%item_path2%")); - replace(item.cmdLine, Zstr("%item_folder%"), Zstr("%folder_path%")); - replace(item.cmdLine, Zstr("%item2_folder%"), Zstr("%folder_path2%")); - - replace(item.cmdLine, Zstr("explorer /select, \"%item_path%\""), Zstr("explorer /select, \"%local_path%\"")); - replace(item.cmdLine, Zstr("\"%item_path%\""), Zstr("\"%local_path%\"")); - replace(item.cmdLine, Zstr("xdg-open \"%item_path%\""), Zstr("xdg-open \"%local_path%\"")); - replace(item.cmdLine, Zstr("open -R \"%item_path%\""), Zstr("open -R \"%local_path%\"")); - replace(item.cmdLine, Zstr("open \"%item_path%\""), Zstr("open \"%local_path%\"")); - - if (contains(makeUpperCopy(item.cmdLine), Zstr("WINMERGEU.EXE")) || - contains(makeUpperCopy(item.cmdLine), Zstr("PSPAD.EXE"))) - { - replace(item.cmdLine, Zstr("%item_path%"), Zstr("%local_path%")); - replace(item.cmdLine, Zstr("%item_path2%"), Zstr("%local_path2%")); - } - } - //TODO: remove macro migration after some time! 2016-07-18 - for (ExternalApp& item : cfg.gui.externalApps) - replace(item.cmdLine, Zstr("%item_folder%"), Zstr("%folder_path%")); //TODO: remove after migration! 2019-11-30 if (formatVer < 15) for (ExternalApp& item : cfg.gui.externalApps) @@ -1900,6 +1855,7 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) replace(item.cmdLine, Zstr("%folder_path2%"), Zstr("%parent_path2%")); } + //last update check inGui["LastOnlineCheck" ](cfg.gui.lastUpdateCheck); inGui["LastOnlineVersion"](cfg.gui.lastOnlineVersion); diff --git a/FreeFileSync/Source/config.h b/FreeFileSync/Source/config.h index e76d4770..72bc3990 100644 --- a/FreeFileSync/Source/config.h +++ b/FreeFileSync/Source/config.h @@ -206,8 +206,8 @@ struct XmlGlobalSettings wxString guiPerspectiveLast; //used by wxAuiManager } mainDlg; - Zstring defaultExclusionFilter = "/.Trash-*/" "\n" - "/.recycle/"; + Zstring defaultExclusionFilter = "*/.Trash-*/" "\n" + "*/.recycle/"; size_t folderHistoryMax = 20; std::vector<Zstring> versioningFolderHistory; @@ -221,10 +221,11 @@ struct XmlGlobalSettings std::vector<ExternalApp> externalApps { - //default external app descriptions will be translated "on the fly"!!! - //CONTRACT: first entry will be used for [Enter] or mouse double-click! - { L"Browse directory", Zstr("xdg-open \"%parent_path%\"") }, - { L"Open with default application", Zstr("xdg-open \"%local_path%\"") }, + /* CONTRACT: first entry: show item in file browser + + default external app descriptions will be translated "on the fly"!!! */ + { L"Browse directory", "xdg-open \"%parent_path%\"" }, + { L"Open with default application", "xdg-open \"%local_path%\"" }, //mark for extraction: _("Browse directory") Linux doesn't use the term "folder" }; diff --git a/FreeFileSync/Source/log_file.cpp b/FreeFileSync/Source/log_file.cpp index c89d4403..0b6e25c2 100644 --- a/FreeFileSync/Source/log_file.cpp +++ b/FreeFileSync/Source/log_file.cpp @@ -11,7 +11,6 @@ #include <wx/datetime.h> #include "ffs_paths.h" #include "afs/concrete.h" - using namespace zen; using namespace fff; using AFS = AbstractFileSystem; @@ -32,7 +31,7 @@ std::string generateLogHeaderTxt(const ProcessSummary& s, const ErrorLog& log, i headerLine += (headerLine.empty() ? "" : " + ") + utfTo<std::string>(jobName); if (!headerLine.empty()) - headerLine += " "; + headerLine += ' '; const TimeComp tc = getLocalTime(std::chrono::system_clock::to_time_t(s.startTime)); //returns empty string on failure headerLine += utfTo<std::string>(formatTime(formatDateTag, tc) + Zstr(" [") + formatTime(formatTimeTag, tc) + Zstr(']')); @@ -40,7 +39,7 @@ std::string generateLogHeaderTxt(const ProcessSummary& s, const ErrorLog& log, i //assemble summary box std::vector<std::string> summary; summary.emplace_back(); - summary.push_back(tabSpace + utfTo<std::string>(getSyncResultLabel(s.resultStatus))); + summary.push_back(tabSpace + utfTo<std::string>(getSyncResultLabel(s.syncResult))); summary.emplace_back(); const ErrorLog::Stats logCount = log.getStats(); @@ -90,7 +89,7 @@ std::string generateLogHeaderTxt(const ProcessSummary& s, const ErrorLog& log, i break; } if (logFailTotal > previewCount) - output += " [...] " + utfTo<std::string>(replaceCpy(_P("Showing %y of 1 row", "Showing %y of %x rows", logFailTotal), //%x used as plural form placeholder! + output += " [...] " + utfTo<std::string>(replaceCpy(_P("Showing %y of 1 item", "Showing %y of %x items", logFailTotal), //%x used as plural form placeholder! L"%y", formatNumber(previewCount))) + '\n'; output += std::string(SEPARATION_LINE_LEN, '_') + "\n\n\n"; } @@ -104,7 +103,7 @@ std::string generateLogFooterTxt(const std::wstring& logFilePath, int logItemsTo std::string output; if (logItemsTotal > logItemsPreviewMax) - output += " [...] " + utfTo<std::string>(replaceCpy(_P("Showing %y of 1 row", "Showing %y of %x rows", logItemsTotal), //%x used as plural form placeholder! + output += " [...] " + utfTo<std::string>(replaceCpy(_P("Showing %y of 1 item", "Showing %y of %x items", logItemsTotal), //%x used as plural form placeholder! L"%y", formatNumber(logItemsPreviewMax))) + '\n'; return output += '\n' + std::string(SEPARATION_LINE_LEN, '_') + '\n' + @@ -114,7 +113,7 @@ std::string generateLogFooterTxt(const std::wstring& logFilePath, int logItemsTo (!cm.model .empty() ? L" - " + cm.model : L"") + (!cm.vendor.empty() ? L" - " + cm.vendor : L"") + L'\n' + - _("Log file") + L": " + logFilePath) + '\n'; + _("Log file:") + L' ' + logFilePath) + '\n'; } @@ -185,7 +184,7 @@ std::wstring generateLogTitle(const ProcessSummary& s) if (!jobNamesFmt.empty()) title += jobNamesFmt + L' '; - switch (s.resultStatus) + switch (s.syncResult) { case SyncResult::finishedSuccess: title += utfTo<std::wstring>("\xe2\x9c\x94" "\xef\xb8\x8f"); break; //✔️ case SyncResult::finishedWarning: title += utfTo<std::wstring>("\xe2\x9a\xa0" "\xef\xb8\x8f"); break; //⚠️ @@ -228,7 +227,7 @@ std::string generateLogHeaderHtml(const ProcessSummary& s, const ErrorLog& log, htmlTxt(formatTime(formatDateTag, tc)) + " " + htmlTxt(formatTime(formatTimeTag, tc)) + "</span></div>\n"; std::string resultsStatusImage; - switch (s.resultStatus) + switch (s.syncResult) { case SyncResult::finishedSuccess: resultsStatusImage = "result-succes.png"; break; case SyncResult::finishedWarning: resultsStatusImage = "result-warning.png"; break; @@ -239,7 +238,7 @@ std::string generateLogHeaderHtml(const ProcessSummary& s, const ErrorLog& log, <div style="margin:10px 0; display:inline-block; border-radius:7px; background:#f8f8f8; box-shadow:1px 1px 4px #888; overflow:hidden;"> <div style="background-color:white; border-bottom:1px solid #AAA; font-size:larger; padding:10px;"> <img src="https://freefilesync.org/images/log/)" + resultsStatusImage + R"(" width="32" height="32" alt="" style="vertical-align:middle;"> - <span style="font-weight:600; vertical-align:middle;">)" + htmlTxt(getSyncResultLabel(s.resultStatus)) + R"(</span> + <span style="font-weight:600; vertical-align:middle;">)" + htmlTxt(getSyncResultLabel(s.syncResult)) + R"(</span> </div> <table role="presentation" class="summary-table" style="border-spacing:0; margin-left:10px; padding:5px 10px;">)"; @@ -314,7 +313,7 @@ std::string generateLogHeaderHtml(const ProcessSummary& s, const ErrorLog& log, )"; if (logFailTotal > previewCount) output += R"( <div><span style="font-weight:600; padding:0 10px;">[…]</span>)" + - htmlTxt(replaceCpy(_P("Showing %y of 1 row", "Showing %y of %x rows", logFailTotal), //%x used as plural form placeholder! + htmlTxt(replaceCpy(_P("Showing %y of 1 item", "Showing %y of %x items", logFailTotal), //%x used as plural form placeholder! L"%y", formatNumber(previewCount))) + "</div>\n"; output += R"( <div style="border-bottom: 1px solid #AAA; margin: 5px 0;"></div><br> @@ -337,7 +336,7 @@ std::string generateLogFooterHtml(const std::wstring& logFilePath, int logItemsT if (logItemsTotal > logItemsPreviewMax) output += R"( <div><span style="font-weight:600; padding:0 10px;">[…]</span>)" + - htmlTxt(replaceCpy(_P("Showing %y of 1 row", "Showing %y of %x rows", logItemsTotal), //%x used as plural form placeholder! + htmlTxt(replaceCpy(_P("Showing %y of 1 item", "Showing %y of %x items", logItemsTotal), //%x used as plural form placeholder! L"%y", formatNumber(logItemsPreviewMax))) + "</div>\n"; return output += R"( <br> @@ -351,7 +350,7 @@ std::string generateLogFooterHtml(const std::wstring& logFilePath, int logItemsT (!cm.vendor.empty() ? " – " + htmlTxt(cm.vendor) : "") + R"(</span> </div> <div style="font-size:small;"> - <img src="https://freefilesync.org/images/log/log.png" width="24" height="24" alt=")" + htmlTxt(_("Log file")) + R"(:" style="vertical-align:middle;"> + <img src="https://freefilesync.org/images/log/log.png" width="24" height="24" alt=")" + htmlTxt(_("Log file:")) + R"(" style="vertical-align:middle;"> <span style="font-family: Consolas,'Courier New',Courier,monospace; vertical-align:middle;">)" + htmlTxt(logFilePath) + R"(</span> </div> </body> @@ -540,12 +539,28 @@ Zstring fff::getDefaultLogFolderPath() { return getConfigDirPathPf() + Zstr("Log //"Backup FreeFileSync 2013-09-15 015052.123.html" //"Backup FreeFileSync 2013-09-15 015052.123 [Error].html" +//"Backup FreeFileSync + RealTimeSync 2013-09-15 015052.123 [Error].log" AbstractPath fff::generateLogFilePath(LogFileFormat logFormat, const ProcessSummary& summary, const Zstring& altLogFolderPathPhrase /*optional*/) { //const std::string colon = "\xcb\xb8"; //="modifier letter raised colon" => regular colon is forbidden in file names on Windows and OS X //=> too many issues, most notably cmd.exe is not Unicode-aware: https://freefilesync.org/forum/viewtopic.php?t=1679 - //assemble logfile name + Zstring jobNamesFmt; + if (!summary.jobNames.empty()) + { + for (const std::wstring& jobName : summary.jobNames) + if (const Zstring jobNameZ = utfTo<Zstring>(jobName); + jobNamesFmt.size() + jobNameZ.size() > 200) + { + jobNamesFmt += Zstr("[...] + "); //avoid hitting file system name length limitations: "lpMaximumComponentLength is commonly 255 characters" + break; //https://freefilesync.org/forum/viewtopic.php?t=7113 + } + else + jobNamesFmt += jobNameZ + Zstr(" + "); + + jobNamesFmt.resize(jobNamesFmt.size() - 3); + } + const TimeComp tc = getLocalTime(std::chrono::system_clock::to_time_t(summary.startTime)); if (tc == TimeComp()) throw FileError(L"Failed to determine current time: " + numberTo<std::wstring>(summary.startTime.time_since_epoch().count())); @@ -553,21 +568,9 @@ AbstractPath fff::generateLogFilePath(LogFileFormat logFormat, const ProcessSumm const auto timeMs = std::chrono::duration_cast<std::chrono::milliseconds>(summary.startTime.time_since_epoch()).count() % 1000; assert(std::chrono::duration_cast<std::chrono::seconds>(summary.startTime.time_since_epoch()).count() == std::chrono::system_clock::to_time_t(summary.startTime)); - Zstring logFileName; - if (!summary.jobNames.empty()) - { - for (const std::wstring& jobName : summary.jobNames) - logFileName += utfTo<Zstring>(jobName) + Zstr(" + "); - logFileName.resize(logFileName.size() - 2); - } - - logFileName += formatTime(Zstr("%Y-%m-%d %H%M%S"), tc) + - Zstr(".") + printNumber<Zstring>(Zstr("%03d"), static_cast<int>(timeMs)); //[ms] should yield a fairly unique name - static_assert(TIME_STAMP_LENGTH == 21); - const std::wstring failStatus = [&] { - switch (summary.resultStatus) + switch (summary.syncResult) { case SyncResult::finishedSuccess: break; case SyncResult::finishedWarning: return _("Warning"); @@ -576,6 +579,15 @@ AbstractPath fff::generateLogFilePath(LogFileFormat logFormat, const ProcessSumm } return std::wstring(); }(); + //------------------------------------------------------------------ + + Zstring logFileName = jobNamesFmt; + if (!logFileName.empty()) + logFileName += Zstr(' '); + + logFileName += formatTime(Zstr("%Y-%m-%d %H%M%S"), tc) + + Zstr(".") + printNumber<Zstring>(Zstr("%03d"), static_cast<int>(timeMs)); //[ms] should yield a fairly unique name + static_assert(TIME_STAMP_LENGTH == 21); if (!failStatus.empty()) logFileName += STATUS_BEGIN_TOKEN + utfTo<Zstring>(failStatus) + STATUS_END_TOKEN; diff --git a/FreeFileSync/Source/return_codes.h b/FreeFileSync/Source/return_codes.h index ad9318c6..c1d30d5a 100644 --- a/FreeFileSync/Source/return_codes.h +++ b/FreeFileSync/Source/return_codes.h @@ -12,18 +12,18 @@ namespace fff { -enum FfsReturnCode //as returned after process exit +enum FfsExitCode //as returned on process exit { - FFS_RC_SUCCESS = 0, - FFS_RC_WARNING, - FFS_RC_ERROR, - FFS_RC_ABORTED, - FFS_RC_EXCEPTION, + FFS_EXIT_SUCCESS = 0, + FFS_EXIT_WARNING, + FFS_EXIT_ERROR, + FFS_EXIT_ABORTED, + FFS_EXIT_EXCEPTION, }; inline -void raiseReturnCode(FfsReturnCode& rc, FfsReturnCode rcProposed) +void raiseExitCode(FfsExitCode& rc, FfsExitCode rcProposed) { if (rc < rcProposed) rc = rcProposed; @@ -40,37 +40,16 @@ enum class SyncResult inline -FfsReturnCode mapToReturnCode(SyncResult syncStatus) +std::wstring getSyncResultLabel(SyncResult syncResult) { - switch (syncStatus) + switch (syncResult) { - case SyncResult::finishedSuccess: - return FFS_RC_SUCCESS; - case SyncResult::finishedWarning: - return FFS_RC_WARNING; - case SyncResult::finishedError: - return FFS_RC_ERROR; - case SyncResult::aborted: - return FFS_RC_ABORTED; - } - assert(false); - return FFS_RC_ABORTED; -} - - -inline -std::wstring getSyncResultLabel(SyncResult resultStatus) -{ - switch (resultStatus) - { - case SyncResult::finishedSuccess: - return _("Completed successfully"); - case SyncResult::finishedWarning: - return _("Completed with warnings"); - case SyncResult::finishedError: - return _("Completed with errors"); - case SyncResult::aborted: - return _("Stopped"); + //*INDENT-OFF* + case SyncResult::finishedSuccess: return _("Completed successfully"); + case SyncResult::finishedWarning: return _("Completed with warnings"); + case SyncResult::finishedError: return _("Completed with errors"); + case SyncResult::aborted: return _("Stopped"); + //*INDENT-ON* } assert(false); return std::wstring(); diff --git a/FreeFileSync/Source/status_handler.h b/FreeFileSync/Source/status_handler.h index 9058033b..7c22f2cd 100644 --- a/FreeFileSync/Source/status_handler.h +++ b/FreeFileSync/Source/status_handler.h @@ -8,11 +8,11 @@ #define STATUS_HANDLER_H_81704805908341534 #include <vector> -#include <chrono> -#include <thread> -#include <string> -#include <zen/i18n.h> -#include <zen/basic_math.h> +//#include <chrono> +//#include <thread> +//#include <string> +//#include <zen/i18n.h> +//#include <zen/basic_math.h> #include "base/process_callback.h" #include "return_codes.h" @@ -72,7 +72,7 @@ struct Statistics struct ProcessSummary { std::chrono::system_clock::time_point startTime; - SyncResult resultStatus = SyncResult::aborted; + SyncResult syncResult = SyncResult::aborted; std::vector<std::wstring> jobNames; //may be empty ProgressStats statsProcessed; ProgressStats statsTotal; @@ -169,24 +169,6 @@ private: std::optional<AbortTrigger> abortRequested_; }; - -//------------------------------------------------------------------------------------------ - -inline -void delayAndCountDown(const std::wstring& operationName, std::chrono::seconds delay, const std::function<void(const std::wstring& msg)>& notifyStatus) -{ - assert(notifyStatus && !zen::endsWith(operationName, L".")); - - const auto delayUntil = std::chrono::steady_clock::now() + delay; - for (auto now = std::chrono::steady_clock::now(); now < delayUntil; now = std::chrono::steady_clock::now()) - { - const auto timeMs = std::chrono::duration_cast<std::chrono::milliseconds>(delayUntil - now).count(); - if (notifyStatus) - notifyStatus(operationName + L"... " + _P("1 sec", "%x sec", numeric::integerDivideRoundUp(timeMs, 1000))); - - std::this_thread::sleep_for(UI_UPDATE_INTERVAL / 2); - } -} } #endif //STATUS_HANDLER_H_81704805908341534 diff --git a/FreeFileSync/Source/ui/batch_status_handler.cpp b/FreeFileSync/Source/ui/batch_status_handler.cpp index 40098bca..034946c4 100644 --- a/FreeFileSync/Source/ui/batch_status_handler.cpp +++ b/FreeFileSync/Source/ui/batch_status_handler.cpp @@ -9,29 +9,33 @@ #include <zen/shutdown.h> #include <wx+/popup_dlg.h> #include <wx/app.h> +#include <wx/sound.h> #include "../afs/concrete.h" #include "../base/resolve_path.h" #include "../log_file.h" +#include "status_handler_impl.h" using namespace zen; using namespace fff; BatchStatusHandler::BatchStatusHandler(bool showProgress, - bool autoCloseDialog, const std::wstring& jobName, - const Zstring& soundFileSyncComplete, const std::chrono::system_clock::time_point& startTime, bool ignoreErrors, - BatchErrorHandling batchErrorHandling, size_t automaticRetryCount, std::chrono::seconds automaticRetryDelay, - PostSyncAction postSyncAction) : - batchErrorHandling_(batchErrorHandling), + const Zstring& soundFileSyncComplete, + bool autoCloseDialog, + PostSyncAction postSyncAction, + BatchErrorHandling batchErrorHandling) : + jobName_(jobName), + startTime_(startTime), automaticRetryCount_(automaticRetryCount), automaticRetryDelay_(automaticRetryDelay), + soundFileSyncComplete_(soundFileSyncComplete), progressDlg_(SyncProgressDialog::create([this] { userRequestAbort(); }, *this, nullptr /*parentWindow*/, showProgress, autoCloseDialog, -startTime, { jobName }, soundFileSyncComplete, ignoreErrors, automaticRetryCount, [&] +{ jobName }, startTime, ignoreErrors, automaticRetryCount, [&] { switch (postSyncAction) { @@ -45,8 +49,7 @@ startTime, { jobName }, soundFileSyncComplete, ignoreErrors, automaticRetryCount assert(false); return PostSyncAction2::none; }())), -jobName_(jobName), - startTime_(startTime) +batchErrorHandling_(batchErrorHandling) { //ATTENTION: "progressDlg_" is an unmanaged resource!!! However, at this point we already consider construction complete! => //ZEN_ON_SCOPE_FAIL( cleanup(); ); //destructor call would lead to member double clean-up!!! @@ -70,7 +73,7 @@ BatchStatusHandler::Result BatchStatusHandler::reportResults(const Zstring& post progressDlg_->timerSetStatus(false /*active*/); //keep correct summary window stats considering count down timer, system sleep //determine post-sync status irrespective of further errors during tear-down - const SyncResult resultStatus = [&] + const SyncResult syncResult = [&] { if (getAbortStatus()) { @@ -88,11 +91,11 @@ BatchStatusHandler::Result BatchStatusHandler::reportResults(const Zstring& post return SyncResult::finishedSuccess; }(); - assert(resultStatus == SyncResult::aborted || currentPhase() == ProcessPhase::synchronizing); + assert(syncResult == SyncResult::aborted || currentPhase() == ProcessPhase::synchronizing); const ProcessSummary summary { - startTime_, resultStatus, { jobName_ }, + startTime_, syncResult, { jobName_ }, getStatsCurrent(), getStatsTotal (), totalTime @@ -101,83 +104,66 @@ BatchStatusHandler::Result BatchStatusHandler::reportResults(const Zstring& post const AbstractPath logFilePath = generateLogFilePath(logFormat, summary, altLogFolderPathPhrase); //e.g. %AppData%\FreeFileSync\Logs\Backup FreeFileSync 2013-09-15 015052.123 [Error].log - if (const Zstring cmdLine = trimCpy(postSyncCommand); - !cmdLine.empty()) - { - if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user) - ; //user cancelled => don't run post sync command! - else if (postSyncCondition == PostSyncCondition::COMPLETION || - (postSyncCondition == PostSyncCondition::ERRORS) == (resultStatus == SyncResult::aborted || - resultStatus == SyncResult::finishedError)) - try - { - ////---------------------------------------------------------------------- - //::wxSetEnv(L"logfile_path", AFS::getDisplayPath(logFilePath)); - ////---------------------------------------------------------------------- - const Zstring cmdLineExp = expandMacros(cmdLine); - errorLog_.logMsg(_("Executing command:") + L' ' + utfTo<std::wstring>(cmdLineExp), MSG_TYPE_INFO); - shellExecute(cmdLineExp, ExecutionType::async, false /*hideConsole*/); //throw FileError - } - catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); } - } - - //---------------------------- save log file ------------------------------ auto notifyStatusNoThrow = [&](const std::wstring& msg) { try { updateStatus(msg); /*throw AbortProcess*/ } catch (AbortProcess&) {} }; - if (const std::string notifyEmail = trimCpy(emailNotifyAddress); - !notifyEmail.empty()) - { - if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user) - ; //user cancelled => don't send email notification! - else if (emailNotifyCondition == ResultsNotification::always || - (emailNotifyCondition == ResultsNotification::errorWarning && (resultStatus == SyncResult::aborted || - resultStatus == SyncResult::finishedError || - resultStatus == SyncResult::finishedWarning)) || - (emailNotifyCondition == ResultsNotification::errorOnly && (resultStatus == SyncResult::aborted || - resultStatus == SyncResult::finishedError))) - try - { - sendLogAsEmail(notifyEmail, summary, errorLog_, logFilePath, notifyStatusNoThrow); //throw FileError - } - catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); } - } - - 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, summary, errorLog_, logfilesMaxAgeDays, logFormat, logFilePathsToKeep, notifyStatusNoThrow); //throw FileError - } - catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); } - - //----------------- post sync action ------------------------ - auto mayRunAfterCountDown = [&](const std::wstring& operationName) - { - auto notifyStatusThrowOnCancel = [&](const std::wstring& msg) - { - try { updateStatus(msg); /*throw AbortProcess*/ } - catch (AbortProcess&) - { - if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user) - throw; - } - }; - - if (progressDlg_->getWindowIfVisible()) - try - { - delayAndCountDown(operationName, std::chrono::seconds(5), notifyStatusThrowOnCancel); //throw AbortProcess - } - catch (AbortProcess&) { return false; } - - return true; - }; - bool autoClose = false; FinalRequest finalRequest = FinalRequest::none; + bool suspend = false; if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user) - ; //user cancelled => don't run post sync action! + ; /* user cancelled => don't run post sync command + => don't send email notification + => don't run post sync action + => don't play sound notification */ else + { + //--------------------- post sync command ---------------------- + if (const Zstring cmdLine = trimCpy(postSyncCommand); + !cmdLine.empty()) + if (postSyncCondition == PostSyncCondition::COMPLETION || + (postSyncCondition == PostSyncCondition::ERRORS) == (syncResult == SyncResult::aborted || + syncResult == SyncResult::finishedError)) + ////---------------------------------------------------------------------- + //::wxSetEnv(L"logfile_path", AFS::getDisplayPath(logFilePath)); + ////---------------------------------------------------------------------- + runCommandAndLogErrors(expandMacros(cmdLine), errorLog_); + + //--------------------- email notification ---------------------- + if (const std::string notifyEmail = trimCpy(emailNotifyAddress); + !notifyEmail.empty()) + if (emailNotifyCondition == ResultsNotification::always || + (emailNotifyCondition == ResultsNotification::errorWarning && (syncResult == SyncResult::aborted || + syncResult == SyncResult::finishedError || + syncResult == SyncResult::finishedWarning)) || + (emailNotifyCondition == ResultsNotification::errorOnly && (syncResult == SyncResult::aborted || + syncResult == SyncResult::finishedError))) + try + { + sendLogAsEmail(notifyEmail, summary, errorLog_, logFilePath, notifyStatusNoThrow); //throw FileError + } + catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); } + + //--------------------- post sync actions ---------------------- + auto mayRunAfterCountDown = [&](const std::wstring& operationName) + { + if (progressDlg_->getWindowIfVisible()) + try + { + auto notifyStatusThrowOnCancel = [&](const std::wstring& msg) + { + try { updateStatus(msg); /*throw AbortProcess*/ } + catch (AbortProcess&) + { + if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user) + throw; + } + }; + delayAndCountDown(operationName, std::chrono::seconds(5), notifyStatusThrowOnCancel); //throw AbortProcess + } + catch (AbortProcess&) { return false; } + + return true; + }; switch (progressDlg_->getOptionPostSyncAction()) { case PostSyncAction2::none: @@ -188,12 +174,10 @@ BatchStatusHandler::Result BatchStatusHandler::reportResults(const Zstring& post break; case PostSyncAction2::sleep: if (mayRunAfterCountDown(_("System: Sleep"))) - try - { - suspendSystem(); //throw FileError - autoClose = progressDlg_->getOptionAutoCloseDialog(); - } - catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); } + { + autoClose = progressDlg_->getOptionAutoCloseDialog(); + suspend = true; + } break; case PostSyncAction2::shutdown: if (mayRunAfterCountDown(_("System: Shut down"))) @@ -203,6 +187,38 @@ BatchStatusHandler::Result BatchStatusHandler::reportResults(const Zstring& post } break; } + + //--------------------- sound notification ---------------------- + if (!autoClose) //only play when showing results dialog + if (!soundFileSyncComplete_.empty()) + { + //wxWidgets shows modal error dialog by default => NO! + wxLog* oldLogTarget = wxLog::SetActiveTarget(new wxLogStderr); //transfer and receive ownership! + ZEN_ON_SCOPE_EXIT(delete wxLog::SetActiveTarget(oldLogTarget)); + + wxSound::Play(utfTo<wxString>(soundFileSyncComplete_), wxSOUND_ASYNC); + } + //if (::GetForegroundWindow() != GetHWND()) + // RequestUserAttention(); -> probably too much since task bar is already colorized with Taskbar::STATUS_ERROR or STATUS_NORMAL + } + + //--------------------- 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, summary, errorLog_, logfilesMaxAgeDays, logFormat, logFilePathsToKeep, notifyStatusNoThrow); //throw FileError + } + catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); } + //---------------------------------------------------------- + + + if (suspend) //...*before* results dialog is shown + try + { + suspendSystem(); //throw FileError + } + catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); } + if (switchToGuiRequested_) //-> avoid recursive yield() calls, thous switch not before ending batch mode { autoClose = true; @@ -213,10 +229,10 @@ BatchStatusHandler::Result BatchStatusHandler::reportResults(const Zstring& post progressDlg_->destroy(autoClose, true /*restoreParentFrame: n/a here*/, - resultStatus, errorLogFinal); + syncResult, errorLogFinal); progressDlg_ = nullptr; - return { resultStatus, finalRequest, logFilePath }; + return { syncResult, errorLogFinal.ref().getStats(), finalRequest, logFilePath }; } @@ -300,7 +316,7 @@ ProcessCallback::Response BatchStatusHandler::reportError(const std::wstring& ms if (retryNumber < automaticRetryCount_) { errorLog_.logMsg(msg + L"\n-> " + _("Automatic retry"), MSG_TYPE_INFO); - delayAndCountDown(_("Automatic retry") + (automaticRetryCount_ <= 1 ? L"" : L' ' + numberTo<std::wstring>(retryNumber + 1) + L"/" + numberTo<std::wstring>(automaticRetryCount_)), + delayAndCountDown(_("Automatic retry") + (automaticRetryCount_ <= 1 ? L"" : L' ' + numberTo<std::wstring>(retryNumber + 1) + L"/" + numberTo<std::wstring>(automaticRetryCount_)), automaticRetryDelay_, [&](const std::wstring& statusMsg) { this->updateStatus(_("Error") + L": " + statusMsg); }); //throw AbortProcess return ProcessCallback::retry; } diff --git a/FreeFileSync/Source/ui/batch_status_handler.h b/FreeFileSync/Source/ui/batch_status_handler.h index 2e59ac7e..c748720c 100644 --- a/FreeFileSync/Source/ui/batch_status_handler.h +++ b/FreeFileSync/Source/ui/batch_status_handler.h @@ -21,15 +21,15 @@ class BatchStatusHandler : public StatusHandler { public: BatchStatusHandler(bool showProgress, - bool autoCloseDialog, const std::wstring& jobName, //should not be empty for a batch job! - const Zstring& soundFileSyncComplete, const std::chrono::system_clock::time_point& startTime, bool ignoreErrors, - BatchErrorHandling batchErrorHandling, size_t automaticRetryCount, std::chrono::seconds automaticRetryDelay, - PostSyncAction postSyncAction); //noexcept!! + const Zstring& soundFileSyncComplete, + bool autoCloseDialog, + PostSyncAction postSyncAction, + BatchErrorHandling batchErrorHandling); //noexcept!! ~BatchStatusHandler(); void initNewPhase (int itemsTotal, int64_t bytesTotal, ProcessPhase phaseID) override; // @@ -49,7 +49,8 @@ public: }; struct Result { - SyncResult resultStatus; + SyncResult syncResult; + zen::ErrorLog::Stats logStats; FinalRequest finalRequest; AbstractPath logFilePath; }; @@ -58,14 +59,16 @@ public: const std::string& emailNotifyAddress, ResultsNotification emailNotifyCondition); //noexcept!! private: - bool switchToGuiRequested_ = false; - const BatchErrorHandling batchErrorHandling_; - zen::ErrorLog errorLog_; //list of non-resolved errors and warnings + const std::wstring jobName_; + const std::chrono::system_clock::time_point startTime_; const size_t automaticRetryCount_; const std::chrono::seconds automaticRetryDelay_; + const Zstring soundFileSyncComplete_; + SyncProgressDialog* progressDlg_; //managed to have the same lifetime as this handler! - const std::wstring jobName_; - const std::chrono::system_clock::time_point startTime_; + zen::ErrorLog errorLog_; //list of non-resolved errors and warnings + const BatchErrorHandling batchErrorHandling_; + bool switchToGuiRequested_ = false; }; } diff --git a/FreeFileSync/Source/ui/cfg_grid.cpp b/FreeFileSync/Source/ui/cfg_grid.cpp index fca67704..b262c1f4 100644 --- a/FreeFileSync/Source/ui/cfg_grid.cpp +++ b/FreeFileSync/Source/ui/cfg_grid.cpp @@ -585,7 +585,7 @@ private: try { if (std::optional<Zstring> nativePath = AFS::getNativeItemPath(item->cfgItem.logFilePath)) - openWithDefaultApplication(*nativePath); //throw FileError + openWithDefaultApp(*nativePath); //throw FileError else assert(false); assert(!AFS::isNullPath(item->cfgItem.logFilePath)); //see getRowMouseHover() diff --git a/FreeFileSync/Source/ui/file_grid.cpp b/FreeFileSync/Source/ui/file_grid.cpp index 93708754..cc194246 100644 --- a/FreeFileSync/Source/ui/file_grid.cpp +++ b/FreeFileSync/Source/ui/file_grid.cpp @@ -484,36 +484,48 @@ private: rectTmp.x += extent.GetWidth(); rectTmp.width -= extent.GetWidth(); }; + auto drawFilePath = [&](std::wstring path) + { + //path components should follow the app layout direction and are NOT a single piece of text! + //caveat: add Bidi support only during rendering and not in getValue() or AFS::getDisplayPath(): e.g. support "open file in Explorer" + assert(!contains(path, slashBidi_) && !contains(path, bslashBidi_)); + replace(path, L"/", slashBidi_); + replace(path, L"\\", bslashBidi_); + drawTextBlock(path); + }; - const std::wstring cellValue = getValue(row, colType); + const std::wstring& cellValue = getValue(row, colType); switch (static_cast<ColumnTypeRim>(colType)) { case ColumnTypeRim::ITEM_PATH: { if (!iconMgr_) - drawTextBlock(cellValue); + drawFilePath(cellValue); else { auto it = cellValue.end(); while (it != cellValue.begin()) //reverse iteration: 1. check 2. decrement 3. evaluate { --it; - if (*it == '\\' || *it == '/') + if (*it == L'\\' || *it == L'/') { ++it; break; } } - const std::wstring pathPrefix(cellValue.begin(), it); - const std::wstring itemName(it, cellValue.end()); + /*const */std::wstring pathPrefix(cellValue.begin(), it); + const std::wstring itemName(it, cellValue.end()); + + if (!pathPrefix.empty()) + pathPrefix.pop_back(); //don't really need the trailing slash // Partitioning: // __________________________________________________ // | gap | path prefix | gap | icon | gap | item name | // -------------------------------------------------- if (!pathPrefix.empty()) - drawTextBlock(pathPrefix); + drawFilePath(pathPrefix); //draw file icon rectTmp.x += gridGap_; @@ -576,7 +588,7 @@ private: rectTmp.x += iconSize; rectTmp.width -= iconSize; - drawTextBlock(itemName); + drawFilePath(itemName); } } break; @@ -736,18 +748,22 @@ private: AFS::getDisplayPath(fsObj->getAbstractPath<side>()) : utfTo<std::wstring>(fsObj->getRelativePath<side>()); + assert(!contains(toolTip, slashBidi_) && !contains(toolTip, bslashBidi_)); + replace(toolTip, L"/", slashBidi_); + replace(toolTip, L"\\", bslashBidi_); + visitFSObject(*fsObj, [](const FolderPair& folder) {}, [&](const FilePair& file) { toolTip += L'\n' + - _("Size:") + L' ' + zen::formatFilesizeShort(file.getFileSize<side>()) + L'\n' + - _("Date:") + L' ' + zen::formatUtcToLocalTime(file.getLastWriteTime<side>()); + _("Size:") + L' ' + formatFilesizeShort(file.getFileSize<side>()) + L'\n' + + _("Date:") + L' ' + formatUtcToLocalTime(file.getLastWriteTime<side>()); }, [&](const SymlinkPair& symlink) { toolTip += L'\n' + - _("Date:") + L' ' + zen::formatUtcToLocalTime(symlink.getLastWriteTime<side>()); + _("Date:") + L' ' + formatUtcToLocalTime(symlink.getLastWriteTime<side>()); }); } return toolTip; @@ -760,6 +776,10 @@ private: std::vector<char> failedLoads_; //effectively a vector<bool> of size "number of rows" std::optional<wxBitmap> renderBuf_; //avoid costs of recreating this temporary variable + + const std::wstring slashBidi_ = (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft ? RTL_MARK : LTR_MARK) + std::wstring() + L"/"; + const std::wstring bslashBidi_ = (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft ? RTL_MARK : LTR_MARK) + std::wstring() + L"\\"; + //no need for LTR/RTL marks on both sides: text follows main direction if slash is between two strong characters with different directions }; diff --git a/FreeFileSync/Source/ui/folder_selector.cpp b/FreeFileSync/Source/ui/folder_selector.cpp index 8e2037ba..27e9f42d 100644 --- a/FreeFileSync/Source/ui/folder_selector.cpp +++ b/FreeFileSync/Source/ui/folder_selector.cpp @@ -142,8 +142,11 @@ void FolderSelector::onItemPathDropped(FileDropEvent& event) if (!droppedPathsFilter_ || droppedPathsFilter_(itemPaths)) { - auto fmtShellPath = [](const Zstring& shellItemPath) + auto fmtShellPath = [](Zstring shellItemPath) { + if (endsWith(shellItemPath, Zstr(' '))) //prevent createAbstractPath() from trimming legit trailing blank! + shellItemPath += FILE_NAME_SEPARATOR; + const AbstractPath itemPath = createAbstractPath(shellItemPath); try { @@ -208,6 +211,7 @@ void FolderSelector::onSelectFolder(wxCommandEvent& event) } } + Zstring shellItemPath; wxDirDialog dirPicker(parent_, _("Select a folder"), utfTo<wxString>(defaultFolderPath)); //put modal wxWidgets dialogs on stack: creating on freestore leads to memleak! //-> following doesn't seem to do anything at all! still "Show hidden" is available as a context menu option: @@ -215,9 +219,14 @@ void FolderSelector::onSelectFolder(wxCommandEvent& event) if (dirPicker.ShowModal() != wxID_OK) return; - const Zstring newFolderPathPhrase = utfTo<Zstring>(dirPicker.GetPath()); + shellItemPath = utfTo<Zstring>(dirPicker.GetPath()); + if (endsWith(shellItemPath, Zstr(' '))) //prevent createAbstractPath() from trimming legit trailing blank! + shellItemPath += FILE_NAME_SEPARATOR; + + //make sure FFS-specific explicit MTP-syntax is applied! + const Zstring newFolderPathPhrase = AFS::getInitPathPhrase(createAbstractPath(shellItemPath)); //noexcept - setFolderPathPhrase(newFolderPathPhrase, &folderComboBox_, folderComboBox_, staticText_); + setPath(newFolderPathPhrase); //notify action invoked by user wxCommandEvent dummy(EVENT_ON_FOLDER_SELECTED); @@ -237,7 +246,7 @@ void FolderSelector::onSelectAltFolder(wxCommandEvent& event) if (showCloudSetupDialog(parent_, folderPathPhrase, parallelOps, get(parallelOpsDisabledReason)) != ReturnSmallDlg::BUTTON_OKAY) return; - setFolderPathPhrase(folderPathPhrase, &folderComboBox_, folderComboBox_, staticText_); + setPath(folderPathPhrase); if (setDeviceParallelOps_) setDeviceParallelOps_(folderPathPhrase, parallelOps); diff --git a/FreeFileSync/Source/ui/gui_generated.cpp b/FreeFileSync/Source/ui/gui_generated.cpp index 78092e06..124bce47 100644 --- a/FreeFileSync/Source/ui/gui_generated.cpp +++ b/FreeFileSync/Source/ui/gui_generated.cpp @@ -851,8 +851,6 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const bSizer287->Add( m_bpButtonViewTypeSyncAction, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); m_bpButtonViewContext = new wxBitmapButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); - m_bpButtonViewContext->SetToolTip( _("dummy") ); - bSizer287->Add( m_bpButtonViewContext, 0, wxRIGHT|wxEXPAND, 5 ); @@ -4597,9 +4595,18 @@ OptionsDlgGenerated::OptionsDlgGenerated( wxWindow* parent, wxWindowID id, const wxBoxSizer* bSizer289; bSizer289 = new wxBoxSizer( wxHORIZONTAL ); + wxBoxSizer* bSizer2971; + bSizer2971 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapConsole = new wxStaticBitmap( m_panel39, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer2971->Add( m_bitmapConsole, 0, wxALIGN_CENTER_VERTICAL, 5 ); + m_staticText85 = new wxStaticText( m_panel39, wxID_ANY, _("Customize context menu:"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticText85->Wrap( -1 ); - bSizer289->Add( m_staticText85, 1, wxRIGHT, 5 ); + bSizer2971->Add( m_staticText85, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 ); + + + bSizer289->Add( bSizer2971, 1, 0, 5 ); wxFlexGridSizer* fgSizer25; fgSizer25 = new wxFlexGridSizer( 0, 2, 0, 10 ); diff --git a/FreeFileSync/Source/ui/gui_generated.h b/FreeFileSync/Source/ui/gui_generated.h index 07867a58..00e51eb1 100644 --- a/FreeFileSync/Source/ui/gui_generated.h +++ b/FreeFileSync/Source/ui/gui_generated.h @@ -1057,6 +1057,7 @@ protected: wxButton* m_buttonSelectSoundSyncDone; wxBitmapButton* m_bpButtonPlaySyncDone; wxStaticLine* m_staticline3611; + wxStaticBitmap* m_bitmapConsole; wxStaticText* m_staticText85; wxStaticText* m_staticText174; wxStaticText* m_staticText175; diff --git a/FreeFileSync/Source/ui/gui_status_handler.cpp b/FreeFileSync/Source/ui/gui_status_handler.cpp index 6ad01cd2..aa635da2 100644 --- a/FreeFileSync/Source/ui/gui_status_handler.cpp +++ b/FreeFileSync/Source/ui/gui_status_handler.cpp @@ -8,12 +8,14 @@ #include <zen/shell_execute.h> #include <zen/shutdown.h> #include <wx/app.h> +#include <wx/sound.h> #include <wx/wupdlock.h> #include <wx+/popup_dlg.h> #include "main_dlg.h" #include "../afs/concrete.h" #include "../base/resolve_path.h" #include "../log_file.h" +#include "status_handler_impl.h" using namespace zen; using namespace fff; @@ -141,7 +143,7 @@ StatusHandlerTemporaryPanel::Result StatusHandlerTemporaryPanel::reportResults() const auto totalTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - startTime_); //determine post-sync status irrespective of further errors during tear-down - const SyncResult resultStatus = [&] + const SyncResult syncResult = [&] { if (getAbortStatus()) { @@ -160,7 +162,7 @@ StatusHandlerTemporaryPanel::Result StatusHandlerTemporaryPanel::reportResults() const ProcessSummary summary { - startTime_, resultStatus, {} /*jobName*/, + startTime_, syncResult, {} /*jobName*/, getStatsCurrent(), getStatsTotal (), totalTime @@ -230,7 +232,7 @@ ProcessCallback::Response StatusHandlerTemporaryPanel::reportError(const std::ws if (retryNumber < automaticRetryCount_) { errorLog_.logMsg(msg + L"\n-> " + _("Automatic retry"), MSG_TYPE_INFO); - delayAndCountDown(_("Automatic retry") + (automaticRetryCount_ <= 1 ? L"" : L' ' + numberTo<std::wstring>(retryNumber + 1) + L"/" + numberTo<std::wstring>(automaticRetryCount_)), + delayAndCountDown(_("Automatic retry") + (automaticRetryCount_ <= 1 ? L"" : L' ' + numberTo<std::wstring>(retryNumber + 1) + L"/" + numberTo<std::wstring>(automaticRetryCount_)), automaticRetryDelay_, [&](const std::wstring& statusMsg) { this->updateStatus(_("Error") + L": " + statusMsg); }); //throw AbortProcess return ProcessCallback::retry; } @@ -332,19 +334,20 @@ void StatusHandlerTemporaryPanel::OnAbortCompare(wxCommandEvent& event) //######################################################################################################## StatusHandlerFloatingDialog::StatusHandlerFloatingDialog(wxFrame* parentDlg, + const std::vector<std::wstring>& jobNames, const std::chrono::system_clock::time_point& startTime, bool ignoreErrors, size_t automaticRetryCount, std::chrono::seconds automaticRetryDelay, - const std::vector<std::wstring>& jobNames, const Zstring& soundFileSyncComplete, bool& autoCloseDialog) : + jobNames_(jobNames), + startTime_(startTime), + automaticRetryCount_(automaticRetryCount), + automaticRetryDelay_(automaticRetryDelay), + soundFileSyncComplete_(soundFileSyncComplete), progressDlg_(SyncProgressDialog::create([this] { userRequestAbort(); }, *this, parentDlg, true /*showProgress*/, autoCloseDialog, -startTime, jobNames, soundFileSyncComplete, ignoreErrors, automaticRetryCount, PostSyncAction2::none)), - automaticRetryCount_(automaticRetryCount), - automaticRetryDelay_(automaticRetryDelay), - jobNames_(jobNames), - startTime_(startTime), +jobNames, startTime, ignoreErrors, automaticRetryCount, PostSyncAction2::none)), autoCloseDialogOut_(autoCloseDialog) {} @@ -365,7 +368,7 @@ StatusHandlerFloatingDialog::Result StatusHandlerFloatingDialog::reportResults(c progressDlg_->timerSetStatus(false /*active*/); //keep correct summary window stats considering count down timer, system sleep //determine post-sync status irrespective of further errors during tear-down - const SyncResult resultStatus = [&] + const SyncResult syncResult = [&] { if (getAbortStatus()) { @@ -384,11 +387,11 @@ StatusHandlerFloatingDialog::Result StatusHandlerFloatingDialog::reportResults(c return SyncResult::finishedSuccess; }(); - assert(resultStatus == SyncResult::aborted || currentPhase() == ProcessPhase::synchronizing); + assert(syncResult == SyncResult::aborted || currentPhase() == ProcessPhase::synchronizing); const ProcessSummary summary { - startTime_, resultStatus, jobNames_, + startTime_, syncResult, jobNames_, getStatsCurrent(), getStatsTotal (), totalTime @@ -397,83 +400,67 @@ StatusHandlerFloatingDialog::Result StatusHandlerFloatingDialog::reportResults(c const AbstractPath logFilePath = generateLogFilePath(logFormat, summary, altLogFolderPathPhrase); //e.g. %AppData%\FreeFileSync\Logs\Backup FreeFileSync 2013-09-15 015052.123 [Error].log - if (const Zstring cmdLine = trimCpy(postSyncCommand); - !cmdLine.empty()) - { - if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user) - ; //user cancelled => don't run post sync command! - else if (postSyncCondition == PostSyncCondition::COMPLETION || - (postSyncCondition == PostSyncCondition::ERRORS) == (resultStatus == SyncResult::aborted || - resultStatus == SyncResult::finishedError)) - try - { - ////---------------------------------------------------------------------- - //::wxSetEnv(L"logfile_path", AFS::getDisplayPath(logFilePath)); - ////---------------------------------------------------------------------- - const Zstring cmdLineExp = expandMacros(cmdLine); - errorLog_.logMsg(_("Executing command:") + L' ' + utfTo<std::wstring>(cmdLineExp), MSG_TYPE_INFO); - shellExecute(cmdLineExp, ExecutionType::async, false /*hideConsole*/); //throw FileError - } - catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); } - } - - //---------------------------- save log file ------------------------------ auto notifyStatusNoThrow = [&](const std::wstring& msg) { try { updateStatus(msg); /*throw AbortProcess*/ } catch (AbortProcess&) {} }; - if (const std::string notifyEmail = trimCpy(emailNotifyAddress); - !notifyEmail.empty()) - { - if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user) - ; //user cancelled => don't send email notification! - else if (emailNotifyCondition == ResultsNotification::always || - (emailNotifyCondition == ResultsNotification::errorWarning && (resultStatus == SyncResult::aborted || - resultStatus == SyncResult::finishedError || - resultStatus == SyncResult::finishedWarning)) || - (emailNotifyCondition == ResultsNotification::errorOnly && (resultStatus == SyncResult::aborted || - resultStatus == SyncResult::finishedError))) - try - { - sendLogAsEmail(notifyEmail, summary, errorLog_, logFilePath, notifyStatusNoThrow); //throw FileError - } - catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); } - } + bool autoClose = false; + FinalRequest finalRequest = FinalRequest::none; + bool suspend = false; - 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 + if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user) + ; /* user cancelled => don't run post sync command + => don't send email notification + => don't run post sync action + => don't play sound notification */ + else { - //do NOT use tryReportingError()! saving log files should not be cancellable! - saveLogFile(logFilePath, summary, errorLog_, logfilesMaxAgeDays, logFormat, logFilePathsToKeep, notifyStatusNoThrow); //throw FileError - } - catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); } + //--------------------- post sync command ---------------------- + if (const Zstring cmdLine = trimCpy(postSyncCommand); + !cmdLine.empty()) + if (postSyncCondition == PostSyncCondition::COMPLETION || + (postSyncCondition == PostSyncCondition::ERRORS) == (syncResult == SyncResult::aborted || + syncResult == SyncResult::finishedError)) + ////---------------------------------------------------------------------- + //::wxSetEnv(L"logfile_path", AFS::getDisplayPath(logFilePath)); + ////---------------------------------------------------------------------- + runCommandAndLogErrors(expandMacros(cmdLine), errorLog_); + + //--------------------- email notification ---------------------- + if (const std::string notifyEmail = trimCpy(emailNotifyAddress); + !notifyEmail.empty()) + if (emailNotifyCondition == ResultsNotification::always || + (emailNotifyCondition == ResultsNotification::errorWarning && (syncResult == SyncResult::aborted || + syncResult == SyncResult::finishedError || + syncResult == SyncResult::finishedWarning)) || + (emailNotifyCondition == ResultsNotification::errorOnly && (syncResult == SyncResult::aborted || + syncResult == SyncResult::finishedError))) + try + { + sendLogAsEmail(notifyEmail, summary, errorLog_, logFilePath, notifyStatusNoThrow); //throw FileError + } + catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); } - //----------------- post sync action ------------------------ - auto mayRunAfterCountDown = [&](const std::wstring& operationName) - { - auto notifyStatusThrowOnCancel = [&](const std::wstring& msg) + //--------------------- post sync actions ---------------------- + auto mayRunAfterCountDown = [&](const std::wstring& operationName) { - try { updateStatus(msg); /*throw AbortProcess*/ } - catch (AbortProcess&) - { - if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user) - throw; - } - }; - - if (progressDlg_->getWindowIfVisible()) - try - { - delayAndCountDown(operationName, std::chrono::seconds(5), notifyStatusThrowOnCancel); //throw AbortProcess - } - catch (AbortProcess&) { return false; } - - return true; - }; + if (progressDlg_->getWindowIfVisible()) + try + { + auto notifyStatusThrowOnCancel = [&](const std::wstring& msg) + { + try { updateStatus(msg); /*throw AbortProcess*/ } + catch (AbortProcess&) + { + if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user) + throw; + } + }; + delayAndCountDown(operationName, std::chrono::seconds(5), notifyStatusThrowOnCancel); //throw AbortProcess + } + catch (AbortProcess&) { return false; } - bool autoClose = false; - FinalRequest finalRequest = FinalRequest::none; + return true; + }; - if (getAbortStatus() && *getAbortStatus() == AbortTrigger::user) - ; //user cancelled => don't run post sync action! - else switch (progressDlg_->getOptionPostSyncAction()) { case PostSyncAction2::none: @@ -485,12 +472,10 @@ StatusHandlerFloatingDialog::Result StatusHandlerFloatingDialog::reportResults(c break; case PostSyncAction2::sleep: if (mayRunAfterCountDown(_("System: Sleep"))) - try - { - suspendSystem(); //throw FileError - autoClose = progressDlg_->getOptionAutoCloseDialog(); - } - catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); } + { + autoClose = progressDlg_->getOptionAutoCloseDialog(); + suspend = true; + } break; case PostSyncAction2::shutdown: if (mayRunAfterCountDown(_("System: Shut down"))) @@ -501,12 +486,44 @@ StatusHandlerFloatingDialog::Result StatusHandlerFloatingDialog::reportResults(c break; } + //--------------------- sound notification ---------------------- + if (!autoClose) //only play when showing results dialog + if (!soundFileSyncComplete_.empty()) + { + //wxWidgets shows modal error dialog by default => NO! + wxLog* oldLogTarget = wxLog::SetActiveTarget(new wxLogStderr); //transfer and receive ownership! + ZEN_ON_SCOPE_EXIT(delete wxLog::SetActiveTarget(oldLogTarget)); + + wxSound::Play(utfTo<wxString>(soundFileSyncComplete_), wxSOUND_ASYNC); + } + //if (::GetForegroundWindow() != GetHWND()) + // RequestUserAttention(); -> probably too much since task bar is already colorized with Taskbar::STATUS_ERROR or STATUS_NORMAL + } + + //--------------------- 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, summary, errorLog_, logfilesMaxAgeDays, logFormat, logFilePathsToKeep, notifyStatusNoThrow); //throw FileError + } + catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); } + //---------------------------------------------------------- + + + if (suspend) //...*before* results dialog is shown + try + { + suspendSystem(); //throw FileError + } + catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); } + + auto errorLogFinal = makeSharedRef<const ErrorLog>(std::move(errorLog_)); autoCloseDialogOut_ = //output parameter owned by SyncProgressDialog (evaluate *after* user closed the results dialog) progressDlg_->destroy(autoClose, finalRequest == FinalRequest::none /*restoreParentFrame*/, - resultStatus, errorLogFinal).autoCloseDialog; + syncResult, errorLogFinal).autoCloseDialog; progressDlg_ = nullptr; return { summary, errorLogFinal, finalRequest, logFilePath }; @@ -570,7 +587,7 @@ ProcessCallback::Response StatusHandlerFloatingDialog::reportError(const std::ws if (retryNumber < automaticRetryCount_) { errorLog_.logMsg(msg + L"\n-> " + _("Automatic retry"), MSG_TYPE_INFO); - delayAndCountDown(_("Automatic retry") + (automaticRetryCount_ <= 1 ? L"" : L' ' + numberTo<std::wstring>(retryNumber + 1) + L"/" + numberTo<std::wstring>(automaticRetryCount_)), + delayAndCountDown(_("Automatic retry") + (automaticRetryCount_ <= 1 ? L"" : L' ' + numberTo<std::wstring>(retryNumber + 1) + L"/" + numberTo<std::wstring>(automaticRetryCount_)), automaticRetryDelay_, [&](const std::wstring& statusMsg) { this->updateStatus(_("Error") + L": " + statusMsg); }); //throw AbortProcess return ProcessCallback::retry; } diff --git a/FreeFileSync/Source/ui/gui_status_handler.h b/FreeFileSync/Source/ui/gui_status_handler.h index 5f671a82..2a9e00d2 100644 --- a/FreeFileSync/Source/ui/gui_status_handler.h +++ b/FreeFileSync/Source/ui/gui_status_handler.h @@ -60,11 +60,11 @@ class StatusHandlerFloatingDialog : public StatusHandler { public: StatusHandlerFloatingDialog(wxFrame* parentDlg, + const std::vector<std::wstring>& jobNames, const std::chrono::system_clock::time_point& startTime, bool ignoreErrors, size_t automaticRetryCount, std::chrono::seconds automaticRetryDelay, - const std::vector<std::wstring>& jobNames, const Zstring& soundFileSyncComplete, bool& autoCloseDialog); //noexcept! ~StatusHandlerFloatingDialog(); @@ -96,12 +96,14 @@ public: const std::string& emailNotifyAddress, ResultsNotification emailNotifyCondition); //noexcept!! private: - SyncProgressDialog* progressDlg_; //managed to have the same lifetime as this handler! - zen::ErrorLog errorLog_; - const size_t automaticRetryCount_; - const std::chrono::seconds automaticRetryDelay_; const std::vector<std::wstring> jobNames_; const std::chrono::system_clock::time_point startTime_; + const size_t automaticRetryCount_; + const std::chrono::seconds automaticRetryDelay_; + const Zstring soundFileSyncComplete_; + + SyncProgressDialog* progressDlg_; //managed to have the same lifetime as this handler! + zen::ErrorLog errorLog_; bool& autoCloseDialogOut_; //owned by SyncProgressDialog }; } diff --git a/FreeFileSync/Source/ui/main_dlg.cpp b/FreeFileSync/Source/ui/main_dlg.cpp index 77c4c1c9..0896185c 100644 --- a/FreeFileSync/Source/ui/main_dlg.cpp +++ b/FreeFileSync/Source/ui/main_dlg.cpp @@ -57,6 +57,8 @@ using namespace fff; namespace { const size_t EXT_APP_MASS_INVOKE_THRESHOLD = 10; //more is likely a user mistake (Explorer uses limit of 15) +const size_t EXT_APP_MAX_TOTAL_WAIT_TIME_MS = 1000; + const int TOP_BUTTON_OPTIMAL_WIDTH_DIP = 170; const std::chrono::milliseconds LAST_USED_CFG_EXISTENCE_CHECK_TIME_MAX(500); const std::chrono::milliseconds FILE_GRID_POST_UPDATE_DELAY(400); @@ -700,7 +702,7 @@ MainDialog::MainDialog(const Zstring& globalConfigFilePath, this->Connect(newItem->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(MainDialog::OnMenuUpdateAvailable)); menu->Append(newItem); //pass ownership - const std::wstring blackStar = utfTo<std::wstring>("\xE2\x98\x85"); //"BLACK STAR" + const std::wstring blackStar = utfTo<std::wstring>("★"); m_menubar->Append(menu, blackStar + L' ' + replaceCpy(_("FreeFileSync %x is available!"), L"%x", utfTo<std::wstring>(globalSettings.gui.lastOnlineVersion)) + L' ' + blackStar); } @@ -1466,6 +1468,7 @@ void collectNonNativeFiles(const std::vector<FileSystemObject*>& selectedRows, c template <SelectedSide side> void invokeCommandLine(const Zstring& commandLinePhrase, //throw FileError + bool openWithDefaultAppRequested, const std::vector<FileSystemObject*>& selection, const TempFileBuffer& tempFileBuf) { @@ -1500,17 +1503,32 @@ void invokeCommandLine(const Zstring& commandLinePhrase, //throw FileError if (localPath .empty()) localPath = replaceCpy(utfTo<Zstring>(L"<" + _("Local path not available for %x.") + L">"), Zstr("%x"), itemPath ); if (localPath2.empty()) localPath2 = replaceCpy(utfTo<Zstring>(L"<" + _("Local path not available for %x.") + L">"), Zstr("%x"), itemPath2); - Zstring command = commandLinePhrase; - replace(command, Zstr("%item_path%"), itemPath); - replace(command, Zstr("%item_path2%"), itemPath2); - replace(command, Zstr("%local_path%"), localPath); - replace(command, Zstr("%local_path2%"), localPath2); - replace(command, Zstr("%item_name%"), itemName); - replace(command, Zstr("%item_name2%"), itemName2); - replace(command, Zstr("%parent_path%"), folderPath); - replace(command, Zstr("%parent_path2%"), folderPath2); - - shellExecute(command, selection.size() > EXT_APP_MASS_INVOKE_THRESHOLD ? ExecutionType::sync : ExecutionType::async, false/*hideConsole*/); //throw FileError + Zstring cmdLine = expandMacros(commandLinePhrase); + replace(cmdLine, Zstr("%item_path%"), itemPath); + replace(cmdLine, Zstr("%item_path2%"), itemPath2); + replace(cmdLine, Zstr("%local_path%"), localPath); + replace(cmdLine, Zstr("%local_path2%"), localPath2); + replace(cmdLine, Zstr("%item_name%"), itemName); + replace(cmdLine, Zstr("%item_name2%"), itemName2); + replace(cmdLine, Zstr("%parent_path%"), folderPath); + replace(cmdLine, Zstr("%parent_path2%"), folderPath2); + + if (openWithDefaultAppRequested) //not strictly needed, but: 1. better error reporting (Windows) 2. not async => avoid zombies (Linux/macOS) + openWithDefaultApp(localPath); //throw FileError + else + try + { + std::optional<int> timeoutMs; + if (selection.size() <= EXT_APP_MASS_INVOKE_THRESHOLD) + timeoutMs = EXT_APP_MAX_TOTAL_WAIT_TIME_MS / EXT_APP_MASS_INVOKE_THRESHOLD; //run async, but give consoleExecute() some "time to fail" + //else: run synchronously + + if (const auto [exitCode, output] = consoleExecute(cmdLine, timeoutMs); //throw SysError, SysErrorTimeOut + exitCode != 0) + throw zen::SysError(formatSystemError(utfTo<std::string>(commandLinePhrase), replaceCpy(_("Exit code %x"), L"%x", numberTo<std::wstring>(exitCode)), output)); + } + catch (SysErrorTimeOut&) {} //child process not failed yet => probably fine :> + catch (const zen::SysError& e) { throw FileError(replaceCpy(_("Command %x failed."), L"%x", fmtPath(cmdLine)), e.toString()); } } } } @@ -1521,10 +1539,20 @@ void MainDialog::openExternalApplication(const Zstring& commandLinePhrase, bool const std::vector<FileSystemObject*>& selectionRight) { const XmlGlobalSettings::Gui defaultCfg; - const bool openFileBrowserRequested = !defaultCfg.externalApps.empty() && defaultCfg.externalApps[0].cmdLine == commandLinePhrase; + const bool showInFileBrowserRequested = defaultCfg.externalApps.size() >= 1 && defaultCfg.externalApps[0].cmdLine == commandLinePhrase; + const bool openWithDefaultAppRequested = defaultCfg.externalApps.size() >= 2 && defaultCfg.externalApps[1].cmdLine == commandLinePhrase; + + auto openFolderInFileBrowser = [this](const AbstractPath& folderPath) + { + try + { + openWithDefaultApp(utfTo<Zstring>(AFS::getDisplayPath(folderPath))); //throw FileError + } + catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } + }; //support fallback instead of an error in this special case - if (openFileBrowserRequested) + if (showInFileBrowserRequested) { if (selectionLeft.size() + selectionRight.size() > 1) //do not open more than one Explorer instance! { @@ -1535,15 +1563,6 @@ void MainDialog::openExternalApplication(const Zstring& commandLinePhrase, bool return openExternalApplication(commandLinePhrase, leftSide, {}, { selectionRight[0] }); } - auto openFolderInFileBrowser = [this](const AbstractPath& folderPath) - { - try - { - openWithDefaultApplication(utfTo<Zstring>(AFS::getDisplayPath(folderPath))); //throw FileError - } - catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } - }; - if (selectionLeft.empty() && selectionRight.empty()) return openFolderInFileBrowser(leftSide ? createAbstractPath(firstFolderPair_->getValues().folderPathPhraseLeft) : @@ -1621,19 +1640,16 @@ void MainDialog::openExternalApplication(const Zstring& commandLinePhrase, bool setLastOperationLog(r.summary, r.errorLog); - if (r.summary.resultStatus == SyncResult::aborted) + if (r.summary.syncResult == SyncResult::aborted) return; //updateGui(); -> not needed } //######################################################################################## - - const Zstring cmdExpanded = expandMacros(commandLinePhrase); - try { - invokeCommandLine< LEFT_SIDE>(cmdExpanded, selectionLeft, tempFileBuf_); //throw FileError - invokeCommandLine<RIGHT_SIDE>(cmdExpanded, selectionRight, tempFileBuf_); // + invokeCommandLine< LEFT_SIDE>(commandLinePhrase, openWithDefaultAppRequested, selectionLeft, tempFileBuf_); //throw FileError + invokeCommandLine<RIGHT_SIDE>(commandLinePhrase, openWithDefaultAppRequested, selectionRight, tempFileBuf_); // } catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } } @@ -3872,7 +3888,7 @@ void MainDialog::OnCompare(wxCommandEvent& event) setLastOperationLog(r.summary, r.errorLog); - if (r.summary.resultStatus == SyncResult::aborted) + if (r.summary.syncResult == SyncResult::aborted) return updateGui(); //refresh grid in ANY case! (also on abort) @@ -3911,15 +3927,15 @@ void MainDialog::OnCompare(wxCommandEvent& event) fp.setFocus(m_buttonSync); //mark selected cfg files as "in sync" when there is nothing to do: https://freefilesync.org/forum/viewtopic.php?t=4991 - if (r.summary.resultStatus == SyncResult::finishedSuccess) + if (r.summary.syncResult == SyncResult::finishedSuccess) { const SyncStatistics st(folderCmp_); if (st.createCount() + st.updateCount() + st.deleteCount() == 0) { - flashStatusInformation(_("All files are in sync")); - updateConfigLastRunStats(std::chrono::system_clock::to_time_t(startTime), r.summary.resultStatus, getNullPath() /*logFilePath*/); + flashStatusInformation(_("No files to synchronize")); + updateConfigLastRunStats(std::chrono::system_clock::to_time_t(startTime), r.summary.syncResult, getNullPath() /*logFilePath*/); } } } @@ -4064,11 +4080,10 @@ void MainDialog::OnStartSync(wxCommandEvent& event) //run this->enableAllElements() BEFORE "exitRequest" buf AFTER StatusHandlerFloatingDialog::reportResults() //class handling status updates and error messages - StatusHandlerFloatingDialog statusHandler(this, syncStartTime, + StatusHandlerFloatingDialog statusHandler(this, jobNames, syncStartTime, guiCfg.mainCfg.ignoreErrors, guiCfg.mainCfg.automaticRetryCount, guiCfg.mainCfg.automaticRetryDelay, - jobNames, globalCfg_.soundFileSyncFinished, globalCfg_.autoCloseProgressDialog); try @@ -4120,7 +4135,7 @@ void MainDialog::OnStartSync(wxCommandEvent& event) setLastOperationLog(r.summary, r.errorLog.ptr()); //update last sync stats for the selected cfg files - updateConfigLastRunStats(std::chrono::system_clock::to_time_t(syncStartTime), r.summary.resultStatus, r.logFilePath); + updateConfigLastRunStats(std::chrono::system_clock::to_time_t(syncStartTime), r.summary.syncResult, r.logFilePath); //remove empty rows: just a beautification, invalid rows shouldn't cause issues filegrid::getDataView(*m_gridMainC).removeInvalidRows(); @@ -4143,7 +4158,7 @@ void MainDialog::OnStartSync(wxCommandEvent& event) { onQueryEndSession(); //(try to) save GlobalSettings.xml => don't block shutdown if failed!!! shutdownSystem(); //throw FileError - terminateProcess(0 /*exitCode*/); //no point in continuing and saving cfg again in ~MainDialog()/onQueryEndSession() while the OS will kill us anytime! + terminateProcess(FFS_EXIT_SUCCESS); //no point in continuing and saving cfg again in ~MainDialog()/onQueryEndSession() while the OS will kill us anytime! } catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } //[!] ignores current error handling setting, BUT this is not a sync error! @@ -4323,7 +4338,7 @@ void MainDialog::setLastOperationLog(const ProcessSummary& summary, const std::s { const wxBitmap syncResultImage = [&] { - switch (summary.resultStatus) + switch (summary.syncResult) { case SyncResult::finishedSuccess: return getResourceImage(L"result_success"); @@ -4339,7 +4354,7 @@ void MainDialog::setLastOperationLog(const ProcessSummary& summary, const std::s const wxImage logOverlayImage = [&] { - //don't use "resultStatus": There may be errors after sync, e.g. failure to save log file/send email! + //don't use "syncResult": There may be errors after sync, e.g. failure to save log file/send email! if (errorLog) { const ErrorLog::Stats logCount = errorLog->getStats(); @@ -4352,7 +4367,7 @@ void MainDialog::setLastOperationLog(const ProcessSummary& summary, const std::s }(); m_bitmapSyncResult->SetBitmap(syncResultImage); - m_staticTextSyncResult->SetLabel(getSyncResultLabel(summary.resultStatus)); + m_staticTextSyncResult->SetLabel(getSyncResultLabel(summary.syncResult)); m_staticTextItemsProcessed->SetLabel(formatNumber(summary.statsProcessed.items)); @@ -4819,7 +4834,7 @@ void MainDialog::setStatusBarFileStats(FileView::FileStats statsLeft, wxString statusCenterNew; if (filegrid::getDataView(*m_gridMainC).rowsTotal() > 0) { - statusCenterNew = _P("Showing %y of 1 row", "Showing %y of %x rows", filegrid::getDataView(*m_gridMainC).rowsTotal()); + statusCenterNew = _P("Showing %y of 1 item", "Showing %y of %x items", filegrid::getDataView(*m_gridMainC).rowsTotal()); replace(statusCenterNew, L"%y", formatNumber(filegrid::getDataView(*m_gridMainC).rowsOnView())); //%x used as plural form placeholder! } diff --git a/FreeFileSync/Source/ui/progress_indicator.cpp b/FreeFileSync/Source/ui/progress_indicator.cpp index 7e1ee3d9..2ac9e3c8 100644 --- a/FreeFileSync/Source/ui/progress_indicator.cpp +++ b/FreeFileSync/Source/ui/progress_indicator.cpp @@ -8,7 +8,7 @@ #include <memory> #include <wx/imaglist.h> #include <wx/wupdlock.h> -#include <wx/sound.h> +//#include <wx/sound.h> #include <wx/app.h> #include <zen/basic_math.h> #include <zen/format_unit.h> @@ -647,14 +647,13 @@ public: wxFrame* parentFrame, bool showProgress, bool autoCloseDialog, - const std::chrono::system_clock::time_point& syncStartTime, const std::vector<std::wstring>& jobNames, - const Zstring& soundFileSyncComplete, + const std::chrono::system_clock::time_point& syncStartTime, bool ignoreErrors, size_t automaticRetryCount, PostSyncAction2 postSyncAction); - Result destroy(bool autoClose, bool restoreParentFrame, SyncResult resultStatus, const SharedRef<const zen::ErrorLog>& log) override; + Result destroy(bool autoClose, bool restoreParentFrame, SyncResult syncResult, const SharedRef<const zen::ErrorLog>& log) override; wxWindow* getWindowIfVisible() override { return this->IsShown() ? this : nullptr; } //workaround OS X bug: if "this" is used as parent window for a modal dialog then this dialog will erroneously un-hide its parent! @@ -692,7 +691,7 @@ private: void OnMinimizeToTray(wxCommandEvent& event) { minimizeToTray(); } //void OnToggleIgnoreErrors(wxCommandEvent& event) { updateStaticGui(); } - void showSummary(SyncResult resultStatus, const SharedRef<const ErrorLog>& log); + void showSummary(SyncResult syncResult, const SharedRef<const ErrorLog>& log); void minimizeToTray(); void resumeFromSystray(); @@ -706,7 +705,6 @@ private: const std::chrono::system_clock::time_point& syncStartTime_; const wxString jobName_; - const Zstring soundFileSyncComplete_; StopWatch stopWatch_; wxFrame* parentFrame_; //optional @@ -750,9 +748,8 @@ SyncProgressDialogImpl<TopLevelDialog>::SyncProgressDialogImpl(long style, //wxF wxFrame* parentFrame, bool showProgress, bool autoCloseDialog, - const std::chrono::system_clock::time_point& syncStartTime, const std::vector<std::wstring>& jobNames, - const Zstring& soundFileSyncComplete, + const std::chrono::system_clock::time_point& syncStartTime, bool ignoreErrors, size_t automaticRetryCount, PostSyncAction2 postSyncAction) : @@ -771,7 +768,6 @@ SyncProgressDialogImpl<TopLevelDialog>::SyncProgressDialogImpl(long style, //wxF return tmp; } ()), -soundFileSyncComplete_(soundFileSyncComplete), parentFrame_(parentFrame), userRequestAbort_(userRequestAbort), syncStat_(&syncStat) @@ -1266,13 +1262,13 @@ void SyncProgressDialogImpl<TopLevelDialog>::updateStaticGui() //depends on "syn template <class TopLevelDialog> -void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult resultStatus, const SharedRef<const ErrorLog>& log) +void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult syncResult, const SharedRef<const ErrorLog>& log) { assert(syncStat_); //at the LATEST(!) to prevent access to currentStatusHandler //enable okay and close events; may be set in this method ONLY - //In wxWidgets 2.9.3 upwards, the wxWindow::Reparent() below fails on GTK and OS X if window is frozen! http://forums.codeblocks.org/index.php?topic=13388.45 + //In wxWidgets 2.9.3 upwards, the wxWindow::Reparent() below fails on GTK and OS X if window is frozen! https://forums.codeblocks.org/index.php?topic=13388.45 paused_ = false; //you never know? @@ -1326,7 +1322,7 @@ void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult resultStatus const wxBitmap statusImage = [&] { - switch (resultStatus) + switch (syncResult) { case SyncResult::finishedSuccess: return getResourceImage(L"result_success"); @@ -1341,12 +1337,12 @@ void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult resultStatus }(); pnl_.m_bitmapStatus->SetBitmap(statusImage); - pnl_.m_staticTextPhase->SetLabel(getSyncResultLabel(resultStatus)); + pnl_.m_staticTextPhase->SetLabel(getSyncResultLabel(syncResult)); //pnl_.m_bitmapStatus->SetToolTip(); -> redundant //show status on Windows 7 taskbar if (taskbar_.get()) - switch (resultStatus) + switch (syncResult) { case SyncResult::finishedSuccess: case SyncResult::finishedWarning: @@ -1360,7 +1356,7 @@ void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult resultStatus } //---------------------------------- - setExternalStatus(getSyncResultLabel(resultStatus), wxString()); + setExternalStatus(getSyncResultLabel(syncResult), wxString()); //this->EnableCloseButton(true); @@ -1440,35 +1436,12 @@ void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult resultStatus pnl_.m_panelItemStats->Layout(); pnl_.m_panelTimeStats->Layout(); - //play (optional) sound notification after sync has completed -> only play when waiting on results dialog, seems to be pointless otherwise! - switch (resultStatus) - { - case SyncResult::aborted: - warn_static("we really should play sound if cancel on error is set, and only not play if user-aborted") - break; - case SyncResult::finishedError: - case SyncResult::finishedWarning: - case SyncResult::finishedSuccess: - if (!soundFileSyncComplete_.empty()) - { - //wxWidgets shows modal error dialog by default => NO! - wxLog* oldLogTarget = wxLog::SetActiveTarget(new wxLogStderr); //transfer and receive ownership! - ZEN_ON_SCOPE_EXIT(delete wxLog::SetActiveTarget(oldLogTarget)); - - wxSound::Play(utfTo<wxString>(soundFileSyncComplete_), wxSOUND_ASYNC); - } - - //if (::GetForegroundWindow() != GetHWND()) - // RequestUserAttention(); -> probably too much since task bar is already colorized with Taskbar::STATUS_ERROR or STATUS_NORMAL - break; - } - //Raise(); -> don't! user may be watching a movie in the meantime ;) note: resumeFromSystray() also calls Raise()! } template <class TopLevelDialog> -auto SyncProgressDialogImpl<TopLevelDialog>::destroy(bool autoClose, bool restoreParentFrame, SyncResult resultStatus, const SharedRef<const ErrorLog>& log) -> Result +auto SyncProgressDialogImpl<TopLevelDialog>::destroy(bool autoClose, bool restoreParentFrame, SyncResult syncResult, const SharedRef<const ErrorLog>& log) -> Result { if (autoClose) { @@ -1480,7 +1453,7 @@ auto SyncProgressDialogImpl<TopLevelDialog>::destroy(bool autoClose, bool restor } else { - showSummary(resultStatus, log); + showSummary(syncResult, log); //wait until user closes the dialog by pressing "okay" while (!okayPressed_) @@ -1645,9 +1618,8 @@ SyncProgressDialog* SyncProgressDialog::create(const std::function<void()>& user wxFrame* parentWindow, //may be nullptr bool showProgress, bool autoCloseDialog, - const std::chrono::system_clock::time_point& syncStartTime, const std::vector<std::wstring>& jobNames, - const Zstring& soundFileSyncComplete, + const std::chrono::system_clock::time_point& syncStartTime, bool ignoreErrors, size_t automaticRetryCount, PostSyncAction2 postSyncAction) @@ -1657,12 +1629,12 @@ SyncProgressDialog* SyncProgressDialog::create(const std::function<void()>& user //due to usual "wxBugs", wxDialog on OS X does not float on its parent; wxFrame OTOH does => hack! //https://groups.google.com/forum/#!topic/wx-users/J5SjjLaBOQE return new SyncProgressDialogImpl<wxDialog>(wxDEFAULT_DIALOG_STYLE | wxMAXIMIZE_BOX | wxMINIMIZE_BOX | wxRESIZE_BORDER, [&](wxDialog& progDlg) { return parentWindow; }, - userRequestAbort, syncStat, parentWindow, showProgress, autoCloseDialog, syncStartTime, jobNames, soundFileSyncComplete, ignoreErrors, automaticRetryCount, postSyncAction); + userRequestAbort, syncStat, parentWindow, showProgress, autoCloseDialog, jobNames, syncStartTime, ignoreErrors, automaticRetryCount, postSyncAction); } else //FFS batch job { auto dlg = new SyncProgressDialogImpl<wxFrame>(wxDEFAULT_FRAME_STYLE, [](wxFrame& progDlg) { return &progDlg; }, - userRequestAbort, syncStat, parentWindow, showProgress, autoCloseDialog, syncStartTime, jobNames, soundFileSyncComplete, ignoreErrors, automaticRetryCount, postSyncAction); + userRequestAbort, syncStat, parentWindow, showProgress, autoCloseDialog, jobNames, syncStartTime, ignoreErrors, automaticRetryCount, postSyncAction); //only top level windows should have an icon: dlg->SetIcon(getFfsIcon()); diff --git a/FreeFileSync/Source/ui/progress_indicator.h b/FreeFileSync/Source/ui/progress_indicator.h index 300078fa..1be7defc 100644 --- a/FreeFileSync/Source/ui/progress_indicator.h +++ b/FreeFileSync/Source/ui/progress_indicator.h @@ -62,14 +62,13 @@ struct SyncProgressDialog wxFrame* parentWindow, //may be nullptr bool showProgress, bool autoCloseDialog, - const std::chrono::system_clock::time_point& syncStartTime, const std::vector<std::wstring>& jobNames, - const Zstring& soundFileSyncComplete, + const std::chrono::system_clock::time_point& syncStartTime, bool ignoreErrors, size_t automaticRetryCount, PostSyncAction2 postSyncAction); struct Result { bool autoCloseDialog; }; - virtual Result destroy(bool autoClose, bool restoreParentFrame, SyncResult resultStatus, const zen::SharedRef<const zen::ErrorLog>& log) = 0; + virtual Result destroy(bool autoClose, bool restoreParentFrame, SyncResult syncResult, const zen::SharedRef<const zen::ErrorLog>& log) = 0; //--------------------------------------------------------------------------- virtual wxWindow* getWindowIfVisible() = 0; //may be nullptr; don't abuse, use as parent for modal dialogs only! diff --git a/FreeFileSync/Source/ui/small_dlgs.cpp b/FreeFileSync/Source/ui/small_dlgs.cpp index 1e38285c..9ed7713b 100644 --- a/FreeFileSync/Source/ui/small_dlgs.cpp +++ b/FreeFileSync/Source/ui/small_dlgs.cpp @@ -213,7 +213,7 @@ private: static bool acceptFileDrop(const std::vector<Zstring>& shellItemPaths); void onKeyFileDropped(FileDropEvent& event); - Zstring getFolderPathPhrase() const; + AbstractPath getFolderPath() const; enum class CloudType { @@ -305,48 +305,51 @@ CloudSetupDlg::CloudSetupDlg(wxWindow* parent, Zstring& folderPathPhrase, size_t if (acceptsItemPathPhraseGdrive(folderPathPhrase)) { type_ = CloudType::gdrive; - const GdrivePath gdrivePath = getResolvedGooglePath(folderPathPhrase); //noexcept + const AbstractPath folderPath = createItemPathGdrive(folderPathPhrase); + const Zstring userEmail = extractGdriveEmail(folderPath.afsDevice); //noexcept - const int selIdx = m_listBoxGdriveUsers->FindString(utfTo<wxString>(gdrivePath.userEmail), false /*caseSensitive*/); - if (selIdx != wxNOT_FOUND) + if (const int selIdx = m_listBoxGdriveUsers->FindString(utfTo<wxString>(userEmail), false /*caseSensitive*/); + selIdx != wxNOT_FOUND) { m_listBoxGdriveUsers->EnsureVisible(selIdx); m_listBoxGdriveUsers->SetSelection(selIdx); } else m_listBoxGdriveUsers->DeselectAll(); - m_staticTextGdriveUser->SetLabel (utfTo<wxString>(gdrivePath.userEmail)); - m_textCtrlServerPath ->ChangeValue(utfTo<wxString>(FILE_NAME_SEPARATOR + gdrivePath.itemPath.value)); + m_staticTextGdriveUser->SetLabel (utfTo<wxString>(userEmail)); + m_textCtrlServerPath ->ChangeValue(utfTo<wxString>(FILE_NAME_SEPARATOR + folderPath.afsPath.value)); } else if (acceptsItemPathPhraseSftp(folderPathPhrase)) { type_ = CloudType::sftp; - const SftpPathInfo pi = getResolvedSftpPath(folderPathPhrase); //noexcept - - if (pi.login.port > 0) - m_textCtrlPort->ChangeValue(numberTo<wxString>(pi.login.port)); - m_textCtrlServer ->ChangeValue(utfTo<wxString>(pi.login.server)); - m_textCtrlUserName ->ChangeValue(utfTo<wxString>(pi.login.username)); - sftpAuthType_ = pi.login.authType; - m_textCtrlPasswordHidden->ChangeValue(utfTo<wxString>(pi.login.password)); - m_textCtrlKeyfilePath ->ChangeValue(utfTo<wxString>(pi.login.privateKeyFilePath)); - m_textCtrlServerPath ->ChangeValue(utfTo<wxString>(FILE_NAME_SEPARATOR + pi.afsPath.value)); - m_spinCtrlTimeout ->SetValue(pi.login.timeoutSec); - m_spinCtrlChannelCountSftp->SetValue(pi.login.traverserChannelsPerConnection); + const AbstractPath folderPath = createItemPathSftp(folderPathPhrase); + const SftpLoginInfo login = extractSftpLogin(folderPath.afsDevice); //noexcept + + if (login.port > 0) + m_textCtrlPort->ChangeValue(numberTo<wxString>(login.port)); + m_textCtrlServer ->ChangeValue(utfTo<wxString>(login.server)); + m_textCtrlUserName ->ChangeValue(utfTo<wxString>(login.username)); + sftpAuthType_ = login.authType; + m_textCtrlPasswordHidden->ChangeValue(utfTo<wxString>(login.password)); + m_textCtrlKeyfilePath ->ChangeValue(utfTo<wxString>(login.privateKeyFilePath)); + m_textCtrlServerPath ->ChangeValue(utfTo<wxString>(FILE_NAME_SEPARATOR + folderPath.afsPath.value)); + m_spinCtrlTimeout ->SetValue(login.timeoutSec); + m_spinCtrlChannelCountSftp->SetValue(login.traverserChannelsPerConnection); } else if (acceptsItemPathPhraseFtp(folderPathPhrase)) { type_ = CloudType::ftp; - const FtpPathInfo pi = getResolvedFtpPath(folderPathPhrase); //noexcept - - if (pi.login.port > 0) - m_textCtrlPort->ChangeValue(numberTo<wxString>(pi.login.port)); - m_textCtrlServer ->ChangeValue(utfTo<wxString>(pi.login.server)); - m_textCtrlUserName ->ChangeValue(utfTo<wxString>(pi.login.username)); - m_textCtrlPasswordHidden ->ChangeValue(utfTo<wxString>(pi.login.password)); - m_textCtrlServerPath ->ChangeValue(utfTo<wxString>(FILE_NAME_SEPARATOR + pi.afsPath.value)); - (pi.login.useTls ? m_radioBtnEncryptSsl : m_radioBtnEncryptNone)->SetValue(true); - m_spinCtrlTimeout ->SetValue(pi.login.timeoutSec); + const AbstractPath folderPath = createItemPathFtp(folderPathPhrase); + const FtpLoginInfo login = extractFtpLogin(folderPath.afsDevice); //noexcept + + if (login.port > 0) + m_textCtrlPort->ChangeValue(numberTo<wxString>(login.port)); + m_textCtrlServer ->ChangeValue(utfTo<wxString>(login.server)); + m_textCtrlUserName ->ChangeValue(utfTo<wxString>(login.username)); + m_textCtrlPasswordHidden ->ChangeValue(utfTo<wxString>(login.password)); + m_textCtrlServerPath ->ChangeValue(utfTo<wxString>(FILE_NAME_SEPARATOR + folderPath.afsPath.value)); + (login.useTls ? m_radioBtnEncryptSsl : m_radioBtnEncryptNone)->SetValue(true); + m_spinCtrlTimeout ->SetValue(login.timeoutSec); } m_spinCtrlConnectionCount->SetValue(parallelOps); @@ -452,10 +455,9 @@ void CloudSetupDlg::OnGdriveUserSelect(wxCommandEvent& event) void CloudSetupDlg::OnDetectServerChannelLimit(wxCommandEvent& event) { assert (type_ == CloudType::sftp); - const SftpPathInfo pi = getResolvedSftpPath(getFolderPathPhrase()); //noexcept try { - const int channelCountMax = getServerMaxChannelsPerConnection(pi.login); //throw FileError + const int channelCountMax = getServerMaxChannelsPerConnection(extractSftpLogin(getFolderPath().afsDevice)); //throw FileError m_spinCtrlChannelCountSftp->SetValue(channelCountMax); } catch (const FileError& e) @@ -586,13 +588,15 @@ void CloudSetupDlg::updateGui() } -Zstring CloudSetupDlg::getFolderPathPhrase() const +AbstractPath CloudSetupDlg::getFolderPath() const { + //clean up (messy) user input, but no trim: support folders with trailing blanks! + const AfsPath serverRelPath = sanitizeDeviceRelativePath(utfTo<Zstring>(m_textCtrlServerPath->GetValue())); + switch (type_) { case CloudType::gdrive: - return condenseToGoogleFolderPathPhrase(utfTo<Zstring>(m_staticTextGdriveUser->GetLabel()), - utfTo<Zstring>(m_textCtrlServerPath ->GetValue())); //noexcept + return AbstractPath(condenseToGdriveDevice(utfTo<Zstring>(m_staticTextGdriveUser->GetLabel())), serverRelPath); //noexcept case CloudType::sftp: { @@ -605,10 +609,7 @@ Zstring CloudSetupDlg::getFolderPathPhrase() const login.password = utfTo<Zstring>((m_checkBoxShowPassword->GetValue() ? m_textCtrlPasswordVisible : m_textCtrlPasswordHidden)->GetValue()); login.timeoutSec = m_spinCtrlTimeout->GetValue(); login.traverserChannelsPerConnection = m_spinCtrlChannelCountSftp->GetValue(); - - auto serverPath = utfTo<Zstring>(m_textCtrlServerPath->GetValue()); - //clean up (messy) user input: - return condenseToSftpFolderPathPhrase(login, serverPath); //noexcept + return AbstractPath(condenseToSftpDevice(login), serverRelPath); //noexcept } case CloudType::ftp: @@ -620,28 +621,26 @@ Zstring CloudSetupDlg::getFolderPathPhrase() const login.password = utfTo<Zstring>((m_checkBoxShowPassword->GetValue() ? m_textCtrlPasswordVisible : m_textCtrlPasswordHidden)->GetValue()); login.useTls = m_radioBtnEncryptSsl->GetValue(); login.timeoutSec = m_spinCtrlTimeout->GetValue(); - - auto serverPath = utfTo<Zstring>(m_textCtrlServerPath->GetValue()); - //clean up (messy) user input: - return condenseToFtpFolderPathPhrase(login, serverPath); //noexcept + return AbstractPath(condenseToFtpDevice(login), serverRelPath); //noexcept } } assert(false); - return Zstr(""); + return createAbstractPath(Zstr("")); } void CloudSetupDlg::OnBrowseCloudFolder(wxCommandEvent& event) { - AbstractPath folderPath = createAbstractPath(getFolderPathPhrase()); //noexcept + AbstractPath folderPath = getFolderPath(); //noexcept if (!AFS::getParentPath(folderPath)) try //for (S)FTP it makes more sense to start with the home directory rather than root (which often denies access!) { if (type_ == CloudType::sftp) - folderPath.afsPath = getSftpHomePath(getResolvedSftpPath(getFolderPathPhrase()).login); //throw FileError + folderPath.afsPath = getSftpHomePath(extractSftpLogin(folderPath.afsDevice)); //throw FileError + if (type_ == CloudType::ftp) - folderPath.afsPath = getFtpHomePath(getResolvedFtpPath(getFolderPathPhrase()).login); //throw FileError + folderPath.afsPath = getFtpHomePath(extractFtpLogin(folderPath.afsDevice)); //throw FileError } catch (const FileError& e) { @@ -667,7 +666,7 @@ void CloudSetupDlg::OnOkay(wxCommandEvent& event) } //------------------------------------------------------------- - folderPathPhraseOut_ = getFolderPathPhrase(); + folderPathPhraseOut_ = AFS::getInitPathPhrase(getFolderPath()); parallelOpsOut_ = m_spinCtrlConnectionCount->GetValue(); EndModal(ReturnSmallDlg::BUTTON_OKAY); @@ -1116,6 +1115,7 @@ OptionsDlg::OptionsDlg(wxWindow* parent, XmlGlobalSettings& globalSettings) : m_bitmapWarnings ->SetBitmap(shrinkImage(getResourceImage(L"msg_warning").ConvertToImage(), fastFromDIP(20))); m_bitmapLogFile ->SetBitmap(shrinkImage(getResourceImage(L"log_file" ).ConvertToImage(), fastFromDIP(20))); m_bitmapNotificationSounds->SetBitmap (getResourceImage(L"notification_sounds")); + m_bitmapConsole ->SetBitmap(shrinkImage(getResourceImage(L"command_line").ConvertToImage(), fastFromDIP(20))); m_bitmapCompareDone ->SetBitmap (getResourceImage(L"compare_sicon")); m_bitmapSyncDone ->SetBitmap (getResourceImage(L"file_sync_sicon")); m_bpButtonPlayCompareDone ->SetBitmapLabel(getResourceImage(L"play_sound")); @@ -1123,7 +1123,7 @@ OptionsDlg::OptionsDlg(wxWindow* parent, XmlGlobalSettings& globalSettings) : m_bpButtonAddRow ->SetBitmapLabel(getResourceImage(L"item_add")); m_bpButtonRemoveRow ->SetBitmapLabel(getResourceImage(L"item_remove")); - m_staticTextAllDialogsShown->SetLabel(L'(' + _("All dialogs shown") + L')'); + m_staticTextAllDialogsShown->SetLabel(L'(' + _("No dialogs hidden") + L')'); m_staticTextResetDialogs->Wrap(std::max(fastFromDIP(250), m_buttonRestoreDialogs ->GetSize().x + @@ -1386,7 +1386,7 @@ void OptionsDlg::OnShowLogFolder(wxHyperlinkEvent& event) { try { - openWithDefaultApplication(getDefaultLogFolderPath()); //throw FileError + openWithDefaultApp(getDefaultLogFolderPath()); //throw FileError } catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } } diff --git a/FreeFileSync/Source/ui/status_handler_impl.h b/FreeFileSync/Source/ui/status_handler_impl.h new file mode 100644 index 00000000..b20ecaf0 --- /dev/null +++ b/FreeFileSync/Source/ui/status_handler_impl.h @@ -0,0 +1,69 @@ +// ***************************************************************************** +// * 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 STATUS_HANDLER_IMPL_H_145234543248059083415565 +#define STATUS_HANDLER_IMPL_H_145234543248059083415565 + +//#include <vector> +#include <chrono> +#include <thread> +#include <zen/zstring.h> +//#include <string> +#include <zen/i18n.h> +//#include <zen/string_tools.h> +//#include <zen/basic_math.h> +//#include "base/process_callback.h" +//#include "return_codes.h" + + +namespace fff +{ +namespace +{ +void delayAndCountDown(const std::wstring& operationName, std::chrono::seconds delay, const std::function<void(const std::wstring& msg)>& notifyStatus) +{ + assert(notifyStatus && !zen::endsWith(operationName, L".")); + + const auto delayUntil = std::chrono::steady_clock::now() + delay; + for (auto now = std::chrono::steady_clock::now(); now < delayUntil; now = std::chrono::steady_clock::now()) + { + const auto timeMs = std::chrono::duration_cast<std::chrono::milliseconds>(delayUntil - now).count(); + if (notifyStatus) + notifyStatus(operationName + L"... " + _P("1 sec", "%x sec", numeric::integerDivideRoundUp(timeMs, 1000))); + + std::this_thread::sleep_for(UI_UPDATE_INTERVAL / 2); + } +} + + +void runCommandAndLogErrors(const Zstring& cmdLine, zen::ErrorLog& errorLog) +{ + using namespace zen; + + 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)), output)); + + errorLog.logMsg(_("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 :> + { + errorLog.logMsg(_("Executing command:") + L' ' + utfTo<std::wstring>(cmdLine), MSG_TYPE_INFO); + } + catch (const SysError& e) + { + errorLog.logMsg(replaceCpy(_("Command %x failed."), L"%x", fmtPath(cmdLine)) + L"\n\n" + e.toString(), MSG_TYPE_ERROR); + } +} +} +} + +#endif //STATUS_HANDLER_IMPL_H_145234543248059083415565 diff --git a/FreeFileSync/Source/ui/sync_cfg.cpp b/FreeFileSync/Source/ui/sync_cfg.cpp index fe3c00da..b637e0c7 100644 --- a/FreeFileSync/Source/ui/sync_cfg.cpp +++ b/FreeFileSync/Source/ui/sync_cfg.cpp @@ -436,7 +436,7 @@ showMultipleCfgs_(showMultipleCfgs) assert(!m_listBoxFolderPair->IsSorted()); - m_listBoxFolderPair->Append(_("Main config")); + m_listBoxFolderPair->Append(_("All folder pairs")); for (const LocalPairConfig& lpc : localPairConfig) { std::wstring fpName = getShortDisplayNameForFolderPair(createAbstractPath(lpc.folderPathPhraseLeft ), diff --git a/FreeFileSync/Source/ui/tray_icon.cpp b/FreeFileSync/Source/ui/tray_icon.cpp index 883f4235..2e2ada8c 100644 --- a/FreeFileSync/Source/ui/tray_icon.cpp +++ b/FreeFileSync/Source/ui/tray_icon.cpp @@ -133,7 +133,7 @@ public: //Windows User Experience Guidelines: show the context menu rather than doing *nothing* on single left clicks; however: //MSDN: "Double-clicking the left mouse button actually generates a sequence of four messages: WM_LBUTTONDOWN, WM_LBUTTONUP, WM_LBUTTONDBLCLK, and WM_LBUTTONUP." - //Reference: https://msdn.microsoft.com/en-us/library/windows/desktop/ms645606 + //Reference: https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttondblclk //=> the only way to distinguish single left click and double-click is to wait wxSystemSettings::GetMetric(wxSYS_DCLICK_MSEC) (480ms) which is way too long! } diff --git a/FreeFileSync/Source/ui/tree_grid.cpp b/FreeFileSync/Source/ui/tree_grid.cpp index e9a4ab62..4277111c 100644 --- a/FreeFileSync/Source/ui/tree_grid.cpp +++ b/FreeFileSync/Source/ui/tree_grid.cpp @@ -1055,7 +1055,7 @@ private: { case WXK_LEFT: case WXK_NUMPAD_LEFT: - case WXK_NUMPAD_SUBTRACT: //https://msdn.microsoft.com/en-us/library/ms971323#atg_keyboardshortcuts_windows_shortcut_keys + case WXK_NUMPAD_SUBTRACT: //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/dnacc/guidelines-for-keyboard-user-interface-design#atg_keyboardshortcuts_windows_shortcut_keys switch (treeDataView_.getStatus(row)) { case TreeView::STATUS_EXPANDED: diff --git a/FreeFileSync/Source/version/version.h b/FreeFileSync/Source/version/version.h index d101d97a..e45bf525 100644 --- a/FreeFileSync/Source/version/version.h +++ b/FreeFileSync/Source/version/version.h @@ -3,7 +3,7 @@ namespace fff { -const char ffsVersion[] = "10.22"; //internal linkage! +const char ffsVersion[] = "10.23"; //internal linkage! const char FFS_VERSION_SEPARATOR = '.'; } diff --git a/libcurl/curl_wrap.h b/libcurl/curl_wrap.h index 40694e71..40a330ef 100644 --- a/libcurl/curl_wrap.h +++ b/libcurl/curl_wrap.h @@ -151,7 +151,7 @@ void applyCurlOptions(CURL* easyHandle, const std::vector<CurlOption>& options) { const CURLcode rc = ::curl_easy_setopt(easyHandle, opt.option, opt.value); if (rc != CURLE_OK) - throw SysError(formatSystemError(L"curl_easy_setopt " + numberTo<std::wstring>(static_cast<int>(opt.option)), + throw SysError(formatSystemError("curl_easy_setopt(" + numberTo<std::string>(static_cast<int>(opt.option)) + ")", formatCurlStatusCode(rc), utfTo<std::wstring>(::curl_easy_strerror(rc)))); } } diff --git a/libcurl/rest.cpp b/libcurl/rest.cpp index 0d14dfc2..9b609935 100644 --- a/libcurl/rest.cpp +++ b/libcurl/rest.cpp @@ -33,7 +33,7 @@ HttpSession::Result HttpSession::perform(const std::string& serverRelPath, { easyHandle_ = ::curl_easy_init(); if (!easyHandle_) - throw SysError(formatSystemError(L"curl_easy_init", formatCurlStatusCode(CURLE_OUT_OF_MEMORY), std::wstring())); + throw SysError(formatSystemError("curl_easy_init", formatCurlStatusCode(CURLE_OUT_OF_MEMORY), L"")); } else ::curl_easy_reset(easyHandle_); @@ -168,7 +168,7 @@ HttpSession::Result HttpSession::perform(const std::string& serverRelPath, std::wstring errorMsg = trimCpy(utfTo<std::wstring>(curlErrorBuf)); //optional if (httpStatus != 0) //optional - errorMsg += (errorMsg.empty() ? L"" : L"\n") + formatHttpStatus(httpStatus); + errorMsg += (errorMsg.empty() ? L"" : L"\n") + formatHttpError(httpStatus); #if 0 //utfTo<std::wstring>(::curl_easy_strerror(ec)) is uninteresting //use CURLINFO_OS_ERRNO ?? https://curl.haxx.se/libcurl/c/CURLINFO_OS_ERRNO.html @@ -177,7 +177,7 @@ HttpSession::Result HttpSession::perform(const std::string& serverRelPath, if (nativeErrorCode != 0) errorMsg += (errorMsg.empty() ? L"" : L"\n") + std::wstring(L"Native error code: ") + numberTo<std::wstring>(nativeErrorCode); #endif - throw SysError(formatSystemError(L"curl_easy_perform", formatCurlStatusCode(rcPerf), errorMsg)); + throw SysError(formatSystemError("curl_easy_perform", formatCurlStatusCode(rcPerf), errorMsg)); } lastSuccessfulUseTime_ = std::chrono::steady_clock::now(); diff --git a/xBRZ/src/xbrz.cpp b/xBRZ/src/xbrz.cpp index e2c25810..81153375 100644 --- a/xBRZ/src/xbrz.cpp +++ b/xBRZ/src/xbrz.cpp @@ -7,6 +7,7 @@ // * to link the code of this program with the following libraries * // * (or with modified versions that use the same licenses), and distribute * // * linked combinations including the two: MAME, FreeFileSync, Snes9x, ePSXe * +// * * // * You must obey the GNU General Public License in all respects for all of * // * the code used other than MAME, FreeFileSync, Snes9x, ePSXe. * // * If you modify this file, you may extend this exception to your version * @@ -15,10 +16,10 @@ // **************************************************************************** #include "xbrz.h" -#include <cassert> -#include <vector> #include <algorithm> +#include <cassert> #include <cmath> //std::sqrt +#include <vector> #include "xbrz_tools.h" using namespace xbrz; @@ -1256,8 +1257,8 @@ void bilinearScaleCpu(const uint32_t* src, int srcWidth, int srcHeight, void bilinearScaleAmp(const uint32_t* src, int srcWidth, int srcHeight, //throw concurrency::runtime_exception /**/ uint32_t* trg, int trgWidth, int trgHeight) { - //C++ AMP reference: https://msdn.microsoft.com/en-us/library/hh289390.aspx - //introduction to C++ AMP: https://msdn.microsoft.com/en-us/magazine/hh882446.aspx + //C++ AMP reference: https://docs.microsoft.com/en-us/cpp/parallel/amp/reference/reference-cpp-amp + //introduction to C++ AMP: https://docs.microsoft.com/en-us/archive/msdn-magazine/2012/april/c-a-code-based-introduction-to-c-amp using namespace concurrency; //TODO: pitch @@ -1276,7 +1277,7 @@ void bilinearScaleAmp(const uint32_t* src, int srcWidth, int srcHeight, //throw const int x = idx[1]; //Perf notes: // -> float-based calculation is (almost) 2x as fas as double! - // -> no noticeable improvement via tiling: https://msdn.microsoft.com/en-us/magazine/hh882447.aspx + // -> no noticeable improvement via tiling: https://docs.microsoft.com/en-us/archive/msdn-magazine/2012/april/c-amp-introduction-to-tiling-in-c-amp // -> no noticeable improvement with restrict(amp,cpu) // -> iterating over y-axis only is significantly slower! // -> pre-calculating x,y-dependent variables in a buffer + array_view<> is ~ 20 % slower! diff --git a/xBRZ/src/xbrz.h b/xBRZ/src/xbrz.h index 492fb43a..c0778cf1 100644 --- a/xBRZ/src/xbrz.h +++ b/xBRZ/src/xbrz.h @@ -7,6 +7,7 @@ // * to link the code of this program with the following libraries * // * (or with modified versions that use the same licenses), and distribute * // * linked combinations including the two: MAME, FreeFileSync, Snes9x, ePSXe * +// * * // * You must obey the GNU General Public License in all respects for all of * // * the code used other than MAME, FreeFileSync, Snes9x, ePSXe. * // * If you modify this file, you may extend this exception to your version * diff --git a/xBRZ/src/xbrz_config.h b/xBRZ/src/xbrz_config.h index fcfda99a..84f82dcf 100644 --- a/xBRZ/src/xbrz_config.h +++ b/xBRZ/src/xbrz_config.h @@ -7,6 +7,7 @@ // * to link the code of this program with the following libraries * // * (or with modified versions that use the same licenses), and distribute * // * linked combinations including the two: MAME, FreeFileSync, Snes9x, ePSXe * +// * * // * You must obey the GNU General Public License in all respects for all of * // * the code used other than MAME, FreeFileSync, Snes9x, ePSXe. * // * If you modify this file, you may extend this exception to your version * diff --git a/xBRZ/src/xbrz_tools.h b/xBRZ/src/xbrz_tools.h index b8bb8aa0..cd6acc63 100644 --- a/xBRZ/src/xbrz_tools.h +++ b/xBRZ/src/xbrz_tools.h @@ -7,6 +7,7 @@ // * to link the code of this program with the following libraries * // * (or with modified versions that use the same licenses), and distribute * // * linked combinations including the two: MAME, FreeFileSync, Snes9x, ePSXe * +// * * // * You must obey the GNU General Public License in all respects for all of * // * the code used other than MAME, FreeFileSync, Snes9x, ePSXe. * // * If you modify this file, you may extend this exception to your version * diff --git a/zen/basic_math.h b/zen/basic_math.h index 26dda9a6..0a226555 100644 --- a/zen/basic_math.h +++ b/zen/basic_math.h @@ -271,7 +271,7 @@ template <class InputIterator> inline double stdDeviation(InputIterator first, InputIterator last, double* arithMean) { //implementation minimizing rounding errors, see: https://en.wikipedia.org/wiki/Standard_deviation - //combined with technique avoiding overflow, see: http://www.netlib.org/blas/dnrm2.f -> only 10% performance degradation + //combined with technique avoiding overflow, see: https://www.netlib.org/blas/dnrm2.f -> only 10% performance degradation size_t n = 0; double mean = 0; diff --git a/zen/dir_watcher.cpp b/zen/dir_watcher.cpp index 307b48e5..d02e229e 100644 --- a/zen/dir_watcher.cpp +++ b/zen/dir_watcher.cpp @@ -52,7 +52,7 @@ DirWatcher::DirWatcher(const Zstring& dirPath) : //throw FileError //init pimpl_->notifDescr = ::inotify_init(); if (pimpl_->notifDescr == -1) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(baseDirPath_)), L"inotify_init"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(baseDirPath_)), "inotify_init"); ZEN_ON_SCOPE_FAIL( ::close(pimpl_->notifDescr); ); @@ -61,10 +61,10 @@ DirWatcher::DirWatcher(const Zstring& dirPath) : //throw FileError { int flags = ::fcntl(pimpl_->notifDescr, F_GETFL); if (flags != -1) - initSuccess = ::fcntl(pimpl_->notifDescr, F_SETFL, flags | O_NONBLOCK) != -1; + initSuccess = ::fcntl(pimpl_->notifDescr, F_SETFL, flags | O_NONBLOCK) == 0; } if (!initSuccess) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(baseDirPath_)), L"fcntl"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(baseDirPath_)), "fcntl"); //add watches for (const Zstring& subDirPath : fullFolderList) @@ -85,10 +85,10 @@ DirWatcher::DirWatcher(const Zstring& dirPath) : //throw FileError const ErrorCode ec = getLastError(); //copy before directly/indirectly making other system calls! if (ec == ENOSPC) //fix misleading system message "No space left on device" throw FileError(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(subDirPath)), - formatSystemError(L"inotify_add_watch", L"ENOSPC", + formatSystemError("inotify_add_watch", L"ENOSPC", L"The user limit on the total number of inotify watches was reached or the kernel failed to allocate a needed resource.")); - throw FileError(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(subDirPath)), formatSystemError(L"inotify_add_watch", ec)); + throw FileError(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(subDirPath)), formatSystemError("inotify_add_watch", ec)); } pimpl_->watchedPaths.emplace(wd, subDirPath); @@ -102,7 +102,7 @@ DirWatcher::~DirWatcher() } -std::vector<DirWatcher::Entry> DirWatcher::getChanges(const std::function<void()>& requestUiUpdate, std::chrono::milliseconds cbInterval) //throw FileError +std::vector<DirWatcher::Change> DirWatcher::fetchChanges(const std::function<void()>& requestUiUpdate, std::chrono::milliseconds cbInterval) //throw FileError { std::vector<std::byte> buffer(512 * (sizeof(struct ::inotify_event) + NAME_MAX + 1)); @@ -117,12 +117,12 @@ std::vector<DirWatcher::Entry> DirWatcher::getChanges(const std::function<void() if (bytesRead < 0) { if (errno == EAGAIN) //this error is ignored in all inotify wrappers I found - return std::vector<Entry>(); + return std::vector<Change>(); - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(baseDirPath_)), L"read"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot monitor directory %x."), L"%x", fmtPath(baseDirPath_)), "read"); } - std::vector<Entry> output; + std::vector<Change> output; ssize_t bytePos = 0; while (bytePos < bytesRead) @@ -140,15 +140,15 @@ std::vector<DirWatcher::Entry> DirWatcher::getChanges(const std::function<void() if ((evt.mask & IN_CREATE) || (evt.mask & IN_MOVED_TO)) - output.push_back({ ACTION_CREATE, itemPath }); + output.push_back({ ChangeType::create, itemPath }); else if ((evt.mask & IN_MODIFY) || (evt.mask & IN_CLOSE_WRITE)) - output.push_back({ ACTION_UPDATE, itemPath }); + output.push_back({ ChangeType::update, itemPath }); else if ((evt.mask & IN_DELETE ) || (evt.mask & IN_DELETE_SELF) || (evt.mask & IN_MOVE_SELF ) || (evt.mask & IN_MOVED_FROM)) - output.push_back({ ACTION_DELETE, itemPath }); + output.push_back({ ChangeType::remove, itemPath }); } } bytePos += sizeof(struct ::inotify_event) + evt.len; diff --git a/zen/dir_watcher.h b/zen/dir_watcher.h index 4d514e89..3103039d 100644 --- a/zen/dir_watcher.h +++ b/zen/dir_watcher.h @@ -16,23 +16,23 @@ namespace zen { -//Windows: ReadDirectoryChangesW https://msdn.microsoft.com/en-us/library/aa365465 +//Windows: ReadDirectoryChangesW https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw //Linux: inotify https://linux.die.net/man/7/inotify -//OS X: kqueue https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man2/kqueue.2.html +//macOS: kqueue https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man2/kqueue.2.html //watch directory including subdirectories /* !Note handling of directories!: Windows: removal of top watched directory is NOT notified when watching the dir handle, e.g. brute force usb stick removal, (watchting for GUID_DEVINTERFACE_WPD OTOH works fine!) - however manual unmount IS notified (e.g. usb stick removal, then re-insert), but watching is stopped! + however manual unmount IS notified (e.g. USB stick removal, then re-insert), but watching is stopped! Renaming of top watched directory handled incorrectly: Not notified(!) + additional changes in subfolders now do report FILE_ACTION_MODIFIED for directory (check that should prevent this fails!) Linux: newly added subdirectories are reported but not automatically added for watching! -> reset Dirwatcher! - removal of top watched directory is NOT notified! + removal of base directory is NOT notified! - OS X: everything works as expected; renaming of top level folder is also detected + macOS: everything works as expected; renaming of base directory is also detected Overcome all issues portably: check existence of top watched directory externally + reinstall watch after changes in directory structure (added directories) are detected */ @@ -42,21 +42,22 @@ public: DirWatcher(const Zstring& dirPath); //throw FileError ~DirWatcher(); - enum ActionType + enum class ChangeType { - ACTION_CREATE, //informal! - ACTION_UPDATE, //use for debugging/logging only! - ACTION_DELETE, // + create, //informal! + update, //use for debugging/logging only! + remove, // + baseFolderUnavailable, //1. not existing or 2. can't access }; - struct Entry + struct Change { - ActionType action = ACTION_CREATE; + ChangeType type = ChangeType::create; Zstring itemPath; }; //extract accumulated changes since last call - std::vector<Entry> getChanges(const std::function<void()>& requestUiUpdate, std::chrono::milliseconds cbInterval); //throw FileError + std::vector<Change> fetchChanges(const std::function<void()>& requestUiUpdate, std::chrono::milliseconds cbInterval); //throw FileError private: DirWatcher (const DirWatcher&) = delete; diff --git a/zen/file_access.cpp b/zen/file_access.cpp index 4f6704d2..cb5a45ed 100644 --- a/zen/file_access.cpp +++ b/zen/file_access.cpp @@ -98,7 +98,7 @@ ItemType zen::getItemType(const Zstring& itemPath) //throw FileError { struct ::stat itemInfo = {}; if (::lstat(itemPath.c_str(), &itemInfo) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(itemPath)), L"lstat"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(itemPath)), "lstat"); if (S_ISLNK(itemInfo.st_mode)) return ItemType::SYMLINK; @@ -174,7 +174,7 @@ uint64_t zen::getFreeDiskSpace(const Zstring& path) //throw FileError, returns 0 { struct ::statfs info = {}; if (::statfs(path.c_str(), &info) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot determine free disk space for %x."), L"%x", fmtPath(path)), L"statfs"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot determine free disk space for %x."), L"%x", fmtPath(path)), "statfs"); return static_cast<uint64_t>(info.f_bsize) * info.f_bavail; } @@ -184,7 +184,7 @@ VolumeId zen::getVolumeId(const Zstring& itemPath) //throw FileError { struct ::stat fileInfo = {}; if (::stat(itemPath.c_str(), &fileInfo) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(itemPath)), L"stat"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(itemPath)), "stat"); return fileInfo.st_dev; } @@ -194,7 +194,7 @@ uint64_t zen::getFileSize(const Zstring& filePath) //throw FileError { struct ::stat fileInfo = {}; if (::stat(filePath.c_str(), &fileInfo) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(filePath)), L"stat"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(filePath)), "stat"); return fileInfo.st_size; } @@ -211,7 +211,7 @@ Zstring zen::getTempFolderPath() //throw FileError void zen::removeFilePlain(const Zstring& filePath) //throw FileError { - const wchar_t functionName[] = L"unlink"; + const char* functionName = "unlink"; if (::unlink(filePath.c_str()) != 0) { ErrorCode ec = getLastError(); //copy before directly/indirectly making other system calls! @@ -231,7 +231,7 @@ void zen::removeSymlinkPlain(const Zstring& linkPath) //throw FileError void zen::removeDirectoryPlain(const Zstring& dirPath) //throw FileError { - const wchar_t functionName[] = L"rmdir"; + const char* functionName = "rmdir"; if (::rmdir(dirPath.c_str()) != 0) { ErrorCode ec = getLastError(); //copy before making other system calls! @@ -241,7 +241,7 @@ void zen::removeDirectoryPlain(const Zstring& dirPath) //throw FileError if (symlinkExists) { if (::unlink(dirPath.c_str()) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtPath(dirPath)), L"unlink"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtPath(dirPath)), "unlink"); return; } throw FileError(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtPath(dirPath)), formatSystemError(functionName, ec)); @@ -312,7 +312,7 @@ void moveAndRenameFileSub(const Zstring& pathFrom, const Zstring& pathTo, bool r auto throwException = [&](int ec) { const std::wstring errorMsg = replaceCpy(replaceCpy(_("Cannot move file %x to %y."), L"%x", L'\n' + fmtPath(pathFrom)), L"%y", L'\n' + fmtPath(pathTo)); - const std::wstring errorDescr = formatSystemError(L"rename", ec); + const std::wstring errorDescr = formatSystemError("rename", ec); if (ec == EXDEV) throw ErrorMoveUnsupported(errorMsg, errorDescr); @@ -329,7 +329,7 @@ void moveAndRenameFileSub(const Zstring& pathFrom, const Zstring& pathTo, bool r { struct ::stat infoSrc = {}; if (::lstat(pathFrom.c_str(), &infoSrc) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(pathFrom)), L"stat"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(pathFrom)), "stat"); struct ::stat infoTrg = {}; if (::lstat(pathTo.c_str(), &infoTrg) == 0) @@ -394,16 +394,16 @@ void setWriteTimeNative(const Zstring& itemPath, const struct ::timespec& modTim //in other cases utimensat() returns EINVAL for CIFS/NTFS drives, but open+futimens works: https://freefilesync.org/forum/viewtopic.php?t=387 const int fdFile = ::open(itemPath.c_str(), O_WRONLY | O_APPEND | O_CLOEXEC); //2017-07-04: O_WRONLY | O_APPEND seems to avoid EOPNOTSUPP on gvfs SFTP! if (fdFile == -1) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write modification time of %x."), L"%x", fmtPath(itemPath)), L"open"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write modification time of %x."), L"%x", fmtPath(itemPath)), "open"); ZEN_ON_SCOPE_EXIT(::close(fdFile)); if (::futimens(fdFile, newTimes) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write modification time of %x."), L"%x", fmtPath(itemPath)), L"futimens"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write modification time of %x."), L"%x", fmtPath(itemPath)), "futimens"); } else { if (::utimensat(AT_FDCWD, itemPath.c_str(), newTimes, AT_SYMLINK_NOFOLLOW) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write modification time of %x."), L"%x", fmtPath(itemPath)), L"utimensat"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write modification time of %x."), L"%x", fmtPath(itemPath)), "utimensat"); } } @@ -442,7 +442,7 @@ void copySecurityContext(const Zstring& source, const Zstring& target, ProcSymli errno == EOPNOTSUPP) //extended attributes are not supported by the filesystem return; - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read security context of %x."), L"%x", fmtPath(source)), L"getfilecon"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read security context of %x."), L"%x", fmtPath(source)), "getfilecon"); } ZEN_ON_SCOPE_EXIT(::freecon(contextSource)); @@ -470,7 +470,7 @@ void copySecurityContext(const Zstring& source, const Zstring& target, ProcSymli ::setfilecon(target.c_str(), contextSource) : ::lsetfilecon(target.c_str(), contextSource); if (rv3 < 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write security context of %x."), L"%x", fmtPath(target)), L"setfilecon"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write security context of %x."), L"%x", fmtPath(target)), "setfilecon"); } #endif } @@ -488,26 +488,26 @@ void zen::copyItemPermissions(const Zstring& sourcePath, const Zstring& targetPa if (procSl == ProcSymlink::FOLLOW) { if (::stat(sourcePath.c_str(), &fileInfo) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read permissions of %x."), L"%x", fmtPath(sourcePath)), L"stat"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read permissions of %x."), L"%x", fmtPath(sourcePath)), "stat"); if (::chown(targetPath.c_str(), fileInfo.st_uid, fileInfo.st_gid) != 0) // may require admin rights! - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(targetPath)), L"chown"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(targetPath)), "chown"); if (::chmod(targetPath.c_str(), fileInfo.st_mode) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(targetPath)), L"chmod"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(targetPath)), "chmod"); } else { if (::lstat(sourcePath.c_str(), &fileInfo) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read permissions of %x."), L"%x", fmtPath(sourcePath)), L"lstat"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read permissions of %x."), L"%x", fmtPath(sourcePath)), "lstat"); if (::lchown(targetPath.c_str(), fileInfo.st_uid, fileInfo.st_gid) != 0) // may require admin rights! - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(targetPath)), L"lchown"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(targetPath)), "lchown"); const bool isSymlinkTarget = getItemType(targetPath) == ItemType::SYMLINK; //throw FileError if (!isSymlinkTarget && //setting access permissions doesn't make sense for symlinks on Linux: there is no lchmod() ::chmod(targetPath.c_str(), fileInfo.st_mode) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(targetPath)), L"chmod"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(targetPath)), "chmod"); } } @@ -515,19 +515,25 @@ void zen::copyItemPermissions(const Zstring& sourcePath, const Zstring& targetPa void zen::createDirectory(const Zstring& dirPath) //throw FileError, ErrorTargetExisting { + auto getErrorMsg = [&] { return replaceCpy(_("Cannot create directory %x."), L"%x", fmtPath(dirPath)); }; + + //deliberately don't support creating irregular folders like "...." https://social.technet.microsoft.com/Forums/windows/en-US/ffee2322-bb6b-4fdf-86f9-8f93cf1fa6cb/ + if (endsWith(dirPath, Zstr(' ')) || + endsWith(dirPath, Zstr('.'))) + throw FileError(getErrorMsg(), replaceCpy<std::wstring>(L"Invalid trailing character \"%x\".", L"%x", utfTo<std::wstring>(dirPath.end()[-1]))); + const mode_t mode = S_IRWXU | S_IRWXG | S_IRWXO; //0777, default for newly created directories if (::mkdir(dirPath.c_str(), mode) != 0) { const int lastError = errno; //copy before directly or indirectly making other system calls! - const std::wstring errorMsg = replaceCpy(_("Cannot create directory %x."), L"%x", fmtPath(dirPath)); - const std::wstring errorDescr = formatSystemError(L"mkdir", lastError); + const std::wstring errorDescr = formatSystemError("mkdir", lastError); if (lastError == EEXIST) - throw ErrorTargetExisting(errorMsg, errorDescr); + throw ErrorTargetExisting(getErrorMsg(), errorDescr); //else if (lastError == ENOENT) // throw ErrorTargetPathMissing(errorMsg, errorDescr); - throw FileError(errorMsg, errorDescr); + throw FileError(getErrorMsg(), errorDescr); } } @@ -575,7 +581,7 @@ void zen::copySymlink(const Zstring& sourcePath, const Zstring& targetPath, bool const Zstring linkPath = getSymlinkTargetRaw(sourcePath); //throw FileError; accept broken symlinks if (::symlink(linkPath.c_str(), targetPath.c_str()) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(replaceCpy(_("Cannot copy symbolic link %x to %y."), L"%x", L'\n' + fmtPath(sourcePath)), L"%y", L'\n' + fmtPath(targetPath)), L"symlink"); + THROW_LAST_FILE_ERROR(replaceCpy(replaceCpy(_("Cannot copy symbolic link %x to %y."), L"%x", L'\n' + fmtPath(sourcePath)), L"%y", L'\n' + fmtPath(targetPath)), "symlink"); //allow only consistent objects to be created -> don't place before ::symlink(); targetPath may already exist! ZEN_ON_SCOPE_FAIL(try { removeSymlinkPlain(targetPath); /*throw FileError*/ } @@ -584,7 +590,7 @@ void zen::copySymlink(const Zstring& sourcePath, const Zstring& targetPath, bool //file times: essential for syncing a symlink: enforce this! (don't just try!) struct ::stat sourceInfo = {}; if (::lstat(sourcePath.c_str(), &sourceInfo) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(sourcePath)), L"lstat"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(sourcePath)), "lstat"); setWriteTimeNative(targetPath, sourceInfo.st_mtim, ProcSymlink::DIRECT); //throw FileError @@ -605,7 +611,7 @@ FileCopyResult copyFileOsSpecific(const Zstring& sourceFile, //throw FileError, struct ::stat sourceInfo = {}; if (::fstat(fileIn.getHandle(), &sourceInfo) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(sourceFile)), L"fstat"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(sourceFile)), "fstat"); const mode_t mode = sourceInfo.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO); //analog to "cp" which copies "mode" (considering umask) by default //it seems we don't need S_IWUSR, not even for the setFileTime() below! (tested with source file having different user/group!) @@ -616,7 +622,7 @@ FileCopyResult copyFileOsSpecific(const Zstring& sourceFile, //throw FileError, { const int ec = errno; //copy before making other system calls! const std::wstring errorMsg = replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(targetFile)); - const std::wstring errorDescr = formatSystemError(L"open", ec); + const std::wstring errorDescr = formatSystemError("open", ec); if (ec == EEXIST) throw ErrorTargetExisting(errorMsg, errorDescr); @@ -639,7 +645,7 @@ FileCopyResult copyFileOsSpecific(const Zstring& sourceFile, //throw FileError, struct ::stat targetInfo = {}; if (::fstat(fileOut.getHandle(), &targetInfo) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(targetFile)), L"fstat"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(targetFile)), "fstat"); //close output file handle before setting file time; also good place to catch errors when closing stream! fileOut.finalize(); //throw FileError, (X) essentially a close() since buffers were already flushed @@ -649,7 +655,7 @@ FileCopyResult copyFileOsSpecific(const Zstring& sourceFile, //throw FileError, { //we cannot set the target file times (::futimes) while the file descriptor is still open after a write operation: //this triggers bugs on samba shares where the modification time is set to current time instead. - //Linux: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=340236 + //Linux: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=340236 // http://comments.gmane.org/gmane.linux.file-systems.cifs/2854 //OS X: https://freefilesync.org/forum/viewtopic.php?t=356 setWriteTimeNative(targetFile, sourceInfo.st_mtim, ProcSymlink::FOLLOW); //throw FileError diff --git a/zen/file_io.cpp b/zen/file_io.cpp index b78259e0..942f367f 100644 --- a/zen/file_io.cpp +++ b/zen/file_io.cpp @@ -37,7 +37,7 @@ void FileBase::close() //throw FileError //no need to clean-up on failure here (just like there is no clean on FileOutput::write failure!) => FileOutput is not transactional! if (::close(fileHandle_) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getFilePath())), L"close"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getFilePath())), "close"); } //---------------------------------------------------------------------------------------------------- @@ -73,10 +73,10 @@ FileBase::FileHandle openHandleForRead(const Zstring& filePath) //throw FileErro } //else: let ::open() fail for errors like "not existing" - //don't use O_DIRECT: http://yarchive.net/comp/linux/o_direct.html + //don't use O_DIRECT: https://yarchive.net/comp/linux/o_direct.html const FileBase::FileHandle fileHandle = ::open(filePath.c_str(), O_RDONLY | O_CLOEXEC); if (fileHandle == -1) //don't check "< 0" -> docu seems to allow "-2" to be a valid file handle - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(filePath)), L"open"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(filePath)), "open"); return fileHandle; //pass ownership } } @@ -92,7 +92,7 @@ FileInput::FileInput(const Zstring& filePath, const IOCallback& notifyUnbuffered { //optimize read-ahead on input file: if (::posix_fadvise(getHandle(), 0, 0, POSIX_FADV_SEQUENTIAL) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(filePath)), L"posix_fadvise"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(filePath)), "posix_fadvise"); } @@ -113,9 +113,9 @@ size_t FileInput::tryRead(void* buffer, size_t bytesToRead) //throw FileError, E //read() on macOS: https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man2/read.2.html if (bytesRead < 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(getFilePath())), L"read"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(getFilePath())), "read"); if (static_cast<size_t>(bytesRead) > bytesToRead) //better safe than sorry - throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(getFilePath())), L"ReadFile: buffer overflow."); //user should never see this + throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(getFilePath())), formatSystemError("ReadFile", L"", L"Buffer overflow.")); //if ::read is interrupted (EINTR) right in the middle, it will return successfully with "bytesRead < bytesToRead" @@ -180,7 +180,7 @@ FileBase::FileHandle openHandleForWrite(const Zstring& filePath, FileOutput::Acc { const int ec = errno; //copy before making other system calls! const std::wstring errorMsg = replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(filePath)); - const std::wstring errorDescr = formatSystemError(L"open", ec); + const std::wstring errorDescr = formatSystemError("open", ec); if (ec == EEXIST) throw ErrorTargetExisting(errorMsg, errorDescr); @@ -231,10 +231,10 @@ size_t FileOutput::tryWrite(const void* buffer, size_t bytesToWrite) //throw Fil if (bytesWritten == 0) //comment in safe-read.c suggests to treat this as an error due to buggy drivers errno = ENOSPC; - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getFilePath())), L"write"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getFilePath())), "write"); } if (bytesWritten > static_cast<ssize_t>(bytesToWrite)) //better safe than sorry - throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getFilePath())), L"write: buffer overflow."); //user should never see this + throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getFilePath())), formatSystemError("write", L"", L"Buffer overflow.")); //if ::write() is interrupted (EINTR) right in the middle, it will return successfully with "bytesWritten < bytesToWrite"! return bytesWritten; diff --git a/zen/file_traverser.cpp b/zen/file_traverser.cpp index 2d2a0cce..48185516 100644 --- a/zen/file_traverser.cpp +++ b/zen/file_traverser.cpp @@ -26,7 +26,7 @@ void zen::traverseFolder(const Zstring& dirPath, { DIR* folder = ::opendir(dirPath.c_str()); //directory must NOT end with path separator, except "/" if (!folder) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot open directory %x."), L"%x", fmtPath(dirPath)), L"opendir"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot open directory %x."), L"%x", fmtPath(dirPath)), "opendir"); ZEN_ON_SCOPE_EXIT(::closedir(folder)); //never close nullptr handles! -> crash for (;;) @@ -38,7 +38,7 @@ void zen::traverseFolder(const Zstring& dirPath, if (errno == 0) //errno left unchanged => no more items return; - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(dirPath)), L"readdir"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(dirPath)), "readdir"); //don't retry but restart dir traversal on error! https://devblogs.microsoft.com/oldnewthing/20140612-00/?p=753/ } @@ -51,7 +51,7 @@ void zen::traverseFolder(const Zstring& dirPath, const Zstring& itemName = itemNameRaw; if (itemName.empty()) //checks result of normalizeUtfForPosix, too! - throw FileError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(dirPath)), L"readdir: Data corruption; item with empty name."); + throw FileError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(dirPath)), formatSystemError("readdir", L"", L"Data corruption; item with empty name.")); const Zstring& itemPath = appendSeparator(dirPath) + itemName; @@ -59,7 +59,7 @@ void zen::traverseFolder(const Zstring& dirPath, try { if (::lstat(itemPath.c_str(), &statData) != 0) //lstat() does not resolve symlinks - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(itemPath)), L"lstat"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(itemPath)), "lstat"); } catch (const FileError& e) { @@ -27,7 +27,7 @@ std::string generateGUID() //creates a 16-byte GUID #if __GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 25) //getentropy() requires glibc 2.25 (ldd --version) PS: CentOS 7 is on 2.17 if (::getentropy(&guid[0], guid.size()) != 0) //"The maximum permitted value for the length argument is 256" throw std::runtime_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Failed to generate GUID." + "\n\n" + - utfTo<std::string>(formatSystemError(L"getentropy", errno))); + utfTo<std::string>(formatSystemError("getentropy", errno))); #else class RandomGeneratorPosix { @@ -36,7 +36,7 @@ std::string generateGUID() //creates a 16-byte GUID { if (fd_ == -1) throw std::runtime_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Failed to generate GUID." + "\n\n" + - utfTo<std::string>(formatSystemError(L"open", errno))); + utfTo<std::string>(formatSystemError("open", errno))); } ~RandomGeneratorPosix() { ::close(fd_); } @@ -48,7 +48,7 @@ std::string generateGUID() //creates a 16-byte GUID const ssize_t bytesRead = ::read(fd_, static_cast<char*>(buf) + offset, size - offset); if (bytesRead < 1) //0 means EOF => error in this context (should check for buffer overflow, too?) throw std::runtime_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Failed to generate GUID." + "\n\n" + - utfTo<std::string>(formatSystemError(L"read", bytesRead < 0 ? errno : EIO))); + utfTo<std::string>(formatSystemError("read", bytesRead < 0 ? errno : EIO))); offset += bytesRead; assert(offset <= size); } diff --git a/zen/http.cpp b/zen/http.cpp index c6a390de..848b2cb3 100644 --- a/zen/http.cpp +++ b/zen/http.cpp @@ -59,7 +59,7 @@ public: else //HTTP default port: 80, see %WINDIR%\system32\drivers\etc\services socket_ = std::make_unique<Socket>(server, Zstr("http")); //throw SysError - //we don't support "chunked and gzip transfer encoding" => HTTP 1.0 + //we don't support "chunked and gzip transfer encoding" => HTTP 1.0 => no "Content-Length" support! headers["Host" ] = utfTo<std::string>(server); //only required for HTTP/1.1 but a few servers expect it even for HTTP/1.0 headers["User-Agent"] = utfTo<std::string>(userAgent); headers["Accept" ] = "*/*"; //won't hurt? @@ -194,7 +194,8 @@ private: contentRemaining_ -= bytesReceived; if (bytesReceived == 0 && contentRemaining_ > 0) - throw SysError(replaceCpy<std::wstring>(L"HttpInputStream::tryRead: incomplete server response; %x more bytes expected.", L"%x", numberTo<std::wstring>(contentRemaining_))); + throw SysError(formatSystemError("HttpInputStream::tryRead", L"", L"Incomplete server response: " + + numberTo<std::wstring>(contentRemaining_) + L" more bytes expected.")); return bytesReceived; //"zero indicates end of file" } @@ -259,8 +260,8 @@ std::unique_ptr<HttpInputStream::Impl> sendHttpRequestImpl(const Zstring& url, } else { - if (httpStatus != 200) //HTTP_STATUS_OK(200) - throw SysError(formatHttpStatus(httpStatus)); //e.g. HTTP_STATUS_NOT_FOUND(404) + if (httpStatus != 200) //HTTP_STATUS_OK + throw SysError(formatHttpError(httpStatus)); //e.g. "HTTP status 404: Not found." return response; } @@ -380,9 +381,9 @@ bool zen::internetIsAlive() //noexcept } -std::wstring zen::formatHttpStatus(int sc) +std::wstring zen::formatHttpError(int sc) { - const wchar_t* statusText = [&] //https://en.wikipedia.org/wiki/List_of_HTTP_status_codes + const wchar_t* statusDescr = [&] //https://en.wikipedia.org/wiki/List_of_HTTP_status_codes { switch (sc) { @@ -455,10 +456,7 @@ std::wstring zen::formatHttpStatus(int sc) } }(); - if (strLength(statusText) == 0) - return trimCpy(replaceCpy<std::wstring>(L"HTTP status %x.", L"%x", numberTo<std::wstring>(sc))); - else - return trimCpy(replaceCpy<std::wstring>(L"HTTP status %x: ", L"%x", numberTo<std::wstring>(sc)) + statusText); + return formatSystemError("", L"HTTP status " + numberTo<std::wstring>(sc), statusDescr); } @@ -54,7 +54,7 @@ HttpInputStream sendHttpPost(const Zstring& url, const IOCallback& notifyUnbufferedIO /*throw X*/); //throw SysError, X bool internetIsAlive(); //noexcept -std::wstring formatHttpStatus(int httpStatus); +std::wstring formatHttpError(int httpStatus); bool isValidEmail(const std::string& email); std::string htmlSpecialChars(const std::string_view& str); diff --git a/zen/open_ssl.cpp b/zen/open_ssl.cpp index 0f1da3fc..1feeb1a9 100644 --- a/zen/open_ssl.cpp +++ b/zen/open_ssl.cpp @@ -18,7 +18,7 @@ using namespace zen; #error FFS, we are royally screwed! #endif -static_assert(OPENSSL_VERSION_NUMBER >= 0x1010105fL, "OpenSSL version too old"); +static_assert(OPENSSL_VERSION_NUMBER >= 0x10100000L, "OpenSSL version too old"); void zen::openSslInit() @@ -57,16 +57,16 @@ thread_local OpenSslThreadCleanUp tearDownOpenSslThreadData; openssl dgst -sha256 -verify public.pem -signature file.sig file.txt */ -std::wstring formatOpenSSLError(const std::wstring& functionName, unsigned long ec) +std::wstring formatOpenSSLError(const char* functionName, unsigned long ec) { char errorBuf[256] = {}; //== buffer size used by ERR_error_string(); err.c: it seems the message uses at most ~200 bytes ::ERR_error_string_n(ec, errorBuf, sizeof(errorBuf)); //includes null-termination - return formatSystemError(functionName, replaceCpy(_("Error Code %x"), L"%x", numberTo<std::wstring>(ec)), utfTo<std::wstring>(errorBuf)); + return formatSystemError(functionName, replaceCpy(_("Error code %x"), L"%x", numberTo<std::wstring>(ec)), utfTo<std::wstring>(errorBuf)); } -std::wstring formatLastOpenSSLError(const std::wstring& functionName) +std::wstring formatLastOpenSSLError(const char* functionName) { const auto ec = ::ERR_peek_last_error(); ::ERR_clear_error(); //clean up for next OpenSSL operation on this thread @@ -80,19 +80,19 @@ std::shared_ptr<EVP_PKEY> generateRsaKeyPair(int bits) //throw SysError EVP_PKEY_CTX* keyCtx = ::EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, //int id, nullptr); //ENGINE* e if (!keyCtx) - throw SysError(formatLastOpenSSLError(L"EVP_PKEY_CTX_new_id")); + throw SysError(formatLastOpenSSLError("EVP_PKEY_CTX_new_id")); ZEN_ON_SCOPE_EXIT(::EVP_PKEY_CTX_free(keyCtx)); if (::EVP_PKEY_keygen_init(keyCtx) != 1) - throw SysError(formatLastOpenSSLError(L"EVP_PKEY_keygen_init")); + throw SysError(formatLastOpenSSLError("EVP_PKEY_keygen_init")); //"RSA keys set the key length during key generation rather than parameter generation" if (::EVP_PKEY_CTX_set_rsa_keygen_bits(keyCtx, bits) <= 0) //"[...] return a positive value for success" => effectively returns "1" - throw SysError(formatLastOpenSSLError(L"EVP_PKEY_CTX_set_rsa_keygen_bits")); + throw SysError(formatLastOpenSSLError("EVP_PKEY_CTX_set_rsa_keygen_bits")); EVP_PKEY* keyPair = nullptr; if (::EVP_PKEY_keygen(keyCtx, &keyPair) != 1) - throw SysError(formatLastOpenSSLError(L"EVP_PKEY_keygen")); + throw SysError(formatLastOpenSSLError("EVP_PKEY_keygen")); return std::shared_ptr<EVP_PKEY>(keyPair, ::EVP_PKEY_free); } @@ -101,11 +101,11 @@ std::shared_ptr<EVP_PKEY> generateRsaKeyPair(int bits) //throw SysError using BioToEvpFunc = EVP_PKEY* (*)(BIO* bp, EVP_PKEY** x, pem_password_cb* cb, void* u); -std::shared_ptr<EVP_PKEY> streamToEvpKey(const std::string& keyStream, BioToEvpFunc bioToEvp, const wchar_t* functionName) //throw SysError +std::shared_ptr<EVP_PKEY> streamToEvpKey(const std::string& keyStream, BioToEvpFunc bioToEvp, const char* functionName) //throw SysError { BIO* bio = ::BIO_new_mem_buf(keyStream.c_str(), static_cast<int>(keyStream.size())); if (!bio) - throw SysError(formatLastOpenSSLError(L"BIO_new_mem_buf")); + throw SysError(formatLastOpenSSLError("BIO_new_mem_buf")); ZEN_ON_SCOPE_EXIT(::BIO_free_all(bio)); if (EVP_PKEY* evp = bioToEvp(bio, //BIO* bp, @@ -119,11 +119,11 @@ std::shared_ptr<EVP_PKEY> streamToEvpKey(const std::string& keyStream, BioToEvpF using BioToRsaFunc = RSA* (*)(BIO* bp, RSA** x, pem_password_cb* cb, void* u); -std::shared_ptr<EVP_PKEY> streamToEvpKey(const std::string& keyStream, BioToRsaFunc bioToRsa, const wchar_t* functionName) //throw SysError +std::shared_ptr<EVP_PKEY> streamToEvpKey(const std::string& keyStream, BioToRsaFunc bioToRsa, const char* functionName) //throw SysError { BIO* bio = ::BIO_new_mem_buf(keyStream.c_str(), static_cast<int>(keyStream.size())); if (!bio) - throw SysError(formatLastOpenSSLError(L"BIO_new_mem_buf")); + throw SysError(formatLastOpenSSLError("BIO_new_mem_buf")); ZEN_ON_SCOPE_EXIT(::BIO_free_all(bio)); RSA* rsa = bioToRsa(bio, //BIO* bp, @@ -136,11 +136,11 @@ std::shared_ptr<EVP_PKEY> streamToEvpKey(const std::string& keyStream, BioToRsaF EVP_PKEY* evp = ::EVP_PKEY_new(); if (!evp) - throw SysError(formatLastOpenSSLError(L"EVP_PKEY_new")); + throw SysError(formatLastOpenSSLError("EVP_PKEY_new")); std::shared_ptr<EVP_PKEY> sharedKey(evp, ::EVP_PKEY_free); if (::EVP_PKEY_set1_RSA(evp, rsa) != 1) //no ownership transfer (internally ref-counted) - throw SysError(formatLastOpenSSLError(L"EVP_PKEY_set1_RSA")); + throw SysError(formatLastOpenSSLError("EVP_PKEY_set1_RSA")); return sharedKey; } @@ -153,13 +153,13 @@ std::shared_ptr<EVP_PKEY> streamToKey(const std::string& keyStream, RsaStreamTyp { case RsaStreamType::pkix: return publicKey ? - streamToEvpKey(keyStream, ::PEM_read_bio_PUBKEY, L"PEM_read_bio_PUBKEY") : //throw SysError - streamToEvpKey(keyStream, ::PEM_read_bio_PrivateKey, L"PEM_read_bio_PrivateKey"); // + streamToEvpKey(keyStream, ::PEM_read_bio_PUBKEY, "PEM_read_bio_PUBKEY") : //throw SysError + streamToEvpKey(keyStream, ::PEM_read_bio_PrivateKey, "PEM_read_bio_PrivateKey"); // case RsaStreamType::pkcs1: return publicKey ? - streamToEvpKey(keyStream, ::PEM_read_bio_RSAPublicKey, L"PEM_read_bio_RSAPublicKey") : //throw SysError - streamToEvpKey(keyStream, ::PEM_read_bio_RSAPrivateKey, L"PEM_read_bio_RSAPrivateKey"); // + streamToEvpKey(keyStream, ::PEM_read_bio_RSAPublicKey, "PEM_read_bio_RSAPublicKey") : //throw SysError + streamToEvpKey(keyStream, ::PEM_read_bio_RSAPrivateKey, "PEM_read_bio_RSAPrivateKey"); // case RsaStreamType::raw: break; @@ -171,7 +171,7 @@ std::shared_ptr<EVP_PKEY> streamToKey(const std::string& keyStream, RsaStreamTyp &tmp, /*changes tmp pointer itself!*/ //const unsigned char** pp, static_cast<long>(keyStream.size())); //long length if (!evp) - throw SysError(formatLastOpenSSLError(publicKey ? L"d2i_PublicKey" : L"d2i_PrivateKey")); + throw SysError(formatLastOpenSSLError(publicKey ? "d2i_PublicKey" : "d2i_PrivateKey")); return std::shared_ptr<EVP_PKEY>(evp, ::EVP_PKEY_free); } @@ -179,11 +179,11 @@ std::shared_ptr<EVP_PKEY> streamToKey(const std::string& keyStream, RsaStreamTyp using EvpToBioFunc = int (*)(BIO* bio, EVP_PKEY* evp); -std::string evpKeyToStream(EVP_PKEY* evp, EvpToBioFunc evpToBio, const wchar_t* functionName) //throw SysError +std::string evpKeyToStream(EVP_PKEY* evp, EvpToBioFunc evpToBio, const char* functionName) //throw SysError { BIO* bio = ::BIO_new(BIO_s_mem()); if (!bio) - throw SysError(formatLastOpenSSLError(L"BIO_new")); + throw SysError(formatLastOpenSSLError("BIO_new")); ZEN_ON_SCOPE_EXIT(::BIO_free_all(bio)); if (evpToBio(bio, evp) != 1) @@ -191,44 +191,44 @@ std::string evpKeyToStream(EVP_PKEY* evp, EvpToBioFunc evpToBio, const wchar_t* //--------------------------------------------- const int keyLen = BIO_pending(bio); if (keyLen < 0) - throw SysError(formatLastOpenSSLError(L"BIO_pending")); + throw SysError(formatLastOpenSSLError("BIO_pending")); if (keyLen == 0) - throw SysError(L"BIO_pending failed."); //no more error details + throw SysError(formatSystemError("BIO_pending", L"", L"Unexpected failure.")); //no more error details std::string keyStream(keyLen, '\0'); if (::BIO_read(bio, &keyStream[0], keyLen) != keyLen) - throw SysError(formatLastOpenSSLError(L"BIO_read")); + throw SysError(formatLastOpenSSLError("BIO_read")); return keyStream; } using RsaToBioFunc = int (*)(BIO* bp, RSA* x); -std::string evpKeyToStream(EVP_PKEY* evp, RsaToBioFunc rsaToBio, const wchar_t* functionName) //throw SysError +std::string evpKeyToStream(EVP_PKEY* evp, RsaToBioFunc rsaToBio, const char* functionName) //throw SysError { BIO* bio = ::BIO_new(BIO_s_mem()); if (!bio) - throw SysError(formatLastOpenSSLError(L"BIO_new")); + throw SysError(formatLastOpenSSLError("BIO_new")); ZEN_ON_SCOPE_EXIT(::BIO_free_all(bio)); RSA* rsa = ::EVP_PKEY_get0_RSA(evp); //unowned reference! if (!rsa) - throw SysError(formatLastOpenSSLError(L"EVP_PKEY_get0_RSA")); + throw SysError(formatLastOpenSSLError("EVP_PKEY_get0_RSA")); if (rsaToBio(bio, rsa) != 1) throw SysError(formatLastOpenSSLError(functionName)); //--------------------------------------------- const int keyLen = BIO_pending(bio); if (keyLen < 0) - throw SysError(formatLastOpenSSLError(L"BIO_pending")); + throw SysError(formatLastOpenSSLError("BIO_pending")); if (keyLen == 0) - throw SysError(L"BIO_pending failed."); //no more error details + throw SysError(formatSystemError("BIO_pending", L"", L"Unexpected failure.")); //no more error details std::string keyStream(keyLen, '\0'); if (::BIO_read(bio, &keyStream[0], keyLen) != keyLen) - throw SysError(formatLastOpenSSLError(L"BIO_read")); + throw SysError(formatLastOpenSSLError("BIO_read")); return keyStream; } @@ -266,13 +266,13 @@ std::string keyToStream(EVP_PKEY* evp, RsaStreamType streamType, bool publicKey) { case RsaStreamType::pkix: return publicKey ? - evpKeyToStream(evp, ::PEM_write_bio_PUBKEY, L"PEM_write_bio_PUBKEY") : //throw SysError - evpKeyToStream(evp, ::PEM_write_bio_PrivateKey2, L"PEM_write_bio_PrivateKey"); // + evpKeyToStream(evp, ::PEM_write_bio_PUBKEY, "PEM_write_bio_PUBKEY") : //throw SysError + evpKeyToStream(evp, ::PEM_write_bio_PrivateKey2, "PEM_write_bio_PrivateKey"); // case RsaStreamType::pkcs1: return publicKey ? - evpKeyToStream(evp, ::PEM_write_bio_RSAPublicKey2, L"PEM_write_bio_RSAPublicKey") : //throw SysError - evpKeyToStream(evp, ::PEM_write_bio_RSAPrivateKey2, L"PEM_write_bio_RSAPrivateKey"); // + evpKeyToStream(evp, ::PEM_write_bio_RSAPublicKey2, "PEM_write_bio_RSAPublicKey") : //throw SysError + evpKeyToStream(evp, ::PEM_write_bio_RSAPrivateKey2, "PEM_write_bio_RSAPrivateKey"); // case RsaStreamType::raw: break; @@ -281,7 +281,7 @@ std::string keyToStream(EVP_PKEY* evp, RsaStreamType streamType, bool publicKey) unsigned char* buf = nullptr; const int bufSize = (publicKey ? ::i2d_PublicKey : ::i2d_PrivateKey)(evp, &buf); if (bufSize <= 0) - throw SysError(formatLastOpenSSLError(publicKey ? L"i2d_PublicKey" : L"i2d_PrivateKey")); + throw SysError(formatLastOpenSSLError(publicKey ? "i2d_PublicKey" : "i2d_PrivateKey")); ZEN_ON_SCOPE_EXIT(::OPENSSL_free(buf)); //memory is only allocated for bufSize > 0 return { reinterpret_cast<const char*>(buf), static_cast<size_t>(bufSize) }; @@ -294,7 +294,7 @@ std::string createSignature(const std::string& message, EVP_PKEY* privateKey) // //https://www.openssl.org/docs/manmaster/man3/EVP_DigestSign.html EVP_MD_CTX* mdctx = ::EVP_MD_CTX_create(); if (!mdctx) - throw SysError(L"EVP_MD_CTX_create failed."); //no more error details + throw SysError(formatSystemError("EVP_MD_CTX_create", L"", L"Unexpected failure.")); //no more error details ZEN_ON_SCOPE_EXIT(::EVP_MD_CTX_destroy(mdctx)); if (::EVP_DigestSignInit(mdctx, //EVP_MD_CTX* ctx, @@ -302,18 +302,18 @@ std::string createSignature(const std::string& message, EVP_PKEY* privateKey) // EVP_sha256(), //const EVP_MD* type, nullptr, //ENGINE* e, privateKey) != 1) //EVP_PKEY* pkey - throw SysError(formatLastOpenSSLError(L"EVP_DigestSignInit")); + throw SysError(formatLastOpenSSLError("EVP_DigestSignInit")); if (::EVP_DigestSignUpdate(mdctx, //EVP_MD_CTX* ctx, message.c_str(), //const void* d, message.size()) != 1) //size_t cnt - throw SysError(formatLastOpenSSLError(L"EVP_DigestSignUpdate")); + throw SysError(formatLastOpenSSLError("EVP_DigestSignUpdate")); size_t sigLenMax = 0; //"first call to EVP_DigestSignFinal returns the maximum buffer size required" if (::EVP_DigestSignFinal(mdctx, //EVP_MD_CTX* ctx, nullptr, //unsigned char* sigret, &sigLenMax) != 1) //size_t* siglen - throw SysError(formatLastOpenSSLError(L"EVP_DigestSignFinal")); + throw SysError(formatLastOpenSSLError("EVP_DigestSignFinal")); std::string signature(sigLenMax, '\0'); size_t sigLen = sigLenMax; @@ -321,7 +321,7 @@ std::string createSignature(const std::string& message, EVP_PKEY* privateKey) // if (::EVP_DigestSignFinal(mdctx, //EVP_MD_CTX* ctx, reinterpret_cast<unsigned char*>(&signature[0]), //unsigned char* sigret, &sigLen) != 1) //size_t* siglen - throw SysError(formatLastOpenSSLError(L"EVP_DigestSignFinal")); + throw SysError(formatLastOpenSSLError("EVP_DigestSignFinal")); signature.resize(sigLen); return signature; @@ -333,7 +333,7 @@ void verifySignature(const std::string& message, const std::string& signature, E //https://www.openssl.org/docs/manmaster/man3/EVP_DigestVerify.html EVP_MD_CTX* mdctx = ::EVP_MD_CTX_create(); if (!mdctx) - throw SysError(L"EVP_MD_CTX_create failed."); //no more error details + throw SysError(formatSystemError("EVP_MD_CTX_create", L"", L"Unexpected failure.")); //no more error details ZEN_ON_SCOPE_EXIT(::EVP_MD_CTX_destroy(mdctx)); if (::EVP_DigestVerifyInit(mdctx, //EVP_MD_CTX* ctx, @@ -341,17 +341,17 @@ void verifySignature(const std::string& message, const std::string& signature, E EVP_sha256(), //const EVP_MD* type, nullptr, //ENGINE* e, publicKey) != 1) //EVP_PKEY* pkey - throw SysError(formatLastOpenSSLError(L"EVP_DigestVerifyInit")); + throw SysError(formatLastOpenSSLError("EVP_DigestVerifyInit")); if (::EVP_DigestVerifyUpdate(mdctx, //EVP_MD_CTX* ctx, message.c_str(), //const void* d, message.size()) != 1) //size_t cnt - throw SysError(formatLastOpenSSLError(L"EVP_DigestVerifyUpdate")); + throw SysError(formatLastOpenSSLError("EVP_DigestVerifyUpdate")); if (::EVP_DigestVerifyFinal(mdctx, //EVP_MD_CTX* ctx, reinterpret_cast<const unsigned char*>(signature.c_str()), //const unsigned char* sig, signature.size()) != 1) //size_t siglen - throw SysError(formatLastOpenSSLError(L"EVP_DigestVerifyFinal")); + throw SysError(formatLastOpenSSLError("EVP_DigestVerifyFinal")); } } @@ -494,31 +494,31 @@ public: ctx_ = ::SSL_CTX_new(::TLS_client_method()); if (!ctx_) - throw SysError(formatLastOpenSSLError(L"SSL_CTX_new")); + throw SysError(formatLastOpenSSLError("SSL_CTX_new")); ssl_ = ::SSL_new(ctx_); if (!ssl_) - throw SysError(formatLastOpenSSLError(L"SSL_new")); + throw SysError(formatLastOpenSSLError("SSL_new")); BIO* bio = ::BIO_new_socket(socket, BIO_NOCLOSE); if (!bio) - throw SysError(formatLastOpenSSLError(L"BIO_new_socket")); + throw SysError(formatLastOpenSSLError("BIO_new_socket")); ::SSL_set0_rbio(ssl_, bio); //pass ownership if (::BIO_up_ref(bio) != 1) - throw SysError(formatLastOpenSSLError(L"BIO_up_ref")); + throw SysError(formatLastOpenSSLError("BIO_up_ref")); ::SSL_set0_wbio(ssl_, bio); //pass ownership assert(::SSL_get_mode(ssl_) == SSL_MODE_AUTO_RETRY); //verify OpenSSL default ::SSL_set_mode(ssl_, SSL_MODE_ENABLE_PARTIAL_WRITE); if (::SSL_set_tlsext_host_name(ssl_, server.c_str()) != 1) //enable SNI (Server Name Indication) - throw SysError(formatLastOpenSSLError(L"SSL_set_tlsext_host_name")); + throw SysError(formatLastOpenSSLError("SSL_set_tlsext_host_name")); if (caCertFilePath) { if (!::SSL_CTX_load_verify_locations(ctx_, utfTo<std::string>(*caCertFilePath).c_str(), nullptr)) - throw SysError(formatLastOpenSSLError(L"SSL_CTX_load_verify_locations")); + throw SysError(formatLastOpenSSLError("SSL_CTX_load_verify_locations")); //alternative: SSL_CTX_set_default_verify_paths(): use OpenSSL default paths considering SSL_CERT_FILE environment variable //1. enable check for valid certificate: see SSL_get_verify_result() @@ -526,18 +526,18 @@ public: //2. enable check that the certificate matches our host: see SSL_get_verify_result() if (::SSL_set1_host(ssl_, server.c_str()) != 1) //no ownership transfer - throw SysError(L"SSL_set1_host failed."); //no more error details + throw SysError(formatSystemError("SSL_set1_host", L"", L"Unexpected failure.")); //no more error details } const int rv = ::SSL_connect(ssl_); //implicitly calls SSL_set_connect_state() if (rv != 1) - throw SysError(formatLastOpenSSLError(L"SSL_connect") + L' ' + formatSslErrorCode(::SSL_get_error(ssl_, rv))); + throw SysError(formatLastOpenSSLError("SSL_connect") + L' ' + formatSslErrorCode(::SSL_get_error(ssl_, rv))); if (caCertFilePath) { const long verifyResult = ::SSL_get_verify_result(ssl_); if (verifyResult != X509_V_OK) - throw SysError(formatSystemError(L"SSL_get_verify_result", formatX509ErrorCode(verifyResult), L"")); + throw SysError(formatSystemError("SSL_get_verify_result", formatX509ErrorCode(verifyResult), L"")); } } @@ -570,17 +570,20 @@ public: return 0; //EOF + close_notify alert warn_static("find a better solution for SSL_read_ex + EOF") - //"sslError == SSL_ERROR_SYSCALL && ::ERR_peek_last_error() == 0" => obsolete as of OpenSSL 1.1.1e - //https://github.com/openssl/openssl/issues/10880#issuecomment-575746226 +#if OPENSSL_VERSION_NUMBER == 0x1010105fL //OpenSSL 1.1.1e const auto ec = ::ERR_peek_last_error(); if (sslError == SSL_ERROR_SSL && ERR_GET_REASON(ec) == SSL_R_UNEXPECTED_EOF_WHILE_READING) //EOF: only expected for HTTP/1.0 return 0; - - throw SysError(formatLastOpenSSLError(L"SSL_read_ex") + L' ' + formatSslErrorCode(sslError)); +#else //obsolete handling, at least in OpenSSL 1.1.1e (but valid again with OpenSSL 1.1.1f!) + //https://github.com/openssl/openssl/issues/10880#issuecomment-575746226 + if ((sslError == SSL_ERROR_SYSCALL && ::ERR_peek_last_error() == 0)) //EOF: only expected for HTTP/1.0 + return 0; +#endif + throw SysError(formatLastOpenSSLError("SSL_read_ex") + L' ' + formatSslErrorCode(sslError)); } assert(bytesReceived > 0); //SSL_read_ex() considers EOF an error! if (bytesReceived > bytesToRead) //better safe than sorry - throw SysError(L"SSL_read_ex: buffer overflow."); + throw SysError(formatSystemError("SSL_read_ex", L"", L"Buffer overflow.")); return bytesReceived; //"zero indicates end of file" } @@ -593,12 +596,12 @@ public: size_t bytesWritten = 0; const int rv = ::SSL_write_ex(ssl_, buffer, bytesToWrite, &bytesWritten); if (rv != 1) - throw SysError(formatLastOpenSSLError(L"SSL_write_ex") + L' ' + formatSslErrorCode(::SSL_get_error(ssl_, rv))); + throw SysError(formatLastOpenSSLError("SSL_write_ex") + L' ' + formatSslErrorCode(::SSL_get_error(ssl_, rv))); if (bytesWritten > bytesToWrite) - throw SysError(L"SSL_write_ex: buffer overflow."); + throw SysError(formatSystemError("SSL_write_ex", L"", L"Buffer overflow.")); if (bytesWritten == 0) - throw SysError(L"SSL_write_ex: zero bytes processed"); + throw SysError(formatSystemError("SSL_write_ex", L"", L"Zero bytes processed.")); return bytesWritten; } @@ -728,7 +731,7 @@ std::string zen::convertPuttyKeyToPkix(const std::string& keyStream, const std:: EVP_CIPHER_CTX* cipCtx = ::EVP_CIPHER_CTX_new(); if (!cipCtx) - throw SysError(L"EVP_CIPHER_CTX_new failed."); //no more error details + throw SysError(formatSystemError("EVP_CIPHER_CTX_new", L"", L"Unexpected failure.")); //no more error details ZEN_ON_SCOPE_EXIT(::EVP_CIPHER_CTX_free(cipCtx)); if (::EVP_DecryptInit_ex(cipCtx, //EVP_CIPHER_CTX* ctx, @@ -736,10 +739,10 @@ std::string zen::convertPuttyKeyToPkix(const std::string& keyStream, const std:: nullptr, //ENGINE* impl, key, //const unsigned char* key, => implied length of 256 bit! nullptr) != 1) //const unsigned char* iv - throw SysError(formatLastOpenSSLError(L"EVP_DecryptInit_ex")); + throw SysError(formatLastOpenSSLError("EVP_DecryptInit_ex")); if (::EVP_CIPHER_CTX_set_padding(cipCtx, 0 /*padding*/) != 1) - throw SysError(L"EVP_CIPHER_CTX_set_padding failed."); //no more error details + throw SysError(formatSystemError("EVP_CIPHER_CTX_set_padding", L"", L"Unexpected failure.")); //no more error details privateBlob.resize(privateBlobEnc.size() + ::EVP_CIPHER_block_size(EVP_aes_256_cbc())); //"EVP_DecryptUpdate() should have room for (inl + cipher_block_size) bytes" @@ -750,13 +753,13 @@ std::string zen::convertPuttyKeyToPkix(const std::string& keyStream, const std:: &decLen1, //int* outl, reinterpret_cast<const unsigned char*>(privateBlobEnc.c_str()), //const unsigned char* in, static_cast<int>(privateBlobEnc.size())) != 1) //int inl - throw SysError(formatLastOpenSSLError(L"EVP_DecryptUpdate")); + throw SysError(formatLastOpenSSLError("EVP_DecryptUpdate")); int decLen2 = 0; if (::EVP_DecryptFinal_ex(cipCtx, //EVP_CIPHER_CTX* ctx, reinterpret_cast<unsigned char*>(&privateBlob[decLen1]), //unsigned char* outm, &decLen2) != 1) //int* outl - throw SysError(formatLastOpenSSLError(L"EVP_DecryptFinal_ex")); + throw SysError(formatLastOpenSSLError("EVP_DecryptFinal_ex")); privateBlob.resize(decLen1 + decLen2); } @@ -790,11 +793,11 @@ std::string zen::convertPuttyKeyToPkix(const std::string& keyStream, const std:: static_cast<int>(macData.size()), //int n, reinterpret_cast<unsigned char*>(md), //unsigned char* md, &mdLen)) //unsigned int* md_len - throw SysError(L"HMAC failed."); //no more error details + throw SysError(formatSystemError("HMAC", L"", L"Unexpected failure.")); //no more error details const bool hashValid = mac == std::string_view(md, mdLen); if (!hashValid) - throw SysError(keyEncrypted ? L"MAC validation failed: wrong passphrase or corrupted key" : L"MAC validation failed: corrupted key"); + throw SysError(formatSystemError("HMAC", L"", keyEncrypted ? L"Validation failed: wrong passphrase or corrupted key" : L"Validation failed: corrupted key")); //---------------------------------------------------------- auto extractString = [](auto& it, auto itEnd) @@ -823,7 +826,7 @@ std::string zen::convertPuttyKeyToPkix(const std::string& keyStream, const std:: { BIGNUM* bn = ::BN_new(); if (!bn) - throw SysError(formatLastOpenSSLError(L"BN_new")); + throw SysError(formatLastOpenSSLError("BN_new")); return std::unique_ptr<BIGNUM, BnFree>(bn); }; @@ -833,7 +836,7 @@ std::string zen::convertPuttyKeyToPkix(const std::string& keyStream, const std:: BIGNUM* bn = ::BN_bin2bn(reinterpret_cast<const unsigned char*>(&bytes[0]), static_cast<int>(bytes.size()), nullptr); if (!bn) - throw SysError(formatLastOpenSSLError(L"BN_bin2bn")); + throw SysError(formatLastOpenSSLError("BN_bin2bn")); return std::unique_ptr<BIGNUM, BnFree>(bn); }; @@ -866,43 +869,43 @@ std::string zen::convertPuttyKeyToPkix(const std::string& keyStream, const std:: BN_CTX* bnCtx = BN_CTX_new(); if (!bnCtx) - throw SysError(formatLastOpenSSLError(L"BN_CTX_new")); + throw SysError(formatLastOpenSSLError("BN_CTX_new")); ZEN_ON_SCOPE_EXIT(::BN_CTX_free(bnCtx)); if (::BN_sub(tmp.get(), p.get(), BN_value_one()) != 1) - throw SysError(formatLastOpenSSLError(L"BN_sub")); + throw SysError(formatLastOpenSSLError("BN_sub")); if (::BN_mod(dmp1.get(), d.get(), tmp.get(), bnCtx) != 1) - throw SysError(formatLastOpenSSLError(L"BN_mod")); + throw SysError(formatLastOpenSSLError("BN_mod")); if (::BN_sub(tmp.get(), q.get(), BN_value_one()) != 1) - throw SysError(formatLastOpenSSLError(L"BN_sub")); + throw SysError(formatLastOpenSSLError("BN_sub")); if (::BN_mod(dmq1.get(), d.get(), tmp.get(), bnCtx) != 1) - throw SysError(formatLastOpenSSLError(L"BN_mod")); + throw SysError(formatLastOpenSSLError("BN_mod")); //---------------------------------------------------------- RSA* rsa = ::RSA_new(); if (!rsa) - throw SysError(formatLastOpenSSLError(L"RSA_new")); + throw SysError(formatLastOpenSSLError("RSA_new")); ZEN_ON_SCOPE_EXIT(::RSA_free(rsa)); if (::RSA_set0_key(rsa, n.release(), e.release(), d.release()) != 1) //pass BIGNUM ownership - throw SysError(formatLastOpenSSLError(L"RSA_set0_key")); + throw SysError(formatLastOpenSSLError("RSA_set0_key")); if (::RSA_set0_factors(rsa, p.release(), q.release()) != 1) - throw SysError(formatLastOpenSSLError(L"RSA_set0_factors")); + throw SysError(formatLastOpenSSLError("RSA_set0_factors")); if (::RSA_set0_crt_params(rsa, dmp1.release(), dmq1.release(), iqmp.release()) != 1) - throw SysError(formatLastOpenSSLError(L"RSA_set0_crt_params")); + throw SysError(formatLastOpenSSLError("RSA_set0_crt_params")); EVP_PKEY* evp = ::EVP_PKEY_new(); if (!evp) - throw SysError(formatLastOpenSSLError(L"EVP_PKEY_new")); + throw SysError(formatLastOpenSSLError("EVP_PKEY_new")); ZEN_ON_SCOPE_EXIT(::EVP_PKEY_free(evp)); if (::EVP_PKEY_set1_RSA(evp, rsa) != 1) //no ownership transfer (internally ref-counted) - throw SysError(formatLastOpenSSLError(L"EVP_PKEY_set1_RSA")); + throw SysError(formatLastOpenSSLError("EVP_PKEY_set1_RSA")); return keyToStream(evp, RsaStreamType::pkix, false /*publicKey*/); //throw SysError } @@ -918,22 +921,22 @@ std::string zen::convertPuttyKeyToPkix(const std::string& keyStream, const std:: DSA* dsa = ::DSA_new(); if (!dsa) - throw SysError(formatLastOpenSSLError(L"DSA_new")); + throw SysError(formatLastOpenSSLError("DSA_new")); ZEN_ON_SCOPE_EXIT(::DSA_free(dsa)); if (::DSA_set0_pqg(dsa, p.release(), q.release(), g.release()) != 1) //pass BIGNUM ownership - throw SysError(formatLastOpenSSLError(L"DSA_set0_pqg")); + throw SysError(formatLastOpenSSLError("DSA_set0_pqg")); if (::DSA_set0_key(dsa, pub.release(), pri.release()) != 1) - throw SysError(formatLastOpenSSLError(L"DSA_set0_key")); + throw SysError(formatLastOpenSSLError("DSA_set0_key")); EVP_PKEY* evp = ::EVP_PKEY_new(); if (!evp) - throw SysError(formatLastOpenSSLError(L"EVP_PKEY_new")); + throw SysError(formatLastOpenSSLError("EVP_PKEY_new")); ZEN_ON_SCOPE_EXIT(::EVP_PKEY_free(evp)); if (::EVP_PKEY_set1_DSA(evp, dsa) != 1) //no ownership transfer (internally ref-counted) - throw SysError(formatLastOpenSSLError(L"EVP_PKEY_set1_DSA")); + throw SysError(formatLastOpenSSLError("EVP_PKEY_set1_DSA")); return keyToStream(evp, RsaStreamType::pkix, false /*publicKey*/); //throw SysError } @@ -963,16 +966,16 @@ std::string zen::convertPuttyKeyToPkix(const std::string& keyStream, const std:: EC_KEY* ecKey = ::EC_KEY_new_by_curve_name(curveNid); if (!ecKey) - throw SysError(formatLastOpenSSLError(L"EC_KEY_new_by_curve_name")); + throw SysError(formatLastOpenSSLError("EC_KEY_new_by_curve_name")); ZEN_ON_SCOPE_EXIT(::EC_KEY_free(ecKey)); const EC_GROUP* ecGroup = ::EC_KEY_get0_group(ecKey); if (!ecGroup) - throw SysError(formatLastOpenSSLError(L"EC_KEY_get0_group")); + throw SysError(formatLastOpenSSLError("EC_KEY_get0_group")); EC_POINT* ecPoint = ::EC_POINT_new(ecGroup); if (!ecPoint) - throw SysError(formatLastOpenSSLError(L"EC_POINT_new")); + throw SysError(formatLastOpenSSLError("EC_POINT_new")); ZEN_ON_SCOPE_EXIT(::EC_POINT_free(ecPoint)); if (::EC_POINT_oct2point(ecGroup, //const EC_GROUP* group, @@ -980,21 +983,21 @@ std::string zen::convertPuttyKeyToPkix(const std::string& keyStream, const std:: reinterpret_cast<const unsigned char*>(&pointStream[0]), //const unsigned char* buf, pointStream.size(), //size_t len, nullptr) != 1) //BN_CTX* ctx - throw SysError(formatLastOpenSSLError(L"EC_POINT_oct2point")); + throw SysError(formatLastOpenSSLError("EC_POINT_oct2point")); if (::EC_KEY_set_public_key(ecKey, ecPoint) != 1) //no ownership transfer (internally ref-counted) - throw SysError(formatLastOpenSSLError(L"EC_KEY_set_public_key")); + throw SysError(formatLastOpenSSLError("EC_KEY_set_public_key")); if (::EC_KEY_set_private_key(ecKey, pri.get()) != 1) //no ownership transfer (internally ref-counted) - throw SysError(formatLastOpenSSLError(L"EC_KEY_set_private_key")); + throw SysError(formatLastOpenSSLError("EC_KEY_set_private_key")); EVP_PKEY* evp = ::EVP_PKEY_new(); if (!evp) - throw SysError(formatLastOpenSSLError(L"EVP_PKEY_new")); + throw SysError(formatLastOpenSSLError("EVP_PKEY_new")); ZEN_ON_SCOPE_EXIT(::EVP_PKEY_free(evp)); if (::EVP_PKEY_set1_EC_KEY(evp, ecKey) != 1) //no ownership transfer (internally ref-counted) - throw SysError(formatLastOpenSSLError(L"EVP_PKEY_set1_EC_KEY")); + throw SysError(formatLastOpenSSLError("EVP_PKEY_set1_EC_KEY")); return keyToStream(evp, RsaStreamType::pkix, false /*publicKey*/); //throw SysError } @@ -1009,7 +1012,7 @@ std::string zen::convertPuttyKeyToPkix(const std::string& keyStream, const std:: reinterpret_cast<const unsigned char*>(&priStream[0]), //const unsigned char* priv, priStream.size()); //size_t len if (!evpPriv) - throw SysError(formatLastOpenSSLError(L"EVP_PKEY_new_raw_private_key")); + throw SysError(formatLastOpenSSLError("EVP_PKEY_new_raw_private_key")); ZEN_ON_SCOPE_EXIT(::EVP_PKEY_free(evpPriv)); return keyToStream(evpPriv, RsaStreamType::pkix, false /*publicKey*/); //throw SysError @@ -32,7 +32,7 @@ namespace zen // => wxStopWatch implementation uses QueryPerformanceCounter: https://github.com/wxWidgets/wxWidgets/blob/17d72a48ffd4d8ff42eed070ac48ee2de50ceabd/src/common/stopwatch.cpp // => whatever the problem was, it's almost certainly not caused by QueryPerformanceCounter(): // MSDN: "How often does QPC roll over? Not less than 100 years from the most recent system boot" -// https://msdn.microsoft.com/en-us/library/windows/desktop/dn553408#How_often_does_QPC_roll_over_ +// https://docs.microsoft.com/en-us/windows/win32/sysinfo/acquiring-high-resolution-time-stamps#How_often_does_QPC_roll_over // // => using the system clock is problematic: https://freefilesync.org/forum/viewtopic.php?t=5280 // diff --git a/zen/process_priority.cpp b/zen/process_priority.cpp index 5aa9a0ce..b7b4e029 100644 --- a/zen/process_priority.cpp +++ b/zen/process_priority.cpp @@ -15,7 +15,7 @@ struct PreventStandby::Impl {}; PreventStandby::PreventStandby() {} PreventStandby::~PreventStandby() {} -//solution for GNOME?: http://people.gnome.org/~mccann/gnome-session/docs/gnome-session.html#org.gnome.SessionManager.Inhibit +//solution for GNOME?: https://people.gnome.org/~mccann/gnome-session/docs/gnome-session.html#org.gnome.SessionManager.Inhibit struct ScheduleForBackgroundProcessing::Impl {}; ScheduleForBackgroundProcessing::ScheduleForBackgroundProcessing() {} @@ -25,7 +25,7 @@ ScheduleForBackgroundProcessing::~ScheduleForBackgroundProcessing() {} struct ScheduleForBackgroundProcessing { - required functions ioprio_get/ioprio_set are not part of glibc: https://linux.die.net/man/2/ioprio_set - - and probably never will: http://sourceware.org/bugzilla/show_bug.cgi?id=4464 + - and probably never will: https://sourceware.org/bugzilla/show_bug.cgi?id=4464 - /usr/include/linux/ioprio.h not available on Ubuntu, so we can't use it instead ScheduleForBackgroundProcessing() : oldIoPrio(getIoPriority(IOPRIO_WHO_PROCESS, ::getpid())) diff --git a/zen/recycler.cpp b/zen/recycler.cpp index f4fd870b..4d6ea1fd 100644 --- a/zen/recycler.cpp +++ b/zen/recycler.cpp @@ -31,12 +31,8 @@ bool zen::recycleOrDeleteIfExists(const Zstring& itemPath) //throw FileError if (!type) return false; - const std::wstring errorMsg = replaceCpy(_("Unable to move %x to the recycle bin."), L"%x", fmtPath(itemPath)); - if (!error) - throw FileError(errorMsg, L"g_file_trash: unknown error."); //user should never see this - //implement same behavior as in Windows: if recycler is not existing, delete permanently - if (error->code == G_IO_ERROR_NOT_SUPPORTED) + if (error && error->code == G_IO_ERROR_NOT_SUPPORTED) { if (*type == ItemType::FOLDER) removeDirectoryPlainRecursion(itemPath); //throw FileError @@ -45,9 +41,10 @@ bool zen::recycleOrDeleteIfExists(const Zstring& itemPath) //throw FileError return true; } - throw FileError(errorMsg, formatSystemError(L"g_file_trash", - replaceCpy(_("Error Code %x"), L"%x", numberTo<std::wstring>(error->code)), - utfTo<std::wstring>(error->message))); + throw FileError(replaceCpy(_("Unable to move %x to the recycle bin."), L"%x", fmtPath(itemPath)), + formatSystemError("g_file_trash", + error ? replaceCpy(_("Error code %x"), L"%x", numberTo<std::wstring>(error->code)) : L"", + error ? utfTo<std::wstring>(error->message) : L"Unknown error.")); //g_quark_to_string(error->domain) } return true; diff --git a/zen/shell_execute.cpp b/zen/shell_execute.cpp new file mode 100644 index 00000000..63696568 --- /dev/null +++ b/zen/shell_execute.cpp @@ -0,0 +1,250 @@ +// ***************************************************************************** +// * 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 "shell_execute.h" +#include <chrono> +#include "guid.h" +#include "file_access.h" +#include "file_io.h" + + #include <unistd.h> //fork, pipe + #include <sys/wait.h> //waitpid + #include <fcntl.h> + +using namespace zen; + + +std::vector<Zstring> zen::parseCommandline(const Zstring& cmdLine) +{ + std::vector<Zstring> args; + //"Parsing C++ Command-Line Arguments": https://docs.microsoft.com/en-us/cpp/cpp/parsing-cpp-command-line-arguments + //we do the job ourselves! both wxWidgets and ::CommandLineToArgvW() parse "C:\" "D:\" as single line C:\" D:\" + //-> "solution": we just don't support protected quotation mark! + + auto itStart = cmdLine.end(); //end() means: no token + for (auto it = cmdLine.begin(); it != cmdLine.end(); ++it) + if (*it == Zstr(' ')) //space commits token + { + if (itStart != cmdLine.end()) + { + args.emplace_back(itStart, it); + itStart = cmdLine.end(); //expect consecutive blanks! + } + } + else + { + //start new token + if (itStart == cmdLine.end()) + itStart = it; + + if (*it == Zstr('"')) + { + it = std::find(it + 1, cmdLine.end(), Zstr('"')); + if (it == cmdLine.end()) + break; + } + } + if (itStart != cmdLine.end()) + args.emplace_back(itStart, cmdLine.end()); + + for (Zstring& str : args) + if (str.size() >= 2 && startsWith(str, Zstr('"')) && endsWith(str, Zstr('"'))) + str = Zstring(str.c_str() + 1, str.size() - 2); + + return args; +} + + + + +std::pair<int /*exit code*/, std::wstring> zen::consoleExecute(const Zstring& cmdLine, std::optional<int> timeoutMs) //throw SysError, SysErrorTimeOut +{ + const Zstring tempFilePath = appendSeparator(getTempFolderPath()) + //throw FileError + Zstr("FFS-") + utfTo<Zstring>(formatAsHexString(generateGUID())); + /* can't use popen(): does NOT return the exit code on Linux (despite the documentation!), although it works correctly on macOS + => use pipes instead: https://linux.die.net/man/2/waitpid + bonus: no need for "2>&1" to redirect STDERR to STDOUT + + What about premature exit via SysErrorTimeOut? + Linux: child process' end of the pipe *still works* even after the parent process is gone: + There does not seem to be any output buffer size limit + no observable strain on system memory or disk space! :) + macOS: child process exits if parent end of pipe is closed: fuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu.......... + + => solution: buffer output in temporary file + + Unresolved problem: premature exit via SysErrorTimeOut (=> no waitpid()) creates zombie proceses: + "As long as a zombie is not removed from the system via a wait, + it will consume a slot in the kernel process table, and if this table fills, + it will not be possible to create further processes." */ + + const int EC_CHILD_LAUNCH_FAILED = 120; //avoid 127: used by the system, e.g. failure to execute due to missing .so file + + const int fdTempFile = ::open(tempFilePath.c_str(), O_CREAT | O_EXCL | O_RDWR | O_CLOEXEC, + S_IRUSR | S_IWUSR); //0600 + if (fdTempFile == -1) + THROW_LAST_SYS_ERROR("open"); + auto guardTmpFile = makeGuard<ScopeGuardRunMode::onExit>([&] { ::close(fdTempFile); }); + + //"deleting while handles are open" == FILE_FLAG_DELETE_ON_CLOSE + if (::unlink(tempFilePath.c_str()) != 0) + THROW_LAST_SYS_ERROR("unlink"); + + //-------------------------------------------------------------- + //waitpid() is a useless pile of garbage without time out => check EOF from dummy pipe instead + int pipe[2] = {}; + if (::pipe2(pipe, O_CLOEXEC) != 0) + THROW_LAST_SYS_ERROR("pipe2"); + + + const int fdLifeSignR = pipe[0]; //for parent process + const int fdLifeSignW = pipe[1]; //for child process + ZEN_ON_SCOPE_EXIT(::close(fdLifeSignR)); + auto guardFdLifeSignW = makeGuard<ScopeGuardRunMode::onExit>([&] { ::close(fdLifeSignW ); }); + //-------------------------------------------------------------- + + //follow implemenation of ::system(): https://github.com/lattera/glibc/blob/master/sysdeps/posix/system.c + const pid_t pid = ::fork(); + if (pid < 0) //pids are never negative, empiric proof: https://linux.die.net/man/2/wait + THROW_LAST_SYS_ERROR("fork"); + + if (pid == 0) //child process + try + { + //first task: set STDOUT redirection in case an error needs to be reported + if (::dup2(fdTempFile, STDOUT_FILENO) != STDOUT_FILENO) //O_CLOEXEC does NOT propagate with dup2() + THROW_LAST_SYS_ERROR("dup2(STDOUT)"); + + if (::dup2(fdTempFile, STDERR_FILENO) != STDERR_FILENO) //O_CLOEXEC does NOT propagate with dup2() + THROW_LAST_SYS_ERROR("dup2(STDERR)"); + + //avoid blocking scripts waiting for user input + // => appending " < /dev/null" is not good enough! e.g. hangs for: read -p "still hanging here"; echo fuuuuu... + const int fdDevNull = ::open("/dev/null", O_RDONLY | O_CLOEXEC); + if (fdDevNull == -1) //don't check "< 0" -> docu seems to allow "-2" to be a valid file handle + THROW_LAST_SYS_ERROR("open(/dev/null)"); + ZEN_ON_SCOPE_EXIT(::close(fdDevNull)); + + if (::dup2(fdDevNull, STDIN_FILENO) != STDIN_FILENO) //O_CLOEXEC does NOT propagate with dup2() + THROW_LAST_SYS_ERROR("dup2(STDIN)"); + + //*leak* the fd and have it closed automatically on child process exit after execv() + if (::dup(fdLifeSignW) == -1) //O_CLOEXEC does NOT propagate with dup() + THROW_LAST_SYS_ERROR("dup(fdLifeSignW)"); + + + const char* argv[] = { "sh", "-c", cmdLine.c_str(), nullptr }; + /*int rv =*/::execv("/bin/sh", const_cast<char**>(argv)); //only returns if an error occurred + //safe to cast away const: https://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html + // "The statement about argv[] and envp[] being constants is included to make explicit to future + // writers of language bindings that these objects are completely constant. Due to a limitation of + // the ISO C standard, it is not possible to state that idea in standard C." + THROW_LAST_SYS_ERROR("execv"); + } + catch (const SysError& e) + { + ::puts(utfTo<std::string>(e.toString()).c_str()); + ::fflush(stdout); //note: stderr is unbuffered by default + ::_exit(EC_CHILD_LAUNCH_FAILED); //[!] avoid flushing I/O buffers or doing other clean up from child process like with "exit()"! + } + //else: parent process + + + if (timeoutMs) + { + guardFdLifeSignW.dismiss(); + ::close(fdLifeSignW); //[!] make sure we get EOF when fd is closed by child! + + if (::fcntl(fdLifeSignR, F_SETFL, O_NONBLOCK) != 0) + THROW_LAST_SYS_ERROR("fcntl(O_NONBLOCK)"); + + const auto endTime = std::chrono::steady_clock::now() + std::chrono::milliseconds(*timeoutMs); + for (;;) //EINTR handling? => allow interrupt!? + { + //read until EAGAIN + char buf[16]; + const ssize_t bytesRead = ::read(fdLifeSignR, buf, sizeof(buf)); + if (bytesRead < 0) + { + if (errno != EAGAIN) + THROW_LAST_SYS_ERROR("read"); + } + else if (bytesRead > 0) + throw SysError(formatSystemError("read", L"", L"Unexpected data.")); + else //bytesRead == 0: EOF + break; + + //wait for stream input + const auto now = std::chrono::steady_clock::now(); + if (now > endTime) + throw SysErrorTimeOut(_P("Operation timed out after 1 second.", "Operation timed out after %x seconds.", *timeoutMs / 1000)); + + const auto waitTimeMs = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - now).count(); + + struct ::timeval tv = {}; + tv.tv_sec = static_cast<long>(waitTimeMs / 1000); + tv.tv_usec = static_cast<long>(waitTimeMs - tv.tv_sec * 1000) * 1000; + + fd_set rfd = {}; //includes FD_ZERO + FD_SET(fdLifeSignR, &rfd); + + if (const int rv = ::select(fdLifeSignR + 1, //int nfds, + &rfd, //fd_set* readfds, + nullptr, //fd_set* writefds, + nullptr, //fd_set* exceptfds, + &tv); //struct timeval* timeout + rv < 0) + THROW_LAST_SYS_ERROR("select"); + else if (rv == 0) + throw SysErrorTimeOut(_P("Operation timed out after 1 second.", "Operation timed out after %x seconds.", *timeoutMs / 1000)); + } + } + + //https://linux.die.net/man/2/waitpid + int statusCode = 0; + if (::waitpid(pid, //pid_t pid + &statusCode, //int* status + 0) != pid) //int options + THROW_LAST_SYS_ERROR("waitpid"); + + + if (::lseek(fdTempFile, 0, SEEK_SET) != 0) + THROW_LAST_SYS_ERROR("lseek"); + + guardTmpFile.dismiss(); + FileInput streamIn(fdTempFile, tempFilePath, nullptr /*notifyUnbufferedIO*/); //takes ownership! + const std::wstring output = utfTo<std::wstring>(bufferedLoad<std::string>(streamIn)); //throw FileError + + + if (!WIFEXITED(statusCode)) //signalled, crashed? + throw SysError(formatSystemError("waitpid", WIFSIGNALED(statusCode) ? + L"Killed by signal " + numberTo<std::wstring>(WTERMSIG(statusCode)) : + L"Exit status " + numberTo<std::wstring>(statusCode), + utfTo<std::wstring>(trimCpy(output)))); + + const int exitCode = WEXITSTATUS(statusCode); //precondition: "WIFEXITED() == true" + if (exitCode == EC_CHILD_LAUNCH_FAILED || //child process should already have provided details to STDOUT + exitCode == 127) //details should have been streamed to STDERR: used by /bin/sh, e.g. failure to execute due to missing .so file + throw SysError(utfTo<std::wstring>(trimCpy(output))); + + return { exitCode, output }; +} + + +void zen::openWithDefaultApp(const Zstring& itemPath) //throw FileError +{ + try + { + const Zstring cmdTemplate = R"(xdg-open "%x")"; //doesn't block => no need for time out! + const Zstring cmdLine = replaceCpy(cmdTemplate, Zstr("%x"), itemPath); + + if (const auto [exitCode, output] = consoleExecute(cmdLine, std::nullopt /*timeoutMs*/); //throw SysError, (SysErrorTimeOut) + exitCode != 0) + throw SysError(formatSystemError(utfTo<std::string>(cmdTemplate), replaceCpy(_("Exit code %x"), L"%x", numberTo<std::wstring>(exitCode)), output)); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(itemPath)), e.toString()); } +} + + diff --git a/zen/shell_execute.h b/zen/shell_execute.h index faea4bd9..b80cf2ba 100644 --- a/zen/shell_execute.h +++ b/zen/shell_execute.h @@ -9,105 +9,18 @@ #include "file_error.h" - #include <unistd.h> //fork() - #include <stdlib.h> //::system() - namespace zen { -//launch commandline and report errors via popup dialog -//Windows: COM needs to be initialized before calling this function! -enum class ExecutionType -{ - sync, - async -}; - -namespace -{ - - -int shellExecute(const Zstring& command, ExecutionType type, bool hideConsole) //throw FileError -{ - /* - we cannot use wxExecute due to various issues: - - screws up encoding on OS X for non-ASCII characters - - does not provide any reasonable error information - - uses a zero-sized dummy window as a hack to keep focus which leaves a useless empty icon in ALT-TAB list in Windows - */ - if (type == ExecutionType::sync) - { - //Posix ::system() - execute a shell command - const int rv = ::system(command.c_str()); //do NOT use std::system as its documentation says nothing about "WEXITSTATUS(rv)", etc... - if (rv == -1 || WEXITSTATUS(rv) == 127) - throw FileError(_("Incorrect command line:") + L' ' + utfTo<std::wstring>(command)); - //https://linux.die.net/man/3/system "In case /bin/sh could not be executed, the exit status will be that of a command that does exit(127)" - //Bonus: For an incorrect command line /bin/sh also returns with 127! - - return /*int exitCode = */ WEXITSTATUS(rv); - } - else - { - //follow implemenation of ::system() except for waitpid(): - const pid_t pid = ::fork(); - if (pid < 0) //pids are never negative, empiric proof: https://linux.die.net/man/2/wait - THROW_LAST_FILE_ERROR(_("Incorrect command line:") + L' ' + utfTo<std::wstring>(command), L"fork"); - - if (pid == 0) //child process - { - const char* argv[] = { "sh", "-c", command.c_str(), nullptr }; - /*int rv =*/::execv("/bin/sh", const_cast<char**>(argv)); - //safe to cast away const: http://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html - // "The statement about argv[] and envp[] being constants is included to make explicit to future - // writers of language bindings that these objects are completely constant. Due to a limitation of - // the ISO C standard, it is not possible to state that idea in standard C." - - //"execv() only returns if an error has occurred. The return value is -1, and errno is set to indicate the error." - ::_exit(127); //[!] avoid flushing I/O buffers or doing other clean up from child process like with "exit(127)"! - } - //else //parent process - return 0; - } -} - - -std::string getCommandOutput(const Zstring& command) //throw SysError -{ - //https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/popen.3.html - FILE* pipe = ::popen(command.c_str(), "r"); - if (!pipe) - THROW_LAST_SYS_ERROR(L"popen"); - ZEN_ON_SCOPE_EXIT(::pclose(pipe)); +std::vector<Zstring> parseCommandline(const Zstring& cmdLine); - std::string output; - const size_t blockSize = 64 * 1024; - do - { - output.resize(output.size() + blockSize); - //caveat: SIGCHLD is NOT ignored under macOS debugger => EINTR inside fread() => call ::siginterrupt(SIGCHLD, false) during startup - const size_t bytesRead = ::fread(&*(output.end() - blockSize), 1, blockSize, pipe); - if (::ferror(pipe)) - THROW_LAST_SYS_ERROR(L"fread"); +DEFINE_NEW_SYS_ERROR(SysErrorTimeOut) +[[nodiscard]] std::pair<int /*exit code*/, std::wstring> consoleExecute(const Zstring& cmdLine, std::optional<int> timeoutMs); //throw SysError, SysErrorTimeOut +/* limitations: Windows: cmd.exe returns exit code 1 if file not found (instead of throwing SysError) => nodiscard! + Linux/macOS: SysErrorTimeOut leaves zombie process behind */ - if (bytesRead > blockSize) - throw SysError(L"fread: buffer overflow"); - - if (bytesRead < blockSize) - output.resize(output.size() - (blockSize - bytesRead)); //caveat: unsigned arithmetics - } - while (!::feof(pipe)); - - return output; -} -} - - -inline -void openWithDefaultApplication(const Zstring& itemPath) //throw FileError -{ - shellExecute("xdg-open \"" + itemPath + '"', ExecutionType::async, false /*hideConsole*/); //throw FileError -} +void openWithDefaultApp(const Zstring& itemPath); //throw FileError } #endif //SHELL_EXECUTE_H_23482134578134134 diff --git a/zen/shutdown.cpp b/zen/shutdown.cpp index 89da55ee..21e24527 100644 --- a/zen/shutdown.cpp +++ b/zen/shutdown.cpp @@ -15,19 +15,31 @@ using namespace zen; void zen::shutdownSystem() //throw FileError { - //https://linux.die.net/man/2/reboot => needs admin rights! - - //"systemctl" should work without admin rights: - shellExecute("systemctl poweroff", ExecutionType::sync, false/*hideConsole*/); //throw FileError - + try + { + //https://linux.die.net/man/2/reboot => needs admin rights! + //"systemctl" should work without admin rights: + const auto [exitCode, output] = consoleExecute("systemctl poweroff", std::nullopt /*timeoutMs*/); //throw SysError, (SysErrorTimeOut) + if (!trimCpy(output).empty()) //see comment in suspendSystem() + throw SysError(output); + + } + catch (const SysError& e) { throw FileError(_("Unable to shut down the system."), e.toString()); } } void zen::suspendSystem() //throw FileError { - //"systemctl" should work without admin rights: - shellExecute("systemctl suspend", ExecutionType::sync, false/*hideConsole*/); //throw FileError - + try + { + //"systemctl" should work without admin rights: + const auto [exitCode, output] = consoleExecute("systemctl suspend", std::nullopt /*timeoutMs*/); //throw SysError, (SysErrorTimeOut) + //why does "systemctl suspend" return exit code 1 despite apparent success!?? + if (!trimCpy(output).empty()) //at least we can assume "no output" on success + throw SysError(output); + + } + catch (const SysError& e) { throw FileError(_("Unable to shut down the system."), e.toString()); } } diff --git a/zen/socket.h b/zen/socket.h index 3bd0a2a0..f1d26450 100644 --- a/zen/socket.h +++ b/zen/socket.h @@ -42,19 +42,19 @@ public: const int rcGai = ::getaddrinfo(server.c_str(), serviceName.c_str(), &hints, &servinfo); if (rcGai != 0) - throw SysError(formatSystemError(L"getaddrinfo", replaceCpy(_("Error Code %x"), L"%x", numberTo<std::wstring>(rcGai)), utfTo<std::wstring>(::gai_strerror(rcGai)))); + throw SysError(formatSystemError("getaddrinfo", replaceCpy(_("Error code %x"), L"%x", numberTo<std::wstring>(rcGai)), utfTo<std::wstring>(::gai_strerror(rcGai)))); if (!servinfo) - throw SysError(L"getaddrinfo: empty server info"); + throw SysError(formatSystemError("getaddrinfo", L"", L"Empty server info.")); const auto getConnectedSocket = [](const auto& /*::addrinfo*/ ai) { SocketType testSocket = ::socket(ai.ai_family, ai.ai_socktype, ai.ai_protocol); if (testSocket == invalidSocket) - THROW_LAST_SYS_ERROR_WSA(L"socket"); + THROW_LAST_SYS_ERROR_WSA("socket"); ZEN_ON_SCOPE_FAIL(closeSocket(testSocket)); if (::connect(testSocket, ai.ai_addr, static_cast<int>(ai.ai_addrlen)) != 0) - THROW_LAST_SYS_ERROR_WSA(L"connect"); + THROW_LAST_SYS_ERROR_WSA("connect"); return testSocket; }; @@ -102,10 +102,10 @@ size_t tryReadSocket(SocketType socket, void* buffer, size_t bytesToRead) //thro break; } if (bytesReceived < 0) - THROW_LAST_SYS_ERROR_WSA(L"recv"); + THROW_LAST_SYS_ERROR_WSA("recv"); if (static_cast<size_t>(bytesReceived) > bytesToRead) //better safe than sorry - throw SysError(L"recv: buffer overflow."); + throw SysError(formatSystemError("recv", L"", L"Buffer overflow.")); return bytesReceived; //"zero indicates end of file" } @@ -127,11 +127,11 @@ size_t tryWriteSocket(SocketType socket, const void* buffer, size_t bytesToWrite break; } if (bytesWritten < 0) - THROW_LAST_SYS_ERROR_WSA(L"send"); + THROW_LAST_SYS_ERROR_WSA("send"); if (bytesWritten > static_cast<int>(bytesToWrite)) - throw SysError(L"send: buffer overflow."); + throw SysError(formatSystemError("send", L"", L"Buffer overflow.")); if (bytesWritten == 0) - throw SysError(L"send: zero bytes processed"); + throw SysError(formatSystemError("send", L"", L"Zero bytes processed.")); return bytesWritten; } @@ -143,7 +143,7 @@ inline void shutdownSocketSend(SocketType socket) //throw SysError { if (::shutdown(socket, SHUT_WR) != 0) - THROW_LAST_SYS_ERROR_WSA(L"shutdown"); + THROW_LAST_SYS_ERROR_WSA("shutdown"); } } diff --git a/zen/string_base.h b/zen/string_base.h index 42e1bdf3..5922c3ff 100644 --- a/zen/string_base.h +++ b/zen/string_base.h @@ -566,8 +566,8 @@ const Char& Zbase<Char, SP>::operator[](size_t pos) const template <class Char, template <class> class SP> inline Char& Zbase<Char, SP>::operator[](size_t pos) { - assert(pos < length()); //design by contract! no runtime check! reserve(length()); //make unshared! + assert(pos < length()); //design by contract! no runtime check! return rawStr_[pos]; } diff --git a/zen/string_tools.h b/zen/string_tools.h index 40a4ea52..cd26f5fd 100644 --- a/zen/string_tools.h +++ b/zen/string_tools.h @@ -83,6 +83,7 @@ template <class Num, class S> Num stringTo(const S& str); std::pair<char, char> hexify (unsigned char c, bool upperCase = true); char unhexify(char high, char low); +std::string formatAsHexString(const std::string& blob); //bytes -> (human-readable) hex string template <class S, class T, class Num> S printNumber(const T& format, const Num& number); //format a single number using std::snprintf() @@ -848,6 +849,20 @@ char unhexify(char high, char low) } +inline +std::string formatAsHexString(const std::string& blob) +{ + std::string output; + for (const char c : blob) + { + const auto [high, low] = hexify(c, false /*upperCase*/); + output += high; + output += low; + } + return output; +} + + } #endif //STRING_TOOLS_H_213458973046 diff --git a/zen/symlink_target.h b/zen/symlink_target.h index 2393013e..077fd4b3 100644 --- a/zen/symlink_target.h +++ b/zen/symlink_target.h @@ -42,9 +42,9 @@ Zstring getSymlinkRawTargetString_impl(const Zstring& linkPath) //throw FileErro const ssize_t bytesWritten = ::readlink(linkPath.c_str(), &buffer[0], BUFFER_SIZE); if (bytesWritten < 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(linkPath)), L"readlink"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(linkPath)), "readlink"); if (bytesWritten >= static_cast<ssize_t>(BUFFER_SIZE)) //detect truncation, not an error for readlink! - throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(linkPath)), L"readlink: buffer truncated."); + throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(linkPath)), formatSystemError("readlink", L"", L"Buffer truncated.")); return Zstring(&buffer[0], bytesWritten); //readlink does not append 0-termination! } @@ -55,7 +55,7 @@ Zstring getResolvedSymlinkPath_impl(const Zstring& linkPath) //throw FileError using namespace zen; char* targetPath = ::realpath(linkPath.c_str(), nullptr); if (!targetPath) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot determine final path for %x."), L"%x", fmtPath(linkPath)), L"realpath"); + THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot determine final path for %x."), L"%x", fmtPath(linkPath)), "realpath"); ZEN_ON_SCOPE_EXIT(::free(targetPath)); return targetPath; } diff --git a/zen/sys_error.cpp b/zen/sys_error.cpp index b802780b..f9747d45 100644 --- a/zen/sys_error.cpp +++ b/zen/sys_error.cpp @@ -162,29 +162,30 @@ std::wstring formatSystemErrorCode(ErrorCode ec) ZEN_CHECK_CASE_FOR_CONSTANT(ERFKILL); ZEN_CHECK_CASE_FOR_CONSTANT(EHWPOISON); default: - return replaceCpy(_("Error Code %x"), L"%x", numberTo<std::wstring>(ec)); + return replaceCpy(_("Error code %x"), L"%x", numberTo<std::wstring>(ec)); } } } -std::wstring zen::formatSystemError(const std::wstring& functionName, ErrorCode ec) +std::wstring zen::formatSystemError(const std::string& functionName, ErrorCode ec) { return formatSystemError(functionName, formatSystemErrorCode(ec), getSystemErrorDescription(ec)); } -std::wstring zen::formatSystemError(const std::wstring& functionName, const std::wstring& errorCode, const std::wstring& errorMsg) +std::wstring zen::formatSystemError(const std::string& functionName, const std::wstring& errorCode, const std::wstring& errorMsg) { - std::wstring output = errorCode + L':'; + std::wstring output = errorCode; const std::wstring errorMsgFmt = trimCpy(errorMsg); - if (!errorMsgFmt.empty()) - { - output += L' '; - output += errorMsgFmt; - } + if (!errorCode.empty() && !errorMsgFmt.empty()) + output += L": "; + + output += errorMsgFmt; + + if (!functionName.empty()) + output += L" [" + utfTo<std::wstring>(functionName) + L']'; - output += L" [" + functionName + L']'; - return output; + return trimCpy(output); } diff --git a/zen/sys_error.h b/zen/sys_error.h index 6bef45ea..2dd3c188 100644 --- a/zen/sys_error.h +++ b/zen/sys_error.h @@ -22,8 +22,8 @@ namespace zen ErrorCode getLastError(); -std::wstring formatSystemError(const std::wstring& functionName, const std::wstring& errorCode, const std::wstring& errorMsg); -std::wstring formatSystemError(const std::wstring& functionName, ErrorCode ec); +std::wstring formatSystemError(const std::string& functionName, const std::wstring& errorCode, const std::wstring& errorMsg); +std::wstring formatSystemError(const std::string& functionName, ErrorCode ec); //A low-level exception class giving (non-translated) detail information only - same conceptional level like "GetLastError()"! diff --git a/zen/system.cpp b/zen/system.cpp index 9401b94f..d9a169c7 100644 --- a/zen/system.cpp +++ b/zen/system.cpp @@ -29,7 +29,7 @@ std::wstring zen::getUserName() //throw FileError struct passwd buffer2 = {}; struct passwd* pwsEntry = nullptr; if (::getpwuid_r(userIdNo, &buffer2, &buffer[0], buffer.size(), &pwsEntry) != 0) //getlogin() is deprecated and not working on Ubuntu at all!!! - THROW_LAST_FILE_ERROR(_("Cannot get process information."), L"getpwuid_r"); + THROW_LAST_FILE_ERROR(_("Cannot get process information."), "getpwuid_r"); if (!pwsEntry) throw FileError(_("Cannot get process information."), L"no login found"); //should not happen? @@ -96,9 +96,22 @@ std::wstring zen::getOsDescription() //throw FileError { try { - const std::string osName = trimCpy(getCommandOutput("lsb_release --id -s" )); //throw SysError - const std::string osVersion = trimCpy(getCommandOutput("lsb_release --release -s")); // - return utfTo<std::wstring>(osName + ' ' + osVersion); //e.g. "CentOS 7.7.1908" + std::wstring osName; + std::wstring osVersion; + + if (const auto [exitCode, output] = consoleExecute("lsb_release --id -s", std::nullopt); //throw SysError + exitCode != 0) + throw SysError(formatSystemError("lsb_release --id", replaceCpy(_("Exit code %x"), L"%x", numberTo<std::wstring>(exitCode)), output)); + else + osName = trimCpy(output); + + if (const auto [exitCode, output] = consoleExecute("lsb_release --release -s", std::nullopt); //throw SysError + exitCode != 0) + throw SysError(formatSystemError("lsb_release --release", replaceCpy(_("Exit code %x"), L"%x", numberTo<std::wstring>(exitCode)), output)); + else + osVersion = trimCpy(output); + + return osName + L' ' + osVersion; //e.g. "CentOS 7.7.1908" } catch (const SysError& e) { throw FileError(_("Cannot get process information."), e.toString()); } @@ -121,7 +121,7 @@ bool isValid(const std::tm& t) auto inRange = [](int value, int minVal, int maxVal) { return minVal <= value && value <= maxVal; }; - //http://www.cplusplus.com/reference/clibrary/ctime/tm/ + //https://www.cplusplus.com/reference/clibrary/ctime/tm/ return inRange(t.tm_sec, 0, 61) && inRange(t.tm_min, 0, 59) && inRange(t.tm_hour, 0, 23) && diff --git a/zen/zlib_wrap.cpp b/zen/zlib_wrap.cpp index 685843c3..dba890ee 100644 --- a/zen/zlib_wrap.cpp +++ b/zen/zlib_wrap.cpp @@ -55,7 +55,7 @@ size_t zen::impl::zlib_compress(const void* src, size_t srcLen, void* trg, size_ // Z_MEM_ERROR: not enough memory // Z_BUF_ERROR: not enough room in the output buffer if (rv != Z_OK || bufferSize > trgLen) - throw SysError(formatSystemError(L"zlib compress2", formatZlibStatusCode(rv), L"")); + throw SysError(formatSystemError("zlib compress2", formatZlibStatusCode(rv), L"")); return bufferSize; } @@ -73,7 +73,7 @@ size_t zen::impl::zlib_decompress(const void* src, size_t srcLen, void* trg, siz // Z_BUF_ERROR: not enough room in the output buffer // Z_DATA_ERROR: input data was corrupted or incomplete if (rv != Z_OK || bufferSize > trgLen) - throw SysError(formatSystemError(L"zlib uncompress", formatZlibStatusCode(rv), L"")); + throw SysError(formatSystemError("zlib uncompress", formatZlibStatusCode(rv), L"")); return bufferSize; } @@ -98,7 +98,7 @@ public: memLevel, //int memLevel Z_DEFAULT_STRATEGY); //int strategy if (rv != Z_OK) - throw SysError(formatSystemError(L"zlib deflateInit2", formatZlibStatusCode(rv), L"")); + throw SysError(formatSystemError("zlib deflateInit2", formatZlibStatusCode(rv), L"")); } ~Impl() @@ -133,7 +133,7 @@ public: if (rv == Z_STREAM_END) return bytesToRead - gzipStream_.avail_out; if (rv != Z_OK) - throw SysError(formatSystemError(L"zlib deflate", formatZlibStatusCode(rv), L"")); + throw SysError(formatSystemError("zlib deflate", formatZlibStatusCode(rv), L"")); if (gzipStream_.avail_out == 0) return bytesToRead; diff --git a/zen/zlib_wrap.h b/zen/zlib_wrap.h index 3db609da..41d7428a 100644 --- a/zen/zlib_wrap.h +++ b/zen/zlib_wrap.h @@ -113,7 +113,7 @@ BinContainer decompress(const BinContainer& stream) //throw SysError &*contOut.begin(), static_cast<size_t>(uncompressedSize)); //throw SysError if (bytesWritten != static_cast<size_t>(uncompressedSize)) - throw SysError(L"zlib error: bytes written != uncompressed size"); + throw SysError(formatSystemError("zlib_decompress", L"", L"bytes written != uncompressed size.")); } return contOut; } diff --git a/zen/zstring.cpp b/zen/zstring.cpp index 82082df0..8b16e02d 100644 --- a/zen/zstring.cpp +++ b/zen/zstring.cpp @@ -59,7 +59,7 @@ Zstring getUnicodeNormalForm(const Zstring& str) { gchar* outStr = ::g_utf8_normalize(str.c_str(), str.length(), G_NORMALIZE_DEFAULT_COMPOSE); if (!outStr) - throw SysError(L"g_utf8_normalize: conversion failed. (" + utfTo<std::wstring>(str) + L')'); + throw SysError(formatSystemError("g_utf8_normalize(" + utfTo<std::string>(str) + ')', L"", L"Conversion failed.")); ZEN_ON_SCOPE_EXIT(::g_free(outStr)); return outStr; diff --git a/zen/zstring.h b/zen/zstring.h index d5d8c588..e34d14a3 100644 --- a/zen/zstring.h +++ b/zen/zstring.h @@ -35,7 +35,7 @@ Zstring makeUpperCopy(const Zstring& str); Zstring getUnicodeNormalForm(const Zstring& str); // "In fact, Unicode declares that there is an equivalence relationship between decomposed and composed sequences, // and conformant software should not treat canonically equivalent sequences, whether composed or decomposed or something in between, as different." -// http://www.win.tue.nl/~aeb/linux/uc/nfc_vs_nfd.html +// https://www.win.tue.nl/~aeb/linux/uc/nfc_vs_nfd.html struct LessUnicodeNormal { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return getUnicodeNormalForm(lhs) < getUnicodeNormalForm(rhs);} }; |