fix: collapse plugincapabilityid

This commit is contained in:
Ian Chua
2026-07-16 18:18:07 +08:00
parent b6f98d9592
commit f4414dd72b
14 changed files with 361 additions and 428 deletions

View File

@@ -26,6 +26,7 @@ namespace {
constexpr const char* KEY_PLUGIN = "plugin_key";
constexpr const char* KEY_CAPABILITY = "capability";
constexpr const char* KEY_TYPE = "capability_type";
constexpr const char* KEY_VERSION = "plugin_version";
constexpr const char* KEY_CAP_CONFIG = "cap_config";
@@ -35,23 +36,24 @@ std::string string_field(const nlohmann::json& entry, const char* key)
return it != entry.end() && it->is_string() ? it->get<std::string>() : std::string();
}
bool is_recognized_entry(const nlohmann::json& entry, CapabilityConfigId& id)
bool is_recognized_entry(const nlohmann::json& entry, PluginCapabilityId& id)
{
if (!entry.is_object())
return false;
id.plugin_key = string_field(entry, KEY_PLUGIN);
id.capability = string_field(entry, KEY_CAPABILITY);
return !id.plugin_key.empty() && !id.capability.empty();
id.name = string_field(entry, KEY_CAPABILITY);
id.type = plugin_capability_type_from_string(string_field(entry, KEY_TYPE));
return !id.empty();
}
CapabilityConfigEntry decode_entry(const CapabilityConfigId& id, const nlohmann::json& entry)
CapabilityConfigEntry decode_entry(const PluginCapabilityId& id, const nlohmann::json& entry)
{
CapabilityConfigEntry result;
result.id = id;
result.plugin_version = string_field(entry, KEY_VERSION);
const auto cap_it = entry.find(KEY_CAP_CONFIG);
result.cap_config = cap_it != entry.end() ? *cap_it : nlohmann::json::object();
result.config = cap_it != entry.end() ? *cap_it : nlohmann::json::object();
return result;
}
@@ -65,11 +67,6 @@ std::string running_plugin_version(const std::string& plugin_key)
return descriptor.installed_version.empty() ? descriptor.version : descriptor.installed_version;
}
CapabilityConfigId make_id(const PluginCapabilityId& id)
{
return CapabilityConfigId{id.plugin_key, id.name};
}
// Null wherever the plugin host runs without the GUI app (the unit tests). wxGetApp() dereferences
// the app unconditionally, so ask wxWidgets instead.
const PresetBundle* active_preset_bundle()
@@ -81,31 +78,15 @@ const PresetBundle* active_preset_bundle()
// PluginLoader stamps both halves onto the instance when it materializes the capability, so the
// caller never supplies them and cannot name another capability's entry. Empty means the instance
// was never materialized: refuse rather than read or clobber a wrong entry.
std::pair<std::string, std::string> capability_identity(const PluginCapabilityInterface& capability, const char* api_name)
PluginCapabilityId capability_identity(const PluginCapabilityInterface& capability, const char* api_name)
{
std::pair<std::string, std::string> id{capability.audit_plugin_key(), capability.name()};
if (id.first.empty() || id.second.empty())
const PluginCapabilityId id = capability.identity();
if (id.empty())
throw std::runtime_error(std::string(api_name) + "() is only available on a capability loaded by the plugin host");
return id;
}
// The capability type identifies the preset that owns its override. If a plugin raises while
// reporting its type, resolve against the base config instead of failing the capability config API.
PluginCapabilityId capability_full_identity(const PluginCapabilityInterface& capability, const char* api_name)
{
const auto [plugin_key, capability_name] = capability_identity(capability, api_name);
PluginCapabilityType type = PluginCapabilityType::Unknown;
try {
type = capability.get_type();
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning) << "Capability '" << capability_name << "' of plugin '" << plugin_key
<< "': get_type() failed (" << ex.what() << "); reading the base config only";
}
return {type, capability_name, plugin_key};
}
} // namespace
CapabilityConfigDocument CapabilityConfigDocument::from_entries(const nlohmann::json& entries)
@@ -115,8 +96,8 @@ CapabilityConfigDocument CapabilityConfigDocument::from_entries(const nlohmann::
return document;
for (const nlohmann::json& entry : entries) {
CapabilityConfigId id;
if (is_recognized_entry(entry, id))
PluginCapabilityId id;
if (is_recognized_entry(entry, id) || (!id.plugin_key.empty() && !id.name.empty()))
document.m_entries[id] = entry;
else
document.m_opaque_entries.push_back(entry);
@@ -131,41 +112,56 @@ CapabilityConfigDocument CapabilityConfigDocument::from_root_json(const nlohmann
return entries != root.end() ? from_entries(*entries) : CapabilityConfigDocument();
}
std::optional<CapabilityConfigEntry> CapabilityConfigDocument::find(const CapabilityConfigId& id) const
std::optional<CapabilityConfigEntry> CapabilityConfigDocument::find(const PluginCapabilityId& id) const
{
const auto it = m_entries.find(id);
if (it == m_entries.end())
return std::nullopt;
return decode_entry(it->first, it->second);
if (it != m_entries.end())
return decode_entry(it->first, it->second);
// Legacy config.json entries have no capability type. Keep them addressable by the new
// typed API until that capability is saved again.
if (id.type != PluginCapabilityType::Unknown) {
const auto legacy = m_entries.find({PluginCapabilityType::Unknown, id.name, id.plugin_key});
if (legacy != m_entries.end())
return decode_entry(id, legacy->second);
}
return std::nullopt;
}
bool CapabilityConfigDocument::contains(const CapabilityConfigId& id) const
bool CapabilityConfigDocument::contains(const PluginCapabilityId& id) const
{
return m_entries.find(id) != m_entries.end();
return find(id).has_value();
}
bool CapabilityConfigDocument::upsert(CapabilityConfigEntry entry)
{
if (entry.id.plugin_key.empty() || entry.id.capability.empty())
if (entry.id.empty())
return false;
if (entry.id.type != PluginCapabilityType::Unknown)
m_entries.erase({PluginCapabilityType::Unknown, entry.id.name, entry.id.plugin_key});
nlohmann::json serialized = nlohmann::json::object();
const auto existing = m_entries.find(entry.id);
if (existing != m_entries.end() && existing->second.is_object())
serialized = existing->second;
serialized[KEY_PLUGIN] = entry.id.plugin_key;
serialized[KEY_CAPABILITY] = entry.id.capability;
serialized[KEY_CAPABILITY] = entry.id.name;
serialized[KEY_TYPE] = plugin_capability_type_to_string(entry.id.type);
serialized[KEY_VERSION] = entry.plugin_version;
serialized[KEY_CAP_CONFIG] = entry.cap_config;
serialized[KEY_CAP_CONFIG] = entry.config;
m_entries[entry.id] = std::move(serialized);
return true;
}
bool CapabilityConfigDocument::erase(const CapabilityConfigId& id)
bool CapabilityConfigDocument::erase(const PluginCapabilityId& id)
{
return m_entries.erase(id) != 0;
bool erased = m_entries.erase(id) != 0;
if (id.type != PluginCapabilityType::Unknown)
erased = m_entries.erase({PluginCapabilityType::Unknown, id.name, id.plugin_key}) != 0 || erased;
return erased;
}
bool CapabilityConfigDocument::empty() const
@@ -260,39 +256,34 @@ bool PluginConfig::save()
return true;
}
void PluginConfig::save_config(const std::string& plugin_key,
const std::string& capability_name,
const std::string& version,
const nlohmann::json& config)
{ save_config({plugin_key, capability_name, version, config}); }
bool PluginConfig::store_capability_config(const std::string& plugin_key,
const std::string& capability_name,
const nlohmann::json& config)
bool PluginConfig::store_capability_config(const PluginCapabilityId& id, const nlohmann::json& config)
{
save_config({plugin_key, capability_name, running_plugin_version(plugin_key), config});
if (id.empty())
return false;
save_config({id, running_plugin_version(id.plugin_key), config});
return save();
}
void PluginConfig::save_config(const BaseConfig& config)
void PluginConfig::save_config(const CapabilityConfigEntry& config)
{
if (config.empty()) {
BOOST_LOG_TRIVIAL(error) << "PluginConfig: refusing to store a config without a plugin key and capability name";
if (config.id.empty()) {
BOOST_LOG_TRIVIAL(error) << "PluginConfig: refusing to store a config without a complete capability identity";
return;
}
std::lock_guard<std::mutex> lock(m_mutex);
m_dirty = m_document.upsert(CapabilityConfigEntry{{config.plugin_key, config.capability_name}, config.plugin_version, config.config}) || m_dirty;
m_dirty = m_document.upsert(config) || m_dirty;
}
bool PluginConfig::erase_capability_config(const std::string& plugin_key, const std::string& capability_name)
bool PluginConfig::erase_capability_config(const PluginCapabilityId& id)
{
if (plugin_key.empty() || capability_name.empty())
if (id.empty())
return false;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_document.erase({plugin_key, capability_name}))
if (!m_document.erase(id))
return true;
m_dirty = true;
}
@@ -300,19 +291,16 @@ bool PluginConfig::erase_capability_config(const std::string& plugin_key, const
return save();
}
BaseConfig PluginConfig::get_config(const std::string& plugin_key, const std::string& capability_name) const
std::optional<CapabilityConfigEntry> PluginConfig::get_config(const PluginCapabilityId& id) const
{
std::lock_guard<std::mutex> lock(m_mutex);
const auto entry = m_document.find({plugin_key, capability_name});
if (!entry)
return BaseConfig();
return BaseConfig{entry->id.plugin_key, entry->id.capability, entry->plugin_version, entry->cap_config};
return m_document.find(id);
}
bool PluginConfig::has_config(const std::string& plugin_key, const std::string& capability_name) const
bool PluginConfig::has_config(const PluginCapabilityId& id) const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_document.contains({plugin_key, capability_name});
return m_document.contains(id);
}
bool PluginConfig::dirty() const
@@ -358,22 +346,22 @@ EffectiveCapabilityConfig PresetPluginConfigService::get_effective_config(const
const PluginCapabilityId& id) const
{
EffectiveCapabilityConfig result;
result.id = make_id(id);
result.id = id;
result.running_plugin_version = running_plugin_version(id.plugin_key);
const BaseConfig base = PluginManager::instance().get_config().get_config(id.plugin_key, id.name);
result.has_base_config = !base.empty();
const auto base = PluginManager::instance().get_config().get_config(id);
result.has_base_config = base.has_value();
if (const auto entry = overrides.find(result.id)) {
result.has_preset_override = true;
result.config = entry->cap_config;
result.config = entry->config;
result.stored_plugin_version = entry->plugin_version;
return result;
}
if (result.has_base_config) {
result.config = base.config;
result.stored_plugin_version = base.plugin_version;
result.config = base->config;
result.stored_plugin_version = base->plugin_version;
}
return result;
}
@@ -382,20 +370,19 @@ MutationResult PresetPluginConfigService::set_preset_override(CapabilityConfigDo
const PluginCapabilityId& id,
const nlohmann::json& value) const
{
MutationResult result;
const CapabilityConfigId config_id = make_id(id);
const std::string version = running_plugin_version(id.plugin_key);
MutationResult result;
const std::string version = running_plugin_version(id.plugin_key);
// A no-op is a successful unchanged result: re-saving the displayed value must not mark the
// preset dirty.
const auto existing = overrides.find(config_id);
if (existing && existing->cap_config == value && existing->plugin_version == version) {
const auto existing = overrides.find(id);
if (existing && existing->config == value && existing->plugin_version == version) {
result.ok = true;
result.effective = get_effective_config(overrides, id);
return result;
}
overrides.upsert({config_id, version, value});
overrides.upsert({id, version, value});
result.ok = true;
result.changed = true;
@@ -408,7 +395,7 @@ MutationResult PresetPluginConfigService::remove_preset_override(CapabilityConfi
{
MutationResult result;
result.ok = true;
result.changed = overrides.erase(make_id(id));
result.changed = overrides.erase(id);
result.effective = get_effective_config(overrides, id);
return result;
}
@@ -458,21 +445,19 @@ nlohmann::json capability_get_config(const PluginCapabilityInterface& capability
{
// Shares its resolution with the dialogs, so a capability reads back exactly the config its UI
// showed as effective. Stored in neither layer: an empty object, indexable unconditionally.
return active_capability_config(capability_full_identity(capability, "get_config")).config;
return active_capability_config(capability_identity(capability, "get_config")).config;
}
std::string capability_get_config_version(const PluginCapabilityInterface& capability)
{
// Must resolve through the same layer as get_config(), or a plugin would migrate one layer's
// config by another's version stamp.
return active_capability_config(capability_full_identity(capability, "get_config_version")).stored_plugin_version;
return active_capability_config(capability_identity(capability, "get_config_version")).stored_plugin_version;
}
bool capability_save_config(const PluginCapabilityInterface& capability, const nlohmann::json& config)
{
const auto [plugin_key, capability_name] = capability_identity(capability, "save_config");
return PluginManager::instance().get_config().store_capability_config(plugin_key, capability_name, config);
return PluginManager::instance().get_config().store_capability_config(capability_identity(capability, "save_config"), config);
}
nlohmann::json PluginConfig::capabilities_payload(const std::vector<PluginCapabilityId>& caps)
@@ -480,7 +465,7 @@ nlohmann::json PluginConfig::capabilities_payload(const std::vector<PluginCapabi
nlohmann::json payload = nlohmann::json::array();
for (const PluginCapabilityId& id : caps) {
// A capability unloaded since the list was built has nothing to configure.
const auto capability = PluginManager::instance().get_plugin_capability(id.plugin_key, id.name, id.type, false);
const auto capability = PluginManager::instance().get_plugin_capability(id, false);
if (!capability)
continue;
@@ -510,7 +495,7 @@ nlohmann::json PluginConfig::get_config_response(const PluginCapabilityId& id)
// Scoped to the full identity, so a stale request from a page that has not caught up with a
// refresh misses rather than reading a different plugin's config.
const auto cap = PluginManager::instance().get_plugin_capability(id.plugin_key, id.name, id.type, false);
const auto cap = PluginManager::instance().get_plugin_capability(id, false);
if (!cap) {
BOOST_LOG_TRIVIAL(warning) << "Ignoring config request for a capability that is no longer loaded. plugin_key="
<< id.plugin_key << " capability_name=" << id.name;
@@ -518,7 +503,8 @@ nlohmann::json PluginConfig::get_config_response(const PluginCapabilityId& id)
return response;
}
response["config"] = PluginManager::instance().get_config().get_config(id.plugin_key, id.name).config;
if (const auto stored = PluginManager::instance().get_config().get_config(id))
response["config"] = stored->config;
if (cap->config_ui_available()) {
// A raising or empty get_config_ui() costs the capability only its custom UI: report the
@@ -564,7 +550,7 @@ nlohmann::json PluginConfig::save_config_response(const PluginCapabilityId& id,
response["ok"] = false;
response["error"] = "";
const auto cap = PluginManager::instance().get_plugin_capability(id.plugin_key, id.name, id.type, false);
const auto cap = PluginManager::instance().get_plugin_capability(id, false);
if (!cap) {
BOOST_LOG_TRIVIAL(warning) << "Refusing to save config for a capability that is no longer loaded. plugin_key="
<< id.plugin_key << " capability_name=" << id.name;
@@ -583,7 +569,7 @@ nlohmann::json PluginConfig::save_config_response(const PluginCapabilityId& id,
}
}
if (!PluginManager::instance().get_config().store_capability_config(id.plugin_key, id.name, parsed)) {
if (!PluginManager::instance().get_config().store_capability_config(id, parsed)) {
BOOST_LOG_TRIVIAL(error) << "Failed to write the plugin config file. plugin_key=" << id.plugin_key
<< " capability_name=" << id.name;
response["error"] = GUI::into_u8(_L("The configuration could not be written to disk. Your changes were not saved."));
@@ -594,7 +580,8 @@ nlohmann::json PluginConfig::save_config_response(const PluginCapabilityId& id,
// Echo back what was persisted, not what the user typed, so the editor reloads from the store.
response["ok"] = true;
response["config"] = PluginManager::instance().get_config().get_config(id.plugin_key, id.name).config;
if (const auto stored = PluginManager::instance().get_config().get_config(id))
response["config"] = stored->config;
return response;
}
@@ -610,7 +597,7 @@ nlohmann::json PluginConfig::restore_config_response(const PluginCapabilityId& i
response["ok"] = false;
response["error"] = "";
const auto cap = PluginManager::instance().get_plugin_capability(id.plugin_key, id.name, id.type, false);
const auto cap = PluginManager::instance().get_plugin_capability(id, false);
if (!cap) {
BOOST_LOG_TRIVIAL(warning) << "Refusing to restore config for a capability that is no longer loaded. plugin_key="
<< id.plugin_key << " capability_name=" << id.name;
@@ -643,7 +630,7 @@ nlohmann::json PluginConfig::restore_config_response(const PluginCapabilityId& i
return response;
}
if (!PluginManager::instance().get_config().store_capability_config(id.plugin_key, id.name, defaults)) {
if (!PluginManager::instance().get_config().store_capability_config(id, defaults)) {
BOOST_LOG_TRIVIAL(error) << "Failed to write the plugin config file while restoring defaults. plugin_key="
<< id.plugin_key << " capability_name=" << id.name;
response["error"] = GUI::into_u8(_L("The configuration could not be written to disk. Nothing was changed."));
@@ -654,7 +641,8 @@ nlohmann::json PluginConfig::restore_config_response(const PluginCapabilityId& i
<< " capability_name=" << id.name;
response["ok"] = true;
response["config"] = PluginManager::instance().get_config().get_config(id.plugin_key, id.name).config;
if (const auto stored = PluginManager::instance().get_config().get_config(id))
response["config"] = stored->config;
return response;
}

View File

@@ -4,44 +4,23 @@
#include <boost/filesystem.hpp>
#include <nlohmann/json.hpp>
#include <slic3r/plugin/PluginFsUtils.hpp>
#include <slic3r/plugin/PluginLoader.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <map>
#include <mutex>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#define PLUGIN_CONFIG_DIR "config.json"
namespace Slic3r {
class PluginCapabilityInterface;
class Preset;
struct PluginCapabilityId;
struct CapabilityConfigId
{
std::string plugin_key;
std::string capability;
friend bool operator<(const CapabilityConfigId& lhs, const CapabilityConfigId& rhs)
{
return lhs.plugin_key < rhs.plugin_key ||
(lhs.plugin_key == rhs.plugin_key && lhs.capability < rhs.capability);
}
friend bool operator==(const CapabilityConfigId& lhs, const CapabilityConfigId& rhs)
{
return lhs.plugin_key == rhs.plugin_key && lhs.capability == rhs.capability;
}
};
struct CapabilityConfigEntry
{
CapabilityConfigId id;
PluginCapabilityId id;
std::string plugin_version;
nlohmann::json cap_config = nlohmann::json::object();
nlohmann::json config = nlohmann::json::object();
};
class CapabilityConfigDocument
@@ -52,63 +31,28 @@ public:
static CapabilityConfigDocument from_root_json(const nlohmann::json& root);
static CapabilityConfigDocument from_entries(const nlohmann::json& entries);
std::optional<CapabilityConfigEntry> find(const CapabilityConfigId& id) const;
bool contains(const CapabilityConfigId& id) const;
std::optional<CapabilityConfigEntry> find(const PluginCapabilityId& id) const;
bool contains(const PluginCapabilityId& id) const;
bool upsert(CapabilityConfigEntry entry);
bool erase(const CapabilityConfigId& id);
bool erase(const PluginCapabilityId& id);
bool empty() const;
nlohmann::json serialize_entries() const;
nlohmann::json root_json() const;
private:
std::map<CapabilityConfigId, nlohmann::json> m_entries;
std::map<PluginCapabilityId, nlohmann::json> m_entries;
std::vector<nlohmann::json> m_opaque_entries;
};
/*
Example config.json shape
{
"config": [
{
"plugin_key": "some_name",
"capability": "capability_name",
"plugin_version": "1.0.0",
"cap_config": { "plugin": "specific", "stuff": "here" }
}
]
}
*/
struct BaseConfig {
std::string plugin_key;
std::string capability_name;
std::string plugin_version;
nlohmann::json config;
// True for the default-constructed instance returned by get_config() on a miss.
bool empty() const { return plugin_key.empty() || capability_name.empty(); }
};
// A preset keeps its plugin capability overrides as one raw JSON string in this ordinary
// ConfigOptionString, so the whole preset lifecycle — load, save, diff/dirty, inheritance, 3MF,
// sync — carries it for free. The plugin layer is the only thing that gives that string meaning.
inline constexpr const char* PLUGIN_OVERRIDES_OPTION_KEY = "plugin_preference_overrides";
// The preset's raw override text, or "" when it stores none.
std::string plugin_overrides_of(const Preset& preset);
// An empty string is a valid, empty document. Returns false and fills `error` when the text is
// present but is not a JSON array of entries; the caller then shows it and edits nothing.
bool parse_plugin_overrides(const std::string& raw, CapabilityConfigDocument& document, std::string& error);
// The document as compact JSON text, and "" once it holds no entries. Empty text — rather than a
// removed option — records "cleared here" against an inheriting parent that has overrides.
std::string serialize_plugin_overrides(const CapabilityConfigDocument& document);
struct EffectiveCapabilityConfig
{
CapabilityConfigId id;
PluginCapabilityId id;
nlohmann::json config = nlohmann::json::object();
bool has_preset_override = false;
@@ -125,11 +69,6 @@ struct MutationResult
EffectiveCapabilityConfig effective;
};
// Resolves a capability's effective config as `preset override -> base config -> none`, and mutates
// the override layer. It works on a CapabilityConfigDocument the caller owns, never on a Preset and
// never on the base config file, which keeps the two layers from writing to each other:
// PluginConfigField holds the document and feeds the edited text back through the normal field/dirty
// pipeline, so the preset is written the way every other setting is.
class PresetPluginConfigService
{
public:
@@ -142,64 +81,28 @@ public:
const PluginCapabilityId& id) const;
};
// The same resolution against the preset that is active right now, rather than a document the
// caller holds: this is what a running capability reads through the Python config API. It falls
// back to the base config when no active preset bundle is available.
EffectiveCapabilityConfig active_capability_config(const PluginCapabilityId& id);
// Store for every plugin capability's configuration, persisted as a single config.json alongside the
// installed plugins. The shape of `cap_config` belongs to the plugin; this class only round-trips it.
//
// A capability is identified by (plugin_key, capability_name). `plugin_version` records which version
// last wrote the entry, so an upgraded plugin can spot a stale config and migrate it. It is
// deliberately not part of the identity: upgrading a plugin must not reset the user's settings.
//
// Plugin code runs on worker threads, so every entry point is mutex-guarded.
class PluginConfig
{
public:
static const std::string plugin_config_file() { return (boost::filesystem::path(get_orca_plugins_dir()) / PLUGIN_CONFIG_DIR).string(); }
// A missing or malformed file leaves the store empty rather than throwing: a bad plugin config
// must not block startup.
void load();
// Rewrites config.json atomically. False means the config on disk is unchanged.
bool save();
void save_config(const std::string& plugin_key, const std::string& capability_name, const std::string& version, const nlohmann::json& config);
void save_config(const BaseConfig& config);
void save_config(const CapabilityConfigEntry& config);
// Replaces one capability's cap_config and writes config.json straight away, stamping the entry
// with the plugin version currently running. Every other entry is round-tripped untouched, so
// saving one capability cannot disturb another's config.
bool store_capability_config(const std::string& plugin_key, const std::string& capability_name, const nlohmann::json& config);
bool erase_capability_config(const std::string& plugin_key, const std::string& capability_name);
bool store_capability_config(const PluginCapabilityId& id, const nlohmann::json& config);
bool erase_capability_config(const PluginCapabilityId& id);
// A default-constructed BaseConfig (see BaseConfig::empty) when there is no stored config.
BaseConfig get_config(const std::string& plugin_key, const std::string& capability_name) const;
bool has_config(const std::string& plugin_key, const std::string& capability_name) const;
std::optional<CapabilityConfigEntry> get_config(const PluginCapabilityId& id) const;
bool has_config(const PluginCapabilityId& id) const;
bool dirty() const;
// ---- Webview-facing helpers, shared by PluginsDialog's Config tab and PluginsConfigDialog ----
// Static because a capability's config is addressed globally by (plugin_key, capability_name)
// through PluginManager's store, not through any one PluginConfig instance. The caller owns the
// UI: it confirms destructive restores and shows status toasts.
// The config sidebar's rows, in the order given. Capabilities no longer loaded are skipped — the
// sidebar only offers what can actually be configured.
static nlohmann::json capabilities_payload(const std::vector<PluginCapabilityId>& caps);
// One capability's stored config, plus its custom HTML UI when it provides one.
static nlohmann::json get_config_response(const PluginCapabilityId& id);
// Persists one capability's config. `config` is either text from the default editor (re-parsed
// here, so malformed JSON can never reach config.json) or a structured value from a custom UI.
static nlohmann::json get_config_response(const PluginCapabilityId& id);tom UI.
static nlohmann::json save_config_response(const PluginCapabilityId& id, const nlohmann::json& config);
// Overwrites one capability's stored config with its get_default_config(). The caller must have
// confirmed with the user first — this does not ask.
static nlohmann::json restore_config_response(const PluginCapabilityId& id);
private:
@@ -208,21 +111,8 @@ private:
bool m_dirty = false;
};
// Host implementations behind the capability-level Python config API (bound onto every capability
// class in PythonPluginBridge). The capability addresses only itself: the (plugin_key,
// capability_name) pair is read off the instance the call arrived on, never passed in from Python,
// so a capability cannot reach another capability's config. Throws std::runtime_error (RuntimeError
// in Python) on an unmaterialized instance.
// The cap_config resolved as the config UI presents it: the active preset's override when it has
// one, otherwise the config stored here (see active_capability_config). Empty object when neither.
nlohmann::json capability_get_config(const PluginCapabilityInterface& capability);
// The plugin version that wrote the config get_config() returns — the same layer it came from — so a
// plugin can migrate a stale cap_config. Empty when the capability has no stored config.
std::string capability_get_config_version(const PluginCapabilityInterface& capability);
// Replaces cap_config and persists. Always writes the store, never a preset: presets are the user's
// to edit, and a plugin saving from a worker thread cannot mark one dirty. A capability whose active
// preset overrides it will therefore keep reading that override back, not what it saved.
bool capability_save_config(const PluginCapabilityInterface& capability, const nlohmann::json& config);
} // namespace Slic3r

View File

@@ -511,21 +511,18 @@ std::vector<std::shared_ptr<PluginCapabilityInterface>> PluginManager::get_plugi
return result;
}
std::shared_ptr<PluginCapabilityInterface> PluginManager::get_plugin_capability(const std::string& plugin_key,
const std::string& capability_name,
PluginCapabilityType type,
bool only_enabled) const
std::shared_ptr<PluginCapabilityInterface> PluginManager::get_plugin_capability(const PluginCapabilityId& id, bool only_enabled) const
{
std::lock_guard<std::mutex> lock(m_mutex);
const Plugin* plugin = find_plugin_locked(plugin_key);
const Plugin* plugin = find_plugin_locked(id.plugin_key);
if (plugin == nullptr)
return nullptr;
for (const auto& capability : plugin->capabilities) {
if (!capability || capability->name() != capability_name)
if (!capability || capability->name() != id.name)
continue;
if (type != PluginCapabilityType::Unknown && capability->type() != type)
if (id.type != PluginCapabilityType::Unknown && capability->type() != id.type)
continue;
if (only_enabled && !capability->is_enabled())
continue;
@@ -740,7 +737,7 @@ void PluginManager::load_plugin(const std::string& plugin_key, bool skip_deps, s
for (const std::string& capability_name : capabilities_to_enable) {
if (std::find(loaded_capability_names.begin(), loaded_capability_names.end(), capability_name) !=
loaded_capability_names.end())
set_capability_enabled(plugin_id, capability_name, true);
set_capability_enabled({PluginCapabilityType::Unknown, capability_name, plugin_id}, true);
}
run_on_load_callbacks(plugin_id);
@@ -852,10 +849,11 @@ void PluginManager::load_plugin_impl(const std::string& plugin_key, bool skip_de
auto config = cap->get_default_config();
if (config.empty())
continue;
if (m_config.has_config(plugin_key, cap->name()))
const PluginCapabilityId id = cap->identity();
if (m_config.has_config(id))
continue;
m_config.save_config(plugin_key, cap->name(), plugin.descriptor.installed_version, config);
m_config.save_config({id, plugin.descriptor.installed_version, config});
}
bool committed = false;
@@ -1007,8 +1005,7 @@ void PluginManager::unload_cloud_plugins()
}
// ── Enable state ────────────────────────────────────────────────────────────────────────────
void PluginManager::set_capability_enabled(const std::string& plugin_key, const std::string& capability_name, bool enabled)
void PluginManager::set_capability_enabled(const PluginCapabilityId& id, bool enabled)
{
PluginCapabilityId changed;
bool did_change = false;
@@ -1016,15 +1013,18 @@ void PluginManager::set_capability_enabled(const std::string& plugin_key, const
{
std::lock_guard<std::mutex> lock(m_mutex);
Plugin* plugin = find_plugin_locked(plugin_key);
Plugin* plugin = find_plugin_locked(id.plugin_key);
if (plugin == nullptr || !plugin->is_loaded())
return;
for (const auto& capability : plugin->capabilities) {
if (!capability || capability->name() != capability_name || capability->is_enabled() == enabled)
if (!capability || capability->name() != id.name ||
(id.type != PluginCapabilityType::Unknown && capability->type() != id.type) ||
capability->is_enabled() == enabled)
continue;
capability->set_enabled(enabled);
changed = PluginCapabilityId{capability->type(), capability->name(), plugin_key};
changed = capability->identity();
did_change = true;
break;
}
@@ -1033,7 +1033,7 @@ void PluginManager::set_capability_enabled(const std::string& plugin_key, const
if (!did_change)
return;
write_loaded_plugin_install_state(plugin_key);
write_loaded_plugin_install_state(id.plugin_key);
if (enabled)
run_on_capability_load_callbacks(changed);
@@ -1043,6 +1043,8 @@ void PluginManager::set_capability_enabled(const std::string& plugin_key, const
void PluginManager::write_loaded_plugin_install_state(const std::string& plugin_key)
{
std::lock_guard<std::mutex> state_lock(m_install_state_mutex);
PluginDescriptor descriptor;
std::vector<std::pair<std::string, bool>> capabilities;
{
@@ -1950,7 +1952,7 @@ ExecutionResult PluginManager::run_script_capability(const std::string& plugin_k
return {};
}
auto cap = get_plugin_capability(plugin_key, capability_name, PluginCapabilityType::Script);
auto cap = get_plugin_capability({PluginCapabilityType::Script, capability_name, plugin_key});
if (!cap)
return {};

View File

@@ -29,20 +29,6 @@ namespace Slic3r {
class OrcaCloudServiceAgent;
// Identity of a single capability, as published to lifecycle subscribers. Purely a message
// payload — capabilities are looked up by (plugin_key, name) linear scan, so unlike the registry
// key this replaces, it needs no hash. Equality is provided for the plugin-config side, which
// de-duplicates the "capabilities in use" list (PluginResolver) with std::unique.
struct PluginCapabilityId
{
PluginCapabilityType type = PluginCapabilityType::Unknown;
std::string name;
std::string plugin_key; // owning package
bool operator==(const PluginCapabilityId& o) const
{ return type == o.type && name == o.name && plugin_key == o.plugin_key; }
};
// One discovered plugin package: one .py/.whl file -> one descriptor + one Python module +
// N materialized capabilities.
//
@@ -157,10 +143,9 @@ public:
const std::string& plugin_key = "", // "" => all plugins
PluginCapabilityType type = PluginCapabilityType::Unknown, // Unknown => all types
bool only_enabled = true) const;
std::shared_ptr<PluginCapabilityInterface> get_plugin_capability(const std::string& plugin_key,
const std::string& capability_name,
PluginCapabilityType type = PluginCapabilityType::Unknown,
bool only_enabled = true) const;
std::shared_ptr<PluginCapabilityInterface> get_plugin_capability(const PluginCapabilityId& id, bool only_enabled = true) const;
// Try to resolve from just capability name and type from presets.
std::shared_ptr<PluginCapabilityInterface> get_plugin_capability(const std::string& capability_name,
PluginCapabilityType type = PluginCapabilityType::Unknown,
bool only_enabled = true) const;
@@ -177,7 +162,7 @@ public:
bool cancel_plugin_load(const std::string& plugin_key);
std::string get_plugin_load_error(const std::string& plugin_key) const;
void set_capability_enabled(const std::string& plugin_key, const std::string& capability_name, bool enabled);
void set_capability_enabled(const PluginCapabilityId& id, bool enabled);
// Sets the cloud user whose _subscribed/{user_id} directory is scanned and installed into.
void set_cloud_user(const std::string& user_id);
@@ -290,6 +275,9 @@ private:
std::map<std::string, std::string> m_load_errors;
mutable std::condition_variable m_load_cv;
// Serialize sidecar snapshots so concurrent capability toggles cannot write stale state out of order.
mutable std::mutex m_install_state_mutex;
std::map<CallbackType, std::vector<PluginLifecycleCompleteFn>> m_callbacks;
std::map<CallbackType, std::vector<CapabilityLifecycleFn>> m_capability_callbacks;
@@ -346,7 +334,7 @@ void execute_capabilities_from_refs(const ConfigOptionStrings& capabilities,
// only_enabled = false so that "not loaded" and "loaded but disabled" stay distinguishable
// and each keeps its own diagnostic.
auto cap = plugin_mgr.get_plugin_capability(plugin_key, cap_name, type, /*only_enabled=*/false);
auto cap = plugin_mgr.get_plugin_capability({type, cap_name, plugin_key}, /*only_enabled=*/false);
if (!cap) {
BOOST_LOG_TRIVIAL(warning) << tag << ": no loaded capability '" << cap_name << "' for plugin '" << plugin_key << "'; skipping";
continue;

View File

@@ -31,7 +31,10 @@ namespace {
// The tracked option in `preset` whose value references `ref`'s capability, or "" when none does.
// Doubles as the "Jump to" target and as the signal that the plugin is still required: a missing
// plugin with no referencing option is dropped from the set.
std::string find_option_for_capability(Preset::Type type, const Preset& preset, const PluginCapabilityRef& ref)
std::string find_option_for_capability(Preset::Type type,
const Preset& preset,
const PluginCapabilityRef& ref,
PluginCapabilityType capability_type = PluginCapabilityType::Unknown)
{
if (type != Preset::TYPE_PRINT && type != Preset::TYPE_PRINTER && type != Preset::TYPE_FILAMENT)
return {};
@@ -42,13 +45,15 @@ std::string find_option_for_capability(Preset::Type type, const Preset& preset,
if (def == nullptr)
return {};
const std::string expected_type = plugin_capability_type_to_string(capability_type);
const auto matches_ref = [&ref](const std::string& value) {
return value == ref.capability_name;
};
for (const std::string& field : preset.config.keys()) {
const ConfigOptionDef* opt_def = def->get(field);
if (opt_def == nullptr || !opt_def->is_plugin_backed())
if (opt_def == nullptr || !opt_def->is_plugin_backed() ||
(capability_type != PluginCapabilityType::Unknown && opt_def->plugin_type != expected_type))
continue;
const ConfigOption* option = preset.config.option(field);
@@ -70,8 +75,12 @@ std::string find_option_for_capability(Preset::Type type, const Preset& preset,
// printer_agent stores AgentInfo::id, so a missing plugin cannot be reverse-mapped through
// the runtime registry. If no regular printer plugin field matched, assume printer_agent.
if (type == Preset::Type::TYPE_PRINTER && preset.config.has("printer_agent"))
if (type == Preset::Type::TYPE_PRINTER && preset.config.has("printer_agent")) {
const ConfigOptionDef* agent_def = def->get("printer_agent");
if (agent_def != nullptr && agent_def->is_plugin_backed() &&
(capability_type == PluginCapabilityType::Unknown || agent_def->plugin_type == expected_type))
return "printer_agent";
}
return {};
}
@@ -129,10 +138,13 @@ void collect_capabilities_in_use(Preset::Type type, const Preset& preset, std::v
if (!PluginManager::instance().try_get_plugin_descriptor(key, descriptor))
continue;
// The loaded capability is also what supplies the type: the manifest ref does not carry one.
// The manifest ref does not carry a type, so require the live capability type to match the
// type declared by the option that references it.
for (const auto& capability : PluginManager::instance().get_plugin_capabilities(descriptor.plugin_key, PluginCapabilityType::Unknown, false))
if (capability && capability->name() == ref.capability_name)
out.push_back(PluginCapabilityId{capability->type(), capability->name(), capability->audit_plugin_key()});
if (capability && capability->type() != PluginCapabilityType::Unknown &&
capability->name() == ref.capability_name &&
!find_option_for_capability(type, preset, ref, capability->type()).empty())
out.push_back(capability->identity());
}
}
@@ -206,7 +218,7 @@ static std::pair<bool, bool> loaded_capability_state(const std::string& plugin_k
if (!mgr.is_plugin_loaded(plugin_key))
return {false, false};
const auto capability = mgr.get_plugin_capability(plugin_key, ref.capability_name, PluginCapabilityType::Unknown,
const auto capability = mgr.get_plugin_capability({PluginCapabilityType::Unknown, ref.capability_name, plugin_key},
/*only_enabled=*/false);
if (!capability)
return {false, false};

View File

@@ -4,8 +4,7 @@
#include <libslic3r/Config.hpp> // PluginCapabilityRef, parse_capability_ref
#include <libslic3r/Preset.hpp> // Preset::Type
#include <libslic3r/PresetBundle.hpp>
#include <slic3r/plugin/PluginManager.hpp> // PluginCapabilityId
#include <slic3r/plugin/PythonPluginInterface.hpp> // PluginCapabilityType
#include <slic3r/plugin/PythonPluginInterface.hpp> // PluginCapabilityId, PluginCapabilityType
#include <cstddef>
#include <functional>
#include <string>

View File

@@ -14,6 +14,27 @@ namespace Slic3r {
enum class PluginCapabilityType { PrinterConnection = 0, Automation, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown };
struct PluginCapabilityId
{
PluginCapabilityType type = PluginCapabilityType::Unknown;
std::string name;
std::string plugin_key;
bool empty() const { return type == PluginCapabilityType::Unknown || name.empty() || plugin_key.empty(); }
friend bool operator==(const PluginCapabilityId& lhs, const PluginCapabilityId& rhs)
{
return lhs.type == rhs.type && lhs.name == rhs.name && lhs.plugin_key == rhs.plugin_key;
}
friend bool operator<(const PluginCapabilityId& lhs, const PluginCapabilityId& rhs)
{
if (lhs.plugin_key != rhs.plugin_key)
return lhs.plugin_key < rhs.plugin_key;
if (lhs.name != rhs.name)
return lhs.name < rhs.name;
return lhs.type < rhs.type;
}
};
inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
{
switch (type) {
@@ -157,6 +178,7 @@ public:
// from Python can tell which capability they are serving.
const std::string& name() const { return m_name; }
PluginCapabilityType type() const { return m_type; }
PluginCapabilityId identity() const { return {m_type, m_name, m_audit_plugin_key}; }
void set_resolved_identity(std::string name, PluginCapabilityType type)
{
m_name = std::move(name);