#include "PresetUpdater.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include "libslic3r/Exception.hpp" #include #include #include "libslic3r/AppConfig.hpp" #include "libslic3r/Preset.hpp" #include "libslic3r/Config.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "json_diff.hpp" #include "libslic3r/format.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/PresetCacheFormat.hpp" #include "libslic3r_version.h" #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/UpdateDialogs.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/format.hpp" #include "slic3r/GUI/NotificationManager.hpp" #include "slic3r/Utils/Http.hpp" #include "slic3r/Utils/bambu_networking.hpp" #include "slic3r/Config/Version.hpp" #include "slic3r/GUI/MarkdownTip.hpp" #include "libslic3r/miniz_extension.hpp" namespace Slic3r::GUI::Config { class Snapshot; } namespace Slic3r::GUI::Config { class SnapshotDB; } namespace fs = boost::filesystem; using Slic3r::GUI::Config::Index; using Slic3r::GUI::Config::Version; using Slic3r::GUI::Config::Snapshot; using Slic3r::GUI::Config::SnapshotDB; // FIXME: Incompat bundle resolution doesn't deal with inherited user presets namespace Slic3r { static const char *INDEX_FILENAME = "index.idx"; static const char *TMP_EXTENSION = ".data"; void copy_file_fix(const fs::path &source, const fs::path &target) { BOOST_LOG_TRIVIAL(debug) << format("PresetUpdater: Copying %1% -> %2%", source, target); std::string error_message; //CopyFileResult cfr = Slic3r::GUI::copy_file_gui(source.string(), target.string(), error_message, false); CopyFileResult cfr = copy_file(source.string(), target.string(), error_message, false); if (cfr != CopyFileResult::SUCCESS) { BOOST_LOG_TRIVIAL(error) << "Copying failed(" << cfr << "): " << error_message; throw Slic3r::CriticalException(GUI::format( _L("Copying of file %1% to %2% failed: %3%"), source, target, error_message)); } // Permissions should be copied from the source file by copy_file(). We are not sure about the source // permissions, let's rewrite them with 644. static constexpr const auto perms = fs::owner_read | fs::owner_write | fs::group_read | fs::others_read; fs::permissions(target, perms); } struct Update { fs::path source; fs::path target; Version version; std::string vendor; //BBS: use changelog string instead of url std::string change_log; std::string descriptions; // Orca: add file filter support std::function file_filter; bool forced_update; //BBS: add directory support bool is_directory {false}; // Orca: a vendor update may be the cache-only form. bool is_opc {false}; Update() {} //BBS: add directory support //BBS: use changelog string instead of url Update(fs::path &&source, fs::path &&target, const Version &version, std::string vendor, std::string changelog, std::string description, bool forced = false, bool is_dir = false) : source(std::move(source)) , target(std::move(target)) , version(version) , vendor(std::move(vendor)) , change_log(std::move(changelog)) , descriptions(std::move(description)) , forced_update(forced) , is_directory(is_dir) {} Update(fs::path &&source, fs::path &&target, const Version &version, std::string vendor, std::string changelog, std::string description, std::function file_filter, bool forced = false, bool is_dir = false) : source(std::move(source)) , target(std::move(target)) , version(version) , vendor(std::move(vendor)) , change_log(std::move(changelog)) , descriptions(std::move(description)) , file_filter(file_filter) , forced_update(forced) , is_directory(is_dir) {} //BBS: add directory support void install() const { if (is_directory) { copy_directory_recursively(source, target, file_filter); } else { copy_file_fix(source, target); // A vendor must be installed in exactly one form. Remove the // representation that would otherwise be stale or shadow this one. boost::system::error_code ec; if (is_opc) { fs::remove(target.parent_path() / (vendor + ".json"), ec); ec.clear(); fs::remove_all(target.parent_path() / vendor, ec); } else { fs::remove(target.parent_path() / (vendor + ".opc"), ec); } } } friend std::ostream& operator<<(std::ostream& os, const Update &self) { os << "Update(" << self.source.string() << " -> " << self.target.string() << ')'; return os; } }; struct Incompat { fs::path bundle; Version version; std::string vendor; //BBS: add directory support bool is_directory {false}; Incompat(fs::path &&bundle, const Version &version, std::string vendor, bool is_dir = false) : bundle(std::move(bundle)) , version(version) , vendor(std::move(vendor)) , is_directory(is_dir) {} void remove() { // Remove the bundle file if (is_directory) { if (fs::exists(bundle)) fs::remove_all(bundle); } else { if (fs::exists(bundle)) fs::remove(bundle); } } friend std::ostream& operator<<(std::ostream& os , const Incompat &self) { os << "Incompat(" << self.bundle.string() << ')'; return os; } }; struct Updates { std::vector incompats; std::vector updates; }; static bool reload_configs_update_gui(); wxDEFINE_EVENT(EVT_SLIC3R_VERSION_ONLINE, wxCommandEvent); wxDEFINE_EVENT(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, wxCommandEvent); struct PresetUpdater::priv { std::vector index_db; bool enabled_version_check; bool enabled_config_update; std::string version_check_url; fs::path cache_path; fs::path rsrc_path; fs::path vendor_path; bool cancel; std::thread thread; bool has_waiting_updates { false }; Updates waiting_updates; bool has_waiting_printer_updates { false }; Updates waiting_printer_updates; // Per-vendor update checking std::set checked_vendors; // Orca (PR #130): changelog text for each vendor, captured in memory during // sync_vendor_config()/check_new_vendors() instead of written beside the cache. std::unordered_map vendor_changelogs; mutable std::mutex vendor_changelogs_mutex; std::vector vendor_check_threads; std::atomic vendor_check_cancel{false}; struct Resource { std::string version; std::string description; std::string url; bool force{false}; std::string cache_root; std::vector sub_caches; }; priv(); void set_download_prefs(AppConfig *app_config); bool get_file(const std::string &url, const fs::path &target_path) const; //BBS: refine preset update logic bool extract_file(const fs::path &source_path, const fs::path &dest_path = {}); void prune_tmp(const std::string& vendor_id) const; void sync_version() const; void parse_version_string(const std::string& body) const; void sync_resources(std::string http_url, std::map &resources, bool check_patch = false, std::string current_version="", std::string changelog_file=""); void sync_vendor_config(const std::string& vendor_id); void check_new_vendors(const std::set& system_vendors, std::function, bool)> callback); void sync_tooltip(std::string http_url, std::string language); void sync_plugins(std::string http_url, std::string plugin_version); void sync_printer_config(std::string http_url); bool get_cached_plugins_version(std::string &cached_version, bool& force); //BBS: refine preset update logic bool install_bundles_rsrc(const std::vector& bundles, bool snapshot) const; void check_installed_vendor_profiles() const; Updates get_printer_config_updates(bool update = false) const; Updates get_config_updates(const Semver& old_slic3r_version) const; bool perform_updates(Updates &&updates, bool snapshot = true) const; void set_waiting_updates(Updates u); }; //BBS: change directories by design PresetUpdater::priv::priv() : cache_path(fs::path(Slic3r::data_dir()) / "ota") , rsrc_path(fs::path(resources_dir()) / "profiles") , vendor_path(fs::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR) , cancel(false) { //BBS: refine preset updater logic enabled_version_check = true; set_download_prefs(GUI::wxGetApp().app_config); // Install indicies from resources. Only installs those that are either missing or older than in resources. check_installed_vendor_profiles(); perform_updates(get_printer_config_updates(), false); // Load indices from the cache directory. //index_db = Index::load_db(); } // Pull relevant preferences from AppConfig void PresetUpdater::priv::set_download_prefs(AppConfig *app_config) { version_check_url = app_config->version_check_url(); auto profile_update_url = app_config->profile_update_url(); if (!profile_update_url.empty() && app_config->get_bool("enable_ota")) enabled_config_update = true; else enabled_config_update = false; } //BBS: refine the Preset Updater logic // Downloads a file (http get operation). Cancels if the Updater is being destroyed. bool PresetUpdater::priv::get_file(const std::string &url, const fs::path &target_path) const { bool res = false; fs::path tmp_path = target_path; tmp_path += format(".%1%%2%", get_current_pid(), TMP_EXTENSION); BOOST_LOG_TRIVIAL(info) << format("[BBS Updater]download file `%1%`, stored to `%2%`, tmp path `%3%`", url, target_path.string(), tmp_path.string()); Slic3r::Http::get(url) .on_progress([this](Slic3r::Http::Progress, bool &cancel_http) { if (cancel) { cancel_http = true; } }) .on_error([&](std::string body, std::string error, unsigned http_status) { (void)body; BOOST_LOG_TRIVIAL(error) << format("[BBS Updater]getting: `%1%`: http status %2%, %3%", url, http_status, error); }) .on_complete([&](std::string body, unsigned /* http_status */) { fs::fstream file(tmp_path, std::ios::out | std::ios::binary | std::ios::trunc); file.write(body.c_str(), body.size()); file.close(); fs::rename(tmp_path, target_path); res = true; }) .perform_sync(); return res; } //BBS: refine preset update logic bool PresetUpdater::priv::extract_file(const fs::path &source_path, const fs::path &dest_path) { const std::string parent_path = (!dest_path.empty() ? dest_path : source_path.parent_path()).string(); return extract_archive_confined(source_path.string(), parent_path); } // Remove a leftover partial archive for the vendor about to be synchronized. void PresetUpdater::priv::prune_tmp(const std::string& vendor_id) const { boost::system::error_code ec; const fs::path tmp_path = cache_path / (vendor_id + TMP_EXTENSION); fs::remove(tmp_path, ec); if (ec) BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]failed to remove " << tmp_path.string() << ": " << ec.message(); } //BBS: refine the Preset Updater logic // Get Slic3rPE version available online, save in AppConfig. void PresetUpdater::priv::sync_version() const { if (! enabled_version_check) { return; } #if 0 Http::get(version_check_url) .size_limit(SLIC3R_VERSION_BODY_MAX) .on_progress([this](Http::Progress, bool &cancel) { cancel = this->cancel; }) .on_error([&](std::string body, std::string error, unsigned http_status) { (void)body; BOOST_LOG_TRIVIAL(error) << format("Error getting: `%1%`: HTTP %2%, %3%", version_check_url, http_status, error); }) .on_complete([&](std::string body, unsigned /* http_status */) { boost::trim(body); parse_version_string(body); }) .perform_sync(); #endif } // Parses version string obtained in sync_version() and sends events to UI thread. // Version string must contain release version on first line. Follows non-mandatory alpha / beta releases on following lines (alpha=2.0.0-alpha1). void PresetUpdater::priv::parse_version_string(const std::string& body) const { #if 0 // 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 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 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 recent_version; for (const std::string& ver_string : prerelease_versions) { boost::optional 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 return; } //BBS: refine the Preset Updater logic // Download vendor indices. Also download new bundles if an index indicates there's a new one available. // Both are saved in cache. void PresetUpdater::priv::sync_resources(std::string http_url, std::map &resources, bool check_patch, std::string current_version_str, std::string changelog_file) { std::map resource_list; BOOST_LOG_TRIVIAL(info) << boost::format("[Orca Updater]: sync_resources get preferred setting version for app version %1%, url: %2%, current_version_str %3%, check_patch %4%")%SLIC3R_APP_NAME%http_url%current_version_str%check_patch; std::string query_params = "?"; bool first = true; for (auto resource_it : resources) { if (cancel) { return; } auto resource_name = resource_it.first; boost::to_lower(resource_name); std::string query_resource = (boost::format("%1%=%2%") % resource_name % resource_it.second.version).str(); if (!first) query_params += "&"; query_params += query_resource; first = false; } std::string url = http_url; url += query_params; Slic3r::Http http = Slic3r::Http::get(url); BOOST_LOG_TRIVIAL(info) << boost::format("[Orca Updater]: sync_resources request_url: %1%")%url; http.on_progress([this](Slic3r::Http::Progress, bool &cancel_http) { if (cancel) { cancel_http = true; } }) .on_complete([&resource_list, resources](std::string body, unsigned) { try { BOOST_LOG_TRIVIAL(info) << "[Orca Updater]: request_resources, body=" << body; json j = json::parse(body); std::string message = j["message"].get(); if (message == "success") { json resource = j.at("resources"); if (resource.is_array()) { for (auto iter = resource.begin(); iter != resource.end(); iter++) { std::string version; std::string url; std::string resource; std::string description; bool force_upgrade = false; for (auto sub_iter = iter.value().begin(); sub_iter != iter.value().end(); sub_iter++) { if (boost::iequals(sub_iter.key(), "type")) { resource = sub_iter.value(); BOOST_LOG_TRIVIAL(trace) << "[Orca Updater]: get version of settings's type, " << sub_iter.value(); } else if (boost::iequals(sub_iter.key(), "version")) { version = sub_iter.value(); } else if (boost::iequals(sub_iter.key(), "description")) { description = sub_iter.value(); } else if (boost::iequals(sub_iter.key(), "url")) { url = sub_iter.value(); } else if (boost::iequals(sub_iter.key(), "force_update")) { force_upgrade = sub_iter.value(); } } BOOST_LOG_TRIVIAL(info) << "[Orca Updater]: get type " << resource << ", version " << version << ", url " << url<<", force_update "<second.version; // Semver current_version = get_version_from_json(vendor_root_config.string()); Semver current_version = current_version_str.empty()?resource.version:current_version_str; bool version_match = ((online_version.maj() == current_version.maj()) && (online_version.min() == current_version.min())); if (version_match && check_patch) { int online_cc_patch = online_version.patch()/100; int current_cc_patch = current_version.patch()/100; if (online_cc_patch != current_cc_patch) { version_match = false; BOOST_LOG_TRIVIAL(warning) << boost::format("[Orca Updater]: online patch CC not match: online_cc_patch=%1%, current_cc_patch=%2%") % online_cc_patch % current_cc_patch; } } if (version_match && (current_version < online_version)) { if (cancel) { return; } // need to download the online files fs::path cache_path(resource.cache_root); std::string online_url = resource_update->second.url; std::string cache_file_path = (fs::temp_directory_path() / (fs::unique_path().string() + TMP_EXTENSION)).string(); BOOST_LOG_TRIVIAL(info) << "[Orca Updater]Downloading resource: " << resource_name << ", version " << online_version.to_string(); if (!get_file(online_url, cache_file_path)) { BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]download resource " << resource_name << " failed, url: " << online_url; continue; } if (cancel) { return; } // remove previous files before if (resource.sub_caches.empty()) { if (fs::exists(cache_path)) { fs::remove_all(cache_path); BOOST_LOG_TRIVIAL(info) << "[Orca Updater]remove cache path " << cache_path.string(); } } else { for (auto sub : resource.sub_caches) { if (fs::exists(cache_path / sub)) { fs::remove_all(cache_path / sub); BOOST_LOG_TRIVIAL(info) << "[Orca Updater]remove cache path " << (cache_path / sub).string(); } } } // extract the file downloaded BOOST_LOG_TRIVIAL(info) << "[Orca Updater]start to unzip the downloaded file " << cache_file_path << " to "<second.version; j["description"] = resource_update->second.description; j["force"] = resource_update->second.force; boost::nowide::ofstream c; c.open(changelog_file, std::ios::out | std::ios::trunc); c << j.dump(1, '\t') << std::endl; c.close(); } catch(std::exception &err) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": save to "<profile_update_url() + "?vendor=" + Http::url_encode(vendor_id) + "&orca_version=" + Http::url_encode(SoftFever_VERSION); std::string online_version_str; // this represents the PROFILE VERSION, not ORCA VERSION std::string download_url_str; std::string changelog; BOOST_LOG_TRIVIAL(info) << "[Orca Updater] fetching vendor update status from " << url; Http::get(url) .timeout_connect(5) .on_progress(check_cancel) .on_error([&vendor_id](std::string body, std::string error, unsigned http_status) { BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] vendor check HTTP error for " << vendor_id << ": " << error; }) .on_complete([&](std::string body, unsigned http_status) { if (http_status != 200) return; try { json j = json::parse(body); BOOST_LOG_TRIVIAL(info) << "[Orca Updater] url: " << url << " returned:" << body; if (j.contains("vendor_version") && j.contains("download_url")) { online_version_str = j["vendor_version"].get(); download_url_str = j["download_url"].get(); changelog = j.value("changelog", std::string()); } } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] vendor check JSON parse failed: " << e.what(); } }) .perform_sync(); if (cancel || vendor_check_cancel) return; if (online_version_str.empty() || download_url_str.empty()) { BOOST_LOG_TRIVIAL(info) << "[Orca Updater] no update available for vendor " << vendor_id; return; } if (cancel || vendor_check_cancel) return; // Clear only this vendor's cached data auto cache_profile_path = cache_path / "profiles"; fs::create_directories(cache_profile_path); boost::system::error_code ec; fs::remove_all(cache_profile_path / vendor_id, ec); fs::remove(cache_profile_path / (vendor_id + ".json"), ec); // Orca: the OPC cache is the vendor's whole installation in one file; clear it too. fs::remove(cache_profile_path / (vendor_id + ".opc"), ec); // Best-effort cleanup of the legacy on-disk changelog written by older builds // (changelogs are now kept in memory - see vendor_changelogs). fs::remove(cache_profile_path / (vendor_id + ".changelog"), ec); // Download the zip BOOST_LOG_TRIVIAL(info) << "[Orca Updater] downloading update for " << vendor_id << " version " << online_version_str; fs::path download_file = cache_path / (vendor_id + TMP_EXTENSION); bool download_ok = false; Http::get(download_url_str) .timeout_connect(5) .on_progress(check_cancel) .on_error([&vendor_id](std::string body, std::string error, unsigned http_status) { BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] download failed for " << vendor_id << ": " << error; }) .on_complete([&](std::string body, unsigned http_status) { if (http_status != 200) return; fs::fstream file(download_file, std::ios::out | std::ios::binary | std::ios::trunc); if (!file.good()) return; file.write(body.c_str(), body.size()); file.close(); if (file.good()) download_ok = true; }) .perform_sync(); if (!download_ok || cancel || vendor_check_cancel) return; // Extract vendor profile bundles under ota/profiles. The downloaded zip contains // either the vendor json/folder or the vendor cache at its root. BOOST_LOG_TRIVIAL(info) << "[Orca Updater] extracting update for " << vendor_id; if (!extract_file(download_file, cache_profile_path)) { BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] extraction failed for " << vendor_id; return; } fs::remove(download_file, ec); if (cancel || vendor_check_cancel) return; const fs::path cached_vendor_json = cache_profile_path / (vendor_id + ".json"); const fs::path cached_vendor_folder = cache_profile_path / vendor_id; const fs::path cached_vendor_opc = cache_profile_path / (vendor_id + ".opc"); bool is_json_update = fs::is_regular_file(cached_vendor_json) && fs::is_directory(cached_vendor_folder) && !fs::is_empty(cached_vendor_folder); bool is_opc_update = fs::is_regular_file(cached_vendor_opc); if (!is_json_update && !is_opc_update) { BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] rejected update for " << vendor_id << ": expected " << vendor_id << ".json and a non-empty " << vendor_id << " directory, or OPC update format."; fs::remove_all(cached_vendor_folder, ec); fs::remove(cached_vendor_json, ec); return; } { std::lock_guard lock(vendor_changelogs_mutex); vendor_changelogs[vendor_id] = std::move(changelog); } BOOST_LOG_TRIVIAL(info) << "[Orca Updater] vendor " << vendor_id << " update cached, notifying UI"; GUI::wxGetApp().CallAfter([] { GUI::wxGetApp().check_config_updates_from_updater(); }); } void PresetUpdater::priv::sync_tooltip(std::string http_url, std::string language) { try { std::string common_version = "00.00.00.00"; std::string language_version = "00.00.00.00"; fs::path cache_root = fs::path(data_dir()) / "resources/tooltip"; try { auto vf = cache_root / "common" / "version"; if (fs::exists(vf)) Slic3r::load_string_file(vf, common_version); vf = cache_root / language / "version"; if (fs::exists(vf)) Slic3r::load_string_file(vf, language_version); } catch (...) {} std::map resources { {"slicer/tooltip/common", { common_version, "", "", false, (cache_root / "common").string() }}, {"slicer/tooltip/" + language, { language_version, "", "", false, (cache_root / language).string() }} }; sync_resources(http_url, resources); for (auto &r : resources) { if (!r.second.url.empty()) { GUI::MarkdownTip::Reload(); break; } } } catch (std::exception& e) { BOOST_LOG_TRIVIAL(warning) << format("[Orca Updater] sync_tooltip: %1%", e.what()); } } // return true means there are plugins files bool PresetUpdater::priv::get_cached_plugins_version(std::string& cached_version, bool &force) { // The OTA plugin cache lives in its own ota/plugins subfolder; the update dialog // (Plater::priv::update_plugin_when_launch) reads the changelog from the same place. auto cache_folder = cache_path / "plugins"; std::string network_library, player_library, live555_library; bool has_plugins = false; #if defined(_MSC_VER) || defined(_WIN32) network_library = cache_folder.string() + "/bambu_networking.dll"; player_library = cache_folder.string() + "/BambuSource.dll"; live555_library = cache_folder.string() + "/live555.dll"; #elif defined(__WXMAC__) network_library = cache_folder.string() + "/libbambu_networking.dylib"; player_library = cache_folder.string() + "/libBambuSource.dylib"; live555_library = cache_folder.string() + "/liblive555.dylib"; #else network_library = cache_folder.string() + "/libbambu_networking.so"; player_library = cache_folder.string() + "/libBambuSource.so"; live555_library = cache_folder.string() + "/liblive555.so"; #endif std::string changelog_file = cache_folder.string() + "/network_plugins.json"; if (boost::filesystem::exists(network_library) && boost::filesystem::exists(player_library) && boost::filesystem::exists(live555_library) && boost::filesystem::exists(changelog_file)) { has_plugins = true; try { boost::nowide::ifstream ifs(changelog_file); json j; ifs >> j; if (j.contains("version")) cached_version = j["version"]; if (j.contains("force")) force = j["force"]; BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< ": cached_version = "< current_headers = Slic3r::Http::get_extra_headers(); current_headers["X-BBL-OS-Type"] = "windows_arm"; Slic3r::Http::set_extra_headers(current_headers); BOOST_LOG_TRIVIAL(info) << boost::format("set X-BBL-OS-Type to windows_arm"); } #endif try { std::map resources { {"slicer/plugins/cloud", { using_version, "", "", false, cache_plugin_folder.string()}} }; sync_resources(http_url, resources, true, plugin_version, "network_plugins.json"); } catch (std::exception& e) { BOOST_LOG_TRIVIAL(warning) << format("[Orca Updater] sync_plugins: %1%", e.what()); } #if defined(__WINDOWS__) if (GUI::wxGetApp().is_running_on_arm64() && !GUI::wxGetApp().use_legacy_network_plugin()) { //set back std::map current_headers = Slic3r::Http::get_extra_headers(); current_headers["X-BBL-OS-Type"] = "windows"; Slic3r::Http::set_extra_headers(current_headers); BOOST_LOG_TRIVIAL(info) << boost::format("set X-BBL-OS-Type back to windows"); } #endif bool result = get_cached_plugins_version(cached_version, force_upgrade); if (result) { BOOST_LOG_TRIVIAL(info) << format("[Orca Updater] found new plugins: %1%, prompt to update, force_upgrade %2%", cached_version, force_upgrade); if (force_upgrade) { auto app_config = GUI::wxGetApp().app_config; if (!app_config) GUI::wxGetApp().plater()->get_notification_manager()->push_notification(GUI::NotificationType::BBLPluginUpdateAvailable); else app_config->set("update_network_plugin", "true"); } else GUI::wxGetApp().plater()->get_notification_manager()->push_notification(GUI::NotificationType::BBLPluginUpdateAvailable); } } void PresetUpdater::priv::sync_printer_config(std::string http_url) { std::string curr_version = SLIC3R_VERSION; std::string using_version = curr_version.substr(0, 6) + "00.00"; std::string cached_version; std::string data_dir_str = data_dir(); boost::filesystem::path data_dir_path(data_dir_str); auto config_folder = data_dir_path / "printers"; auto cache_folder = data_dir_path / "ota" / "printers"; try { auto version_file = config_folder / "version.txt"; if (fs::exists(version_file)) { Slic3r::load_string_file(version_file, curr_version); boost::algorithm::trim(curr_version); } } catch (...) {} try { auto version_file = cache_folder / "version.txt"; if (fs::exists(version_file)) { Slic3r::load_string_file(version_file, cached_version); boost::algorithm::trim(cached_version); } } catch (...) {} if (!cached_version.empty()) { bool need_delete_cache = false; Semver current_semver = curr_version; Semver cached_semver = cached_version; if ((cached_semver.maj() != current_semver.maj()) || (cached_semver.min() != current_semver.min())) { need_delete_cache = true; BOOST_LOG_TRIVIAL(info) << boost::format("cached printer config version %1% not match with current %2%") % cached_version % curr_version; } else if (cached_semver.patch() <= current_semver.patch()) { need_delete_cache = true; BOOST_LOG_TRIVIAL(info) << boost::format("cached printer config version %1% not newer than current %2%") % cached_version % curr_version; } else { using_version = cached_version; } if (need_delete_cache) { boost::system::error_code ec; boost::filesystem::remove_all(cache_folder, ec); cached_version = curr_version; } } try { std::map resources{{"slicer/printer/bbl", {using_version, "", "", false, cache_folder.string()}}}; sync_resources(http_url, resources, false, cached_version, "printer.json"); } catch (std::exception &e) { BOOST_LOG_TRIVIAL(warning) << format("[Orca Updater] sync_printer_config: %1%", e.what()); } bool result = false; try { auto version_file = cache_folder / "version.txt"; if (fs::exists(version_file)) { Slic3r::load_string_file(version_file, cached_version); boost::algorithm::trim(cached_version); result = true; } } catch (...) {} if (result) { BOOST_LOG_TRIVIAL(info) << format("[Orca Updater] found new printer config: %1%, prompt to update", cached_version); waiting_printer_updates = get_printer_config_updates(true); if (waiting_printer_updates.updates.size() > 0) { has_waiting_printer_updates = true; GUI::wxGetApp().plater()->get_notification_manager()->push_notification(GUI::NotificationType::BBLPrinterConfigUpdateAvailable); } } } bool PresetUpdater::priv::install_bundles_rsrc(const std::vector& bundles, bool snapshot) const { // Use the Core library function to install bundles // This function is now in libslic3r so both Core and GUI can use it if (!Slic3r::install_vendor_bundles_from_resources(bundles)) { BOOST_LOG_TRIVIAL(error) << "Failed to install bundles from resources"; return false; } // Snapshot logic is currently commented out in perform_updates, so we don't need to handle it here // If snapshot logic is needed in the future, it can be added here return true; } // Orca: copy/update the vendor profiles from resource to system folder void PresetUpdater::priv::check_installed_vendor_profiles() const { BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:Checking whether the profile from resource is newer"; AppConfig *app_config = GUI::wxGetApp().app_config; const auto enabled_vendors = app_config->vendors(); std::set bundles; // A vendor is named by its profile or, where the build ships preset caches // instead of the raw profile JSONs, by its cache alone. for (const std::string &vendor_name : vendor_names_in(rsrc_path)) { // enabled_vendors lists the vendors whose printer models the user picked, and // neither of these two is ever in it. const auto is_vendor_enabled = (vendor_name == PresetBundle::ORCA_DEFAULT_BUNDLE) || (vendor_name == PresetBundle::ORCA_FILAMENT_LIBRARY) || (enabled_vendors.find(vendor_name) != enabled_vendors.end()); if (is_vendor_installed(vendor_name)) { if (is_vendor_enabled) { // Orca: whichever form of the vendor resources ships at the newer // version is the one installing lays down, and the one to judge // what is installed against. Semver resource_ver = resource_vendor_version(vendor_name); // Orca: a vendor installed as a preset cache has no profile // beside it; the version it was installed at is in the cache. Semver vendor_ver = installed_vendor_version(vendor_name); if (vendor_ver < resource_ver) { BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:found vendor " << vendor_name << " newer version " << resource_ver.to_string() << " from resource, old version " << vendor_ver.to_string(); bundles.insert(vendor_name); } } else { // need to be removed because not installed remove_installed_vendor(vendor_name); } } else if (is_vendor_enabled) { bundles.insert(vendor_name); } } if (bundles.size() > 0) { install_bundles_rsrc(std::vector(bundles.begin(), bundles.end()), false); } } Updates PresetUpdater::priv::get_printer_config_updates(bool update) const { std::string data_dir_str = data_dir(); boost::filesystem::path data_dir_path(data_dir_str); boost::filesystem::path resc_dir_path(resources_dir()); auto config_folder = data_dir_path / "printers"; auto resc_folder = (update ? cache_path : resc_dir_path) / "printers"; std::string curr_version; std::string resc_version; try { Slic3r::load_string_file(resc_folder / "version.txt", resc_version); boost::algorithm::trim(resc_version); } catch (...) {} try { Slic3r::load_string_file(config_folder / "version.txt", curr_version); boost::algorithm::trim(curr_version); } catch (...) {} if (!curr_version.empty()) { Semver curr_ver = curr_version; Semver resc_ver = resc_version; bool version_match = ((resc_ver.maj() == curr_ver.maj()) && (resc_ver.min() == curr_ver.min())); if (!version_match || (curr_ver < resc_ver)) { BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:found newer version " << resc_version << " from resource, old version " << curr_version; } else { return {}; } } Updates updates; Version version; version.config_version = resc_version; std::string change_log; if (update) { std::string changelog_file = (resc_folder / "printer.json").string(); try { boost::nowide::ifstream ifs(changelog_file); json j; ifs >> j; version.comment = j["description"]; } catch (...) {} } updates.updates.emplace_back(std::move(resc_folder), std::move(config_folder), version, "bbl", change_log, version.comment, false, true); return updates; } // Generates a list of bundle updates that are to be performed. // Version of slic3r that was running the last time and which was read out from PrusaSlicer.ini is provided // as a parameter. // Orca: OTA profile updates should be loacated in ota/profiles folder Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version) const { Updates updates; BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:Checking for cached configuration updates..."; auto cache_profile_path = cache_path / "profiles"; if (!fs::exists(cache_profile_path)) return updates; // Orca (PR #130): vendor changelogs are captured in memory during // sync_vendor_config()/check_new_vendors(), not written beside the cache. std::unordered_map changelogs; { std::lock_guard lock(vendor_changelogs_mutex); changelogs = vendor_changelogs; } for (auto &dir_entry : boost::filesystem::directory_iterator(cache_profile_path)) { const auto &path = dir_entry.path(); std::string file_path = path.string(); const bool is_opc_file = boost::iequals(path.extension().string(), ".opc"); if (!is_json_file(file_path) && !is_opc_file) continue; const std::string vendor_name = path.stem().string(); auto print_in_cache = (cache_profile_path / vendor_name / PRESET_PRINT_NAME); auto filament_in_cache = (cache_profile_path / vendor_name / PRESET_FILAMENT_NAME); auto machine_in_cache = (cache_profile_path / vendor_name / PRESET_PRINTER_NAME); // Orca (PR #130): a JSON cache entry is only meaningful next to a non-empty // / preset directory; a stray or half-downloaded .json is // skipped. An .opc cache is a single self-contained file (validated below), // so this check does not apply to it. if (!is_opc_file) { const auto vendor_folder_in_cache = cache_profile_path / vendor_name; if (!fs::is_regular_file(path) || !fs::is_directory(vendor_folder_in_cache) || fs::is_empty(vendor_folder_in_cache)) { BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]:ignoring invalid cached update for " << vendor_name << ": expected " << vendor_name << ".json and a non-empty " << vendor_name << " directory"; continue; } } if (is_vendor_installed(vendor_name) || is_opc_file || fs::exists(print_in_cache) || fs::exists(filament_in_cache) || fs::exists(machine_in_cache)) { // Orca: a vendor installed as a preset cache carries its version there. Semver vendor_ver = installed_vendor_version(vendor_name); Semver cache_ver; std::string description; bool force_update = false; if (is_opc_file) { cache_ver = VendorCacheFile::usable_version(file_path, vendor_name); if (!cache_ver.valid()) { BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]:ignoring unreadable vendor cache " << file_path; continue; } } else { std::map key_values; std::vector keys(3); keys[0] = BBL_JSON_KEY_VERSION; keys[1] = BBL_JSON_KEY_DESCRIPTION; keys[2] = BBL_JSON_KEY_FORCE_UPDATE; get_values_from_json(file_path, keys, key_values); description = key_values[BBL_JSON_KEY_DESCRIPTION]; if (key_values.find(BBL_JSON_KEY_FORCE_UPDATE) != key_values.end()) force_update = (key_values[BBL_JSON_KEY_FORCE_UPDATE] == "1")?true:false; auto config_version = Semver::parse(key_values[BBL_JSON_KEY_VERSION]); if (config_version) cache_ver = *config_version; } // Orca (PR #130): changelog for this vendor was captured in memory at sync time. const auto changelog_it = changelogs.find(vendor_name); std::string changelog = changelog_it != changelogs.end() ? changelog_it->second : std::string(); if (vendor_ver < cache_ver) { BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:need to update settings from " << vendor_ver.to_string() << " to newer version " << cache_ver.to_string() << ", app version " << SLIC3R_VERSION; Version version; version.config_version = cache_ver; version.comment = description; if (is_opc_file) { // A cache contains the vendor profile and all presets. // Install it directly; Update::install removes any // superseded JSON representation. auto &update = updates.updates.emplace_back( std::move(file_path), vendor_path / (vendor_name + ".opc"), std::move(version), vendor_name, changelog, "", force_update, false); update.is_opc = true; } else { // JSON profile and its preset directory are installed // separately, as before. updates.updates.emplace_back(std::move(file_path), vendor_path / (vendor_name + ".json"), std::move(version), vendor_name, changelog, "", force_update, false); updates.updates.emplace_back(cache_profile_path / vendor_name, vendor_path / vendor_name, Version(), vendor_name, "", "", force_update, true); } } else { BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:cached settings for " << vendor_name << " are not newer than installed version, installed " << vendor_ver.to_string() << ", cached " << cache_ver.to_string(); } } } return updates; } //BBS: switch to new BBL.json configs bool PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) const { //std::string vendor_path; //std::string vendor_name; if (updates.incompats.size() > 0) { //if (snapshot) { // BOOST_LOG_TRIVIAL(info) << "Taking a snapshot..."; // if (! GUI::Config::take_config_snapshot_cancel_on_error(*GUI::wxGetApp().app_config, Snapshot::SNAPSHOT_DOWNGRADE, "", // _u8L("Continue and install configuration updates?"))) // return false; //} BOOST_LOG_TRIVIAL(info) << format("[Orca Updater]:Deleting %1% incompatible bundles", updates.incompats.size()); for (auto &incompat : updates.incompats) { BOOST_LOG_TRIVIAL(info) << '\t' << incompat; incompat.remove(); } } else if (updates.updates.size() > 0) { //if (snapshot) { // BOOST_LOG_TRIVIAL(info) << "Taking a snapshot..."; // if (! GUI::Config::take_config_snapshot_cancel_on_error(*GUI::wxGetApp().app_config, Snapshot::SNAPSHOT_UPGRADE, "", // _u8L("Continue and install configuration updates?"))) // return false; //} BOOST_LOG_TRIVIAL(info) << format("[Orca Updater]:Performing %1% updates", updates.updates.size()); for (const auto &update : updates.updates) { BOOST_LOG_TRIVIAL(info) << '\t' << update; update.install(); //if (!update.is_directory) { // vendor_path = update.source.parent_path().string(); // vendor_name = update.vendor; //} } //if (!vendor_path.empty()) { // PresetBundle bundle; // // Throw when parsing invalid configuration. Only valid configuration is supposed to be provided over the air. // bundle.load_vendor_configs_from_json(vendor_path, vendor_name, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable); // BOOST_LOG_TRIVIAL(info) << format("Deleting %1% conflicting presets", bundle.prints.size() + bundle.filaments.size() + bundle.printers.size()); // auto preset_remover = [](const Preset& preset) { // BOOST_LOG_TRIVIAL(info) << '\t' << preset.file; // fs::remove(preset.file); // }; // for (const auto &preset : bundle.prints) { preset_remover(preset); } // for (const auto &preset : bundle.filaments) { preset_remover(preset); } // for (const auto &preset : bundle.printers) { preset_remover(preset); } //} } return true; } void PresetUpdater::priv::set_waiting_updates(Updates u) { waiting_updates = u; has_waiting_updates = true; } PresetUpdater::PresetUpdater() : p(new priv()) {} // Public PresetUpdater::~PresetUpdater() { if (p && p->thread.joinable()) { // This will stop transfers being done by the thread, if any. // Cancelling takes some time, but should complete soon enough. p->cancel = true; p->thread.join(); } if (p) { p->vendor_check_cancel = true; for (auto& t : p->vendor_check_threads) if (t.joinable()) t.join(); } } //BBS: change directories by design //BBS: refine the preset updater logic void PresetUpdater::sync(std::string http_url, std::string language, std::string plugin_version, PresetBundle * /*preset_bundle*/) { //p->set_download_prefs(GUI::wxGetApp().app_config); if (!p->enabled_version_check && !p->enabled_config_update) { return; } p->thread = std::thread([this, http_url, language, plugin_version]() { try { this->p->sync_version(); if (p->cancel) return; // Vendor profile updates are triggered by check_vendor_update() // after the startup printer preset has been restored. this->p->sync_plugins(http_url, plugin_version); this->p->sync_printer_config(http_url); // Orca (PR #130): the filament library is always installed, so refresh it // from the updater on every startup sync rather than deferring to check_vendor_update(). this->p->sync_vendor_config(PresetBundle::ORCA_FILAMENT_LIBRARY); //if (p->cancel) // return; //remove the tooltip currently //this->p->sync_tooltip(http_url, language); } catch (const std::exception &e) { BOOST_LOG_TRIVIAL(error) << "[Orca Updater] background sync failed: " << e.what(); } catch (...) { BOOST_LOG_TRIVIAL(error) << "[Orca Updater] background sync failed with an unknown exception"; } }); } void PresetUpdater::check_vendor_update(const std::string& vendor_id) { if (!p->enabled_config_update) return; if (vendor_id.empty()) return; if (!p->checked_vendors.insert(vendor_id).second) return; p->vendor_check_threads.emplace_back([this, vendor_id]() { try { this->p->prune_tmp(vendor_id); this->p->sync_vendor_config(vendor_id); } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "[Orca Updater] vendor update failed for " << vendor_id << ": " << e.what(); } catch (...) { BOOST_LOG_TRIVIAL(error) << "[Orca Updater] vendor update failed for " << vendor_id << " with an unknown exception"; } }); } // Orca: ask the server which vendors from `system_vendors` have a profile bundle available that // isn't installed yet (or is newer than what's installed). Request body maps vendor id -> currently // installed profile version (unknown/not-yet-installed vendors report "0.0.0"). Response maps // vendor id -> {version, download_url, changelog} for each vendor the server has an update for. // Any such vendor is downloaded and cached under ota/profiles the same way sync_vendor_config() // does; the caller's check_config_updates_from_updater() -> get_config_updates()/perform_updates() // flow then installs the cached profiles into data_dir()/system. // // Mirrors check_vendor_update()/sync_vendor_config(): the network query and the download/extract // work run on a background thread (vendor_check_threads), never on the calling (UI) thread. Only // the confirmation dialog (which must run on the UI thread) and the final callback are marshaled // back via CallAfter(). void PresetUpdater::priv::check_new_vendors(const std::set& system_vendors, std::function, bool)> callback) { vendor_check_threads.emplace_back([this, system_vendors, callback]() { AppConfig* app_config = GUI::wxGetApp().app_config; std::string url = app_config->profile_update_url() + "/new?orcaslicer_version=" + Http::url_encode(SoftFever_VERSION); auto check_cancel = [this](Http::Progress, bool& cancel_http) { if (cancel || vendor_check_cancel) cancel_http = true; }; json request_body = json::object(); BOOST_LOG_TRIVIAL(info) << "[Orca Updater] checking new vendors for:"; for (const auto& vendor_id : system_vendors) { // Orca: installed_vendor_version() reads whichever form the vendor is // installed as - the .json profile or the .opc preset cache stamp - // so a cache-only vendor is not reported as version 0.0.0 and then // endlessly re-offered by the server. Semver installed_ver = installed_vendor_version(vendor_id); request_body[vendor_id] = installed_ver.to_string(); BOOST_LOG_TRIVIAL(info) << vendor_id << " (installed version " << installed_ver.to_string() << ")"; } BOOST_LOG_TRIVIAL(info) << "[Orca Updater] new vendor check request url: " << url; BOOST_LOG_TRIVIAL(info) << "[Orca Updater] new vendor check request body: " << request_body.dump(2); json response_json; bool got_response = false; auto post = Http::post(url); post.timeout_connect(5); post.on_progress(check_cancel); post.header("Content-Type", "application/json"); post.on_error([](std::string body, std::string error, unsigned http_status) { BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] new vendor check HTTP error: " << error; }) .on_complete([&response_json, &got_response](std::string body, unsigned http_status) { if (http_status != 200) return; try { json j = json::parse(body); if (j.is_object()) { response_json = std::move(j); got_response = true; } } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] new vendor check JSON parse failed: " << e.what(); } }); post.set_post_body(request_body.dump()); post.perform_sync(); if (cancel || vendor_check_cancel) return; if (!got_response) { GUI::wxGetApp().CallAfter([callback]() { callback({}, false); }); return; } // Collect candidates before touching the filesystem or network again. struct NewVendorCandidate { std::string vendor_id; Semver version; std::string download_url; std::string changelog; }; std::vector candidates; for (auto it = response_json.begin(); it != response_json.end(); ++it) { const json& entry = it.value(); if (!entry.is_object()) continue; std::string download_url_str = entry.value("download_url", std::string()); if (download_url_str.empty()) continue; NewVendorCandidate candidate; candidate.vendor_id = it.key(); auto parsed_ver = Semver::parse(entry.value("version", std::string())); candidate.version = parsed_ver ? *parsed_ver : Semver(); candidate.download_url = std::move(download_url_str); candidate.changelog = entry.value("changelog", std::string()); candidates.push_back(std::move(candidate)); } if (candidates.empty()) { GUI::wxGetApp().CallAfter([callback]() { callback({}, false); }); return; } // Orca: the confirmation dialog must run on the UI thread; if confirmed, the actual // download/install work is dispatched back onto a new background thread from there, // same as check_vendor_update() does for a single vendor. GUI::wxGetApp().CallAfter([this, candidates, callback]() { std::vector updates_msg; for (const auto& candidate : candidates) updates_msg.emplace_back(candidate.vendor_id, candidate.version, std::string(), candidate.changelog); GUI::MsgUpdateConfig dlg(updates_msg); if (dlg.ShowModal() != wxID_OK) { BOOST_LOG_TRIVIAL(info) << "[Orca Updater] user declined installing new vendors"; callback({}, true); return; } // Orca: the actual download runs on a background thread below (so it doesn't block the // UI), but that also means nothing visibly happens for the several seconds it can take // (longer still if a retry kicks in) — push a notification so it's clear work is // ongoing rather than looking hung. { std::string vendor_list; for (const auto& candidate : candidates) { if (!vendor_list.empty()) vendor_list += ", "; vendor_list += candidate.vendor_id; } GUI::wxGetApp().plater()->get_notification_manager()->push_notification( _u8L("Downloading new vendor profile(s): ") + vendor_list + _u8L("...")); } vendor_check_threads.emplace_back([this, candidates, callback]() { auto check_cancel = [this](Http::Progress, bool& cancel_http) { if (cancel || vendor_check_cancel) cancel_http = true; }; std::vector new_vendor_ids; std::vector failed_vendor_ids; auto cache_profile_path = cache_path / "profiles"; fs::create_directories(cache_profile_path); boost::system::error_code ec; for (const auto& candidate : candidates) { if (cancel || vendor_check_cancel) break; const std::string& vendor_id = candidate.vendor_id; const std::string& download_url_str = candidate.download_url; std::string changelog = candidate.changelog; BOOST_LOG_TRIVIAL(info) << "[Orca Updater] downloading new vendor " << vendor_id << " version " << candidate.version.to_string(); // Clear only this vendor's cached data, same as sync_vendor_config(). fs::remove_all(cache_profile_path / vendor_id, ec); fs::remove(cache_profile_path / (vendor_id + ".json"), ec); fs::path download_file = cache_path / (vendor_id + TMP_EXTENSION); bool download_ok = false; // Orca: same retry pattern as Plater.cpp's project download — a single-shot // 5s connect timeout against GitHub's redirect chain is prone to transient // failures (DNS/connect hiccups) that succeed a moment later, so retry a few // times before giving up rather than failing the whole vendor on one blip. int retry_count = 0; const int max_retries = 3; bool keep_trying = true; while (keep_trying && retry_count < max_retries) { retry_count++; Http::get(download_url_str) .timeout_connect(5) .on_progress(check_cancel) .on_error([&vendor_id, &retry_count](std::string body, std::string error, unsigned http_status) { BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] download failed for new vendor " << vendor_id << " (attempt " << retry_count << "/" << max_retries << "): " << error; }) .on_complete([&](std::string body, unsigned http_status) { if (http_status != 200) return; fs::fstream file(download_file, std::ios::out | std::ios::binary | std::ios::trunc); if (!file.good()) return; file.write(body.c_str(), body.size()); file.close(); if (file.good()) download_ok = true; }) .perform_sync(); keep_trying = !download_ok && !(cancel || vendor_check_cancel); } if (!download_ok || cancel || vendor_check_cancel) { if (!download_ok) failed_vendor_ids.push_back(vendor_id); continue; } BOOST_LOG_TRIVIAL(info) << "[Orca Updater] extracting new vendor " << vendor_id; if (!extract_file(download_file, cache_profile_path)) { BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] extraction failed for new vendor " << vendor_id; fs::remove(download_file, ec); failed_vendor_ids.push_back(vendor_id); continue; } fs::remove(download_file, ec); const fs::path cached_vendor_json = cache_profile_path / (vendor_id + ".json"); const fs::path cached_vendor_folder = cache_profile_path / vendor_id; if (!fs::is_regular_file(cached_vendor_json) || !fs::is_directory(cached_vendor_folder) || fs::is_empty(cached_vendor_folder)) { BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] rejected new vendor " << vendor_id << ": expected " << vendor_id << ".json and a non-empty " << vendor_id << " directory"; fs::remove_all(cached_vendor_folder, ec); fs::remove(cached_vendor_json, ec); failed_vendor_ids.push_back(vendor_id); continue; } { std::lock_guard lock(vendor_changelogs_mutex); vendor_changelogs[vendor_id] = std::move(changelog); } new_vendor_ids.push_back(vendor_id); } if (!new_vendor_ids.empty()) { // Orca: the user already confirmed via the dialog above, so install right away // instead of routing through check_config_updates_from_updater(), which only // queues a passive notification (meant for the silent background per-vendor // check) requiring yet another click + confirmation before anything is copied // into data_dir()/system. GUI::wxGetApp().CallAfter([this, new_vendor_ids] { AppConfig* app_config = GUI::wxGetApp().app_config; Updates updates = get_config_updates(app_config->orig_version()); // Only install the vendors just confirmed; leave any other unrelated // pending cached update (from a background sync_vendor_config()) alone, // still gated behind its own notification/confirmation. std::set confirmed(new_vendor_ids.begin(), new_vendor_ids.end()); Updates filtered; for (auto& update : updates.updates) if (confirmed.count(update.vendor)) filtered.updates.push_back(std::move(update)); if (filtered.updates.empty()) { BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] new vendors cached but no updates detected"; return; } if (!perform_updates(std::move(filtered)) || !reload_configs_update_gui()) { BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] failed to install new vendors"; return; } BOOST_LOG_TRIVIAL(info) << "[Orca Updater] new vendors installed"; for (const auto& vendor_id : new_vendor_ids) { Semver cur_ver = GUI::wxGetApp().preset_bundle->get_vendor_profile_version(vendor_id); GUI::wxGetApp().plater()->get_notification_manager()->push_notification( GUI::NotificationType::PresetUpdateFinished, GUI::NotificationManager::NotificationLevel::ImportantNotificationLevel, Slic3r::format(_u8L("Configuration package: %1% updated to %2%"), vendor_id, cur_ver.to_string())); } }); } if (!failed_vendor_ids.empty()) { GUI::wxGetApp().CallAfter([failed_vendor_ids] { std::string vendor_list; for (const auto& vendor_id : failed_vendor_ids) { if (!vendor_list.empty()) vendor_list += ", "; vendor_list += vendor_id; } GUI::wxGetApp().plater()->get_notification_manager()->push_notification( _u8L("Failed to download vendor profile(s): ") + vendor_list); }); } GUI::wxGetApp().CallAfter([callback, new_vendor_ids]() { callback(new_vendor_ids, false); }); }); }); }); } void PresetUpdater::check_new_vendors(const std::set& system_vendors, std::function, bool)> callback) { p->check_new_vendors(system_vendors, std::move(callback)); } void PresetUpdater::slic3r_update_notify() { if (! p->enabled_version_check) return; } static bool reload_configs_update_gui() { wxString header = _L("Please check any unsaved changes before updating the configuration."); if (!GUI::wxGetApp().check_and_save_current_preset_changes(_L("Configuration updates"), header, false )) return false; // Reload global configuration auto* app_config = GUI::wxGetApp().app_config; // System profiles should not trigger any substitutions, user profiles may trigger substitutions, but these substitutions // were already presented to the user on application start up. Just do substitutions now and keep quiet about it. // However throw on substitutions in system profiles, those shall never happen with system profiles installed over the air. GUI::wxGetApp().preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem); GUI::wxGetApp().load_current_presets(); GUI::wxGetApp().plater()->set_bed_shape(); return true; } PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3r_version, UpdateParams params) const { if (! p->enabled_config_update) { return R_NOOP; } auto updates = p->get_config_updates(old_slic3r_version); if (updates.updates.size() > 0) { bool force_update = false; for (const auto& update : updates.updates) { force_update = (update.forced_update ? true : force_update); } //forced update if (force_update) { BOOST_LOG_TRIVIAL(info) << format("[Orca Updater]:Force updating will start, size %1% ", updates.updates.size()); std::vector bundles; for (const auto& update : updates.updates) { if (update.is_directory) continue; bundles.push_back(update.vendor); } bool ret = p->perform_updates(std::move(updates)); if (!ret) { BOOST_LOG_TRIVIAL(warning) << format("[Orca Updater]:perform_updates failed"); return R_INCOMPAT_EXIT; } ret = reload_configs_update_gui(); if (!ret) { BOOST_LOG_TRIVIAL(warning) << format("[Orca Updater]:reload_configs_update_gui failed"); return R_INCOMPAT_EXIT; } for(auto b : bundles){ Semver cur_ver = GUI::wxGetApp().preset_bundle->get_vendor_profile_version(b); GUI::wxGetApp() .plater() ->get_notification_manager() ->push_notification(GUI::NotificationType::PresetUpdateFinished, GUI::NotificationManager::NotificationLevel::ImportantNotificationLevel, Slic3r::format(_u8L("Configuration package: %1% updated to %2%"), b, cur_ver.to_string())); } return R_UPDATE_INSTALLED; } // regular update if (params == UpdateParams::SHOW_NOTIFICATION) { p->set_waiting_updates(updates); GUI::wxGetApp().plater()->get_notification_manager()->push_notification(GUI::NotificationType::PresetUpdateAvailable); } else { BOOST_LOG_TRIVIAL(info) << format("[Orca Updater]:Configuration package available. size %1%, need to confirm...", p->waiting_updates.updates.size()); std::vector updates_msg; for (const auto& update : updates.updates) { if (update.is_directory) continue; std::string changelog = update.change_log; updates_msg.emplace_back(update.vendor, update.version.config_version, update.descriptions, std::move(changelog)); } GUI::MsgUpdateConfig dlg(updates_msg, params == UpdateParams::FORCED_BEFORE_WIZARD); const auto res = dlg.ShowModal(); if (res == wxID_OK) { BOOST_LOG_TRIVIAL(debug) << "[Orca Updater]:selected yes to update"; if (! p->perform_updates(std::move(updates)) || ! reload_configs_update_gui()) return R_ALL_CANCELED; return R_UPDATE_INSTALLED; } else { BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:selected no for updating"; if (params == UpdateParams::FORCED_BEFORE_WIZARD && res == wxID_CANCEL) return R_ALL_CANCELED; return R_UPDATE_REJECT; } } // MsgUpdateConfig will show after the notificaation is clicked } else { BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:No configuration updates available."; } return R_NOOP; } //BBS: add json related logic bool PresetUpdater::install_bundles_rsrc(std::vector bundles, bool snapshot) const { return p->install_bundles_rsrc(bundles, snapshot); } void PresetUpdater::on_update_notification_confirm() { if (!p->has_waiting_updates) return; BOOST_LOG_TRIVIAL(info) << format("Update of %1% bundles available. Asking for confirmation ...", p->waiting_updates.updates.size()); std::vector updates_msg; for (const auto& update : p->waiting_updates.updates) { //BBS: skip directory if (update.is_directory) continue; std::string changelog = update.change_log; updates_msg.emplace_back(update.vendor, update.version.config_version, update.descriptions, std::move(changelog)); } GUI::MsgUpdateConfig dlg(updates_msg); const auto res = dlg.ShowModal(); if (res == wxID_OK) { BOOST_LOG_TRIVIAL(debug) << "User agreed to perform the update"; if (p->perform_updates(std::move(p->waiting_updates)) && reload_configs_update_gui()) { p->has_waiting_updates = false; } } else { BOOST_LOG_TRIVIAL(info) << "User refused the update"; } } void PresetUpdater::do_printer_config_update() { if (!p->has_waiting_printer_updates) return; BOOST_LOG_TRIVIAL(info) << "Update of printer configs available. Asking for confirmation ..."; std::vector updates_msg; for (const auto &update : p->waiting_printer_updates.updates) { std::string changelog = update.change_log; updates_msg.emplace_back(update.vendor, update.version.config_version, update.descriptions, std::move(changelog)); } GUI::MsgUpdateConfig dlg(updates_msg); const auto res = dlg.ShowModal(); if (res == wxID_OK) { BOOST_LOG_TRIVIAL(debug) << "User agreed to perform the update"; if (p->perform_updates(std::move(p->waiting_printer_updates))) p->has_waiting_printer_updates = false; } else { BOOST_LOG_TRIVIAL(info) << "User refused the update"; } } bool PresetUpdater::version_check_enabled() const { return p->enabled_version_check; } }