From fd3fe75d1ca4ddf1a36eb89d7210a4ec0e8f4afa Mon Sep 17 00:00:00 2001 From: bubnikv Date: Tue, 20 Aug 2019 16:19:30 +0200 Subject: [PATCH] Reworked the rename_file() function on Windows to work reliably and atomically. The code was taken from the llvm project, it is complex and hopefully it covers all the Windows file system quirks. Vojtech has highest hopes, that this will fix the various PrusaSlicer.ini file corruptions. Enabled the locales switching and error handling on Linux as well, where now the missing locales are reported and running the locale-gen tool is recommended. --- src/PrusaSlicer.cpp | 9 +- src/libslic3r/GCode.cpp | 2 +- src/libslic3r/GCodeTimeEstimator.cpp | 2 +- src/libslic3r/Utils.hpp | 2 +- src/libslic3r/utils.cpp | 288 ++++++++++++++++++++++----- src/slic3r/GUI/AppConfig.cpp | 7 +- 6 files changed, 248 insertions(+), 62 deletions(-) diff --git a/src/PrusaSlicer.cpp b/src/PrusaSlicer.cpp index 72cbe150fc..92ff65a843 100644 --- a/src/PrusaSlicer.cpp +++ b/src/PrusaSlicer.cpp @@ -66,7 +66,12 @@ int CLI::run(int argc, char **argv) boost::nowide::nowide_filesystem(); } catch (const std::runtime_error& ex) { std::string caption = std::string(SLIC3R_APP_NAME) + " Error"; - std::string text = std::string("An error occured while setting up locale.\n") + SLIC3R_APP_NAME + " will now terminate.\n\n" + ex.what(); + std::string text = std::string("An error occured while setting up locale.\n") + ( +#if !defined(_WIN32) && !defined(__APPLE__) + // likely some linux system + "You may need to reconfigure the missing locales, likely by running the \"locale-gen\"" and \"dpkg-reconfigure locales\" commands.\n" +#endif + SLIC3R_APP_NAME " will now terminate.\n\n") + ex.what(); #ifdef SLIC3R_GUI if (m_actions.empty()) MessageBoxA(NULL, text.c_str(), caption.c_str(), MB_OK | MB_ICONERROR); @@ -426,7 +431,7 @@ int CLI::run(int argc, char **argv) outfile_final = sla_print.print_statistics().finalize_output_path(outfile); sla_print.export_raster(outfile_final); } - if (outfile != outfile_final && Slic3r::rename_file(outfile, outfile_final) != 0) { + if (outfile != outfile_final && Slic3r::rename_file(outfile, outfile_final)) { boost::nowide::cerr << "Renaming file " << outfile << " to " << outfile_final << " failed" << std::endl; return 1; } diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 7a6f686dfc..8a007023bb 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -607,7 +607,7 @@ void GCode::do_export(Print *print, const char *path, GCodePreviewData *preview_ m_analyzer.reset(); } - if (rename_file(path_tmp, path) != 0) + if (rename_file(path_tmp, path)) throw std::runtime_error( std::string("Failed to rename the output G-code file from ") + path_tmp + " to " + path + '\n' + "Is " + path_tmp + " locked?" + '\n'); diff --git a/src/libslic3r/GCodeTimeEstimator.cpp b/src/libslic3r/GCodeTimeEstimator.cpp index 8a2e1266a8..a03d3c7c85 100644 --- a/src/libslic3r/GCodeTimeEstimator.cpp +++ b/src/libslic3r/GCodeTimeEstimator.cpp @@ -390,7 +390,7 @@ namespace Slic3r { fclose(out); in.close(); - if (rename_file(path_tmp, filename) != 0) + if (rename_file(path_tmp, filename)) throw std::runtime_error(std::string("Failed to rename the output G-code file from ") + path_tmp + " to " + filename + '\n' + "Is " + path_tmp + " locked?" + '\n'); diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index b19027826b..2b1fdb2417 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -61,7 +61,7 @@ extern std::string normalize_utf8_nfc(const char *src); // Safely rename a file even if the target exists. // On Windows, the file explorer (or anti-virus or whatever else) often locks the file // for a short while, so the file may not be movable. Retry while we see recoverable errors. -extern int rename_file(const std::string &from, const std::string &to); +extern std::error_code rename_file(const std::string &from, const std::string &to); // Copy a file, adjust the access attributes, so that the target is writable. extern int copy_file(const std::string &from, const std::string &to); diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index 8fcd611acc..e26ed38394 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -173,67 +173,247 @@ const std::string& data_dir() return g_data_dir; } +#ifdef _WIN32 +// The following helpers are borrowed from the LLVM project https://github.com/llvm +namespace WindowsSupport +{ + template + class ScopedHandle { + typedef typename HandleTraits::handle_type handle_type; + handle_type Handle; + ScopedHandle(const ScopedHandle &other) = delete; + void operator=(const ScopedHandle &other) = delete; + public: + ScopedHandle() : Handle(HandleTraits::GetInvalid()) {} + explicit ScopedHandle(handle_type h) : Handle(h) {} + ~ScopedHandle() { if (HandleTraits::IsValid(Handle)) HandleTraits::Close(Handle); } + handle_type take() { + handle_type t = Handle; + Handle = HandleTraits::GetInvalid(); + return t; + } + ScopedHandle &operator=(handle_type h) { + if (HandleTraits::IsValid(Handle)) + HandleTraits::Close(Handle); + Handle = h; + return *this; + } + // True if Handle is valid. + explicit operator bool() const { return HandleTraits::IsValid(Handle) ? true : false; } + operator handle_type() const { return Handle; } + }; + + struct CommonHandleTraits { + typedef HANDLE handle_type; + static handle_type GetInvalid() { return INVALID_HANDLE_VALUE; } + static void Close(handle_type h) { ::CloseHandle(h); } + static bool IsValid(handle_type h) { return h != GetInvalid(); } + }; + + typedef ScopedHandle ScopedFileHandle; + + std::error_code map_windows_error(unsigned windows_error_code) + { + #define MAP_ERR_TO_COND(x, y) case x: return std::make_error_code(std::errc::y) + switch (windows_error_code) { + MAP_ERR_TO_COND(ERROR_ACCESS_DENIED, permission_denied); + MAP_ERR_TO_COND(ERROR_ALREADY_EXISTS, file_exists); + MAP_ERR_TO_COND(ERROR_BAD_UNIT, no_such_device); + MAP_ERR_TO_COND(ERROR_BUFFER_OVERFLOW, filename_too_long); + MAP_ERR_TO_COND(ERROR_BUSY, device_or_resource_busy); + MAP_ERR_TO_COND(ERROR_BUSY_DRIVE, device_or_resource_busy); + MAP_ERR_TO_COND(ERROR_CANNOT_MAKE, permission_denied); + MAP_ERR_TO_COND(ERROR_CANTOPEN, io_error); + MAP_ERR_TO_COND(ERROR_CANTREAD, io_error); + MAP_ERR_TO_COND(ERROR_CANTWRITE, io_error); + MAP_ERR_TO_COND(ERROR_CURRENT_DIRECTORY, permission_denied); + MAP_ERR_TO_COND(ERROR_DEV_NOT_EXIST, no_such_device); + MAP_ERR_TO_COND(ERROR_DEVICE_IN_USE, device_or_resource_busy); + MAP_ERR_TO_COND(ERROR_DIR_NOT_EMPTY, directory_not_empty); + MAP_ERR_TO_COND(ERROR_DIRECTORY, invalid_argument); + MAP_ERR_TO_COND(ERROR_DISK_FULL, no_space_on_device); + MAP_ERR_TO_COND(ERROR_FILE_EXISTS, file_exists); + MAP_ERR_TO_COND(ERROR_FILE_NOT_FOUND, no_such_file_or_directory); + MAP_ERR_TO_COND(ERROR_HANDLE_DISK_FULL, no_space_on_device); + MAP_ERR_TO_COND(ERROR_INVALID_ACCESS, permission_denied); + MAP_ERR_TO_COND(ERROR_INVALID_DRIVE, no_such_device); + MAP_ERR_TO_COND(ERROR_INVALID_FUNCTION, function_not_supported); + MAP_ERR_TO_COND(ERROR_INVALID_HANDLE, invalid_argument); + MAP_ERR_TO_COND(ERROR_INVALID_NAME, invalid_argument); + MAP_ERR_TO_COND(ERROR_LOCK_VIOLATION, no_lock_available); + MAP_ERR_TO_COND(ERROR_LOCKED, no_lock_available); + MAP_ERR_TO_COND(ERROR_NEGATIVE_SEEK, invalid_argument); + MAP_ERR_TO_COND(ERROR_NOACCESS, permission_denied); + MAP_ERR_TO_COND(ERROR_NOT_ENOUGH_MEMORY, not_enough_memory); + MAP_ERR_TO_COND(ERROR_NOT_READY, resource_unavailable_try_again); + MAP_ERR_TO_COND(ERROR_OPEN_FAILED, io_error); + MAP_ERR_TO_COND(ERROR_OPEN_FILES, device_or_resource_busy); + MAP_ERR_TO_COND(ERROR_OUTOFMEMORY, not_enough_memory); + MAP_ERR_TO_COND(ERROR_PATH_NOT_FOUND, no_such_file_or_directory); + MAP_ERR_TO_COND(ERROR_BAD_NETPATH, no_such_file_or_directory); + MAP_ERR_TO_COND(ERROR_READ_FAULT, io_error); + MAP_ERR_TO_COND(ERROR_RETRY, resource_unavailable_try_again); + MAP_ERR_TO_COND(ERROR_SEEK, io_error); + MAP_ERR_TO_COND(ERROR_SHARING_VIOLATION, permission_denied); + MAP_ERR_TO_COND(ERROR_TOO_MANY_OPEN_FILES, too_many_files_open); + MAP_ERR_TO_COND(ERROR_WRITE_FAULT, io_error); + MAP_ERR_TO_COND(ERROR_WRITE_PROTECT, permission_denied); + MAP_ERR_TO_COND(WSAEACCES, permission_denied); + MAP_ERR_TO_COND(WSAEBADF, bad_file_descriptor); + MAP_ERR_TO_COND(WSAEFAULT, bad_address); + MAP_ERR_TO_COND(WSAEINTR, interrupted); + MAP_ERR_TO_COND(WSAEINVAL, invalid_argument); + MAP_ERR_TO_COND(WSAEMFILE, too_many_files_open); + MAP_ERR_TO_COND(WSAENAMETOOLONG, filename_too_long); + default: + return std::error_code(windows_error_code, std::system_category()); + } + #undef MAP_ERR_TO_COND + } + + static std::error_code rename_internal(HANDLE from_handle, const std::wstring &wide_to, bool replace_if_exists) + { + std::vector rename_info_buf(sizeof(FILE_RENAME_INFO) - sizeof(wchar_t) + (wide_to.size() * sizeof(wchar_t))); + FILE_RENAME_INFO &rename_info = *reinterpret_cast(rename_info_buf.data()); + rename_info.ReplaceIfExists = replace_if_exists; + rename_info.RootDirectory = 0; + rename_info.FileNameLength = DWORD(wide_to.size() * sizeof(wchar_t)); + std::copy(wide_to.begin(), wide_to.end(), &rename_info.FileName[0]); + + ::SetLastError(ERROR_SUCCESS); + if (! ::SetFileInformationByHandle(from_handle, FileRenameInfo, &rename_info, (DWORD)rename_info_buf.size())) { + unsigned Error = GetLastError(); + if (Error == ERROR_SUCCESS) + Error = ERROR_CALL_NOT_IMPLEMENTED; // Wine doesn't always set error code. + return map_windows_error(Error); + } + + return std::error_code(); + } + + static std::error_code real_path_from_handle(HANDLE H, std::wstring &buffer) + { + buffer.resize(MAX_PATH + 1); + DWORD CountChars = ::GetFinalPathNameByHandleW(H, (LPWSTR)buffer.data(), (DWORD)buffer.size() - 1, FILE_NAME_NORMALIZED); + if (CountChars > buffer.size()) { + // The buffer wasn't big enough, try again. In this case the return value *does* indicate the size of the null terminator. + buffer.resize((size_t)CountChars); + CountChars = ::GetFinalPathNameByHandleW(H, (LPWSTR)buffer.data(), (DWORD)buffer.size() - 1, FILE_NAME_NORMALIZED); + } + if (CountChars == 0) + return map_windows_error(GetLastError()); + buffer.resize(CountChars); + return std::error_code(); + } + + std::error_code rename(const std::string &from, const std::string &to) + { + // Convert to utf-16. + std::wstring wide_from = boost::nowide::widen(from); + std::wstring wide_to = boost::nowide::widen(to); + + ScopedFileHandle from_handle; + // Retry this a few times to defeat badly behaved file system scanners. + for (unsigned retry = 0; retry != 200; ++ retry) { + if (retry != 0) + ::Sleep(10); + from_handle = ::CreateFileW((LPWSTR)wide_from.data(), GENERIC_READ | DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (from_handle) + break; + } + if (! from_handle) + return map_windows_error(GetLastError()); + + // We normally expect this loop to succeed after a few iterations. If it + // requires more than 200 tries, it's more likely that the failures are due to + // a true error, so stop trying. + for (unsigned retry = 0; retry != 200; ++ retry) { + auto errcode = rename_internal(from_handle, wide_to, true); + + if (errcode == std::error_code(ERROR_CALL_NOT_IMPLEMENTED, std::system_category())) { + // Wine doesn't support SetFileInformationByHandle in rename_internal. + // Fall back to MoveFileEx. + if (std::error_code errcode2 = real_path_from_handle(from_handle, wide_from)) + return errcode2; + if (::MoveFileExW((LPCWSTR)wide_from.data(), (LPCWSTR)wide_to.data(), MOVEFILE_REPLACE_EXISTING)) + return std::error_code(); + return map_windows_error(GetLastError()); + } + + if (! errcode || errcode != std::errc::permission_denied) + return errcode; + + // The destination file probably exists and is currently open in another + // process, either because the file was opened without FILE_SHARE_DELETE or + // it is mapped into memory (e.g. using MemoryBuffer). Rename it in order to + // move it out of the way of the source file. Use FILE_FLAG_DELETE_ON_CLOSE + // to arrange for the destination file to be deleted when the other process + // closes it. + ScopedFileHandle to_handle(::CreateFileW((LPCWSTR)wide_to.data(), GENERIC_READ | DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE, NULL)); + if (! to_handle) { + auto errcode = map_windows_error(GetLastError()); + // Another process might have raced with us and moved the existing file + // out of the way before we had a chance to open it. If that happens, try + // to rename the source file again. + if (errcode == std::errc::no_such_file_or_directory) + continue; + return errcode; + } + + BY_HANDLE_FILE_INFORMATION FI; + if (! ::GetFileInformationByHandle(to_handle, &FI)) + return map_windows_error(GetLastError()); + + // Try to find a unique new name for the destination file. + for (unsigned unique_id = 0; unique_id != 200; ++ unique_id) { + std::wstring tmp_filename = wide_to + L".tmp" + std::to_wstring(unique_id); + std::error_code errcode = rename_internal(to_handle, tmp_filename, false); + if (errcode) { + if (errcode == std::make_error_code(std::errc::file_exists) || errcode == std::make_error_code(std::errc::permission_denied)) { + // Again, another process might have raced with us and moved the file + // before we could move it. Check whether this is the case, as it + // might have caused the permission denied error. If that was the + // case, we don't need to move it ourselves. + ScopedFileHandle to_handle2(::CreateFileW((LPCWSTR)wide_to.data(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)); + if (! to_handle2) { + auto errcode = map_windows_error(GetLastError()); + if (errcode == std::errc::no_such_file_or_directory) + break; + return errcode; + } + BY_HANDLE_FILE_INFORMATION FI2; + if (! ::GetFileInformationByHandle(to_handle2, &FI2)) + return map_windows_error(GetLastError()); + if (FI.nFileIndexHigh != FI2.nFileIndexHigh || FI.nFileIndexLow != FI2.nFileIndexLow || FI.dwVolumeSerialNumber != FI2.dwVolumeSerialNumber) + break; + continue; + } + return errcode; + } + break; + } + + // Okay, the old destination file has probably been moved out of the way at + // this point, so try to rename the source file again. Still, another + // process might have raced with us to create and open the destination + // file, so we need to keep doing this until we succeed. + } + + // The most likely root cause. + return std::make_error_code(std::errc::permission_denied); + } +} // namespace WindowsSupport +#endif /* _WIN32 */ // borrowed from LVVM lib/Support/Windows/Path.inc -int rename_file(const std::string &from, const std::string &to) +std::error_code rename_file(const std::string &from, const std::string &to) { - int ec = 0; - #ifdef _WIN32 - - // Convert to utf-16. - std::wstring wide_from = boost::nowide::widen(from); - std::wstring wide_to = boost::nowide::widen(to); - - // Retry while we see recoverable errors. - // System scanners (eg. indexer) might open the source file when it is written - // and closed. - bool TryReplace = true; - - // This loop may take more than 2000 x 1ms to finish. - for (int i = 0; i < 2000; ++ i) { - if (i > 0) - // Sleep 1ms - ::Sleep(1); - if (TryReplace) { - // Try ReplaceFile first, as it is able to associate a new data stream - // with the destination even if the destination file is currently open. - if (::ReplaceFileW(wide_to.data(), wide_from.data(), NULL, 0, NULL, NULL)) - return 0; - DWORD ReplaceError = ::GetLastError(); - ec = -1; // ReplaceError - // If ReplaceFileW returned ERROR_UNABLE_TO_MOVE_REPLACEMENT or - // ERROR_UNABLE_TO_MOVE_REPLACEMENT_2, retry but only use MoveFileExW(). - if (ReplaceError == ERROR_UNABLE_TO_MOVE_REPLACEMENT || - ReplaceError == ERROR_UNABLE_TO_MOVE_REPLACEMENT_2) { - TryReplace = false; - continue; - } - // If ReplaceFileW returned ERROR_UNABLE_TO_REMOVE_REPLACED, retry - // using ReplaceFileW(). - if (ReplaceError == ERROR_UNABLE_TO_REMOVE_REPLACED) - continue; - // We get ERROR_FILE_NOT_FOUND if the destination file is missing. - // MoveFileEx can handle this case. - if (ReplaceError != ERROR_ACCESS_DENIED && ReplaceError != ERROR_FILE_NOT_FOUND && ReplaceError != ERROR_SHARING_VIOLATION) - break; - } - if (::MoveFileExW(wide_from.c_str(), wide_to.c_str(), MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING)) - return 0; - DWORD MoveError = ::GetLastError(); - ec = -1; // MoveError - if (MoveError != ERROR_ACCESS_DENIED && MoveError != ERROR_SHARING_VIOLATION) - break; - } - + return WindowsSupport::rename(from, to); #else - boost::nowide::remove(to.c_str()); - ec = boost::nowide::rename(from.c_str(), to.c_str()); - + return std::make_error_code(static_cast(boost::nowide::rename(from.c_str(), to.c_str()))); #endif - - return ec; } int copy_file(const std::string &from, const std::string &to) diff --git a/src/slic3r/GUI/AppConfig.cpp b/src/slic3r/GUI/AppConfig.cpp index 7ca8c3e5eb..96213447cf 100644 --- a/src/slic3r/GUI/AppConfig.cpp +++ b/src/slic3r/GUI/AppConfig.cpp @@ -98,9 +98,10 @@ void AppConfig::load() pt::read_ini(ifs, tree); } catch (pt::ptree_error& ex) { // Error while parsing config file. We'll customize the error message and rethrow to be displayed. - throw std::runtime_error(wxString::Format(_(L("Error parsing config file, it is probably corrupted. " - "Try to manualy delete the file. Your user profiles will not be affected.\n\n%s\n\n%s")), - AppConfig::config_path(), ex.what()).ToStdString()); + throw std::runtime_error( + _utf8(L("Error parsing PrusaSlicer config file, it is probably corrupted. " + "Try to manualy delete the file to recover from the error. Your user profiles will not be affected.")) + + "\n\n" + AppConfig::config_path() + "\n\n" + ex.what()); } // 2) Parse the property_tree, extract the sections and key / value pairs.