#include "AppUpdater.hpp" #include <atomic> #include <thread> #include <boost/filesystem.hpp> #include <boost/log/trivial.hpp> #include <boost/nowide/fstream.hpp> #include <boost/nowide/convert.hpp> #include <boost/property_tree/ini_parser.hpp> #include <curl/curl.h> #include "slic3r/GUI/format.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/I18N.hpp" #include "slic3r/Utils/Http.hpp" #include "libslic3r/Utils.hpp" #ifdef _WIN32 #include <shellapi.h> #include <Shlobj_core.h> #include <windows.h> #include <KnownFolders.h> #include <shlobj.h> #endif // _WIN32 namespace Slic3r { namespace { #ifdef _WIN32 bool run_file(const boost::filesystem::path& path) { std::string msg; bool res = GUI::create_process(path, std::wstring(), msg); if (!res) { std::string full_message = GUI::format(_utf8("Running downloaded instaler of %1% has failed:\n%2%"), SLIC3R_APP_NAME, msg); BOOST_LOG_TRIVIAL(error) << full_message; // lm: maybe UI error msg? // dk: bellow. (maybe some general show error evt would be better?) wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); evt->SetString(full_message); GUI::wxGetApp().QueueEvent(evt); } return res; } std::string get_downloads_path() { std::string ret; PWSTR path = NULL; HRESULT hr = SHGetKnownFolderPath(FOLDERID_Downloads, 0, NULL, &path); if (SUCCEEDED(hr)) { ret = boost::nowide::narrow(path); } CoTaskMemFree(path); return ret; } #elif __APPLE__ bool run_file(const boost::filesystem::path& path) { if (boost::filesystem::exists(path)) { // attach downloaded dmg file const char* argv1[] = { "hdiutil", "attach", path.string().c_str(), nullptr }; ::wxExecute(const_cast<char**>(argv1), wxEXEC_ASYNC, nullptr); // open inside attached as a folder in finder const char* argv2[] = { "open", "/Volumes/PrusaSlicer", nullptr }; ::wxExecute(const_cast<char**>(argv2), wxEXEC_ASYNC, nullptr); return true; } return false; } std::string get_downloads_path() { // call objective-c implementation return get_downloads_path_mac(); } #else bool run_file(const boost::filesystem::path& path) { return false; } std::string get_downloads_path() { wxString command = "xdg-user-dir DOWNLOAD"; wxArrayString output; GUI::desktop_execute_get_result(command, output); if (output.GetCount() > 0) { return output[0].ToUTF8().data(); //lm:I would use wxString::ToUTF8(), although on Linux, nothing at all should work too. } return std::string(); } #endif // _WIN32 / __apple__ / else } // namespace wxDEFINE_EVENT(EVT_SLIC3R_VERSION_ONLINE, wxCommandEvent); wxDEFINE_EVENT(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, wxCommandEvent); wxDEFINE_EVENT(EVT_SLIC3R_APP_DOWNLOAD_PROGRESS, wxCommandEvent); wxDEFINE_EVENT(EVT_SLIC3R_APP_DOWNLOAD_FAILED, wxCommandEvent); wxDEFINE_EVENT(EVT_SLIC3R_APP_OPEN_FAILED, wxCommandEvent); // priv handles all operations in separate thread // 1) download version file and parse it. // 2) download new app file and open in folder / run it. struct AppUpdater::priv { priv(); // Download file. What happens with the data is specified in completefn. bool http_get_file(const std::string& url , size_t size_limit , std::function<bool(Http::Progress)> progress_fn , std::function<bool(std::string /*body*/, std::string& error_message)> completefn , std::string& error_message ) const; // Download installer / app boost::filesystem::path download_file(const DownloadAppData& data) const; // Run file in m_last_dest_path bool run_downloaded_file(boost::filesystem::path path); // gets version file via http void version_check(const std::string& version_check_url); #if 0 // parsing of Prusaslicer.version2 void parse_version_string_old(const std::string& body) const; #endif // parses ini tree of version file, saves to m_online_version_data and queue event(s) to UI void parse_version_string(const std::string& body); // thread std::thread m_thread; std::atomic_bool m_cancel; std::mutex m_data_mutex; // used to tell if notify user hes about to stop ongoing download std::atomic_bool m_download_ongoing { false }; bool get_download_ongoing() const { return m_download_ongoing; } // read only variable used to init m_online_version_data.target_path boost::filesystem::path m_default_dest_folder; // readonly // DownloadAppData read / write needs to be locked by m_data_mutex DownloadAppData m_online_version_data; DownloadAppData get_app_data(); void set_app_data(DownloadAppData data); // set only before version file is downloaded, to keep information to show info dialog about no updates // should never change during thread run std::atomic_bool m_triggered_by_user {false}; bool get_triggered_by_user() const { return m_triggered_by_user; } }; AppUpdater::priv::priv() : m_cancel (false) #ifdef __linux__ , m_default_dest_folder (boost::filesystem::path("/tmp")) #else , m_default_dest_folder (boost::filesystem::path(data_dir()) / "cache") #endif //_WIN32 { boost::filesystem::path downloads_path = boost::filesystem::path(get_downloads_path()); if (!downloads_path.empty()) { m_default_dest_folder = std::move(downloads_path); } BOOST_LOG_TRIVIAL(trace) << "App updater default download path: " << m_default_dest_folder; //lm:Is this an error? // dk: changed to trace } bool AppUpdater::priv::http_get_file(const std::string& url, size_t size_limit, std::function<bool(Http::Progress)> progress_fn, std::function<bool(std::string /*body*/, std::string& error_message)> complete_fn, std::string& error_message) const { bool res = false; Http::get(url) .size_limit(size_limit) .on_progress([&, progress_fn](Http::Progress progress, bool& cancel) { // progress function returns true as success (to continue) cancel = (m_cancel ? true : !progress_fn(std::move(progress))); if (cancel) { // Lets keep error_message empty here - if there is need to show error dialog, the message will be probably shown by whatever caused the cancel. /* error_message = GUI::format(_utf8("Error getting: `%1%`: Download was canceled."), url); */ BOOST_LOG_TRIVIAL(debug) << "AppUpdater::priv::http_get_file message: "<< error_message; } }) .on_error([&](std::string body, std::string error, unsigned http_status) { error_message = GUI::format("Error getting: `%1%`: HTTP %2%, %3%", url, http_status, error); BOOST_LOG_TRIVIAL(error) << error_message; }) .on_complete([&](std::string body, unsigned /* http_status */) { assert(complete_fn != nullptr); res = complete_fn(body, error_message); }) .perform_sync(); return res; } boost::filesystem::path AppUpdater::priv::download_file(const DownloadAppData& data) const { boost::filesystem::path dest_path; size_t last_gui_progress = 0; size_t expected_size = data.size; dest_path = data.target_path; assert(!dest_path.empty()); if (dest_path.empty()) { std::string line1 = GUI::format(_utf8("Internal download error for url %1%:"), data.url); std::string line2 = _utf8("Destination path is empty."); std::string message = GUI::format("%1%\n%2%", line1, line2); BOOST_LOG_TRIVIAL(error) << message; wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); evt->SetString(message); GUI::wxGetApp().QueueEvent(evt); return boost::filesystem::path(); } boost::filesystem::path tmp_path = dest_path; tmp_path += format(".%1%%2%", get_current_pid(), ".download"); FILE* file; wxString temp_path_wstring(tmp_path.wstring()); file = fopen(temp_path_wstring.c_str(), "wb"); assert(file != NULL); if (file == NULL) { std::string line1 = GUI::format(_utf8("Download from %1% couldn't start:"), data.url); std::string line2 = GUI::format(_utf8("Can't create file at %1%."), tmp_path.string()); std::string message = GUI::format("%1%\n%2%", line1, line2); BOOST_LOG_TRIVIAL(error) << message; wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); evt->SetString(message); GUI::wxGetApp().QueueEvent(evt); return boost::filesystem::path(); } std::string error_message; bool res = http_get_file(data.url, 130 * 1024 * 1024 //2.4.0 windows installer is 65MB //lm:I don't know, but larger. The binaries will grow. // dk: changed to 130, to have 100% more space. We should put this information into version file. // on_progress , [&last_gui_progress, expected_size](Http::Progress progress) { // size check if (progress.dltotal > 0 && progress.dltotal > expected_size) { std::string message = GUI::format("Downloading new %1% has failed. The file has incorrect file size. Aborting download.\nExpected size: %2%\nDownload size: %3%", SLIC3R_APP_NAME, expected_size, progress.dltotal); BOOST_LOG_TRIVIAL(error) << message; wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); evt->SetString(message); GUI::wxGetApp().QueueEvent(evt); return false; } else if (progress.dltotal > 0 && progress.dltotal < expected_size) { // This is possible error, but we cannot know until the download is finished. Somehow the total size can grow during the download. BOOST_LOG_TRIVIAL(info) << GUI::format("Downloading new %1% has incorrect size. The download will continue. \nExpected size: %2%\nDownload size: %3%", SLIC3R_APP_NAME, expected_size, progress.dltotal); } // progress event size_t gui_progress = progress.dltotal > 0 ? 100 * progress.dlnow / progress.dltotal : 0; BOOST_LOG_TRIVIAL(debug) << "App download " << gui_progress << "% " << progress.dlnow << " of " << progress.dltotal; if (last_gui_progress < gui_progress && (last_gui_progress != 0 || gui_progress != 100)) { last_gui_progress = gui_progress; wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_PROGRESS); evt->SetString(GUI::from_u8(std::to_string(gui_progress))); GUI::wxGetApp().QueueEvent(evt); } return true; } // on_complete , [&file, dest_path, tmp_path, expected_size](std::string body, std::string& error_message){ // Size check. Does always 1 char == 1 byte? size_t body_size = body.size(); if (body_size != expected_size) { error_message = GUI::format(_utf8("Downloaded file has wrong size. Expected size: %1% Downloaded size: %2%"), expected_size, body_size); return false; } if (file == NULL) { error_message = GUI::format(_utf8("Can't create file at %1%."), tmp_path.string()); return false; } try { fwrite(body.c_str(), 1, body.size(), file); fclose(file); boost::filesystem::rename(tmp_path, dest_path); } catch (const std::exception& e) { error_message = GUI::format(_utf8("Failed to write to file or to move %1% to %2%:\n%3%"), tmp_path, dest_path, e.what()); return false; } return true; } , error_message ); if (!res) { if (m_cancel) { BOOST_LOG_TRIVIAL(info) << error_message; wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); // FAILED with empty msg only closes progress notification GUI::wxGetApp().QueueEvent(evt); } else { std::string message = (error_message.empty() ? std::string() : GUI::format(_utf8("Downloading new %1% has failed:\n%2%"), SLIC3R_APP_NAME, error_message)); wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); if (!message.empty()) { BOOST_LOG_TRIVIAL(error) << message; evt->SetString(message); } GUI::wxGetApp().QueueEvent(evt); } return boost::filesystem::path(); } return dest_path; } bool AppUpdater::priv::run_downloaded_file(boost::filesystem::path path) { assert(!path.empty()); return run_file(path); } void AppUpdater::priv::version_check(const std::string& version_check_url) { assert(!version_check_url.empty()); std::string error_message; bool res = http_get_file(version_check_url, 1024 // on_progress , [](Http::Progress progress) { return true; } // on_complete , [&](std::string body, std::string& error_message) { boost::trim(body); parse_version_string(body); return true; } , error_message ); //lm:In case the internet is not available, it will report no updates if run by user. // We might save a flag that we don't know or try to run the version_check again, reporting // the failure. // dk: changed to download version every time. Dialog will show if m_triggered_by_user. if (!res) { std::string message = GUI::format("Downloading %1% version file has failed:\n%2%", SLIC3R_APP_NAME, error_message); BOOST_LOG_TRIVIAL(error) << message; if (m_triggered_by_user) { wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); evt->SetString(message); GUI::wxGetApp().QueueEvent(evt); } } } void AppUpdater::priv::parse_version_string(const std::string& body) { size_t start = body.find('['); if (start == std::string::npos) { #if 0 BOOST_LOG_TRIVIAL(error) << "Could not find property tree in version file. Starting old parsing."; parse_version_string_old(body); return; #endif // 0 BOOST_LOG_TRIVIAL(error) << "Could not find property tree in version file. Checking for application update has failed."; // Lets send event with current version, this way if user triggered this check, it will notify him about no new version online. std::string version = Semver().to_string(); wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE); evt->SetString(GUI::from_u8(version)); GUI::wxGetApp().QueueEvent(evt); return; } std::string tree_string = body.substr(start); boost::property_tree::ptree tree; std::stringstream ss(tree_string); try { boost::property_tree::read_ini(ss, tree); } catch (const boost::property_tree::ini_parser::ini_parser_error& err) { //throw Slic3r::RuntimeError(format("Failed reading version file property tree Error: \"%1%\" at line %2%. \nTree:\n%3%", err.message(), err.line(), tree_string).c_str()); BOOST_LOG_TRIVIAL(error) << format("Failed reading version file property tree Error: \"%1%\" at line %2%. \nTree:\n%3%", err.message(), err.line(), tree_string); return; } DownloadAppData new_data; for (const auto& section : tree) { std::string section_name = section.first; // online release version info if (section_name == #ifdef _WIN32 "release:win64" #elif __APPLE__ "release:osx" #else "release:linux" #endif //lm:Related to the ifdefs. We should also support BSD, which behaves similar to Linux in most cases. // Unless you have a reason not to, I would consider doing _WIN32, elif __APPLE__, else ... Not just here. // dk: so its ok now or we need to specify BSD? ) { for (const auto& data : section.second) { if (data.first == "url") { new_data.url = data.second.data(); new_data.target_path = m_default_dest_folder / AppUpdater::get_filename_from_url(new_data.url); BOOST_LOG_TRIVIAL(info) << format("parsing version string: url: %1%", new_data.url); } else if (data.first == "size"){ new_data.size = std::stoi(data.second.data()); BOOST_LOG_TRIVIAL(info) << format("parsing version string: expected size: %1%", new_data.size); } } } // released versions - to be send to UI layer if (section_name == "common") { std::vector<std::string> prerelease_versions; for (const auto& data : section.second) { // release version - save and send to UI layer if (data.first == "release") { std::string version = data.second.data(); boost::optional<Semver> release_version = Semver::parse(version); if (!release_version) { BOOST_LOG_TRIVIAL(error) << format("Received invalid contents from version file: Not a correct semver: `%1%`", version); return; } new_data.version = release_version; // Send after all data is read /* BOOST_LOG_TRIVIAL(info) << format("Got %1% online version: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version); wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE); evt->SetString(GUI::from_u8(version)); GUI::wxGetApp().QueueEvent(evt); */ // prerelease versions - write down to be sorted and send to UI layer } else if (data.first == "alpha") { prerelease_versions.emplace_back(data.second.data()); } else if (data.first == "beta") { prerelease_versions.emplace_back(data.second.data()); } else if (data.first == "rc") { prerelease_versions.emplace_back(data.second.data()); } } // find recent version that is newer than last full release. boost::optional<Semver> recent_version; std::string version_string; for (const std::string& ver_string : prerelease_versions) { boost::optional<Semver> ver = Semver::parse(ver_string); if (ver && *new_data.version < *ver && ((recent_version && *recent_version < *ver) || !recent_version)) { recent_version = ver; version_string = ver_string; } } // send prerelease version to UI layer if (recent_version) { BOOST_LOG_TRIVIAL(info) << format("Got %1% online version: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version_string); wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE); evt->SetString(GUI::from_u8(version_string)); GUI::wxGetApp().QueueEvent(evt); } } } assert(!new_data.url.empty()); assert(new_data.version); // save set_app_data(new_data); // send std::string version = new_data.version.get().to_string(); BOOST_LOG_TRIVIAL(info) << format("Got %1% online version: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version); wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE); evt->SetString(GUI::from_u8(version)); GUI::wxGetApp().QueueEvent(evt); } #if 0 //lm:is this meant to be ressurected? //dk: it is code that parses PrusaSlicer.version2 in 2.4.0, It was deleted from PresetUpdater.cpp and I would keep it here for possible reference. void AppUpdater::priv::parse_version_string_old(const std::string& body) const { // release version std::string version; const auto first_nl_pos = body.find_first_of("\n\r"); if (first_nl_pos != std::string::npos) version = body.substr(0, first_nl_pos); else version = body; boost::optional<Semver> release_version = Semver::parse(version); if (!release_version) { BOOST_LOG_TRIVIAL(error) << format("Received invalid contents from `%1%`: Not a correct semver: `%2%`", SLIC3R_APP_NAME, version); return; } BOOST_LOG_TRIVIAL(info) << format("Got %1% online version: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version); wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE); evt->SetString(GUI::from_u8(version)); GUI::wxGetApp().QueueEvent(evt); // alpha / beta version std::vector<std::string> prerelease_versions; size_t nexn_nl_pos = first_nl_pos; while (nexn_nl_pos != std::string::npos && body.size() > nexn_nl_pos + 1) { const auto last_nl_pos = nexn_nl_pos; nexn_nl_pos = body.find_first_of("\n\r", last_nl_pos + 1); std::string line; if (nexn_nl_pos == std::string::npos) line = body.substr(last_nl_pos + 1); else line = body.substr(last_nl_pos + 1, nexn_nl_pos - last_nl_pos - 1); // alpha if (line.substr(0, 6) == "alpha=") { version = line.substr(6); if (!Semver::parse(version)) { BOOST_LOG_TRIVIAL(error) << format("Received invalid contents for alpha release from `%1%`: Not a correct semver: `%2%`", SLIC3R_APP_NAME, version); return; } prerelease_versions.emplace_back(version); // beta } else if (line.substr(0, 5) == "beta=") { version = line.substr(5); if (!Semver::parse(version)) { BOOST_LOG_TRIVIAL(error) << format("Received invalid contents for beta release from `%1%`: Not a correct semver: `%2%`", SLIC3R_APP_NAME, version); return; } prerelease_versions.emplace_back(version); } } // find recent version that is newer than last full release. boost::optional<Semver> recent_version; for (const std::string& ver_string : prerelease_versions) { boost::optional<Semver> ver = Semver::parse(ver_string); if (ver && *release_version < *ver && ((recent_version && *recent_version < *ver) || !recent_version)) { recent_version = ver; version = ver_string; } } if (recent_version) { BOOST_LOG_TRIVIAL(info) << format("Got %1% online version: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version); wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE); evt->SetString(GUI::from_u8(version)); GUI::wxGetApp().QueueEvent(evt); } } #endif // 0 DownloadAppData AppUpdater::priv::get_app_data() { const std::lock_guard<std::mutex> lock(m_data_mutex); DownloadAppData ret_val(m_online_version_data); return ret_val; } void AppUpdater::priv::set_app_data(DownloadAppData data) { const std::lock_guard<std::mutex> lock(m_data_mutex); m_online_version_data = data; } AppUpdater::AppUpdater() :p(new priv()) { } AppUpdater::~AppUpdater() { if (p && p->m_thread.joinable()) { // This will stop transfers being done by the thread, if any. // Cancelling takes some time, but should complete soon enough. p->m_cancel = true; p->m_thread.join(); } } void AppUpdater::sync_download() { assert(p); // join thread first - it could have been in sync_version if (p->m_thread.joinable()) { // This will stop transfers being done by the thread, if any. // Cancelling takes some time, but should complete soon enough. p->m_cancel = true; p->m_thread.join(); } p->m_cancel = false; DownloadAppData input_data = p->get_app_data(); assert(!input_data.url.empty()); p->m_thread = std::thread( [this, input_data]() { p->m_download_ongoing = true; if (boost::filesystem::path dest_path = p->download_file(input_data); boost::filesystem::exists(dest_path)){ if (input_data.start_after) { p->run_downloaded_file(std::move(dest_path)); } else { GUI::desktop_open_folder(dest_path.parent_path()); } } p->m_download_ongoing = false; }); } void AppUpdater::sync_version(const std::string& version_check_url, bool from_user) { assert(p); // join thread first - it could have been in sync_download if (p->m_thread.joinable()) { // This will stop transfers being done by the thread, if any. // Cancelling takes some time, but should complete soon enough. p->m_cancel = true; p->m_thread.join(); } p->m_triggered_by_user = from_user; p->m_cancel = false; p->m_thread = std::thread( [this, version_check_url]() { p->version_check(version_check_url); }); } void AppUpdater::cancel() { p->m_cancel = true; } bool AppUpdater::cancel_callback() { cancel(); return true; } std::string AppUpdater::get_default_dest_folder() { return p->m_default_dest_folder.string(); } std::string AppUpdater::get_filename_from_url(const std::string& url) { size_t slash = url.rfind('/'); return (slash != std::string::npos ? url.substr(slash + 1) : url); } std::string AppUpdater::get_file_extension_from_url(const std::string& url) { size_t dot = url.rfind('.'); return (dot != std::string::npos ? url.substr(dot) : url); } void AppUpdater::set_app_data(DownloadAppData data) { p->set_app_data(std::move(data)); } DownloadAppData AppUpdater::get_app_data() { return p->get_app_data(); } bool AppUpdater::get_triggered_by_user() const { return p->get_triggered_by_user(); } bool AppUpdater::get_download_ongoing() const { return p->get_download_ongoing(); } } //namespace Slic3r