Feature/per vendor update (#13394)

* init work - refactored OrcaSlice side
backend is not updated yet

* end-to-end flow

* Delete task.md

---------

Co-authored-by: SoftFever <softfeverever@gmail.com>
This commit is contained in:
Ian Chua
2026-05-01 18:03:19 +08:00
committed by GitHub
parent c04be9ab37
commit 70fb1a9578
4 changed files with 153 additions and 120 deletions

View File

@@ -41,7 +41,7 @@ using namespace nlohmann;
namespace Slic3r {
static const std::string VERSION_CHECK_URL = "https://check-version.orcaslicer.com/latest";
static const std::string PROFILE_UPDATE_URL = "https://api.github.com/repos/OrcaSlicer/orcaslicer-profiles/releases/tags";
static const std::string PROFILE_UPDATE_URL = "https://check-version.orcaslicer.com/profile";
static const std::string MODELS_STR = "models";
const std::string AppConfig::SECTION_FILAMENTS = "filaments";

View File

@@ -33,6 +33,7 @@
#include "GUI_App.hpp"
#include "GUI_ObjectList.hpp"
#include "slic3r/Utils/PresetUpdater.hpp"
#include "Plater.hpp"
#include "MainFrame.hpp"
#include "format.hpp"
@@ -2061,6 +2062,12 @@ void Tab::on_presets_changed()
// Check if printer agent needs switching
if (m_type == Preset::TYPE_PRINTER) {
wxGetApp().switch_printer_agent();
// Trigger per-vendor preset update check
const Preset& printer_preset = m_preset_bundle->printers.get_edited_preset();
if (printer_preset.vendor) {
wxGetApp().get_preset_updater()->check_vendor_update(printer_preset.vendor->id);
}
}
bool is_bbl_vendor_preset = m_preset_bundle->is_bbl_vendor();

View File

@@ -4,6 +4,9 @@
#include <boost/filesystem/operations.hpp>
#include <boost/nowide/fstream.hpp>
#include <functional>
#include <atomic>
#include <mutex>
#include <set>
#include <thread>
#include <unordered_map>
#include <ostream>
@@ -199,6 +202,12 @@ struct PresetUpdater::priv
bool has_waiting_printer_updates { false };
Updates waiting_printer_updates;
// Per-vendor update checking
std::set<std::string> checked_vendors;
std::mutex vendor_check_mutex;
std::vector<std::thread> vendor_check_threads;
std::atomic<bool> vendor_check_cancel{false};
struct Resource
{
std::string version;
@@ -219,7 +228,7 @@ struct PresetUpdater::priv
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_config();
void sync_vendor_config(const std::string& vendor_id);
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);
@@ -642,124 +651,102 @@ void PresetUpdater::priv::sync_resources(std::string http_url, std::map<std::str
}
}
// Orca: sync config update for currect App version
void PresetUpdater::priv::sync_config()
// Orca: per-vendor config update check
void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
{
auto cache_profile_path = cache_path;
auto cache_profile_update_file = cache_path / "profiles_update.json";
std::string asset_name;
if (fs::exists(cache_profile_update_file)) {
try {
boost::nowide::ifstream f(cache_profile_update_file.string());
json data = json::parse(f);
if (data.contains("name"))
asset_name = data["name"].get<std::string>();
f.close();
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(error) << "[Orca Updater]: failed to read profiles_update.json when sync_config: " << ex.what() << std::endl;
} catch (...) {
// catch any other errors (that we have no information about)
BOOST_LOG_TRIVIAL(error) << "[Orca Updater]: unknown failure when reading profiles_update.json in sync_config" << std::endl;
}
}
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);
auto profile_update_url = app_config->profile_update_url() + "/" + SoftFever_VERSION;
// parse the assets section and get the latest asset by comparing the name
std::string online_version_str; // this represents the PROFILE VERSION, not ORCA VERSION
std::string download_url_str;
Http::get(profile_update_url)
.on_error([cache_profile_path, cache_profile_update_file](std::string body, std::string error, unsigned http_status) {
// Orca: we check the response body to see if it's "Not Found", if so, it means for the current Orca version we don't have OTA
// updates, we can delete the cache file
if (!body.empty()) {
try {
json j = json::parse(body);
if (j.contains("message") && j["message"].get<std::string>() == "Not Found") {
// The current Orca version does not have any OTA updates, delete the cache file
if (fs::exists(cache_profile_path / "profiles"))
fs::remove_all(cache_profile_path / "profiles");
if (fs::exists(cache_profile_update_file))
fs::remove(cache_profile_update_file);
}
} catch (...) {}
}
BOOST_LOG_TRIVIAL(info) << format("Error getting: `%1%`: HTTP %2%, %3%", "sync_config_orca", http_status, error);
})
Http::get(url)
.timeout_connect(5)
.on_complete([this, asset_name, cache_profile_path, cache_profile_update_file](std::string body, unsigned http_status) {
// Http response OK
if (http_status != 200)
return;
.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);
struct update
{
std::string url;
std::string name;
int ver = -9999;
} latest_update;
if (!(j.contains("message") && j["message"].get<std::string>() == "Not Found")) {
json assets = j.at("assets");
if (assets.is_array()) {
for (auto asset : assets) {
std::string name = asset["name"].get<std::string>();
int versionNumber = -1;
std::regex regexPattern("orcaslicer-profiles_ota_.*\\.([0-9]+)\\.zip$");
std::smatch matches;
if (std::regex_search(name, matches, regexPattern) && matches.size() > 1) {
versionNumber = std::stoi(matches[1].str());
}
if (versionNumber > 0 && versionNumber > latest_update.ver) {
latest_update.url = asset["browser_download_url"].get<std::string>();
latest_update.name = name;
latest_update.ver = versionNumber;
}
}
}
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>();
}
if (cancel)
return;
if (latest_update.ver > 0) {
if (latest_update.name == asset_name)
return;
if (fs::exists(cache_profile_path / "profiles"))
fs::remove_all(cache_profile_path / "profiles");
fs::create_directories(cache_profile_path / "profiles");
// download the file
std::string download_url = latest_update.url;
std::string download_file = (cache_path / (latest_update.name + TMP_EXTENSION)).string();
if (!get_file(download_url, download_file)) {
return;
}
// extract the file downloaded
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]start to unzip the downloaded file " << download_file;
if (!extract_file(download_file, cache_profile_path)) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]extract downloaded file"
<< " failed, path: " << download_file;
return;
}
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]finished unzip the downloaded file " << download_file;
boost::nowide::ofstream f(cache_profile_update_file.string());
json data;
data["name"] = latest_update.name;
f << data << std::endl;
f.close();
} else {
// The current Orca version does not have any OTA updates, delete the cache file
if (fs::exists(cache_profile_path / "profiles"))
fs::remove_all(cache_profile_path / "profiles");
if (fs::exists(cache_profile_update_file))
fs::remove(cache_profile_update_file);
}
} catch (...) {}
} 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);
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
// the vendor json/folder 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;
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)
@@ -1240,8 +1227,7 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
ifs.close();
}
bool version_match = ((vendor_ver.maj() == cache_ver.maj()) && (vendor_ver.min() == cache_ver.min()));
if (version_match && (vendor_ver < cache_ver)) {
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;
@@ -1251,6 +1237,10 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
updates.updates.emplace_back(std::move(file_path), std::move(path_in_vendor.string()), std::move(version), vendor_name, changelog, "", force_update, false);
//Orca: update vendor folder
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();
}
}
}
@@ -1339,6 +1329,12 @@ PresetUpdater::~PresetUpdater()
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
@@ -1353,20 +1349,30 @@ void PresetUpdater::sync(std::string http_url, std::string language, std::string
// into the closure (but perhaps the compiler can elide this).
VendorMap vendors = preset_bundle ? preset_bundle->vendors : VendorMap{};
p->thread = std::thread([this, vendors, http_url, language, plugin_version]() {
// Determine active vendor before entering the thread
std::string active_vendor;
if (preset_bundle) {
const Preset& printer = preset_bundle->printers.get_edited_preset();
if (printer.vendor)
active_vendor = printer.vendor->id;
}
p->thread = std::thread([this, vendors, active_vendor, http_url, language, plugin_version]() {
this->p->prune_tmps();
if (p->cancel)
return;
this->p->sync_version();
if (p->cancel)
return;
if (!vendors.empty()) {
this->p->sync_config();
if (p->cancel)
return;
GUI::wxGetApp().CallAfter([] {
GUI::wxGetApp().check_config_updates_from_updater();
});
// Per-vendor config check for the active vendor at startup
if (!active_vendor.empty() && !vendors.empty()) {
this->p->sync_vendor_config(active_vendor);
if (p->cancel)
return;
{
std::lock_guard<std::mutex> lock(this->p->vendor_check_mutex);
this->p->checked_vendors.insert(active_vendor);
}
}
if (p->cancel)
return;
@@ -1379,6 +1385,25 @@ void PresetUpdater::sync(std::string http_url, std::string language, std::string
});
}
void PresetUpdater::check_vendor_update(const std::string& vendor_id)
{
if (!p->enabled_config_update) return;
if (vendor_id.empty()) return;
std::lock_guard<std::mutex> lock(p->vendor_check_mutex);
if (!p->checked_vendors.insert(vendor_id).second)
return;
p->vendor_check_threads.emplace_back([this, vendor_id]() {
try {
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();
}
});
}
void PresetUpdater::slic3r_update_notify()
{
if (! p->enabled_version_check)

View File

@@ -58,6 +58,7 @@ public:
void on_update_notification_confirm();
void do_printer_config_update();
void check_vendor_update(const std::string& vendor_id);
bool version_check_enabled() const;