Keep User Filament Presets on Printers Their Parent Listed in 2.4.2 (#16367)

* Keep User Filament Presets on Printers Their Parent Listed in 2.4.2

User presets store only their diff, so they inherit the parent's
compatible_printers. Profile changes since 2.4.2 renamed and narrowed many
system filament presets, so user presets made against them disappear from
printers they were made for and the saved selection silently falls back to
another filament at startup.

orca_profile_tool.py legacy-compat writes, per current system filament
preset, the printers it covered in an older release and no longer lists,
kept only where a preset with the same filament_id still covers the printer
and they share an extruder variant. The loader gives these printers to user
presets that inherit their parent's list, without writing any file; saving
a preset detached, cloned or with its own list drops them.

* Ask Before Migrating User Filament Presets to Their Legacy Printers

Instead of giving user presets their parent's 2.4.2 printers in memory,
detect the user presets that inherit their parent's compatible_printers
and are missing printers from the legacy map, and offer to migrate them
at startup or from Help > Migrate Filament Presets.

Migrating first backs up the user folder to
filament_preset_migration_<datetime>_backup.zip in the configuration
folder, and migrates nothing if that fails. It then writes the printers
into each preset's compatible_printers, patching only that key so the
preset keeps the parent name it was saved with and older releases can
still resolve it. Cloud-synced presets are marked for upload.

No asks again at the next launch; "Don't remind me again" stops the
startup prompt.

* Key the legacy filament preset map by the parent's 2.4.2 name

* Check the legacy filament preset map in CI

* Limit the 2.4.2 filament preset migration to presets that need it

Only presets that still name their parent as 2.4.2 did and have no printer
list of their own are offered, and a preset changed after it was found is
left alone. The startup prompt appears once, after upgrading from 2.4.2 or
earlier, as a standard dialog that keeps "Don't remind me again"; Help >
Migrate Filament Presets is unchanged. Migrating no longer overwrites an
unsaved printer-list edit or marks the project modified.

* Ask on Every Start and Take the User Presets Lock Once When Migrating Filament Presets

The startup prompt no longer depends on the version the previous run
recorded: whoever still has user filament presets that lost printers is
asked, including users already on a 2.5.0 nightly or alpha. Answering No
asks again at the next start; "Don't remind me again" stops it.

Migration takes the user presets lock once, around the backup and every
file write, instead of once per preset. A lock another instance holds is
reported once ("another OrcaSlicer instance is saving presets") with
nothing written, rather than as one failed preset after another once the
lock's cool-down kicks in. Detection records each preset's file and the
list it will get, so the writes need no collection lock while the user
presets lock is held, the order InstanceLock requires; the loaded presets
are updated afterwards.

* Run the Legacy Compatible Printers Map Check in check_profile.sh and .ps1

CI's "Check profiles" job checks resources/info/legacy_compatible_printers.json
against the v2.4.2 profiles, and its failure message points at these scripts
as the local reproduction, so they run the same check: legacy_compat, right
after profile_tool. The v2.4.2 tree comes from git archive of the tag, is
cached in the work dir like the other downloads, re-extracted with --refresh,
and fetched from upstream first when the checkout lacks the tag.

* Back Up Before the User Presets Lock and Merge Migrated Printers Into the Live List

The lock guard is meant for a few file operations, and zipping a large user
folder is not one, so the backup is taken before it and the guard covers the
writes only; each write re-reads its file, so a save that slipped in before
is still respected.

The in-memory update adds the lost printers to the list the preset holds at
that moment rather than replacing it with the one found at detection, and
leaves a pending cloud sync state alone: a download waiting to be saved kept
its newer list and its "save" flag, which "update" would have cancelled.

The lock message no longer blames another instance alone, since the lock is
also refused for a while after a miss and when the lock file cannot be
opened.

---------

Co-authored-by: SoftFever <softfeverever@gmail.com>
This commit is contained in:
HanifKoh
2026-10-14 00:02:34 +08:00
committed by GitHub
co-authored by SoftFever
parent a0a4a33c72
commit 5c8a287b5e
16 changed files with 1753 additions and 14 deletions
+113
View File
@@ -82,6 +82,7 @@
#include "LifecycleEvents.hpp"
#include "Utils.hpp"
#include "InstanceLock.hpp"
#include "nlohmann/json.hpp"
#include <sstream>
#include "PlaceholderParser.hpp"
@@ -2758,6 +2759,118 @@ void PresetCollection::update_after_user_presets_loaded()
return;
}
// A user preset file as written: it holds only the keys the preset sets itself, and an inherits that names a
// renamed parent by the old name the preset was made against. Throws if the file can not be read or parsed.
static json read_user_preset_file(const std::string &file)
{
boost::nowide::ifstream in(file);
return json::parse(in);
}
std::vector<LegacyUserFilamentPreset> PresetCollection::legacy_user_filament_presets(const std::map<std::string, std::vector<std::string>> &printers_by_parent)
{
auto has_entry = [&printers_by_parent](const std::string &name) { return printers_by_parent.count(name) > 0; };
std::vector<LegacyUserFilamentPreset> legacy_presets;
// The preset sync thread can add or delete presets meanwhile.
lock();
for (const Preset &preset : m_presets) {
if (!preset.is_user() || preset.inherits().empty())
continue;
const Preset *parent = get_preset_parent(preset);
if (parent == nullptr || !parent->is_system)
continue;
// An empty list already covers every printer.
const std::vector<std::string> &own = preset.config.option<ConfigOptionStrings>("compatible_printers")->values;
if (own.empty())
continue;
// The parent's name in the older release is its current name or one in its renamed_from, so only the
// files of presets whose parent has an entry under one of those are read.
if (!has_entry(parent->name) && std::none_of(parent->renamed_from.begin(), parent->renamed_from.end(), has_entry))
continue;
LegacyUserFilamentPreset legacy{preset.name, parent->name, {}, {}, preset.file, own};
try {
const json j = read_user_preset_file(preset.file);
// A preset whose file sets compatible_printers, even to an empty list, was given its printers on
// purpose and keeps exactly those.
if (j.contains("compatible_printers"))
continue;
legacy.inherits = j.value(BBL_JSON_KEY_INHERITS, std::string());
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": can not read " << preset.file << ": " << err.what();
continue;
}
auto it = printers_by_parent.find(legacy.inherits);
if (it == printers_by_parent.end())
continue;
for (const std::string &printer : it->second)
if (std::find(own.begin(), own.end(), printer) == own.end()) {
legacy.printers.push_back(printer);
legacy.compatible_printers.push_back(printer);
}
if (!legacy.printers.empty())
legacy_presets.push_back(std::move(legacy));
}
unlock();
return legacy_presets;
}
bool PresetCollection::write_legacy_user_filament_preset(const LegacyUserFilamentPreset &legacy)
{
// Only compatible_printers changes in the file. Saving the whole preset would also rewrite an inherits that
// names a renamed parent by its old name, the only name older releases know it by.
try {
json j = read_user_preset_file(legacy.file);
// Changed by hand or by another instance since the preset was found: a list of its own stands, and
// another parent would need other printers.
if (j.contains("compatible_printers") || j.value(BBL_JSON_KEY_INHERITS, std::string()) != legacy.inherits)
return false;
j["compatible_printers"] = legacy.compatible_printers;
if (const std::error_code ec = write_file_atomically(legacy.file, j.dump(1, '\t') + "\n")) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to save " << legacy.file << ": " << ec.message();
return false;
}
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not migrate " << legacy.file << ": " << err.what();
return false;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": user preset " << legacy.preset << " gets " << legacy.printers.size()
<< " printer(s) its parent listed in an older release";
return true;
}
void PresetCollection::apply_legacy_user_filament_presets(const std::vector<LegacyUserFilamentPreset> &written)
{
lock();
for (const LegacyUserFilamentPreset &legacy : written) {
auto it = this->find_preset_internal(legacy.preset);
if (it == m_presets.end() || it->name != legacy.preset)
continue; // deleted by the preset sync thread meanwhile
std::vector<std::string> &saved = it->config.option<ConfigOptionStrings>("compatible_printers", true)->values;
std::vector<std::string> &edited = m_edited_preset.config.option<ConfigOptionStrings>("compatible_printers", true)->values;
// Added to the list the preset holds now rather than replacing it: the cloud sync may have given the
// preset a newer list since it was found, and that list is still to be written out.
std::vector<std::string> printers = saved;
for (const std::string &printer : legacy.printers)
if (std::find(printers.begin(), printers.end(), printer) == printers.end())
printers.push_back(printer);
// A change to the list pending in the settings tab stays pending. Without one, the edited copy and the
// snapshot unsaved changes are measured against take the written list, so nothing reads as modified.
if (m_idx_selected == static_cast<size_t>(it - m_presets.begin()) && edited == saved) {
edited = printers;
if (m_saved_preset.name == it->name)
m_saved_preset.config.option<ConfigOptionStrings>("compatible_printers", true)->values = printers;
}
saved = printers;
// Push the change to the cloud copy, as an edit in the settings tab would. A sync already pending for
// the preset (a download waiting to be saved, a deletion) keeps its state.
if (!it->setting_id.empty() && it->sync_info.empty()) {
it->sync_info = "update";
it->save_info();
}
}
unlock();
}
//BBS: validate_preset
bool PresetCollection::validate_preset(const std::string &preset_name, std::string &inherit_name)
{
+26
View File
@@ -551,6 +551,20 @@ struct PresetConfigSubstitutions {
// PrusaSlicer and reading the user Print / Filament / Printer profiles.
using PresetsConfigSubstitutions = std::vector<PresetConfigSubstitutions>;
// A user filament preset made against an older release that no longer shows on printers its system parent
// listed then, and those printers. Migrating it writes them into the preset's own compatible_printers.
struct LegacyUserFilamentPreset {
std::string preset;
std::string parent;
// The parent's name as the preset file states it: the name the older release knew it by.
std::string inherits;
// The printers the preset lost: what the prompt lists.
std::vector<std::string> printers;
std::string file;
// The list the file gets: the parent's current printers plus the lost ones.
std::vector<std::string> compatible_printers;
};
// Collections of presets of the same type (one of the Print, Filament or Printer type).
class PresetCollection
{
@@ -601,6 +615,18 @@ public:
void save_user_presets(const std::string& dir_path, const std::string& type, std::map<std::string, std::string>& need_to_delete_list);
bool load_user_preset(std::string name, std::map<std::string, std::string> preset_values, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule, const PresetOrigin &load_origin = PresetOrigin(PresetOrigin::Kind::User));
void update_after_user_presets_loaded();
// User presets store only their diff, so a user preset inherits its system parent's compatible_printers
// and disappears from a printer once a profile update narrows that list. Lists the user presets whose
// files do not set compatible_printers, and the printers their parent covered in an older release but
// no longer lists. printers_by_parent is keyed by the parent's name in that release, which is the name
// the preset file's inherits still holds.
std::vector<LegacyUserFilamentPreset> legacy_user_filament_presets(const std::map<std::string, std::vector<std::string>> &printers_by_parent);
// Writes legacy.compatible_printers into the preset's file and keeps every other key. The caller holds the
// user presets lock. False if the file has gained a compatible_printers key or another parent since the
// preset was found, or could not be written.
static bool write_legacy_user_filament_preset(const LegacyUserFilamentPreset &legacy);
// Gives the loaded presets the lists their files were just given, and marks cloud copies for upload.
void apply_legacy_user_filament_presets(const std::vector<LegacyUserFilamentPreset> &written);
//BBS: get user presets
int get_user_presets(PresetBundle *preset_bundle, std::vector<Preset> &result_presets);
void set_sync_info_and_save(std::string name, std::string setting_id, std::string syncinfo, long long update_time, const std::string& user_id);
+85
View File
@@ -2,6 +2,7 @@
#include <boost/filesystem/path.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/directory.hpp>
#include <boost/filesystem/file_status.hpp>
#include <boost/uuid/uuid.hpp>
#include <boost/optional/optional.hpp>
#include <boost/algorithm/string/split.hpp>
@@ -38,6 +39,8 @@
#include "ParallelResolve.hpp"
#include "Preset.hpp"
#include "PresetCacheFormat.hpp"
#include "Time.hpp"
#include "Zipper.hpp"
#include "PrintConfig.hpp"
#include "PublishSettings.hpp"
#include "FilamentMixer.hpp"
@@ -96,6 +99,21 @@ using json = nlohmann::json;
namespace Slic3r {
// Per system filament preset of an older release, by its name then, the printers it listed that the preset it
// now loads as does not (resources/info/legacy_compatible_printers.json, written by orca_profile_tool.py
// legacy-compat). Empty if the file is missing or malformed.
static std::map<std::string, std::vector<std::string>> legacy_compatible_printers()
{
const fs::path path = fs::path(resources_dir()) / "info" / "legacy_compatible_printers.json";
try {
boost::nowide::ifstream in(path.string());
return json::parse(in).at("presets").get<std::map<std::string, std::vector<std::string>>>();
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not read " << path.string() << ": " << err.what();
return {};
}
}
// Project-level options imported from a loaded 3MF into project_config. s_project_options_published
// below is the reduced subset that crosses over in "published" 3MF mode; keep both in sync.
// s_project_options_published additionally carries wipe_tower_rotation_angle, which normal
@@ -1045,6 +1063,73 @@ bool PresetBundle::backup_user_folder() const
}
}
bool PresetBundle::backup_user_folder_to_zip(const std::string &zip_path) const
{
const fs::path user_dir = fs::path(data_dir()) / PRESET_USER_DIR;
BOOST_LOG_TRIVIAL(info) << "Backing up user folder to: " << zip_path;
try {
Zipper zipper(zip_path);
// Directories get their own entries, which extract_archive_confined() needs to recreate them.
zipper.add_entry(std::string(PRESET_USER_DIR) + "/", nullptr, 0);
// The loader follows symlinked folders, and migration writes the presets in them.
for (const fs::directory_entry &entry : fs::recursive_directory_iterator(user_dir, fs::directory_options::follow_directory_symlink)) {
const std::string name = (fs::path(PRESET_USER_DIR) / entry.path().lexically_relative(user_dir)).generic_string();
if (fs::is_directory(entry.status())) {
zipper.add_entry(name + "/", nullptr, 0);
continue;
}
if (!fs::is_regular_file(entry.status()))
continue;
boost::nowide::ifstream in(entry.path().string(), std::ios::binary);
const std::string data((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
if (!in.good() && !in.eof())
throw Slic3r::RuntimeError("can not read " + entry.path().string());
zipper.add_entry(name, data.data(), data.size());
}
zipper.finalize();
return true;
} catch (const std::exception &ex) {
BOOST_LOG_TRIVIAL(error) << "Exception during user folder backup: " << ex.what();
boost::system::error_code ec;
fs::remove(zip_path, ec);
return false;
}
}
std::vector<LegacyUserFilamentPreset> PresetBundle::legacy_user_filament_presets()
{
return filaments.legacy_user_filament_presets(legacy_compatible_printers());
}
PresetBundle::LegacyUserFilamentPresetMigration PresetBundle::migrate_legacy_user_filament_presets(const std::vector<LegacyUserFilamentPreset> &legacy_presets)
{
LegacyUserFilamentPresetMigration result;
std::vector<LegacyUserFilamentPreset> written;
const std::string zip_path = data_dir() + "/filament_preset_migration_" +
Utils::time2str(Utils::TimeZone::utc, Utils::TimeFormat::iso8601Z) + "_backup.zip";
// Before the lock: zipping a large folder takes longer than a guard may be held.
if (!backup_user_folder_to_zip(zip_path))
return result;
result.backup = zip_path;
{
// One hold for every write, so a lock another instance keeps is one outcome rather than a failure per
// preset (the lock backs off for a while after a miss). Held without the collection mutex, the order
// InstanceLock requires; each write re-reads its file, so a save that slipped in before is respected.
const std::string lock_path = user_presets_lock_path();
InstanceLock instance_lock(lock_path);
if (!lock_path.empty() && !instance_lock.locked())
return result;
result.locked = true;
for (const LegacyUserFilamentPreset &legacy : legacy_presets)
if (PresetCollection::write_legacy_user_filament_preset(legacy))
written.push_back(legacy);
else
result.failed.push_back(legacy.preset);
}
filaments.apply_legacy_user_filament_presets(written);
return result;
}
std::optional<FilamentBaseInfo> PresetBundle::get_filament_by_filament_id(const std::string& filament_id, const std::string& printer_name) const
{
if (filament_id.empty())
+15
View File
@@ -376,6 +376,21 @@ public:
bool use_bbl_device_tab();
bool backup_user_folder() const;
// Writes the user preset folder into a zip archive at zip_path, its entries under "user/".
bool backup_user_folder_to_zip(const std::string &zip_path) const;
// User filament presets made against an older release that no longer show on printers their parent
// listed then (resources/info/legacy_compatible_printers.json).
std::vector<LegacyUserFilamentPreset> legacy_user_filament_presets();
// Backs up the user preset folder into a zip in data_dir(), then writes each preset's printers into its file
// under one hold of the user presets lock. Nothing is written when the backup failed (backup empty) or the
// lock could not be taken (locked false); otherwise failed names the presets whose files could not be written.
struct LegacyUserFilamentPresetMigration {
bool locked{false};
std::string backup;
std::vector<std::string> failed;
};
LegacyUserFilamentPresetMigration migrate_legacy_user_filament_presets(const std::vector<LegacyUserFilamentPreset> &legacy_presets);
//BBS: project embedded preset logic
PresetsConfigSubstitutions load_project_embedded_presets(std::vector<Preset*> project_presets, ForwardCompatibilitySubstitutionRule substitution_rule);
+67
View File
@@ -1109,6 +1109,8 @@ void GUI_App::post_init()
}
this->check_new_version_sf();
if (!cw_showed)
this->check_legacy_user_filament_presets(false);
const auto cloud_provider = get_printer_cloud_provider();
if (is_user_login(cloud_provider) && !app_config->get_stealth_mode()) {
// this->check_privacy_version(0);
@@ -6817,6 +6819,71 @@ void GUI_App::reload_settings()
}
}
void GUI_App::check_legacy_user_filament_presets(bool by_user)
{
if (!by_user && app_config->get_bool("legacy_user_filament_presets_dont_remind"))
return;
const std::vector<LegacyUserFilamentPreset> legacy_presets = preset_bundle->legacy_user_filament_presets();
if (legacy_presets.empty()) {
if (by_user)
MessageDialog(mainframe, _L("All your filament presets are available on the printers they were made for."),
_L("Migrate Filament Presets"), wxOK | wxICON_INFORMATION).ShowModal();
return;
}
wxString message = _L("OrcaSlicer 2.5.0 restructured many system filament presets to fix incorrect profiles and prepare "
"for future features. As a result, the presets below no longer show on some printers they were made "
"for. Sorry for the trouble; this is a one-time fix.\n\n"
"Add those printers back to your presets now? Your presets are backed up first. You can also do this "
"later from Help > Migrate Filament Presets.") + "\n";
for (const LegacyUserFilamentPreset &legacy : legacy_presets) {
message += "\n" + format_wxstr(_L("%1% (based on %2%)"), legacy.preset, legacy.parent);
for (const std::string &printer : legacy.printers)
message += "\n " + from_u8(printer);
}
RichMessageDialog dlg(mainframe, message, _L("Migrate Filament Presets"), wxYES | wxNO | wxICON_QUESTION);
if (!by_user)
dlg.ShowCheckBox(_L("Don't remind me again"));
const bool migrate = dlg.ShowModal() == wxID_YES;
if (dlg.IsCheckBoxChecked())
app_config->set_bool("legacy_user_filament_presets_dont_remind", true);
if (!migrate)
return;
const PresetBundle::LegacyUserFilamentPresetMigration migration = preset_bundle->migrate_legacy_user_filament_presets(legacy_presets);
const std::string &backup = migration.backup;
const std::vector<std::string> &failed = migration.failed;
if (backup.empty()) {
show_error(mainframe, _L("Your presets could not be backed up, so they were not migrated."));
return;
}
if (!migration.locked) {
// The lock is also refused for a while after a miss, and when the lock file itself cannot be opened.
show_error(mainframe, _L("The user presets folder could not be locked, so nothing was migrated. Another OrcaSlicer "
"instance may be saving presets; try again later from Help > Migrate Filament Presets."));
return;
}
if (failed.size() == legacy_presets.size()) {
show_error(mainframe, format_wxstr(_L("Your filament presets could not be migrated. A backup of your presets was saved to:\n%1%"), backup));
return;
}
preset_bundle->update_compatible(PresetSelectCompatibleType::Never);
mainframe->update_side_preset_ui();
for (auto tab : tabs_list) {
tab->reload_config();
tab->update_changed_ui();
}
message = format_wxstr(_L("Your filament presets were migrated. Select them again in the filament list where you used them.\n\n"
"A backup of your presets was saved to:\n%1%"), backup);
if (!failed.empty()) {
message += "\n\n" + _L("These presets could not be migrated:");
for (const std::string &name : failed)
message += "\n" + from_u8(name);
}
MessageDialog(mainframe, message, _L("Migrate Filament Presets"), wxOK | (failed.empty() ? wxICON_INFORMATION : wxICON_WARNING)).ShowModal();
}
//BBS reload when logout
void GUI_App::remove_user_presets()
{
+4
View File
@@ -567,6 +567,10 @@ public:
void check_update(bool show_tips, int by_user);
void check_new_version(bool show_tips = false, int by_user = 0);
void check_new_version_sf(bool show_tips = false, int by_user = 0);
// Offers to migrate user filament presets made against an older release that lost printers their parent
// listed then. At startup it asks on every start that finds such presets, unless the user chose not to be
// reminded; by_user also reports when there is nothing to migrate.
void check_legacy_user_filament_presets(bool by_user);
bool process_network_msg(std::string dev_id, std::string msg);
void request_new_version(int by_user);
void enter_force_upgrade();
+4
View File
@@ -2828,6 +2828,10 @@ wxMenu* MainFrame::generate_help_menu()
append_menu_item(helpMenu, wxID_ANY, _L("Show Configuration Folder"), _L("Show Configuration Folder"),
[](wxCommandEvent&) { Slic3r::GUI::desktop_open_datadir_folder(); });
append_menu_item(helpMenu, wxID_ANY, _L("Migrate Filament Presets"),
_L("Bring back filament presets that a profile update removed from printers they were made for"),
[](wxCommandEvent&) { wxGetApp().check_legacy_user_filament_presets(true); });
helpMenu->AppendSeparator();
// Troubleshoot center
+4
View File
@@ -589,6 +589,10 @@ std::vector<NativeCommand> build_command_catalog()
Slic3r::GUI::desktop_open_datadir_folder();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("help_migrate_filament_presets", L("Migrate Filament Presets"), L("Help"), [](const std::string&) {
wxGetApp().check_legacy_user_filament_presets(true);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("help_troubleshoot", L("Troubleshoot Center"), L("Help"), [](const std::string&) {
wxGetApp().troubleshoot();
return AppActionRunResult{AppActionRunResult::Level::Success};