Cut redundant work and duplication from the preset cache paths

The schema fingerprint, filament library version and cache blob are no
longer recomputed, re-read or copied once per vendor on the startup path,
and the setup wizard's completeness check now sees vendors shipped as
caches alone. Duplicated reset/join/slurp blocks are folded into helpers.
This commit is contained in:
SoftFever
2026-07-29 21:56:17 +08:00
parent e5aa9a8cfc
commit ba5ecdfea9
9 changed files with 145 additions and 115 deletions

View File

@@ -6,7 +6,6 @@
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <boost/program_options.hpp> #include <boost/program_options.hpp>
#include <boost/system/error_code.hpp>
#include <iostream> #include <iostream>
using namespace Slic3r; using namespace Slic3r;

View File

@@ -27,6 +27,9 @@
#include <cereal/access.hpp> #include <cereal/access.hpp>
#include <cereal/types/base_class.hpp> #include <cereal/types/base_class.hpp>
// The serialize() members below archive ConfigOption hierarchies through
// cereal::base_class, whose registration machinery lives in polymorphic.hpp.
#include <cereal/types/polymorphic.hpp>
namespace Slic3r { namespace Slic3r {
struct FloatOrPercent struct FloatOrPercent

View File

@@ -16,13 +16,7 @@
#include "Semver.hpp" #include "Semver.hpp"
#include "ProjectTask.hpp" #include "ProjectTask.hpp"
#include <cereal/archives/binary.hpp> #include <cereal/access.hpp>
#include <cereal/cereal.hpp>
#include <cereal/types/map.hpp>
#include <cereal/types/polymorphic.hpp>
#include <cereal/types/set.hpp>
#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>
//BBS: change system directories //BBS: change system directories
#define PRESET_SYSTEM_DIR "system" #define PRESET_SYSTEM_DIR "system"

View File

@@ -6,8 +6,11 @@
#include "PresetBundle.hpp" #include "PresetBundle.hpp"
#include <boost/crc.hpp> #include <boost/crc.hpp>
#include <boost/iostreams/device/array.hpp>
#include <boost/iostreams/stream.hpp>
#include <cereal/archives/binary.hpp> #include <cereal/archives/binary.hpp>
#include <cereal/types/map.hpp> #include <cereal/types/map.hpp>
#include <cereal/types/set.hpp>
#include <cereal/types/string.hpp> #include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp> #include <cereal/types/vector.hpp>
#include "PrintConfig.hpp" #include "PrintConfig.hpp"
@@ -2281,6 +2284,15 @@ Semver installed_vendor_version(const std::string& vendor)
return ver ? *ver : Semver(); return ver ? *ver : Semver();
} }
void remove_installed_vendor(const std::string& vendor)
{
const boost::filesystem::path dir = boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR;
boost::filesystem::remove(dir / (vendor + ".json"));
boost::filesystem::remove(dir / (vendor + ".opc"));
if (boost::filesystem::exists(dir / vendor))
boost::filesystem::remove_all(dir / vendor);
}
std::set<std::string> vendor_names_in(const boost::filesystem::path& dir) std::set<std::string> vendor_names_in(const boost::filesystem::path& dir)
{ {
std::set<std::string> names; std::set<std::string> names;
@@ -2298,21 +2310,21 @@ std::set<std::string> vendor_names_in(const boost::filesystem::path& dir)
// generated before that profile was bumped is out of date, and a cache that cannot // generated before that profile was bumped is out of date, and a cache that cannot
// be read is no installation at all — and the vendor is installed the way it was // be read is no installation at all — and the vendor is installed the way it was
// before caches existed, as its profile and the preset JSONs it points at. Returns // before caches existed, as its profile and the preset JSONs it points at. Returns
// the version the cache is stamped with, or nothing when it is not the form to install. // the version the cache is stamped with, invalid when it is not the form to install.
static std::string installable_cache_version(const boost::filesystem::path& dir, const std::string& vendor) static Semver installable_cache_version(const boost::filesystem::path& dir, const std::string& vendor)
{ {
const auto cache_ver = Semver::parse(PresetBundle::peek_vendor_cache_version((dir / (vendor + ".opc")).string(), vendor)); const auto cache_ver = Semver::parse(PresetBundle::peek_vendor_cache_version((dir / (vendor + ".opc")).string(), vendor));
if (! cache_ver) if (! cache_ver)
return {}; return Semver::invalid();
const Semver profile_ver = get_version_from_json((dir / (vendor + ".json")).string()); const Semver profile_ver = get_version_from_json((dir / (vendor + ".json")).string());
return profile_ver.valid() && *cache_ver < profile_ver ? std::string() : cache_ver->to_string(); return profile_ver.valid() && *cache_ver < profile_ver ? Semver::invalid() : *cache_ver;
} }
Semver resource_vendor_version(const std::string& vendor) Semver resource_vendor_version(const std::string& vendor)
{ {
const boost::filesystem::path dir = boost::filesystem::path(resources_dir()) / "profiles"; const boost::filesystem::path dir = boost::filesystem::path(resources_dir()) / "profiles";
const auto ver = Semver::parse(installable_cache_version(dir, vendor)); const Semver ver = installable_cache_version(dir, vendor);
return ver ? *ver : get_version_from_json((dir / (vendor + ".json")).string()); return ver.valid() ? ver : get_version_from_json((dir / (vendor + ".json")).string());
} }
bool install_vendor_bundles_from_resources( bool install_vendor_bundles_from_resources(
@@ -2347,7 +2359,7 @@ bool install_vendor_bundles_from_resources(
std::string error_message; std::string error_message;
bool installed_cache = false; bool installed_cache = false;
if (! installable_cache_version(rsrc_path, bundle).empty()) { if (installable_cache_version(rsrc_path, bundle).valid()) {
installed_cache = copy_file(cache_in_rsrc.string(), cache_in_vendors.string(), error_message, false) == CopyFileResult::SUCCESS; installed_cache = copy_file(cache_in_rsrc.string(), cache_in_vendors.string(), error_message, false) == CopyFileResult::SUCCESS;
if (! installed_cache) if (! installed_cache)
BOOST_LOG_TRIVIAL(warning) << "Failed to copy " << bundle << ".opc: " << error_message; BOOST_LOG_TRIVIAL(warning) << "Failed to copy " << bundle << ".opc: " << error_message;
@@ -2405,6 +2417,17 @@ bool install_vendor_bundles_from_resources(
return true; return true;
} }
// m_printer_hold_alias survives reset() (and a cache body that failed partway
// in), so every full-bundle rebuild clears all five collections' maps by hand.
void PresetBundle::clear_printer_hold_aliases()
{
this->prints.m_printer_hold_alias.clear();
this->sla_prints.m_printer_hold_alias.clear();
this->filaments.m_printer_hold_alias.clear();
this->sla_materials.m_printer_hold_alias.clear();
this->printers.m_printer_hold_alias.clear();
}
//BBS: add json related logic, load system presets from json //BBS: add json related logic, load system presets from json
std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule) std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule)
{ {
@@ -2456,11 +2479,7 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
// Match a fresh launch before parsing: hold aliases and the error // Match a fresh launch before parsing: hold aliases and the error
// counter survive reset(), and would otherwise leak prior-cycle // counter survive reset(), and would otherwise leak prior-cycle
// state into the library cache the load below writes. // state into the library cache the load below writes.
this->prints.m_printer_hold_alias.clear(); this->clear_printer_hold_aliases();
this->sla_prints.m_printer_hold_alias.clear();
this->filaments.m_printer_hold_alias.clear();
this->sla_materials.m_printer_hold_alias.clear();
this->printers.m_printer_hold_alias.clear();
this->m_errors = 0; this->m_errors = 0;
append(substitutions, this->load_vendor_configs_from_json(dir.string(), orca_lib_vendor, PresetBundle::LoadSystem, compatibility_rule).first); append(substitutions, this->load_vendor_configs_from_json(dir.string(), orca_lib_vendor, PresetBundle::LoadSystem, compatibility_rule).first);
first = false; first = false;
@@ -2479,6 +2498,11 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
std::vector<PresetsConfigSubstitutions> parallel_substitutions(other_vendors.size()); std::vector<PresetsConfigSubstitutions> parallel_substitutions(other_vendors.size());
std::vector<std::string> parallel_errors(other_vendors.size()); std::vector<std::string> parallel_errors(other_vendors.size());
// The filament library version every vendor below is judged against. Fixed
// from here on — step 1 was the last thing that could touch the library on
// disk — so resolve it once instead of once per vendor.
const std::string lib_version = effective_lib_version(dir);
tbb::parallel_for(tbb::blocked_range<size_t>(0, other_vendors.size()), tbb::parallel_for(tbb::blocked_range<size_t>(0, other_vendors.size()),
[&](const tbb::blocked_range<size_t>& range) { [&](const tbb::blocked_range<size_t>& range) {
for (size_t i = range.begin(); i < range.end(); ++i) { for (size_t i = range.begin(); i < range.end(); ++i) {
@@ -2487,7 +2511,7 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
bundle->set_generate_vendor_caches(m_generate_vendor_caches); bundle->set_generate_vendor_caches(m_generate_vendor_caches);
try { try {
auto result = bundle->load_vendor_configs_from_json( auto result = bundle->load_vendor_configs_from_json(
dir.string(), other_vendors[i], PresetBundle::LoadSystem, compatibility_rule, this); dir.string(), other_vendors[i], PresetBundle::LoadSystem, compatibility_rule, this, lib_version);
parallel_substitutions[i] = std::move(result.first); parallel_substitutions[i] = std::move(result.first);
parallel_bundles[i] = std::move(bundle); parallel_bundles[i] = std::move(bundle);
} catch (const std::runtime_error &err) { } catch (const std::runtime_error &err) {
@@ -4953,7 +4977,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
//BBS: Load a config bundle file from json //BBS: Load a config bundle file from json
std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_from_json( std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_from_json(
const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle) const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle,
const std::string &lib_version_hint)
{ {
// Enable substitutions for user config bundle, throw an exception when loading a system profile. // Enable substitutions for user config bundle, throw an exception when loading a system profile.
ConfigSubstitutionContext substitution_context { compatibility_rule }; ConfigSubstitutionContext substitution_context { compatibility_rule };
@@ -4969,7 +4994,7 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
// scans want a slice of one. Validation reads the JSONs whatever is cached. // scans want a slice of one. Validation reads the JSONs whatever is cached.
const boost::filesystem::path dir_path(dir); const boost::filesystem::path dir_path(dir);
const bool cacheable = flags.has(LoadConfigBundleAttribute::LoadSystem) && ! flags.has(LoadConfigBundleAttribute::LoadFilamentOnly); const bool cacheable = flags.has(LoadConfigBundleAttribute::LoadSystem) && ! flags.has(LoadConfigBundleAttribute::LoadFilamentOnly);
if (cacheable && ! validation_mode && this->load_vendor_cache(dir_path, vendor_name)) { if (cacheable && ! validation_mode && this->load_vendor_cache(dir_path, vendor_name, lib_version_hint)) {
size_t presets_loaded = 0; size_t presets_loaded = 0;
for (const PresetCollection* coll : std::initializer_list<const PresetCollection*>{ for (const PresetCollection* coll : std::initializer_list<const PresetCollection*>{
&this->prints, &this->sla_prints, &this->filaments, &this->sla_materials, &this->printers }) &this->prints, &this->sla_prints, &this->filaments, &this->sla_materials, &this->printers })
@@ -5550,7 +5575,8 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
const std::string version = vendor_profile.config_version.to_string(); const std::string version = vendor_profile.config_version.to_string();
// The library is its own reference point; every other vendor's cache holds // The library is its own reference point; every other vendor's cache holds
// filaments resolved against it, and is stamped with the version in effect. // filaments resolved against it, and is stamped with the version in effect.
const std::string lib_version = vendor_name == ORCA_FILAMENT_LIBRARY ? version : effective_lib_version(dir_path); const std::string lib_version = vendor_name == ORCA_FILAMENT_LIBRARY ? version
: ! lib_version_hint.empty() ? lib_version_hint : effective_lib_version(dir_path);
if (! lib_version.empty() && if (! lib_version.empty() &&
! this->save_vendor_cache((dir_path / (vendor_name + ".opc")).string(), vendor_name, version, lib_version)) ! this->save_vendor_cache((dir_path / (vendor_name + ".opc")).string(), vendor_name, version, lib_version))
BOOST_LOG_TRIVIAL(warning) << "PresetBundle: failed to save vendor cache for " << vendor_name; BOOST_LOG_TRIVIAL(warning) << "PresetBundle: failed to save vendor cache for " << vendor_name;
@@ -6218,8 +6244,11 @@ static bool cache_covers_version(const std::string& cached, const std::string& o
// values — serialization_key_ordinal IS the config wire format). Any mismatch // values — serialization_key_ordinal IS the config wire format). Any mismatch
// means bytes written by another build could deserialize into the wrong // means bytes written by another build could deserialize into the wrong
// fields, so the cache is rejected wholesale before its body is read. // fields, so the cache is rejected wholesale before its body is read.
std::string compute_cache_schema_fingerprint() const std::string& compute_cache_schema_fingerprint()
{ {
// Constant for the lifetime of the process (print_config_def is immutable
// after static initialization), and asked for once per cache load and save.
static const std::string fingerprint = [] {
std::string schema; std::string schema;
schema += SLIC3R_VERSION; schema += SLIC3R_VERSION;
schema += ';'; schema += ';';
@@ -6233,6 +6262,8 @@ std::string compute_cache_schema_fingerprint()
boost::crc_32_type crc; boost::crc_32_type crc;
crc.process_bytes(schema.data(), schema.size()); crc.process_bytes(schema.data(), schema.size());
return std::to_string(crc.checksum()); return std::to_string(crc.checksum());
}();
return fingerprint;
} }
} // anonymous namespace } // anonymous namespace
@@ -6268,7 +6299,7 @@ bool PresetBundle::read_cache_blob(const std::string& path, std::string& out_blo
} }
// static // static
void PresetBundle::write_cache_blob(const std::string& path, const std::string& blob) bool PresetBundle::write_cache_blob(const std::string& path, const std::string& blob)
{ {
boost::crc_32_type crc; boost::crc_32_type crc;
crc.process_bytes(blob.data(), blob.size()); crc.process_bytes(blob.data(), blob.size());
@@ -6277,7 +6308,7 @@ void PresetBundle::write_cache_blob(const std::string& path, const std::string&
boost::nowide::ofstream ofs(path, std::ios::binary | std::ios::trunc); boost::nowide::ofstream ofs(path, std::ios::binary | std::ios::trunc);
if (!ofs.is_open()) { if (!ofs.is_open()) {
BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: cannot open for writing: " << path; BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: cannot open for writing: " << path;
return; return false;
} }
CacheFileHeader fhdr; CacheFileHeader fhdr;
fhdr.magic = CACHE_MAGIC; fhdr.magic = CACHE_MAGIC;
@@ -6286,8 +6317,13 @@ void PresetBundle::write_cache_blob(const std::string& path, const std::string&
fhdr.crc32 = crc.checksum(); fhdr.crc32 = crc.checksum();
ofs.write(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr)); ofs.write(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr));
ofs.write(blob.data(), static_cast<std::streamsize>(blob.size())); ofs.write(blob.data(), static_cast<std::streamsize>(blob.size()));
ofs.close(); // flush; close() raises failbit on error
if (! ofs.good())
BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: write failed (" << path << ")";
return ofs.good();
} catch (const std::exception& e) { } catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: write failed (" << path << "): " << e.what(); BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: write failed (" << path << "): " << e.what();
return false;
} }
} }
@@ -6373,9 +6409,7 @@ bool PresetBundle::save_vendor_cache(const std::string& cache_path, const std::s
this->obsolete_presets.printers); this->obsolete_presets.printers);
ar(this->m_errors); ar(this->m_errors);
} }
write_cache_blob(cache_path, body.str()); return write_cache_blob(cache_path, body.str());
std::string verify;
return read_cache_blob(cache_path, verify); // magic + size + CRC
} catch (const std::exception& e) { } catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "PresetBundle: failed to save vendor cache " << cache_path << ": " << e.what(); BOOST_LOG_TRIVIAL(warning) << "PresetBundle: failed to save vendor cache " << cache_path << ": " << e.what();
return false; return false;
@@ -6415,7 +6449,7 @@ std::string PresetBundle::peek_vendor_cache_version(const std::string& cache_pat
} }
} }
bool PresetBundle::load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name) bool PresetBundle::load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const std::string& lib_version_hint)
{ {
// Whichever cache answers is judged against the vendor as installed in `dir`: // Whichever cache answers is judged against the vendor as installed in `dir`:
// the profile there, or — with none, as when the cache is the whole of the // the profile there, or — with none, as when the cache is the whole of the
@@ -6425,7 +6459,7 @@ bool PresetBundle::load_vendor_cache(const boost::filesystem::path& dir, const s
const boost::filesystem::path profile = dir / (vendor_name + ".json"); const boost::filesystem::path profile = dir / (vendor_name + ".json");
const std::string version = boost::filesystem::exists(profile) ? get_vendor_cache_version(profile.string()) const std::string version = boost::filesystem::exists(profile) ? get_vendor_cache_version(profile.string())
: std::string(CACHE_ANY_VERSION); : std::string(CACHE_ANY_VERSION);
const std::string lib_version = effective_lib_version(dir); const std::string lib_version = ! lib_version_hint.empty() ? lib_version_hint : effective_lib_version(dir);
const boost::filesystem::path rsrc = boost::filesystem::path(resources_dir()) / "profiles"; const boost::filesystem::path rsrc = boost::filesystem::path(resources_dir()) / "profiles";
return this->load_vendor_cache((dir / (vendor_name + ".opc")).string(), vendor_name, version, lib_version) return this->load_vendor_cache((dir / (vendor_name + ".opc")).string(), vendor_name, version, lib_version)
|| (dir != rsrc && this->load_vendor_cache((rsrc / (vendor_name + ".opc")).string(), vendor_name, version, lib_version)); || (dir != rsrc && this->load_vendor_cache((rsrc / (vendor_name + ".opc")).string(), vendor_name, version, lib_version));
@@ -6438,7 +6472,9 @@ bool PresetBundle::load_vendor_cache(const std::string& cache_path, const std::s
if (! read_cache_blob(cache_path, blob)) if (! read_cache_blob(cache_path, blob))
return false; return false;
try { try {
std::istringstream body(blob, std::ios::binary); // Read in place: an istringstream would copy the blob (tens of MB for
// the largest vendors) once more just to stream over it.
boost::iostreams::stream<boost::iostreams::array_source> body(blob.data(), blob.size());
cereal::BinaryInputArchive ar(body); cereal::BinaryInputArchive ar(body);
uint32_t cache_version = 0; uint32_t cache_version = 0;
ar(cache_version); ar(cache_version);
@@ -6474,14 +6510,10 @@ bool PresetBundle::load_vendor_cache(const std::string& cache_path, const std::s
this->m_config_maps.clear(); this->m_config_maps.clear();
this->m_filament_id_maps.clear(); this->m_filament_id_maps.clear();
this->m_errors = 0; this->m_errors = 0;
// reset() does not clear m_printer_hold_alias, and a mid-body failure may // A mid-body failure may have left collections the deserialization never
// have left collections it never reached (each load_collection clears its // reached (each load_collection clears its own collection's map only when
// own collection's map only when it runs) with stale aliases. // it runs) with stale aliases.
this->prints.m_printer_hold_alias.clear(); this->clear_printer_hold_aliases();
this->sla_prints.m_printer_hold_alias.clear();
this->filaments.m_printer_hold_alias.clear();
this->sla_materials.m_printer_hold_alias.clear();
this->printers.m_printer_hold_alias.clear();
return false; return false;
} }
} }

View File

@@ -14,6 +14,10 @@
#include <boost/filesystem/path.hpp> #include <boost/filesystem/path.hpp>
#include <unordered_set> #include <unordered_set>
namespace cereal {
class BinaryInputArchive;
class BinaryOutputArchive;
}
#define DEFAULT_USER_FOLDER_NAME "default" #define DEFAULT_USER_FOLDER_NAME "default"
#define BUNDLE_STRUCTURE_JSON_NAME "bundle_structure.json" #define BUNDLE_STRUCTURE_JSON_NAME "bundle_structure.json"
@@ -488,8 +492,11 @@ public:
// not the profile JSONs are still there. A whole-vendor load comes from the // not the profile JSONs are still there. A whole-vendor load comes from the
// vendor's preset cache whenever one covers the profile on disk, and is parsed // vendor's preset cache whenever one covers the profile on disk, and is parsed
// from the JSONs (falling back to the ones in resources) only when none does. // from the JSONs (falling back to the ones in resources) only when none does.
// `lib_version_hint` is the filament library version in effect, when the caller
// loads many vendors and has already resolved it once; empty resolves it here.
std::pair<PresetsConfigSubstitutions, size_t> load_vendor_configs_from_json( std::pair<PresetsConfigSubstitutions, size_t> load_vendor_configs_from_json(
const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr); const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr,
const std::string &lib_version_hint = {});
// Export a config bundle file containing all the presets and the names of the active presets. // Export a config bundle file containing all the presets and the names of the active presets.
//void export_configbundle(const std::string &path, bool export_system_settings = false, bool export_physical_printers = false); //void export_configbundle(const std::string &path, bool export_system_settings = false, bool export_physical_printers = false);
@@ -573,15 +580,17 @@ public:
private: private:
// Load one vendor from its preset cache: the one in `dir`, or — when that is // Load one vendor from its preset cache: the one in `dir`, or — when that is
// missing or stale — the one shipped in resources/profiles, both judged against // missing or stale — the one shipped in resources/profiles, both judged against
// the vendor as installed in `dir` and against the filament library in effect. // the vendor as installed in `dir` and against the filament library in effect
// (resolved here unless the caller passes the already-resolved version).
// False, with this bundle left clean, when neither is usable and the vendor has // False, with this bundle left clean, when neither is usable and the vendor has
// to be parsed. This is how load_vendor_configs_from_json reads a cache. // to be parsed. This is how load_vendor_configs_from_json reads a cache.
bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name); bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const std::string& lib_version_hint = {});
// Read raw cache blob: verify magic, size, CRC. // Read raw cache blob: verify magic, size, CRC.
static bool read_cache_blob(const std::string& path, std::string& out_blob); static bool read_cache_blob(const std::string& path, std::string& out_blob);
// Write a cache blob with the standard 20-byte file header. // Write a cache blob with the standard 20-byte file header. False when the
static void write_cache_blob(const std::string& path, const std::string& blob); // file could not be opened or written whole.
static bool write_cache_blob(const std::string& path, const std::string& blob);
// (De)serialization of one collection's slice for the per-vendor cache: // (De)serialization of one collection's slice for the per-vendor cache:
// every non-default, non-external preset (system or user) plus // every non-default, non-external preset (system or user) plus
@@ -589,6 +598,9 @@ private:
static void save_collection(cereal::BinaryOutputArchive& ar, const PresetCollection& coll); static void save_collection(cereal::BinaryOutputArchive& ar, const PresetCollection& coll);
static void load_collection(cereal::BinaryInputArchive& ar, PresetCollection& coll, const VendorMap& vendors); static void load_collection(cereal::BinaryInputArchive& ar, PresetCollection& coll, const VendorMap& vendors);
// Clear every collection's m_printer_hold_alias, which reset() leaves alone.
void clear_printer_hold_aliases();
// Whether to (re)write a per-vendor cache after a JSON parse. // Whether to (re)write a per-vendor cache after a JSON parse.
bool m_generate_vendor_caches { false }; bool m_generate_vendor_caches { false };
@@ -642,6 +654,10 @@ extern bool is_vendor_installed(const std::string& vendor);
// was stamped with where only the cache is installed. Invalid Semver if neither is. // was stamped with where only the cache is installed. Invalid Semver if neither is.
extern Semver installed_vendor_version(const std::string& vendor); extern Semver installed_vendor_version(const std::string& vendor);
// Remove every form `vendor` can be installed as from data_dir()/system: its
// profile, its preset cache, and its preset directory.
extern void remove_installed_vendor(const std::string& vendor);
// The vendors `dir` holds, sorted: one is named by its profile or, in a build that // The vendors `dir` holds, sorted: one is named by its profile or, in a build that
// ships preset caches instead of the raw profile JSONs, by its cache alone. // ships preset caches instead of the raw profile JSONs, by its cache alone.
extern std::set<std::string> vendor_names_in(const boost::filesystem::path& dir); extern std::set<std::string> vendor_names_in(const boost::filesystem::path& dir);

View File

@@ -1,6 +1,7 @@
#include "WebGuideDialog.hpp" #include "WebGuideDialog.hpp"
#include "ConfigWizard.hpp" #include "ConfigWizard.hpp"
#include <boost/algorithm/string/join.hpp>
#include <boost/filesystem/operations.hpp> #include <boost/filesystem/operations.hpp>
#include <boost/nowide/fstream.hpp> #include <boost/nowide/fstream.hpp>
#include <boost/filesystem/path.hpp> #include <boost/filesystem/path.hpp>
@@ -190,8 +191,7 @@ GuideFrame::GuideFrame(GUI_App *pGUI, long style)
GuideFrame::~GuideFrame() GuideFrame::~GuideFrame()
{ {
m_destroy = true; *m_cancel_token = true; // stop the loading thread and any queued CallAfter lambdas before join
*m_cancel_token = true; // signal any queued CallAfter lambdas before join
if (m_load_task && m_load_task->joinable()) if (m_load_task && m_load_task->joinable())
m_load_task->join(); m_load_task->join();
m_load_task.reset(); m_load_task.reset();
@@ -300,13 +300,19 @@ void GuideFrame::OnNavigationRequest(wxWebViewEvent &evt)
/** /**
* Callback invoked when a navigation request was accepted * Callback invoked when a navigation request was accepted
*/ */
void GuideFrame::init_guide_paths() // The empty shape every profile-loading path starts from or falls back to.
void GuideFrame::reset_profile_json()
{ {
m_ProfileJson = json::parse("{}");
m_ProfileJson["model"] = json::array(); m_ProfileJson["model"] = json::array();
m_ProfileJson["machine"] = json::object(); m_ProfileJson["machine"] = json::object();
m_ProfileJson["filament"] = json::object(); m_ProfileJson["filament"] = json::object();
m_ProfileJson["process"] = json::array(); m_ProfileJson["process"] = json::array();
}
void GuideFrame::init_guide_paths()
{
m_ProfileJson = json::parse("{}");
reset_profile_json();
vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred();
rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred();
@@ -349,7 +355,7 @@ void GuideFrame::OnNavigationComplete(wxWebViewEvent &evt)
try { try {
init_guide_paths(); init_guide_paths();
if (BuildProfileDataFromPresetBundle()) { if (BuildProfileDataFromPresetBundle()) {
if (!m_destroy) if (!*m_cancel_token)
on_profile_loaded(); on_profile_loaded();
} else { } else {
// Presets not yet in memory — delegate to background thread. // Presets not yet in memory — delegate to background thread.
@@ -1184,11 +1190,7 @@ bool GuideFrame::BuildProfileJson(const PresetBundle& bundle, bool require_all_r
if (!nozzle_str.empty()) nozzle_str += ";"; if (!nozzle_str.empty()) nozzle_str += ";";
nozzle_str += v.name; nozzle_str += v.name;
} }
std::string materials_str; const std::string materials_str = boost::algorithm::join(model.default_materials, ";");
for (const auto& m : model.default_materials) {
if (!materials_str.empty()) materials_str += ";";
materials_str += m;
}
boost::filesystem::path cover_path = boost::filesystem::path cover_path =
(boost::filesystem::path(resources_dir()) / "profiles" / vp.id / (model.id + "_cover.png")) (boost::filesystem::path(resources_dir()) / "profiles" / vp.id / (model.id + "_cover.png"))
.make_preferred(); .make_preferred();
@@ -1224,6 +1226,7 @@ bool GuideFrame::BuildProfileJson(const PresetBundle& bundle, bool require_all_r
} }
// Filament map from system filament presets (vendor/type already resolved in config) // Filament map from system filament presets (vendor/type already resolved in config)
const json& machines = m_ProfileJson["machine"];
for (const Preset& p : bundle.filaments()) { for (const Preset& p : bundle.filaments()) {
if (!p.is_system || !p.vendor) continue; if (!p.is_system || !p.vendor) continue;
const auto* fila_vendor = p.config.option<ConfigOptionStrings>("filament_vendor"); const auto* fila_vendor = p.config.option<ConfigOptionStrings>("filament_vendor");
@@ -1236,9 +1239,10 @@ bool GuideFrame::BuildProfileJson(const PresetBundle& bundle, bool require_all_r
std::string model_list; std::string model_list;
if (compat_printers) { if (compat_printers) {
for (const std::string& pname : compat_printers->values) { for (const std::string& pname : compat_printers->values) {
if (m_ProfileJson["machine"].contains(pname)) { auto it = machines.find(pname);
std::string m = m_ProfileJson["machine"][pname]["model"]; if (it != machines.end()) {
std::string n = m_ProfileJson["machine"][pname]["nozzle"]; const std::string m = (*it)["model"];
const std::string n = (*it)["nozzle"];
model_list += "[" + m + "++" + n + "]"; model_list += "[" + m + "++" + n + "]";
} }
} }
@@ -1264,20 +1268,16 @@ bool GuideFrame::BuildProfileJson(const PresetBundle& bundle, bool require_all_r
} }
if (require_all_resource_vendors) { if (require_all_resource_vendors) {
// If rsrc_vendor_dir has vendor JSONs not covered by the current bundle, the // If rsrc_vendor_dir has vendors (profile JSONs, or the preset caches a
// packaged build ships instead) not covered by the current bundle, the
// bundle is incomplete (e.g. dev env where data_dir/system only has // bundle is incomplete (e.g. dev env where data_dir/system only has
// OrcaFilamentLibrary+Custom). Fall back so LoadProfileFamily reads both dirs. // OrcaFilamentLibrary+Custom). Fall back so the slow path reads both dirs.
try { try {
for (const auto& e : boost::filesystem::directory_iterator(rsrc_vendor_dir)) { for (const std::string& name : vendor_names_in(rsrc_vendor_dir)) {
if (e.path().extension().string() != ".json") continue; if (bundle.vendors.find(name) == bundle.vendors.end()) {
const std::string stem = e.path().stem().string(); BOOST_LOG_TRIVIAL(info) << "GuideFrame: vendor '" << name
if (bundle.vendors.find(stem) == bundle.vendors.end()) {
BOOST_LOG_TRIVIAL(info) << "GuideFrame: vendor '" << stem
<< "' in resources but not in preset_bundle — falling back to JSON loading"; << "' in resources but not in preset_bundle — falling back to JSON loading";
m_ProfileJson["model"] = json::array(); reset_profile_json();
m_ProfileJson["machine"] = json::object();
m_ProfileJson["filament"] = json::object();
m_ProfileJson["process"] = json::array();
return false; return false;
} }
} }
@@ -1292,10 +1292,7 @@ bool GuideFrame::BuildProfileJson(const PresetBundle& bundle, bool require_all_r
} catch (const std::exception& e) { } catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileJson failed: " << e.what() BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileJson failed: " << e.what()
<< " — falling back to JSON loading"; << " — falling back to JSON loading";
m_ProfileJson["model"] = json::array(); reset_profile_json();
m_ProfileJson["machine"] = json::object();
m_ProfileJson["filament"] = json::object();
m_ProfileJson["process"] = json::array();
return false; return false;
} }
} }
@@ -1340,7 +1337,7 @@ bool GuideFrame::BuildProfileDataFromVendors()
if (vendor_files.count(filament_library)) if (vendor_files.count(filament_library))
load_vendor(bundle, filament_library, nullptr); load_vendor(bundle, filament_library, nullptr);
for (const auto& entry : vendor_files) { for (const auto& entry : vendor_files) {
if (m_destroy) if (*m_cancel_token)
return false; // as in the scan below: a vendor without a cache is parsed, and that takes time return false; // as in the scan below: a vendor without a cache is parsed, and that takes time
const std::string& vendor = entry.first; const std::string& vendor = entry.first;
// A cache is only ever written for a versioned vendor; a JSON has to be // A cache is only ever written for a versioned vendor; a JSON has to be
@@ -1358,10 +1355,7 @@ bool GuideFrame::BuildProfileDataFromVendors()
return BuildProfileJson(bundle, /*require_all_resource_vendors=*/false); return BuildProfileJson(bundle, /*require_all_resource_vendors=*/false);
} catch (const std::exception& e) { } catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed: " << e.what(); BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed: " << e.what();
m_ProfileJson["model"] = json::array(); reset_profile_json();
m_ProfileJson["machine"] = json::object();
m_ProfileJson["filament"] = json::object();
m_ProfileJson["process"] = json::array();
return false; return false;
} }
} }
@@ -1395,7 +1389,7 @@ int GuideFrame::LoadProfileData()
LoadProfileFamily(w2s(strVendor), iter->path().string()); LoadProfileFamily(w2s(strVendor), iter->path().string());
loaded_vendors.insert(w2s(strVendor)); loaded_vendors.insert(w2s(strVendor));
} }
if (m_destroy) return 0; if (*m_cancel_token) return 0;
} }
boost::filesystem::directory_iterator others_endIter; boost::filesystem::directory_iterator others_endIter;
@@ -1410,7 +1404,7 @@ int GuideFrame::LoadProfileData()
LoadProfileFamily(w2s(strVendor), iter->path().string()); LoadProfileFamily(w2s(strVendor), iter->path().string());
loaded_vendors.insert(w2s(strVendor)); loaded_vendors.insert(w2s(strVendor));
} }
if (m_destroy) return 0; if (*m_cancel_token) return 0;
} }
} }

View File

@@ -87,6 +87,7 @@ public:
bool BuildProfileJson(const PresetBundle& bundle, bool require_all_resource_vendors); bool BuildProfileJson(const PresetBundle& bundle, bool require_all_resource_vendors);
bool BuildProfileDataFromPresetBundle(); bool BuildProfileDataFromPresetBundle();
bool BuildProfileDataFromVendors(); bool BuildProfileDataFromVendors();
void reset_profile_json();
int SaveProfile(); int SaveProfile();
int GetFilamentInfo( std::string VendorDirectory,json & pFilaList, std::string filepath, std::string &sVendor, std::string &sType); int GetFilamentInfo( std::string VendorDirectory,json & pFilaList, std::string filepath, std::string &sVendor, std::string &sType);
@@ -121,9 +122,9 @@ private:
//First Load //First Load
bool bFirstComplete{false}; bool bFirstComplete{false};
std::atomic<bool> m_destroy{false}; // Set once in the destructor. Read through `this` by the loading thread
// Shared cancel token captured by CallAfter lambdas so they don't touch // (joined before `this` dies) and captured as the shared_ptr by CallAfter
// `this` after the destructor has run and the object is freed. // lambdas so they don't touch `this` after the object is freed.
std::shared_ptr<std::atomic<bool>> m_cancel_token{std::make_shared<std::atomic<bool>>(false)}; std::shared_ptr<std::atomic<bool>> m_cancel_token{std::make_shared<std::atomic<bool>>(false)};
std::unique_ptr<boost::thread> m_load_task; std::unique_ptr<boost::thread> m_load_task;

View File

@@ -1069,12 +1069,7 @@ void PresetUpdater::priv::check_installed_vendor_profiles() const
} }
else { else {
//need to be removed because not installed //need to be removed because not installed
const auto path_in_vendor = vendor_path / (vendor_name + ".json"); remove_installed_vendor(vendor_name);
fs::remove(path_in_vendor);
fs::remove(vendor_path / (vendor_name + ".opc"));
const auto path_of_vendor = vendor_path / vendor_name;
if (fs::exists(path_of_vendor))
fs::remove_all(path_of_vendor);
} }
} }
else if (is_vendor_enabled) { else if (is_vendor_enabled) {

View File

@@ -54,6 +54,14 @@ std::string write_versionless_vendor_json(const fs::path& dir, const std::string
return p.string(); return p.string();
} }
// Whole file as bytes, for the byte-identity comparisons below.
std::string slurp(const fs::path& p)
{
std::string s;
load_string_file(p, s);
return s;
}
void corrupt_blob_byte(const std::string& path) void corrupt_blob_byte(const std::string& path)
{ {
std::fstream f(path, std::ios::in | std::ios::out | std::ios::binary); std::fstream f(path, std::ios::in | std::ios::out | std::ios::binary);
@@ -907,10 +915,6 @@ TEST_CASE("printer hold aliases survive a cache round-trip", "[VendorCache]")
REQUIRE(loaded.load_vendor_cache(cache1.string(), vid, "1.0.0","1.0.0")); REQUIRE(loaded.load_vendor_cache(cache1.string(), vid, "1.0.0","1.0.0"));
REQUIRE(save_one_vendor(loaded, cache2.string(), vid, "1.0.0")); REQUIRE(save_one_vendor(loaded, cache2.string(), vid, "1.0.0"));
auto slurp = [](const fs::path& p) {
std::ifstream ifs(p.string(), std::ios::binary);
return std::string(std::istreambuf_iterator<char>(ifs), {});
};
REQUIRE(slurp(cache1) == slurp(cache2)); REQUIRE(slurp(cache1) == slurp(cache2));
} }
@@ -964,10 +968,6 @@ TEST_CASE("a loaded cache re-serializes to byte-identical output", "[VendorCache
PresetBundle loaded; PresetBundle loaded;
REQUIRE(loaded.load_vendor_cache(cache1.string(), "Acme", "1.0.0", "1.0.0")); REQUIRE(loaded.load_vendor_cache(cache1.string(), "Acme", "1.0.0", "1.0.0"));
REQUIRE(loaded.save_vendor_cache(cache2.string(), "Acme", "1.0.0", "1.0.0")); REQUIRE(loaded.save_vendor_cache(cache2.string(), "Acme", "1.0.0", "1.0.0"));
auto slurp = [](const fs::path& p) {
std::ifstream ifs(p.string(), std::ios::binary);
return std::string(std::istreambuf_iterator<char>(ifs), {});
};
REQUIRE(slurp(cache1) == slurp(cache2)); REQUIRE(slurp(cache1) == slurp(cache2));
} }
@@ -1023,11 +1023,7 @@ TEST_CASE("a cache that fails mid-body deserialization is rejected and leaves th
PresetBundle clean; PresetBundle clean;
REQUIRE(out.save_vendor_cache(out_after.string(), vid, "1.0.0","1.0.0")); REQUIRE(out.save_vendor_cache(out_after.string(), vid, "1.0.0","1.0.0"));
REQUIRE(clean.save_vendor_cache(clean_ref.string(), vid, "1.0.0","1.0.0")); REQUIRE(clean.save_vendor_cache(clean_ref.string(), vid, "1.0.0","1.0.0"));
auto slurp2 = [](const fs::path& p) { CHECK(slurp(out_after) == slurp(clean_ref));
std::ifstream ifs(p.string(), std::ios::binary);
return std::string(std::istreambuf_iterator<char>(ifs), {});
};
CHECK(slurp2(out_after) == slurp2(clean_ref));
// The recovery must leave a bundle a caller can still load a good cache into. // The recovery must leave a bundle a caller can still load a good cache into.
REQUIRE(out.load_vendor_cache(valid_cache.string(), vid, "1.0.0","1.0.0")); REQUIRE(out.load_vendor_cache(valid_cache.string(), vid, "1.0.0","1.0.0"));