From 18388ffb6d8be3dcb3852be8af9abd4e2f8062d5 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Fri, 10 Jul 2026 20:00:01 +0800 Subject: [PATCH] feat: plugins config --- src/slic3r/CMakeLists.txt | 2 + src/slic3r/plugin/PluginAuditManager.cpp | 14 +- src/slic3r/plugin/PluginAuditManager.hpp | 15 +- src/slic3r/plugin/PluginCatalog.cpp | 2 +- src/slic3r/plugin/PluginConfig.cpp | 255 ++++++++++++++++++++ src/slic3r/plugin/PluginConfig.hpp | 117 +++++++++ src/slic3r/plugin/PluginFsUtils.cpp | 18 +- src/slic3r/plugin/PluginFsUtils.hpp | 2 + src/slic3r/plugin/PluginLoader.cpp | 3 +- src/slic3r/plugin/PluginManager.cpp | 2 + src/slic3r/plugin/PluginManager.hpp | 4 + src/slic3r/plugin/PyPluginTrampoline.hpp | 7 +- src/slic3r/plugin/PythonPluginBridge.cpp | 3 + src/slic3r/plugin/PythonPluginInterface.hpp | 17 +- 14 files changed, 444 insertions(+), 17 deletions(-) create mode 100644 src/slic3r/plugin/PluginConfig.cpp create mode 100644 src/slic3r/plugin/PluginConfig.hpp diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 647af33174..a96a903f8c 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -604,6 +604,8 @@ set(SLIC3R_GUI_SOURCES plugin/CloudPluginService.hpp plugin/PluginFsUtils.cpp plugin/PluginFsUtils.hpp + plugin/PluginConfig.cpp + plugin/PluginConfig.hpp plugin/PluginCatalog.cpp plugin/PluginCatalog.hpp plugin/PluginLoader.cpp diff --git a/src/slic3r/plugin/PluginAuditManager.cpp b/src/slic3r/plugin/PluginAuditManager.cpp index 1b49cc0cc6..82db703936 100644 --- a/src/slic3r/plugin/PluginAuditManager.cpp +++ b/src/slic3r/plugin/PluginAuditManager.cpp @@ -68,17 +68,22 @@ bool is_inside_allowed_root(const std::filesystem::path& candidate, const std::f // --------------------------------------------------------------------------- thread_local std::string PluginAuditManager::m_current_plugin_key = ""; +thread_local std::string PluginAuditManager::m_current_capability_name = ""; thread_local PluginAuditManager::AuditMode PluginAuditManager::m_audit_mode = PluginAuditManager::AuditMode::Loading; thread_local std::vector PluginAuditManager::m_scoped_allowed_roots; thread_local bool PluginAuditManager::m_has_last_violation = false; thread_local AuditViolation PluginAuditManager::m_last_violation; -ScopedPluginAuditContext::ScopedPluginAuditContext(const std::string& plugin_key, PluginAuditManager::AuditMode mode) +ScopedPluginAuditContext::ScopedPluginAuditContext(const std::string& plugin_key, + const std::string& capability_name, + PluginAuditManager::AuditMode mode) : m_previous_id(PluginAuditManager::instance().current_plugin()) + , m_previous_capability(PluginAuditManager::instance().current_capability()) , m_previous_mode(PluginAuditManager::instance().audit_mode()) , m_previous_scoped_roots(PluginAuditManager::m_scoped_allowed_roots) { PluginAuditManager::instance().set_current_plugin(plugin_key); + PluginAuditManager::instance().set_current_capability(capability_name); PluginAuditManager::instance().set_audit_mode(mode); PluginAuditManager::m_scoped_allowed_roots.clear(); } @@ -86,6 +91,7 @@ ScopedPluginAuditContext::ScopedPluginAuditContext(const std::string& plugin_key ScopedPluginAuditContext::~ScopedPluginAuditContext() { PluginAuditManager::instance().set_current_plugin(m_previous_id); + PluginAuditManager::instance().set_current_capability(m_previous_capability); PluginAuditManager::instance().set_audit_mode(m_previous_mode); PluginAuditManager::m_scoped_allowed_roots = std::move(m_previous_scoped_roots); } @@ -106,6 +112,12 @@ std::string PluginAuditManager::current_plugin() const { return m_current_plugin void PluginAuditManager::clear_current_plugin() { m_current_plugin_key.clear(); } +void PluginAuditManager::set_current_capability(const std::string& capability_name) { m_current_capability_name = capability_name; } + +std::string PluginAuditManager::current_capability() const { return m_current_capability_name; } + +void PluginAuditManager::clear_current_capability() { m_current_capability_name.clear(); } + void PluginAuditManager::add_global_allowed_root(const std::filesystem::path& root) { if (root.empty()) diff --git a/src/slic3r/plugin/PluginAuditManager.hpp b/src/slic3r/plugin/PluginAuditManager.hpp index 713eeb09cc..2e77821c41 100644 --- a/src/slic3r/plugin/PluginAuditManager.hpp +++ b/src/slic3r/plugin/PluginAuditManager.hpp @@ -39,6 +39,14 @@ public: std::string current_plugin() const; void clear_current_plugin(); + // --- current-capability context (thread_local) --- + // The capability whose method is currently executing, within the current plugin. Empty + // while a plugin-wide call runs, and during capture (get_name/get_type), where the + // capability has no cached name yet. + void set_current_capability(const std::string& capability_name); + std::string current_capability() const; + void clear_current_capability(); + // --- allowed-roots registry --- void add_global_allowed_root(const std::filesystem::path& root); void add_scoped_allowed_root(const std::filesystem::path& root); @@ -75,6 +83,7 @@ private: static int audit_hook(const char* event, PyObject* args, void* user_data); static thread_local std::string m_current_plugin_key; + static thread_local std::string m_current_capability_name; static thread_local AuditMode m_audit_mode; static thread_local std::vector m_scoped_allowed_roots; static thread_local bool m_has_last_violation; @@ -84,12 +93,15 @@ private: std::vector m_global_allowed_roots; }; -// RAII guard that sets the current plugin key and restores the previous one. +// RAII guard that sets the current plugin key and capability name, restoring the previous +// pair on scope exit. `capability_name` may be empty for calls that are not scoped to a +// single capability. class ScopedPluginAuditContext { public: explicit ScopedPluginAuditContext( const std::string& plugin_key, + const std::string& capability_name = {}, PluginAuditManager::AuditMode mode = PluginAuditManager::AuditMode::Loading); ~ScopedPluginAuditContext(); @@ -99,6 +111,7 @@ public: private: std::string m_previous_id; + std::string m_previous_capability; PluginAuditManager::AuditMode m_previous_mode; std::vector m_previous_scoped_roots; }; diff --git a/src/slic3r/plugin/PluginCatalog.cpp b/src/slic3r/plugin/PluginCatalog.cpp index e0c0a010f5..0845995920 100644 --- a/src/slic3r/plugin/PluginCatalog.cpp +++ b/src/slic3r/plugin/PluginCatalog.cpp @@ -279,7 +279,7 @@ std::vector PluginCatalog::get_plugin_directories() const }; // Local plugins: {data_dir}/orca_plugins/ - add_or_create_dir(fs::path(data_dir()) / "orca_plugins"); + add_or_create_dir(get_orca_plugins_dir()); // Cloud plugins: {data_dir}/orca_plugins/_subscribed/{user_id}/ if (!cloud_plugin_dir_name.empty()) diff --git a/src/slic3r/plugin/PluginConfig.cpp b/src/slic3r/plugin/PluginConfig.cpp new file mode 100644 index 0000000000..ca76144138 --- /dev/null +++ b/src/slic3r/plugin/PluginConfig.cpp @@ -0,0 +1,255 @@ +#include "PluginConfig.hpp" + +#include + +#include +#include +#include + +#include +#include +#include +#include + +namespace Slic3r { + +namespace { + +constexpr const char* KEY_ENTRIES = "config"; +constexpr const char* KEY_PLUGIN = "plugin_key"; +constexpr const char* KEY_CAPABILITY = "capability"; +constexpr const char* KEY_VERSION = "plugin_version"; +constexpr const char* KEY_CAP_CONFIG = "cap_config"; + +std::string string_field(const nlohmann::json& entry, const char* key) +{ + const auto it = entry.find(key); + return it != entry.end() && it->is_string() ? it->get() : std::string(); +} + +// Rejects entries missing an identity, which could never be looked up again. +bool entry_to_config(const nlohmann::json& entry, BaseConfig& out) +{ + if (!entry.is_object()) + return false; + + out.plugin_key = string_field(entry, KEY_PLUGIN); + out.capability_name = string_field(entry, KEY_CAPABILITY); + out.plugin_version = string_field(entry, KEY_VERSION); + if (out.empty()) + return false; + + const auto cap_config = entry.find(KEY_CAP_CONFIG); + out.config = cap_config != entry.end() ? *cap_config : nlohmann::json::object(); + return true; +} + +nlohmann::json config_to_entry(const BaseConfig& config) +{ + return nlohmann::json{ + {KEY_PLUGIN, config.plugin_key}, + {KEY_CAPABILITY, config.capability_name}, + {KEY_VERSION, config.plugin_version}, + {KEY_CAP_CONFIG, config.config}, + }; +} + +// The version of the plugin package currently running. PluginDescriptor::version is +// overwritten with the latest cloud version when a cloud merge happens, so it can name a +// version that is not the one on disk; installed_version is what actually loaded. +std::string running_plugin_version(const std::string& plugin_key) +{ + PluginDescriptor descriptor; + if (!PluginManager::instance().get_catalog().try_get_valid_plugin_descriptor(plugin_key, descriptor)) + return {}; + return descriptor.installed_version.empty() ? descriptor.version : descriptor.installed_version; +} + +// Identifies the capability whose Python method is currently on the stack. Both halves are +// published by ScopedPluginAuditContext, which every C++ -> Python trampoline call opens. +// A call arriving without them did not come through a capability, so it has no config to +// address and we refuse it rather than reading or clobbering some other capability's entry. +std::pair calling_capability(const char* api_name) +{ + const PluginAuditManager& audit = PluginAuditManager::instance(); + + std::pair id{audit.current_plugin(), audit.current_capability()}; + if (id.first.empty() || id.second.empty()) + throw std::runtime_error(std::string(api_name) + "() must be called from a plugin capability method"); + + return id; +} + +} // namespace + +void PluginConfig::load() +{ + const std::string path = plugin_config_file(); + + std::lock_guard lock(m_mutex); + m_storage.clear(); + m_dirty = false; + + boost::system::error_code ec; + if (!boost::filesystem::exists(path, ec)) + return; + + nlohmann::json root; + try { + boost::nowide::ifstream ifs(path.c_str()); + ifs >> root; + } catch (const std::exception& err) { + BOOST_LOG_TRIVIAL(error) << "PluginConfig: cannot read " << path << ": " << err.what() << "; starting with an empty config"; + return; + } + + const auto entries = root.find(KEY_ENTRIES); + if (entries == root.end() || !entries->is_array()) { + BOOST_LOG_TRIVIAL(warning) << "PluginConfig: " << path << " has no \"" << KEY_ENTRIES << "\" array; starting with an empty config"; + return; + } + + for (const auto& entry : *entries) { + BaseConfig config; + if (!entry_to_config(entry, config)) { + BOOST_LOG_TRIVIAL(warning) << "PluginConfig: skipping entry without a plugin key and capability name"; + continue; + } + m_storage[{config.plugin_key, config.capability_name}] = std::move(config); + } +} + +void PluginConfig::save() +{ + const std::string path = plugin_config_file(); + + std::lock_guard lock(m_mutex); + + nlohmann::json root; + root[KEY_ENTRIES] = nlohmann::json::array(); + for (const auto& [id, config] : m_storage) + root[KEY_ENTRIES].push_back(config_to_entry(config)); + + boost::system::error_code ec; + boost::filesystem::create_directories(boost::filesystem::path(path).parent_path(), ec); + if (ec) { + BOOST_LOG_TRIVIAL(error) << "PluginConfig: cannot create the plugin directory: " << ec.message(); + return; + } + + // Write to a PID-suffixed file and rename it into place, so a crash mid-write cannot + // truncate an existing config. Same approach as AppConfig::save(). + const std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str(); + + boost::nowide::ofstream file; + file.open(path_pid, std::ios::out | std::ios::trunc); + file << root.dump(1, '\t') << std::endl; + file.close(); + if (file.fail()) { + BOOST_LOG_TRIVIAL(error) << "PluginConfig: failed to write " << path_pid << "; keeping the existing config"; + return; + } + + if (const std::error_code rename_ec = rename_file(path_pid, path)) { + BOOST_LOG_TRIVIAL(error) << "PluginConfig: failed to move " << path_pid << " onto " << path << ": " << rename_ec.message(); + return; + } + + m_dirty = false; +} + +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::save_config_text(const std::string& plugin_key, + const std::string& capability_name, + const std::string& version, + const std::string& config) +{ + nlohmann::json parsed = nlohmann::json::parse(config, nullptr, /* allow_exceptions */ false); + if (parsed.is_discarded()) { + BOOST_LOG_TRIVIAL(error) << "PluginConfig: capability '" << capability_name << "' of plugin '" << plugin_key + << "' supplied malformed JSON; config not saved"; + return false; + } + + save_config({plugin_key, capability_name, version, std::move(parsed)}); + return true; +} + +void PluginConfig::save_config(const BaseConfig& config) +{ + if (config.empty()) { + BOOST_LOG_TRIVIAL(error) << "PluginConfig: refusing to store a config without a plugin key and capability name"; + return; + } + + std::lock_guard lock(m_mutex); + m_storage[{config.plugin_key, config.capability_name}] = config; + m_dirty = true; +} + +BaseConfig PluginConfig::get_config(const std::string& plugin_key, const std::string& capability_name) const +{ + std::lock_guard lock(m_mutex); + const auto it = m_storage.find({plugin_key, capability_name}); + return it != m_storage.end() ? it->second : BaseConfig(); +} + +bool PluginConfig::has_config(const std::string& plugin_key, const std::string& capability_name) const +{ + std::lock_guard lock(m_mutex); + return m_storage.count({plugin_key, capability_name}) != 0; +} + +bool PluginConfig::dirty() const +{ + std::lock_guard lock(m_mutex); + return m_dirty; +} + +void PluginConfig::RegisterBindings(pybind11::module_& m) +{ + namespace py = pybind11; + + auto config_submodule = m.def_submodule("config", "Per-capability configuration storage"); + + // Config crosses this boundary as a JSON string, not a dict: the host does not care about + // the shape of cap_config, so it never builds Python objects out of it. Plugins hand the + // string to json.loads/json.dumps themselves. + config_submodule.def( + "get_config", + []() -> py::object { + const auto [plugin_key, capability] = calling_capability("get_config"); + + const BaseConfig config = PluginManager::instance().get_config().get_config(plugin_key, capability); + if (config.empty()) + return py::none(); + + return py::str(config_to_entry(config).dump()); + }, + R"pbdoc(Return this capability's stored config as a JSON string, or None if it has never been saved. + +The object mirrors the on-disk entry: "plugin_key", "capability", "plugin_version" and +"cap_config". "plugin_version" is the version that last wrote the entry, so a plugin can +compare it against its own and migrate "cap_config" before use.)pbdoc"); + + config_submodule.def( + "save_config", + [](const std::string& cap_config) { + const auto [plugin_key, capability] = calling_capability("save_config"); + + return PluginManager::instance().get_config().save_config_text(plugin_key, capability, running_plugin_version(plugin_key), + cap_config); + }, + py::arg("cap_config"), + R"pbdoc(Store this capability's config, given as a JSON string. + +The plugin key, capability name and plugin version are supplied by the host. Returns False +without storing anything if `cap_config` is not valid JSON.)pbdoc"); +} + +} // namespace Slic3r diff --git a/src/slic3r/plugin/PluginConfig.hpp b/src/slic3r/plugin/PluginConfig.hpp new file mode 100644 index 0000000000..a738af992c --- /dev/null +++ b/src/slic3r/plugin/PluginConfig.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#define PLUGIN_CONFIG_DIR "config.json" + +namespace pybind11 { + class module_; +} + +namespace Slic3r { + +/* +Example config.json shape +{ + "config": [ + { + "plugin_key": "some_name", + "capability": "capability_name", + "plugin_version": "1.0.0", + "cap_config": { + "some": "plugin", + "capability": "specific", + "stuff": "here" + } + }, + { + "plugin_key": "some_name", + "capability": "capability_name", + "plugin_version": "1.0.0", + "cap_config": { + "some": "plugin", + "capability": "specific", + "stuff": "here" + } + }, + { + "plugin_key": "some_name", + "capability": "capability_name", + "plugin_version": "1.0.0", + "cap_config": { + "some": "plugin", + "capability": "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(); } +}; + +// Consolidated 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` is metadata +// recording which version last wrote the entry, letting an upgraded plugin spot a stale +// config and migrate it. Version is deliberately not part of the identity, so upgrading a +// plugin does not silently 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(); } + + // Replaces the in-memory store with what is on disk. 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. Clears the dirty flag only once the file is in place. + void 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); + + // Parses `config` as a JSON document, storing nothing and returning false if it is + // malformed. Spelled differently from save_config() on purpose: nlohmann::json converts + // implicitly from const char*, so a `save_config(..., "{}")` overload pair would be + // ambiguous, and a raw string would silently store as a JSON string rather than an object. + bool save_config_text(const std::string& plugin_key, const std::string& capability_name, const std::string& version, const std::string& config); + + // Returns a default-constructed BaseConfig (see BaseConfig::empty) when the capability has + // 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; + + bool dirty() const; + + static void RegisterBindings(pybind11::module_& module); + +private: + // (plugin_key, capability_name) -> entry. Ordered, so config.json serializes stably. + using CapabilityId = std::pair; + + mutable std::mutex m_mutex; + std::map m_storage; + bool m_dirty = false; +}; +} // namespace Slic3r diff --git a/src/slic3r/plugin/PluginFsUtils.cpp b/src/slic3r/plugin/PluginFsUtils.cpp index f62c3ee3f5..6e57d959e4 100644 --- a/src/slic3r/plugin/PluginFsUtils.cpp +++ b/src/slic3r/plugin/PluginFsUtils.cpp @@ -13,10 +13,16 @@ namespace Slic3r { const char* const INSTALL_STATE_FILE = ".install_state.json"; +std::string get_orca_plugins_dir() +{ + namespace fs = boost::filesystem; + return (fs::path(data_dir()) / "orca_plugins").string(); +} + std::string get_cloud_plugin_dir(const std::string& user_id) { namespace fs = boost::filesystem; - return (fs::path(data_dir()) / "orca_plugins" / PLUGIN_SUBSCRIBED_DIR / user_id).string(); + return (fs::path(get_orca_plugins_dir()) / PLUGIN_SUBSCRIBED_DIR / user_id).string(); } boost::filesystem::path resolve_plugin_root_from_descriptor(const PluginDescriptor& descriptor) @@ -30,8 +36,7 @@ boost::filesystem::path resolve_plugin_root_from_descriptor(const PluginDescript return {}; } -bool is_plugin_root_allowed(const boost::filesystem::path& candidate_root, - const std::vector& allowed_dirs) +bool is_plugin_root_allowed(const boost::filesystem::path& candidate_root, const std::vector& allowed_dirs) { boost::system::error_code ec; boost::filesystem::path resolved_root = boost::filesystem::weakly_canonical(candidate_root, ec); @@ -44,8 +49,7 @@ bool is_plugin_root_allowed(const boost::filesystem::path& candidate_root, return false; for (const auto& allowed_dir : allowed_dirs) { - if (is_inside_allowed_root(std::filesystem::path(resolved_root.string()), - std::filesystem::path(allowed_dir))) + if (is_inside_allowed_root(std::filesystem::path(resolved_root.string()), std::filesystem::path(allowed_dir))) return true; } @@ -85,9 +89,7 @@ bool resolve_allowed_plugin_root(const PluginDescriptor& descriptor, return true; } -bool delete_plugin_root(const boost::filesystem::path& resolved_root, - const std::string& plugin_id, - std::string& error) +bool delete_plugin_root(const boost::filesystem::path& resolved_root, const std::string& plugin_id, std::string& error) { namespace fs = boost::filesystem; diff --git a/src/slic3r/plugin/PluginFsUtils.hpp b/src/slic3r/plugin/PluginFsUtils.hpp index 4e95a7aa4d..ddf316b0b2 100644 --- a/src/slic3r/plugin/PluginFsUtils.hpp +++ b/src/slic3r/plugin/PluginFsUtils.hpp @@ -17,6 +17,8 @@ extern const char* const INSTALL_STATE_FILE; // Path: {data_dir}/orca_plugins/_subscribed/{user_id}/ std::string get_cloud_plugin_dir(const std::string& user_id); +std::string get_orca_plugins_dir(); + boost::filesystem::path resolve_plugin_root_from_descriptor(const PluginDescriptor& descriptor); bool is_plugin_root_allowed(const boost::filesystem::path& candidate_root, diff --git a/src/slic3r/plugin/PluginLoader.cpp b/src/slic3r/plugin/PluginLoader.cpp index e5a96b87d3..8d99edf55f 100644 --- a/src/slic3r/plugin/PluginLoader.cpp +++ b/src/slic3r/plugin/PluginLoader.cpp @@ -59,7 +59,7 @@ std::string plugin_package_extension(const boost::filesystem::path& path) boost::filesystem::path local_plugin_root() { - return boost::filesystem::path(data_dir()) / "orca_plugins"; + return boost::filesystem::path(get_orca_plugins_dir()); } boost::filesystem::path local_plugin_install_dir(const boost::filesystem::path& source_path) @@ -973,6 +973,7 @@ void PluginLoader::load_plugin_impl(PluginCatalog& catalog, const std::string& p } loaded_cap->instance->set_audit_plugin_key(descriptor.plugin_key); + loaded_cap->instance->set_audit_capability_name(loaded_cap->name); capability_types.push_back(loaded_cap->type); loaded.capabilities.push_back(capability_id); capabilities.emplace_back(std::move(loaded_cap)); diff --git a/src/slic3r/plugin/PluginManager.cpp b/src/slic3r/plugin/PluginManager.cpp index 7062dfda29..4b2ed56d35 100644 --- a/src/slic3r/plugin/PluginManager.cpp +++ b/src/slic3r/plugin/PluginManager.cpp @@ -1,6 +1,8 @@ #include "PluginManager.hpp" #include "slic3r/GUI/GUI_App.hpp" +#include +#include #include #include "PythonPluginBridge.hpp" diff --git a/src/slic3r/plugin/PluginManager.hpp b/src/slic3r/plugin/PluginManager.hpp index 5655835757..6290dd1c53 100644 --- a/src/slic3r/plugin/PluginManager.hpp +++ b/src/slic3r/plugin/PluginManager.hpp @@ -19,6 +19,7 @@ #include "PluginCatalog.hpp" #include "PluginLoader.hpp" #include "PluginDescriptor.hpp" +#include "PluginConfig.hpp" namespace Slic3r { @@ -58,6 +59,8 @@ public: const PluginCatalog& get_catalog() const { return m_catalog; } PluginLoader& get_loader() { return m_loader; } const PluginLoader& get_loader() const { return m_loader; } + PluginConfig& get_config() { return m_config; } + const PluginConfig& get_config() const { return m_config; } void set_cloud_agent(std::shared_ptr agent) { m_cloud_service.set_cloud_agent(std::move(agent)); } @@ -88,6 +91,7 @@ private: CloudPluginService m_cloud_service; PluginCatalog m_catalog; PluginLoader m_loader; + PluginConfig m_config; mutable std::mutex m_mutex; diff --git a/src/slic3r/plugin/PyPluginTrampoline.hpp b/src/slic3r/plugin/PyPluginTrampoline.hpp index 5ff4514c57..f5bdc902e6 100644 --- a/src/slic3r/plugin/PyPluginTrampoline.hpp +++ b/src/slic3r/plugin/PyPluginTrampoline.hpp @@ -29,13 +29,14 @@ } // Opens the plugin's filesystem audit scope for the duration of a C++ -> Python call -// when this trampoline instance carries a non-empty audit plugin key. Declares a local -// `_orca_audit_scope`. +// when this trampoline instance carries a non-empty audit plugin key. Also publishes the +// calling capability's name, so host APIs invoked from Python can tell which capability +// they are serving. Declares a local `_orca_audit_scope`. #define ORCA_PY_AUDIT_SCOPE(mode) \ std::optional<::Slic3r::ScopedPluginAuditContext> _orca_audit_scope; \ if (const std::string& _orca_audit_key = this->audit_plugin_key(); \ !_orca_audit_key.empty()) \ - _orca_audit_scope.emplace(_orca_audit_key, mode) + _orca_audit_scope.emplace(_orca_audit_key, this->audit_capability_name(), mode) #define ORCA_PY_OVERRIDE_AUDITED(mode, audit_setup, override_macro, ret, base, name, ...) \ do { \ diff --git a/src/slic3r/plugin/PythonPluginBridge.cpp b/src/slic3r/plugin/PythonPluginBridge.cpp index dfc78d14f7..33e7177dcf 100644 --- a/src/slic3r/plugin/PythonPluginBridge.cpp +++ b/src/slic3r/plugin/PythonPluginBridge.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -10,6 +11,7 @@ #include #include "PythonInterpreter.hpp" +#include "PluginConfig.hpp" #include "PluginHostApi.hpp" #include "PyPluginPackage.hpp" #include "PyPluginTrampoline.hpp" @@ -338,6 +340,7 @@ void bind_python_api(pybind11::module_& m) PrinterAgentPluginCapability::RegisterBindings(m, pluginTypes); ScriptPluginCapability::RegisterBindings(m, pluginTypes); PluginHostApi::RegisterBindings(m); + PluginConfig::RegisterBindings(m); BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings"; m.def( diff --git a/src/slic3r/plugin/PythonPluginInterface.hpp b/src/slic3r/plugin/PythonPluginInterface.hpp index e169ddda55..70670480c8 100644 --- a/src/slic3r/plugin/PythonPluginInterface.hpp +++ b/src/slic3r/plugin/PythonPluginInterface.hpp @@ -98,8 +98,13 @@ class PluginCapabilityInterface public: virtual ~PluginCapabilityInterface() = default; - virtual std::string get_name() const = 0; // required — overridden in Python - virtual PluginCapabilityType get_type() const { return PluginCapabilityType::Unknown; } // optional — typed bases override + // Required APIs + virtual std::string get_name() const = 0; + + // Optional APIs + virtual PluginCapabilityType get_type() const { return PluginCapabilityType::Unknown; } + virtual bool has_config() const { return false; } + virtual std::string embed_config_ui() const { return ""; } virtual void on_load() {} virtual void on_unload() {} @@ -110,8 +115,16 @@ public: void set_audit_plugin_key(std::string key) { m_audit_plugin_key = std::move(key); } const std::string& audit_plugin_key() const { return m_audit_plugin_key; } + // The cached get_name() captured at load, paired with the audit plugin key to identify + // which capability a trampoline call belongs to. Cached rather than read live: get_name() + // is itself a trampoline call, so calling it from inside a trampoline would recurse. + // Empty until PluginLoader materializes the capability. + void set_audit_capability_name(std::string name) { m_audit_capability_name = std::move(name); } + const std::string& audit_capability_name() const { return m_audit_capability_name; } + private: std::string m_audit_plugin_key; + std::string m_audit_capability_name; }; } // namespace Slic3r