mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-06 07:11:10 +00:00
* Confine Updater Archive Extraction to the Target Directory The preset updater extracted downloaded archives by appending each entry name to the cache directory, and the network plugin installer did the same for the plugin folder, without checking that the result stays inside it. Move the updater's extraction into libslic3r as extract_archive_confined, which validates every entry with is_path_within_root before writing anything and fails the whole archive if one entry resolves outside the target. The plugin installer now rejects such an entry the same way. Well formed archives extract exactly as before. * Harden Archive Extraction Against Symlinks The plugin installer now creates a symlink entry only when its target is relative and, joined to the link's own directory, passes is_path_within_root, via the new is_symlink_target_within_root helper. Before writing any entry it checks the destination with symlink_status, so an existing symlink, dangling or not, is replaced rather than followed, and it creates parent directories inside the existing error handling. extract_archive_confined replaces a symlink at a destination file the same way. is_path_within_root now ignores a trailing separator on the root, which previously made every path fail the check. * Validate Plugin Symlink Targets Before Replacing Existing Files A symlink entry's target is now read and checked before anything already at its destination is removed or renamed aside, so an archive rejected for its link target leaves the installed plugin files in place. * Reject Paths with an Embedded NUL When Confining Extraction is_path_within_root compared each component with "..", so a name such as "..\0" passed the check. The filesystem calls stop at the NUL and act on a shorter path than the one that was checked: a symlink target read from a plugin archive as raw bytes was created as "..", pointing out of the plugin directory. A path containing a NUL is now rejected before anything touches the filesystem, which covers every caller, including entry names taken from the Unicode Path extra field.
1865 lines
79 KiB
C++
1865 lines
79 KiB
C++
#include "PresetUpdater.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <boost/filesystem/directory.hpp>
|
|
#include <boost/filesystem/operations.hpp>
|
|
#include <boost/filesystem/path.hpp>
|
|
#include <boost/nowide/fstream.hpp>
|
|
#include <functional>
|
|
#include <atomic>
|
|
#include <set>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <mutex>
|
|
#include <unordered_map>
|
|
#include <ostream>
|
|
#include <utility>
|
|
#include <stdexcept>
|
|
#include <boost/format.hpp>
|
|
#include <boost/algorithm/string.hpp>
|
|
#include <boost/filesystem.hpp>
|
|
#include <boost/filesystem/fstream.hpp>
|
|
#include <boost/lexical_cast.hpp>
|
|
#include <boost/log/trivial.hpp>
|
|
|
|
#include <vector>
|
|
#include <wx/app.h>
|
|
#include <wx/msgdlg.h>
|
|
|
|
#include "libslic3r/libslic3r.h"
|
|
#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/ConfigWizard.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/Config/Snapshot.hpp"
|
|
#include "slic3r/GUI/MarkdownTip.hpp"
|
|
#include "libslic3r/miniz_extension.hpp"
|
|
#include "slic3r/GUI/GUI_Utils.hpp"
|
|
|
|
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<bool(const std::string)> 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<bool(const std::string)> 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<Incompat> incompats;
|
|
std::vector<Update> 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> 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<std::string> 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<std::string, std::string> vendor_changelogs;
|
|
mutable std::mutex vendor_changelogs_mutex;
|
|
std::vector<std::thread> vendor_check_threads;
|
|
std::atomic<bool> vendor_check_cancel{false};
|
|
|
|
struct Resource
|
|
{
|
|
std::string version;
|
|
std::string description;
|
|
std::string url;
|
|
bool force{false};
|
|
std::string cache_root;
|
|
std::vector<std::string> 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<std::string, Resource> &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<std::string>& system_vendors,
|
|
std::function<void(std::vector<std::string>, 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<std::string>& 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<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
|
|
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<std::string, Resource> &resources, bool check_patch, std::string current_version_str, std::string changelog_file)
|
|
{
|
|
std::map<std::string, Resource> 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<std::string>();
|
|
|
|
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 "<<force_upgrade;
|
|
|
|
resource_list.emplace(resource, Resource{version, description, url, force_upgrade});
|
|
}
|
|
}
|
|
} else {
|
|
BOOST_LOG_TRIVIAL(error) << "[Orca Updater]: get version of settings failed, body=" << body;
|
|
}
|
|
} catch (std::exception &e) {
|
|
BOOST_LOG_TRIVIAL(error) << (boost::format("[Orca Updater]: get version of settings failed, exception=%1% body=%2%") % e.what() % body).str();
|
|
} catch (...) {
|
|
BOOST_LOG_TRIVIAL(error) << "[Orca Updater]: get version of settings failed, body=" << body;
|
|
}
|
|
})
|
|
.on_error([&](std::string body, std::string error, unsigned status) {
|
|
BOOST_LOG_TRIVIAL(error) << boost::format("[Orca Updater]: status=%1%, error=%2%, body=%3%") % status % error % body;
|
|
})
|
|
.perform_sync();
|
|
|
|
for (auto & resource_it : resources) {
|
|
if (cancel) { return; }
|
|
|
|
auto resource = resource_it.second;
|
|
std::string resource_name = resource_it.first;
|
|
boost::to_lower(resource_name);
|
|
auto resource_update = resource_list.find(resource_name);
|
|
if (resource_update == resource_list.end()) {
|
|
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]Vendor " << resource_name << " can not get setting versions online";
|
|
continue;
|
|
}
|
|
Semver online_version = resource_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 "<<cache_path;
|
|
fs::create_directories(cache_path);
|
|
if (!extract_file(cache_file_path, cache_path)) {
|
|
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]extract resource " << resource_it.first << " failed, path: " << cache_file_path;
|
|
continue;
|
|
}
|
|
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]finished unzip the downloaded file " << cache_file_path;
|
|
|
|
// save the description to disk
|
|
if (changelog_file.empty())
|
|
changelog_file = (cache_path / "changelog.json").string();
|
|
else
|
|
changelog_file = (cache_path / changelog_file).string();
|
|
|
|
try {
|
|
json j;
|
|
//record the headers
|
|
j["version"] = resource_update->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 "<<changelog_file<<" got a generic exception, reason = " << err.what();
|
|
}
|
|
|
|
resource_it.second = resource_update->second;
|
|
}
|
|
else {
|
|
BOOST_LOG_TRIVIAL(warning) << boost::format("[Orca Updater]: online version=%1%, current_version=%2%, no need to download") % online_version.to_string() % current_version.to_string();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Orca: per-vendor config update check
|
|
void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
|
|
{
|
|
if (!enabled_config_update) return;
|
|
|
|
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] checking vendor update for " << vendor_id;
|
|
|
|
auto check_cancel = [this](Http::Progress, bool &cancel_http) {
|
|
if (cancel || vendor_check_cancel) cancel_http = true;
|
|
};
|
|
|
|
AppConfig *app_config = GUI::wxGetApp().app_config;
|
|
std::string url = app_config->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<std::string>();
|
|
download_url_str = j["download_url"].get<std::string>();
|
|
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<std::mutex> 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<std::string, Resource> 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 = "<<cached_version<<", force = " << force;
|
|
}
|
|
catch(nlohmann::detail::parse_error &err) {
|
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse "<<changelog_file<<" got a nlohmann::detail::parse_error, reason = " << err.what();
|
|
//throw ConfigurationError(format("Failed loading json file \"%1%\": %2%", file_path, err.what()));
|
|
}
|
|
}
|
|
|
|
return has_plugins;
|
|
}
|
|
|
|
void PresetUpdater::priv::sync_plugins(std::string http_url, std::string plugin_version)
|
|
{
|
|
if (plugin_version == "00.00.00.00") {
|
|
BOOST_LOG_TRIVIAL(info) << "non need to sync plugins for there is no plugins currently.";
|
|
return;
|
|
}
|
|
std::string curr_version = GUI::wxGetApp().use_legacy_network_plugin() ? BAMBU_NETWORK_AGENT_VERSION_LEGACY : get_latest_network_version();
|
|
std::string using_version = curr_version.substr(0, 9) + "00";
|
|
auto cache_plugin_folder = cache_path / "plugins";
|
|
|
|
// Orca: drop leftovers from the old flat ota/ cache layout (pre ota/plugins) so the
|
|
// stale files cannot linger forever after this layout migration.
|
|
{
|
|
#if defined(_MSC_VER) || defined(_WIN32)
|
|
const char* legacy_names[] = {"bambu_networking.dll", "BambuSource.dll", "live555.dll", "network_plugins.json"};
|
|
#elif defined(__WXMAC__)
|
|
const char* legacy_names[] = {"libbambu_networking.dylib", "libBambuSource.dylib", "liblive555.dylib", "network_plugins.json"};
|
|
#else
|
|
const char* legacy_names[] = {"libbambu_networking.so", "libBambuSource.so", "liblive555.so", "network_plugins.json"};
|
|
#endif
|
|
for (const char* name : legacy_names) {
|
|
boost::system::error_code ec;
|
|
auto legacy_file = cache_path / name;
|
|
if (boost::filesystem::exists(legacy_file, ec))
|
|
boost::filesystem::remove(legacy_file, ec);
|
|
}
|
|
}
|
|
|
|
std::string cached_version;
|
|
bool force_upgrade = false;
|
|
get_cached_plugins_version(cached_version, force_upgrade);
|
|
if (!cached_version.empty()) {
|
|
bool need_delete_cache = false;
|
|
Semver current_semver = curr_version;
|
|
Semver cached_semver = cached_version;
|
|
|
|
int curent_patch_cc = current_semver.patch()/100;
|
|
int cached_patch_cc = cached_semver.patch()/100;
|
|
int curent_patch_dd = current_semver.patch()%100;
|
|
int cached_patch_dd = cached_semver.patch()%100;
|
|
if ((cached_semver.maj() != current_semver.maj())
|
|
|| (cached_semver.min() != current_semver.min())
|
|
|| (curent_patch_cc != cached_patch_cc))
|
|
{
|
|
need_delete_cache = true;
|
|
BOOST_LOG_TRIVIAL(info) << boost::format("cached plugins version %1% not match with current %2%")%cached_version%curr_version;
|
|
}
|
|
else if (cached_patch_dd <= curent_patch_dd) {
|
|
need_delete_cache = true;
|
|
BOOST_LOG_TRIVIAL(info) << boost::format("cached plugins version %1% not newer than current %2%")%cached_version%curr_version;
|
|
}
|
|
else {
|
|
BOOST_LOG_TRIVIAL(info) << boost::format("cached plugins version %1% newer than current %2%")%cached_version%curr_version;
|
|
plugin_version = cached_version;
|
|
}
|
|
|
|
if (need_delete_cache) {
|
|
BOOST_LOG_TRIVIAL(info) << "[remove_old_networking_plugins] remove the plugins directory " << cache_plugin_folder.string();
|
|
try {
|
|
fs::remove_all(cache_plugin_folder);
|
|
} catch (...) {
|
|
BOOST_LOG_TRIVIAL(error) << "Failed removing the plugins directory " << cache_plugin_folder.string();
|
|
}
|
|
}
|
|
}
|
|
|
|
#if defined(__WINDOWS__)
|
|
if (GUI::wxGetApp().is_running_on_arm64() && !GUI::wxGetApp().use_legacy_network_plugin()) {
|
|
//set to arm64 for plugins
|
|
std::map<std::string, std::string> 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<std::string, Resource> 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<std::string, std::string> 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<std::string, Resource> 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<std::string>& 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<std::string> 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<std::string, std::string> changelogs;
|
|
{
|
|
std::lock_guard<std::mutex> 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
|
|
// <vendor>/ preset directory; a stray or half-downloaded <vendor>.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<std::string, std::string> key_values;
|
|
std::vector<std::string> 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<std::string>& system_vendors,
|
|
std::function<void(std::vector<std::string>, 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<NewVendorCandidate> 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<GUI::MsgUpdateConfig::Update> 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<std::string> new_vendor_ids;
|
|
std::vector<std::string> 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<std::mutex> 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<std::string> 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<std::string>& system_vendors,
|
|
std::function<void(std::vector<std::string>, 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<std::string> 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<GUI::MsgUpdateConfig::Update> 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<std::string> 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<GUI::MsgUpdateConfig::Update> 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<GUI::MsgUpdateConfig::Update> 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;
|
|
}
|
|
|
|
}
|