Merge branch 'feat/plugin-feature' into feature/speed-dial

Adapt the speed dial's ActionRegistry to the collapsed
get_plugin_capability(PluginCapabilityId) overload, and restore the
script success/skipped status message the dialog lost when its
PluginScriptRunner refactor was superseded by ActionRegistry.
This commit is contained in:
SoftFever
2026-07-17 19:32:44 +08:00
58 changed files with 4375 additions and 671 deletions
+13 -1
View File
@@ -77,17 +77,22 @@ bool is_inside_allowed_root(const boost::filesystem::path& candidate, const boos
// ---------------------------------------------------------------------------
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<boost::filesystem::path> 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();
}
@@ -95,6 +100,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);
}
@@ -115,6 +121,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 boost::filesystem::path& root)
{
if (root.empty())
+14 -1
View File
@@ -40,6 +40,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 boost::filesystem::path& root);
void add_scoped_allowed_root(const boost::filesystem::path& root);
@@ -76,6 +84,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<boost::filesystem::path> m_scoped_allowed_roots;
static thread_local bool m_has_last_violation;
@@ -85,12 +94,15 @@ private:
std::vector<boost::filesystem::path> 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();
@@ -100,6 +112,7 @@ public:
private:
std::string m_previous_id;
std::string m_previous_capability;
PluginAuditManager::AuditMode m_previous_mode;
std::vector<boost::filesystem::path> m_previous_scoped_roots;
};
+649
View File
@@ -0,0 +1,649 @@
#include "PluginConfig.hpp"
#include <algorithm>
#include <boost/format.hpp>
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
#include <libslic3r/PresetBundle.hpp>
#include <libslic3r/PrintConfig.hpp>
#include <slic3r/GUI/GUI.hpp>
#include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/GUI/I18N.hpp>
#include <slic3r/GUI/format.hpp>
#include <slic3r/plugin/PluginLoader.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include <slic3r/plugin/PythonInterpreter.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <stdexcept>
#include <wx/app.h>
#include <wx/utils.h>
namespace Slic3r {
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";
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>() : std::string();
}
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.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 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.config = cap_it != entry.end() ? *cap_it : nlohmann::json::object();
return result;
}
// PluginDescriptor::version is overwritten with the latest cloud version on a cloud merge, 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().try_get_valid_plugin_descriptor(plugin_key, descriptor))
return {};
return descriptor.installed_version.empty() ? descriptor.version : descriptor.installed_version;
}
// 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()
{
const auto* app = dynamic_cast<const GUI::GUI_App*>(wxApp::GetInstance());
return app == nullptr ? nullptr : app->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.
PluginCapabilityId capability_identity(const PluginCapabilityInterface& capability, const char* api_name)
{
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;
}
} // namespace
CapabilityConfigDocument CapabilityConfigDocument::from_entries(const nlohmann::json& entries)
{
CapabilityConfigDocument document;
if (!entries.is_array())
return document;
for (const nlohmann::json& entry : entries) {
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);
}
return document;
}
CapabilityConfigDocument CapabilityConfigDocument::from_root_json(const nlohmann::json& root)
{
const auto entries = root.find(KeyEntries);
return entries != root.end() ? from_entries(*entries) : CapabilityConfigDocument();
}
std::optional<CapabilityConfigEntry> CapabilityConfigDocument::find(const PluginCapabilityId& id) const
{
const auto it = m_entries.find(id);
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 PluginCapabilityId& id) const
{
return find(id).has_value();
}
bool CapabilityConfigDocument::upsert(CapabilityConfigEntry entry)
{
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.name;
serialized[KEY_TYPE] = plugin_capability_type_to_string(entry.id.type);
serialized[KEY_VERSION] = entry.plugin_version;
serialized[KEY_CAP_CONFIG] = entry.config;
m_entries[entry.id] = std::move(serialized);
return true;
}
bool CapabilityConfigDocument::erase(const PluginCapabilityId& id)
{
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
{
return m_entries.empty() && m_opaque_entries.empty();
}
nlohmann::json CapabilityConfigDocument::serialize_entries() const
{
nlohmann::json result = nlohmann::json::array();
for (const auto& item : m_entries)
result.push_back(item.second);
for (const nlohmann::json& entry : m_opaque_entries)
result.push_back(entry);
return result;
}
nlohmann::json CapabilityConfigDocument::root_json() const
{
nlohmann::json root = nlohmann::json::object();
root[KeyEntries] = serialize_entries();
return root;
}
void PluginConfig::load()
{
const std::string path = plugin_config_file();
std::lock_guard<std::mutex> lock(m_mutex);
m_document = CapabilityConfigDocument();
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(CapabilityConfigDocument::KeyEntries);
if (entries == root.end() || !entries->is_array()) {
BOOST_LOG_TRIVIAL(warning) << "PluginConfig: " << path << " has no \"" << CapabilityConfigDocument::KeyEntries
<< "\" array; starting with an empty config";
return;
}
m_document = CapabilityConfigDocument::from_root_json(root);
}
bool PluginConfig::save()
{
const std::string path = plugin_config_file();
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_dirty)
return true;
const nlohmann::json root = m_document.root_json();
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 false;
}
// 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 false;
}
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 false;
}
m_dirty = false;
return true;
}
bool PluginConfig::store_capability_config(const PluginCapabilityId& id, const nlohmann::json& config)
{
if (id.empty())
return false;
save_config({id, running_plugin_version(id.plugin_key), config});
return save();
}
void PluginConfig::save_config(const CapabilityConfigEntry& config)
{
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(config) || m_dirty;
}
bool PluginConfig::erase_capability_config(const PluginCapabilityId& id)
{
if (id.empty())
return false;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_document.erase(id))
return true;
m_dirty = true;
}
return save();
}
std::optional<CapabilityConfigEntry> PluginConfig::get_config(const PluginCapabilityId& id) const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_document.find(id);
}
bool PluginConfig::has_config(const PluginCapabilityId& id) const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_document.contains(id);
}
bool PluginConfig::dirty() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_dirty;
}
std::string plugin_overrides_of(const Preset& preset)
{
const auto* opt = dynamic_cast<const ConfigOptionString*>(preset.config.option(PLUGIN_OVERRIDES_OPTION_KEY));
return opt == nullptr ? std::string() : opt->value;
}
bool parse_plugin_overrides(const std::string& raw, CapabilityConfigDocument& document, std::string& error)
{
document = CapabilityConfigDocument();
error.clear();
if (raw.empty())
return true;
const nlohmann::json parsed = nlohmann::json::parse(raw, nullptr, /* allow_exceptions */ false);
if (parsed.is_discarded()) {
error = "The preset stores invalid plugin capability configuration JSON.";
return false;
}
if (!parsed.is_array()) {
error = "The preset's plugin capability configuration is not an array and cannot be edited.";
return false;
}
document = CapabilityConfigDocument::from_entries(parsed);
return true;
}
std::string serialize_plugin_overrides(const CapabilityConfigDocument& document)
{
return document.empty() ? std::string() : document.serialize_entries().dump();
}
EffectiveCapabilityConfig PresetPluginConfigService::get_effective_config(const CapabilityConfigDocument& overrides,
const PluginCapabilityId& id) const
{
EffectiveCapabilityConfig result;
result.id = id;
result.running_plugin_version = running_plugin_version(id.plugin_key);
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->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;
}
return result;
}
MutationResult PresetPluginConfigService::set_preset_override(CapabilityConfigDocument& overrides,
const PluginCapabilityId& id,
const nlohmann::json& value) const
{
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(id);
if (existing && existing->config == value && existing->plugin_version == version) {
result.ok = true;
result.effective = get_effective_config(overrides, id);
return result;
}
overrides.upsert({id, version, value});
result.ok = true;
result.changed = true;
result.effective = get_effective_config(overrides, id);
return result;
}
MutationResult PresetPluginConfigService::remove_preset_override(CapabilityConfigDocument& overrides,
const PluginCapabilityId& id) const
{
MutationResult result;
result.ok = true;
result.changed = overrides.erase(id);
result.effective = get_effective_config(overrides, id);
return result;
}
EffectiveCapabilityConfig active_capability_config(const PluginCapabilityId& id)
{
const PresetPluginConfigService service;
CapabilityConfigDocument overrides;
const PresetBundle* bundle = active_preset_bundle();
const Preset* preset = nullptr;
if (bundle != nullptr) {
const std::string type_key = plugin_capability_type_to_string(id.type);
for (const auto& [key, def] : print_config_def.options) {
if (def.plugin_type != type_key)
continue;
const auto& print_options = Preset::print_options();
if (std::find(print_options.begin(), print_options.end(), key) != print_options.end()) {
preset = &bundle->prints.get_edited_preset();
break;
}
const auto& printer_options = Preset::printer_options();
if (std::find(printer_options.begin(), printer_options.end(), key) != printer_options.end()) {
preset = &bundle->printers.get_edited_preset();
break;
}
}
}
if (preset != nullptr) {
std::string error;
if (!parse_plugin_overrides(plugin_overrides_of(*preset), overrides, error)) {
// Text we cannot read is not an override: log it and resolve against the base config.
BOOST_LOG_TRIVIAL(error) << "Preset \"" << preset->name << "\": " << error;
overrides = CapabilityConfigDocument();
}
}
return service.get_effective_config(overrides, id);
}
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_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_identity(capability, "get_config_version")).stored_plugin_version;
}
bool capability_save_config(const PluginCapabilityInterface& capability, const nlohmann::json& 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)
{
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, false);
if (!capability)
continue;
nlohmann::json entry;
entry["plugin_key"] = id.plugin_key;
entry["name"] = id.name;
entry["type"] = plugin_capability_type_display_name(id.type);
entry["type_key"] = plugin_capability_type_to_string(id.type);
entry["has_config_ui"] = capability->config_ui_available();
payload.push_back(std::move(entry));
}
return payload;
}
// Config is sent as a JSON value, not text: the default editor pretty-prints it into its textarea,
// and a custom UI receives it as-is through window.orca.
nlohmann::json PluginConfig::get_config_response(const PluginCapabilityId& id)
{
nlohmann::json response;
response["command"] = "capability_config";
response["plugin_key"] = id.plugin_key;
response["capability_name"] = id.name;
response["capability_type"] = plugin_capability_type_to_string(id.type);
response["config"] = nlohmann::json::object();
response["custom_html"] = "";
response["error"] = "";
// 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, 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;
response["error"] = GUI::into_u8(_L("This capability is no longer available."));
return response;
}
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
// failure and let the page fall back to the default JSON editor over the same stored config.
std::string html;
std::string error;
{
wxBusyCursor busy;
try {
PythonGILState gil;
html = cap->get_config_ui();
} catch (const std::exception& ex) {
error = ex.what();
} catch (...) {
error = "Unknown error";
}
}
if (!error.empty()) {
BOOST_LOG_TRIVIAL(error) << "Plugin capability get_config_ui() failed. plugin_key=" << id.plugin_key
<< " capability_name=" << id.name << " error=" << error;
response["error"] = GUI::into_u8(GUI::format_wxstr(_L("The plugin's configuration UI failed to load (%1%). Showing the default editor."),
GUI::from_u8(error)));
} else if (html.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Plugin capability reports a config UI but returned no HTML. plugin_key=" << id.plugin_key
<< " capability_name=" << id.name;
response["error"] = GUI::into_u8(_L("The plugin's configuration UI was empty. Showing the default editor."));
} else {
response["custom_html"] = html;
}
}
return response;
}
nlohmann::json PluginConfig::save_config_response(const PluginCapabilityId& id, const nlohmann::json& config)
{
nlohmann::json response;
response["command"] = "capability_config_saved";
response["plugin_key"] = id.plugin_key;
response["capability_name"] = id.name;
response["capability_type"] = plugin_capability_type_to_string(id.type);
response["ok"] = false;
response["error"] = "";
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;
response["error"] = GUI::into_u8(_L("This capability is no longer available. Your changes were not saved."));
return response;
}
nlohmann::json parsed = config;
if (config.is_string()) {
// The page validates as the user types, but it is not the authority: re-parse so a malformed
// document is rejected before it can reach config.json.
parsed = nlohmann::json::parse(config.get<std::string>(), nullptr, /* allow_exceptions */ false);
if (parsed.is_discarded()) {
response["error"] = GUI::into_u8(_L("The configuration is not valid JSON. Your changes were not saved."));
return response;
}
}
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."));
return response;
}
BOOST_LOG_TRIVIAL(info) << "Saved plugin capability config. plugin_key=" << id.plugin_key << " capability_name=" << id.name;
// Echo back what was persisted, not what the user typed, so the editor reloads from the store.
response["ok"] = true;
if (const auto stored = PluginManager::instance().get_config().get_config(id))
response["config"] = stored->config;
return response;
}
// The host never invents the default: a capability that does not override get_default_config()
// restores an empty config, which is right for one that applies its own defaults on read.
nlohmann::json PluginConfig::restore_config_response(const PluginCapabilityId& id)
{
nlohmann::json response;
response["command"] = "capability_config_saved";
response["plugin_key"] = id.plugin_key;
response["capability_name"] = id.name;
response["capability_type"] = plugin_capability_type_to_string(id.type);
response["ok"] = false;
response["error"] = "";
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;
response["error"] = GUI::into_u8(_L("This capability is no longer available."));
return response;
}
nlohmann::json defaults;
std::string error;
{
wxBusyCursor busy;
try {
PythonGILState gil;
defaults = cap->get_default_config();
} catch (const std::exception& ex) {
error = ex.what();
} catch (...) {
error = "Unknown error";
}
}
// A raising hook leaves the stored config as it was: better to restore nothing than to wipe the
// user's settings on the strength of a broken plugin.
if (!error.empty()) {
BOOST_LOG_TRIVIAL(error) << "Plugin capability get_default_config() failed. plugin_key=" << id.plugin_key
<< " capability_name=" << id.name << " error=" << error;
response["error"] = GUI::into_u8(GUI::format_wxstr(_L("The plugin could not supply a default configuration (%1%). "
"Nothing was changed."),
GUI::from_u8(error)));
return response;
}
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."));
return response;
}
BOOST_LOG_TRIVIAL(info) << "Restored default plugin capability config. plugin_key=" << id.plugin_key
<< " capability_name=" << id.name;
response["ok"] = true;
if (const auto stored = PluginManager::instance().get_config().get_config(id))
response["config"] = stored->config;
return response;
}
} // namespace Slic3r
+118
View File
@@ -0,0 +1,118 @@
#pragma once
#include <libslic3r/Utils.hpp>
#include <boost/filesystem.hpp>
#include <nlohmann/json.hpp>
#include <slic3r/plugin/PluginFsUtils.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <map>
#include <mutex>
#include <optional>
#include <string>
#include <vector>
#define PLUGIN_CONFIG_DIR "config.json"
namespace Slic3r {
class Preset;
struct CapabilityConfigEntry
{
PluginCapabilityId id;
std::string plugin_version;
nlohmann::json config = nlohmann::json::object();
};
class CapabilityConfigDocument
{
public:
static constexpr const char* KeyEntries = "config";
static CapabilityConfigDocument from_root_json(const nlohmann::json& root);
static CapabilityConfigDocument from_entries(const nlohmann::json& entries);
std::optional<CapabilityConfigEntry> find(const PluginCapabilityId& id) const;
bool contains(const PluginCapabilityId& id) const;
bool upsert(CapabilityConfigEntry entry);
bool erase(const PluginCapabilityId& id);
bool empty() const;
nlohmann::json serialize_entries() const;
nlohmann::json root_json() const;
private:
std::map<PluginCapabilityId, nlohmann::json> m_entries;
std::vector<nlohmann::json> m_opaque_entries;
};
inline constexpr const char* PLUGIN_OVERRIDES_OPTION_KEY = "plugin_config_overrides";
std::string plugin_overrides_of(const Preset& preset);
bool parse_plugin_overrides(const std::string& raw, CapabilityConfigDocument& document, std::string& error);
std::string serialize_plugin_overrides(const CapabilityConfigDocument& document);
struct EffectiveCapabilityConfig
{
PluginCapabilityId id;
nlohmann::json config = nlohmann::json::object();
bool has_preset_override = false;
bool has_base_config = false;
std::string stored_plugin_version;
std::string running_plugin_version;
};
struct MutationResult
{
bool ok = false;
bool changed = false;
std::string error;
EffectiveCapabilityConfig effective;
};
class PresetPluginConfigService
{
public:
EffectiveCapabilityConfig get_effective_config(const CapabilityConfigDocument& overrides,
const PluginCapabilityId& id) const;
MutationResult set_preset_override(CapabilityConfigDocument& overrides,
const PluginCapabilityId& id,
const nlohmann::json& value) const;
MutationResult remove_preset_override(CapabilityConfigDocument& overrides,
const PluginCapabilityId& id) const;
};
EffectiveCapabilityConfig active_capability_config(const PluginCapabilityId& id);
class PluginConfig
{
public:
static const std::string plugin_config_file() { return (boost::filesystem::path(get_orca_plugins_dir()) / PLUGIN_CONFIG_DIR).string(); }
void load();
bool save();
void save_config(const CapabilityConfigEntry& config);
bool store_capability_config(const PluginCapabilityId& id, const nlohmann::json& config);
bool erase_capability_config(const PluginCapabilityId& id);
std::optional<CapabilityConfigEntry> get_config(const PluginCapabilityId& id) const;
bool has_config(const PluginCapabilityId& id) const;
bool dirty() const;
static nlohmann::json capabilities_payload(const std::vector<PluginCapabilityId>& caps);
static nlohmann::json get_config_response(const PluginCapabilityId& id);
static nlohmann::json save_config_response(const PluginCapabilityId& id, const nlohmann::json& config);
static nlohmann::json restore_config_response(const PluginCapabilityId& id);
private:
mutable std::mutex m_mutex;
CapabilityConfigDocument m_document;
bool m_dirty = false;
};
nlohmann::json capability_get_config(const PluginCapabilityInterface& capability);
std::string capability_get_config_version(const PluginCapabilityInterface& capability);
bool capability_save_config(const PluginCapabilityInterface& capability, const nlohmann::json& config);
} // namespace Slic3r
-7
View File
@@ -61,7 +61,6 @@ struct PluginDescriptor
std::string entry_path; // Full path to the installed plugin entry file
std::string entry_package; // Import package/module used for package-based loading
std::vector<std::string> dependencies; // Python dependency requirements declared by plugin package metadata
std::map<std::string, std::string> settings; // [tool.orcaslicer.plugin.settings] table -> per-plugin params (ctx.params)
std::vector<PluginChangelog> changelog; // Cloud release changelog, sorted newest-first when available.
std::string error; // Blocking error message. Non-empty means the plugin is in an error state.
@@ -157,12 +156,6 @@ inline void apply_plugin_metadata_fallbacks(PluginDescriptor& target, const Plug
target.entry_package = fallback.entry_package;
if (target.dependencies.empty())
target.dependencies = fallback.dependencies;
// [tool.orcaslicer.plugin.settings] lives only in the local package's PEP-723 header;
// cloud catalog records never carry it. Without this, every cloud-metadata merge wipes
// the parsed settings and plugins silently run on their built-in defaults (ctx.params
// arrives empty).
if (target.settings.empty())
target.settings = fallback.settings;
}
// Sanitize a value for use as a filesystem name and as a local plugin_key:
+13 -14
View File
@@ -31,10 +31,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)
@@ -48,8 +54,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<std::string>& allowed_dirs)
bool is_plugin_root_allowed(const boost::filesystem::path& candidate_root, const std::vector<std::string>& allowed_dirs)
{
boost::system::error_code ec;
boost::filesystem::path resolved_root = boost::filesystem::weakly_canonical(candidate_root, ec);
@@ -103,9 +108,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;
@@ -432,7 +435,6 @@ bool parse_pep723_toml(const std::string& toml_content,
std::string& out_description,
std::string& out_author,
std::string& out_version,
std::map<std::string, std::string>& out_settings,
std::string& error)
{
out_deps.clear();
@@ -441,7 +443,6 @@ bool parse_pep723_toml(const std::string& toml_content,
out_description.clear();
out_author.clear();
out_version.clear();
out_settings.clear();
TomlSection section = TomlSection::Root;
@@ -466,7 +467,10 @@ bool parse_pep723_toml(const std::string& toml_content,
if (trimmed == "[tool.orcaslicer.plugin]") {
section = TomlSection::OrcaPlugin;
} else if (trimmed == "[tool.orcaslicer.plugin.settings]") {
section = TomlSection::OrcaPluginSettings; // per-plugin params table
// Legacy table, superseded by PluginConfig. Recognized so its keys are ignored
// rather than falling through to Root, where a stray dependencies/requires-python
// key inside it would be parsed as package metadata.
section = TomlSection::OrcaPluginSettings;
} else {
section = TomlSection::Root; // Unknown section — skip.
}
@@ -519,10 +523,6 @@ bool parse_pep723_toml(const std::string& toml_content,
else if (key == "description") out_description = unquote_toml_string(val);
else if (key == "author") out_author = unquote_toml_string(val);
else if (key == "version") out_version = unquote_toml_string(val);
} else if (section == TomlSection::OrcaPluginSettings) {
// collect every key as a string; the plugin parses (int/float/...) what it needs.
if (!key.empty())
out_settings[key] = unquote_toml_string(val);
}
}
@@ -920,7 +920,6 @@ bool read_python_plugin_metadata(const boost::filesystem::path& py_path, PluginD
pep_desc,
pep_author,
pep_version,
descriptor.settings,
pep723_error)) {
error = "Failed to parse PEP 723 metadata: " + pep723_error;
return false;
+70
View File
@@ -2,8 +2,12 @@
#include "PluginDescriptor.hpp"
#include <nlohmann/json.hpp>
#include <pybind11/pybind11.h>
#include <boost/filesystem/path.hpp>
#include <cstdint>
#include <string>
#include <vector>
@@ -13,6 +17,70 @@ namespace Slic3r {
extern const char* const INSTALL_STATE_FILE;
// JSON <-> Python conversion shared by the plugin bindings. The caller must hold the GIL.
// Plugin config and orca.host.ui payloads both cross the boundary as plain JSON-compatible
// values, so both go through these.
inline pybind11::object json_to_py(const nlohmann::json& j)
{
namespace py = pybind11;
using json = nlohmann::json;
switch (j.type()) {
case json::value_t::null: return py::none();
case json::value_t::boolean: return py::bool_(j.get<bool>());
case json::value_t::number_integer: return py::int_(j.get<std::int64_t>());
case json::value_t::number_unsigned: return py::int_(j.get<std::uint64_t>());
case json::value_t::number_float: return py::float_(j.get<double>());
case json::value_t::string: return py::str(j.get<std::string>());
case json::value_t::array: {
py::list lst;
for (const auto& e : j)
lst.append(json_to_py(e));
return lst;
}
case json::value_t::object: {
py::dict d;
for (auto it = j.begin(); it != j.end(); ++it)
d[py::str(it.key())] = json_to_py(it.value());
return d;
}
default: return py::none();
}
}
inline nlohmann::json py_to_json(const pybind11::handle& o)
{
namespace py = pybind11;
using json = nlohmann::json;
if (o.is_none())
return json(nullptr);
if (py::isinstance<py::bool_>(o)) // bool before int (bool subclasses int in Python)
return o.cast<bool>();
if (py::isinstance<py::int_>(o))
return o.cast<std::int64_t>();
if (py::isinstance<py::float_>(o))
return o.cast<double>();
if (py::isinstance<py::str>(o))
return o.cast<std::string>();
if (py::isinstance<py::bytes>(o))
return o.cast<std::string>();
if (py::isinstance<py::dict>(o)) {
json obj = json::object();
for (auto item : py::reinterpret_borrow<py::dict>(o))
obj[py::str(item.first).cast<std::string>()] = py_to_json(item.second);
return obj;
}
if (py::isinstance<py::list>(o) || py::isinstance<py::tuple>(o)) {
json arr = json::array();
for (auto e : o)
arr.push_back(py_to_json(e));
return arr;
}
return py::str(o).cast<std::string>(); // fallback: str()
}
struct PluginInstallState {
std::string installed_from; // "local" | "cloud"
std::string installed_version;
@@ -26,6 +94,8 @@ struct PluginInstallState {
// 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,
-6
View File
@@ -70,9 +70,6 @@ void install_slicing_pipeline_hook()
const std::string plugin_key = ref.uuid.empty() ? ref.name : ref.uuid;
ExecutionResult r;
try {
// Read manager state before acquiring the GIL so this path does not take
// m_mutex in the opposite order to plugin teardown.
const auto plugin_settings = PluginManager::instance().get_plugin_settings(plugin_key);
// GIL is acquired per capability (not once for the whole dispatch) so it
// is released between capabilities.
PythonGILState gil;
@@ -87,9 +84,6 @@ void install_slicing_pipeline_hook()
ctx.step = step;
ctx.print = &print;
ctx.object = object;
// hand the plugin its own [tool.orcaslicer.plugin.settings] as ctx.params
// (same plugin_key the capability was resolved by, so it always matches).
ctx.params = plugin_settings;
r = cap->execute(ctx);
} catch (const CanceledException&) {
throw; // cancellation must reach process(), never become a slicing error
+13
View File
@@ -347,6 +347,19 @@ bool load(const PluginDescriptor& descriptor,
instance->set_resolved_identity(found.name, type);
instance->set_enabled(enabled);
// Cache has_config_ui() once, under this same GIL, so the GUI can pick the
// capability's custom UI vs. the host JSON editor without touching Python. It is
// optional and plugin-authored: a raising or non-bool override only costs this
// capability its custom UI, so it is caught locally rather than failing the load.
try {
instance->set_config_ui_available(instance->has_config_ui());
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning)
<< "Plugin capability '" << found.name << "' of plugin '" << descriptor.plugin_key
<< "': has_config_ui() failed (" << ex.what() << "); falling back to the default JSON editor";
instance->set_config_ui_available(false);
}
capabilities.push_back(std::move(instance));
}
} catch (const std::exception& ex) {
+41 -26
View File
@@ -1,5 +1,6 @@
#include "PluginManager.hpp"
#include <libslic3r/Utils.hpp>
#include <memory>
#include <pybind11/embed.h>
@@ -19,6 +20,7 @@
#include <algorithm>
#include <chrono>
#include <mutex>
#include <slic3r/plugin/PluginConfig.hpp>
#include <slic3r/plugin/PluginLoader.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp>
@@ -83,6 +85,12 @@ bool PluginManager::initialize()
m_initialized = true;
}
// Bring every capability's stored config into memory. Deliberately unconditional and
// independent of which plugins are installed: an entry outlives uninstall/unsubscribe, so a
// plugin that comes back later finds its settings intact. A missing or malformed file just
// leaves the store empty (see PluginConfig::load), never blocking startup.
m_config.load();
// Install the libslic3r hooks (capability resolver, slicing-pipeline dispatcher).
// Uninstalled in shutdown() before the interpreter finalizes.
plugin_hooks::install();
@@ -142,6 +150,12 @@ void PluginManager::shutdown()
unload_all_plugins();
PythonPluginBridge::instance().clear_pending_captures();
// Every config write already goes to disk as it happens (store_capability_config), so this
// only catches an in-memory-only mutation. Note we flush rather than clear: unloading the
// plugins above must never discard their stored config.
if (m_config.dirty())
m_config.save();
// Drop the lifecycle subscriptions taken out during initialize(). Without this a second
// initialize() in the same process re-subscribes the same callbacks on top of the old ones,
// and every load would then write the install-state sidecar once per duplicate.
@@ -496,21 +510,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;
@@ -541,17 +552,6 @@ std::shared_ptr<PluginCapabilityInterface> PluginManager::get_plugin_capability(
return nullptr;
}
std::map<std::string, std::string> PluginManager::get_plugin_settings(const std::string& plugin_key) const
{
std::lock_guard<std::mutex> lock(m_mutex);
const Plugin* plugin = find_plugin_locked(plugin_key);
if (plugin == nullptr || !plugin->is_loaded())
return {};
return plugin->descriptor.settings;
}
// ── Lifecycle ───────────────────────────────────────────────────────────────────────────────
bool PluginManager::is_plugin_loaded(const std::string& plugin_key) const
@@ -736,7 +736,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);
@@ -844,6 +844,17 @@ void PluginManager::load_plugin_impl(const std::string& plugin_key, bool skip_de
return;
}
for (const auto& cap : plugin.capabilities) {
auto config = cap->get_default_config();
if (config.empty())
continue;
const PluginCapabilityId id = cap->identity();
if (m_config.has_config(id))
continue;
m_config.save_config({id, plugin.descriptor.installed_version, config});
}
bool committed = false;
bool cancelled = false;
std::string registry_error;
@@ -993,8 +1004,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;
@@ -1002,15 +1012,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;
}
@@ -1019,7 +1032,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);
@@ -1029,6 +1042,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;
{
@@ -1958,7 +1973,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 {};
+13 -18
View File
@@ -23,21 +23,12 @@
#include "PluginFsUtils.hpp"
#include "PluginDescriptor.hpp"
#include "PluginLoader.hpp"
#include "PluginConfig.hpp"
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 and no equality.
struct PluginCapabilityId
{
PluginCapabilityType type = PluginCapabilityType::Unknown;
std::string name;
std::string plugin_key; // owning package
};
// One discovered plugin package: one .py/.whl file -> one descriptor + one Python module +
// N materialized capabilities.
//
@@ -126,6 +117,9 @@ public:
// Manually trigger a manifest-only rescan. Blocks until discovery is complete.
void rescan_plugins();
PluginConfig& get_config() { return m_config; }
const PluginConfig& get_config() const { return m_config; }
bool is_discovery_complete() const;
bool is_discovery_in_progress() const;
std::string get_discovery_error() const;
@@ -149,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;
@@ -169,10 +162,8 @@ 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);
// The plugin's [tool.orcaslicer.plugin.settings] table (empty if the plugin is unknown). This will be replaced once the config is merged in.
std::map<std::string, std::string> get_plugin_settings(const std::string& plugin_key) const;
// Sets the cloud user whose _subscribed/{user_id} directory is scanned and installed into.
void set_cloud_user(const std::string& user_id);
@@ -264,6 +255,7 @@ private:
bool m_initialized = false;
CloudPluginService m_cloud_service;
PluginConfig m_config;
// Leaf lock: code holding m_mutex must not call Python, acquire the GIL, invoke lifecycle
// callbacks, or re-enter the manager. Live plugin payloads are detached and torn down after
@@ -283,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;
@@ -339,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;
+116 -27
View File
@@ -12,6 +12,7 @@
#include <boost/log/trivial.hpp>
#include <libslic3r/Config.hpp>
#include <libslic3r/PresetBundle.hpp>
#include <slic3r/plugin/PluginLoader.hpp>
#include <vector>
#include <wx/utils.h>
@@ -20,35 +21,39 @@
#include <map>
#include <mutex>
#include <thread>
#include <tuple>
#include <unordered_map>
namespace Slic3r {
namespace {
// Return the name of the tracked option in `preset` whose value references `ref`'s capability, or
// an empty string when no active option uses it. The result doubles as the "Jump to" target and as
// the signal that the plugin is still required: a missing plugin with no referencing option is
// considered resolved and dropped from the missing set.
std::string find_option_for_capability(Preset::Type type, const Preset& preset, const PluginCapabilityRef& ref)
// 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,
PluginCapabilityType capability_type = PluginCapabilityType::Unknown)
{
if (type != Preset::TYPE_PRINT && type != Preset::TYPE_PRINTER && type != Preset::TYPE_FILAMENT)
return {};
// Plugin-bearing options opt in via ConfigOptionDef::is_plugin_backed (a non-empty plugin_type),
// so scan the preset's definition rather than maintaining a hardcoded per-type field list. A typed
// preset's config only contains keys for its own type, so this naturally stays scoped to `type`.
// Options opt in via ConfigOptionDef::is_plugin_backed, so scan the definition rather than keep a
// hardcoded per-type field list. A typed preset's config only holds keys for its own type.
const ConfigDef* def = preset.config.def();
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,16 +75,19 @@ 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 {};
}
} // namespace
// One missing-plugin set per tracked preset type, keyed by the full "name;uuid;capability" ref.
// Only TYPE_PRINT (process), TYPE_PRINTER (machine) and TYPE_FILAMENT are tracked.
// One set per tracked preset type, keyed by the full "name;uuid;capability" ref.
static std::map<Preset::Type, std::unordered_map<std::string, MissingPlugin>> s_missing;
static std::mutex s_missing_mutex;
// Installed-but-inactive capabilities (not loaded, or loaded-but-disabled); resolvable locally.
@@ -92,6 +100,95 @@ static bool is_tracked_type(Preset::Type type)
return type == Preset::TYPE_PRINT || type == Preset::TYPE_PRINTER || type == Preset::TYPE_FILAMENT;
}
std::vector<PluginCapabilityRef> referenced_capabilities(Preset::Type type, const Preset& preset)
{
if (!is_tracked_type(type))
return {};
const auto* manifest = dynamic_cast<const ConfigOptionStrings*>(preset.config.option("plugins"));
if (manifest == nullptr)
return {};
std::vector<PluginCapabilityRef> refs;
for (const std::string& entry : manifest->values) {
const auto ref = parse_capability_ref(entry);
if (!ref)
continue;
if (find_option_for_capability(type, preset, *ref).empty())
continue;
refs.push_back(*ref);
}
return refs;
}
namespace {
// Resolve one preset's referenced capabilities to loaded capability identifiers, appending to `out`.
// A ref not in the catalog, or whose capability is not loaded, is dropped: no instance means nothing
// to configure.
void collect_capabilities_in_use(Preset::Type type, const Preset& preset, std::vector<PluginCapabilityId>& out)
{
for (const PluginCapabilityRef& ref : referenced_capabilities(type, preset)) {
// Cloud plugins resolve by UUID, local plugins by plugin_key.
const std::string key = ref.uuid.empty() ? ref.name : ref.uuid;
if (key.empty())
continue;
PluginDescriptor descriptor;
if (!PluginManager::instance().try_get_plugin_descriptor(key, descriptor))
continue;
// 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->type() != PluginCapabilityType::Unknown &&
capability->name() == ref.capability_name &&
!find_option_for_capability(type, preset, ref, capability->type()).empty())
out.push_back(capability->identity());
}
}
} // namespace
std::vector<PluginCapabilityId> capabilities_in_use(const PresetBundle& preset_bundle, Preset::Type type)
{
if (!is_tracked_type(type))
return {};
std::vector<PluginCapabilityId> result;
if (type == Preset::TYPE_PRINT) {
collect_capabilities_in_use(type, preset_bundle.prints.get_edited_preset(), result);
} else if (type == Preset::TYPE_PRINTER) {
collect_capabilities_in_use(type, preset_bundle.printers.get_edited_preset(), result);
} else {
// Each filament preset is tested against its own config; refresh_missing_plugins cannot do
// that, as it unions the manifests and loses the preset.
for (const std::string& filament_name : preset_bundle.filament_presets)
if (const Preset* filament = preset_bundle.filaments.find_preset(filament_name))
collect_capabilities_in_use(type, *filament, result);
}
std::sort(result.begin(), result.end(), [](const PluginCapabilityId& a, const PluginCapabilityId& b) {
return std::tie(a.plugin_key, a.name, a.type) < std::tie(b.plugin_key, b.name, b.type);
});
result.erase(std::unique(result.begin(), result.end()), result.end());
return result;
}
std::vector<PluginCapabilityId> capabilities_in_use(Preset::Type type, const Preset& preset)
{
std::vector<PluginCapabilityId> result;
if (!is_tracked_type(type))
return result;
collect_capabilities_in_use(type, preset, result);
std::sort(result.begin(), result.end(), [](const PluginCapabilityId& a, const PluginCapabilityId& b) {
return std::tie(a.plugin_key, a.name, a.type) < std::tie(b.plugin_key, b.name, b.type);
});
result.erase(std::unique(result.begin(), result.end()), result.end());
return result;
}
static std::string resolve_cloud_base_url()
{
std::string cloud_base_url = "https://cloud.orcaslicer.com";
@@ -111,9 +208,7 @@ std::string resolve_recovery_url(const PluginCapabilityRef& ref)
return resolve_cloud_base_url() + "/app/plugins/plugin-hub?search=" + Http::url_encode(ref.name);
}
// Reports whether the loaded plugin currently exposes the referenced capability (in any enabled
// state) and whether that capability is enabled. Returns {false, false} when the plugin is not
// loaded or does not provide the capability.
// {present, enabled}; {false, false} when the plugin is not loaded or does not provide the capability.
static std::pair<bool, bool> loaded_capability_state(const std::string& plugin_key, const PluginCapabilityRef& ref)
{
PluginManager& mgr = PluginManager::instance();
@@ -123,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};
@@ -193,7 +288,7 @@ void refresh_missing_plugins(Preset::Type type, const ConfigOptionStrings* manif
continue;
if (!installed) {
// Not on disk — needs download/install (existing behavior).
// Not on disk — needs download/install.
std::string recovery_url = ref->uuid.empty() ? resolve_recovery_url(*ref) : std::string();
missing_set.emplace(entry, MissingPlugin{*ref, std::move(recovery_url), std::move(opt), type, PluginCapabilityType::Unknown});
} else if (!loaded || cap_present) {
@@ -287,7 +382,6 @@ static void report_install_failure(const std::string& message)
void resolve_missing_plugins(const std::vector<std::string>& refs, PluginInstallProgress progress)
{
// Collect the unique cloud UUIDs to install; local refs are handled via the browser flow.
std::vector<std::string> uuids;
for (const std::string& r : refs) {
const auto ref = parse_capability_ref(r);
@@ -311,7 +405,6 @@ void resolve_missing_plugins(const std::vector<std::string>& refs, PluginInstall
const std::string& uuid = uuids[i];
// Use a friendly name for the progress message when the catalog already knows it.
std::string display_name = uuid;
PluginDescriptor known;
if (mgr.try_get_plugin_descriptor(uuid, known) && !known.name.empty())
@@ -345,8 +438,7 @@ void resolve_inactive_plugins(const std::vector<std::string>& refs)
{
PluginManager& mgr = PluginManager::instance();
// Group the requested capabilities by owning plugin so each plugin is loaded once with the full
// set to enable.
// Group by owning plugin so each plugin is loaded once with the full set to enable.
std::map<std::string, std::vector<std::string>> by_plugin;
for (const std::string& r : refs) {
const auto ref = parse_capability_ref(r);
@@ -361,11 +453,9 @@ void resolve_inactive_plugins(const std::vector<std::string>& refs)
if (by_plugin.empty())
return;
// load_plugin loads+enables a not-loaded plugin (async) and enables the listed capabilities on an
// already-loaded one. The fresh-load path does NOT fire the capability-load callback the GUI uses
// to clear the notification, so wait for each load off the UI thread and then re-validate once —
// mirroring the cloud-install flow. This clears the inactive notification, or flips it to broken
// if the loaded plugin turns out not to provide the capability.
// The fresh-load path does NOT fire the capability-load callback the GUI uses to clear the
// notification, so wait for each load off the UI thread and re-validate once. That clears the
// inactive notification, or flips it to broken if the plugin does not provide the capability.
std::vector<std::pair<std::string, std::vector<std::string>>> work(by_plugin.begin(), by_plugin.end());
std::thread([work = std::move(work)]() {
PluginManager& mgr = PluginManager::instance();
@@ -383,7 +473,6 @@ void resolve_inactive_plugins(const std::vector<std::string>& refs)
void open_missing_plugins_on_cloud(const std::vector<std::string>& local_refs)
{
// One missing plugin: deep-link a search for it. Multiple: just open the plugin hub.
if (local_refs.size() == 1) {
if (const auto ref = parse_capability_ref(local_refs.front())) {
wxLaunchDefaultBrowser(GUI::from_u8(resolve_recovery_url(*ref)), wxBROWSER_NEW_WINDOW);
+27 -21
View File
@@ -4,7 +4,7 @@
#include <libslic3r/Config.hpp> // PluginCapabilityRef, parse_capability_ref
#include <libslic3r/Preset.hpp> // Preset::Type
#include <libslic3r/PresetBundle.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp> // PluginCapabilityType
#include <slic3r/plugin/PythonPluginInterface.hpp> // PluginCapabilityId, PluginCapabilityType
#include <cstddef>
#include <functional>
#include <string>
@@ -23,10 +23,9 @@ struct MissingPlugin
PluginCapabilityType type{PluginCapabilityType::Unknown};
};
// Rebuild the missing-plugin set owned by a single preset type from that preset's "plugins"
// manifest, comparing each ref against the live plugin catalog and loaded/enabled capabilities. A
// null/empty manifest clears the set for that type. Only TYPE_PRINT (process), TYPE_PRINTER
// (machine) and TYPE_FILAMENT are tracked; other types are ignored.
// Rebuild one preset type's missing-plugin set from its "plugins" manifest, comparing each ref
// against the live catalog and loaded/enabled capabilities. A null/empty manifest clears the set.
// Only TYPE_PRINT, TYPE_PRINTER and TYPE_FILAMENT are tracked.
void refresh_missing_plugins(Preset::Type type, const ConfigOptionStrings* manifest, const Preset* preset = nullptr);
void refresh_missing_plugins(const PresetBundle& preset_bundle);
@@ -36,22 +35,16 @@ std::vector<MissingPlugin> get_missing_cloud_plugins();
std::vector<MissingPlugin> get_missing_local_plugins();
bool has_missing_plugins();
// Installed-but-inactive capabilities: the plugin has a local package but the referenced capability
// is not active because the plugin is not loaded, or it is loaded but the capability is disabled.
// Resolved locally by loading the plugin and/or enabling the capability — no download.
// Installed-but-inactive: the plugin has a local package but is not loaded, or is loaded with the
// capability disabled. Resolved locally by loading and/or enabling — no download.
std::vector<MissingPlugin> get_inactive_plugins();
bool has_inactive_plugins();
// Broken references: the plugin is installed AND loaded but does not provide the referenced
// capability at all (renamed/removed/outdated plugin). Activation cannot fix these; surfaced as an
// informational notification pointing the user at OrcaCloud to update the plugin.
// capability at all (renamed/removed/outdated plugin). Activation cannot fix these.
std::vector<MissingPlugin> get_broken_plugins();
bool has_broken_plugins();
// Resolution actions invoked from the missing-plugin notifications:
// - cloud refs are subscribed/installed and loaded on a detached worker thread; failures are
// reported through a non-blocking notification. Non-cloud refs are ignored.
// Optional progress hook for the cloud install worker. All three callbacks fire on the worker
// thread; implementations must only touch thread-safe state or marshal to the UI thread.
struct PluginInstallProgress
@@ -64,23 +57,36 @@ struct PluginInstallProgress
std::function<void()> on_finished;
};
// Cloud refs only; local refs are handled via the browser flow. `progress` is optional — a
// default-constructed value preserves the previous silent behavior.
// Subscribe, install and load the cloud refs on a detached worker; failures are reported through a
// non-blocking notification. Local refs are ignored — they go through the browser flow below.
void resolve_missing_plugins(const std::vector<std::string>& refs,
PluginInstallProgress progress = {});
// Activate inactive plugins: load each referenced plugin (passing the capabilities to enable) and/or
// enable already-loaded-but-disabled capabilities. Local only — no network. The loads run on a
// background worker that waits for them and then re-validates the plate, clearing the notification
// (or reclassifying the ref as broken if the loaded plugin turns out not to provide the capability).
// Load each referenced plugin and/or enable its disabled capabilities. Local only — no network. The
// loads run on a background worker that waits for them and then re-validates the plate, clearing the
// notification (or reclassifying the ref as broken if the plugin does not provide the capability).
void resolve_inactive_plugins(const std::vector<std::string>& refs);
// - local refs are opened on the OrcaCloud plugin hub (search when exactly one ref, hub otherwise).
// Opens the OrcaCloud plugin hub (a search when there is exactly one ref, the hub otherwise).
void open_missing_plugins_on_cloud(const std::vector<std::string>& local_refs);
std::string create_full_ref(const PluginCapabilityRef& ref);
std::string resolve_recovery_url(const PluginCapabilityRef& ref);
// The capabilities `preset`'s "plugins" manifest declares AND that one of its plugin-backed options
// (ConfigOptionDef::is_plugin_backed) currently references: a manifest entry nobody points at is not
// in use. Pure preset logic — the catalog and loader are not consulted. Empty for untracked types.
std::vector<PluginCapabilityRef> referenced_capabilities(Preset::Type type, const Preset& preset);
std::vector<PluginCapabilityId> capabilities_in_use(Preset::Type type, const Preset& preset);
// The referenced capabilities of the active preset(s) of `type` that are loaded right now: the set
// that can actually be configured. Missing and broken refs are absent, having no instance to ask for
// a config UI or defaults. A loaded-but-disabled capability IS listed — it still has stored config
// worth editing.
std::vector<PluginCapabilityId> capabilities_in_use(const PresetBundle& preset_bundle, Preset::Type type);
bool check_capability_in_use(const std::string& capability_refs);
} // namespace Slic3r
#endif
+66 -15
View File
@@ -3,20 +3,21 @@
#include <pybind11/embed.h>
#include <boost/log/trivial.hpp>
#include <optional>
#include <stdexcept>
#include "PythonPluginInterface.hpp"
#include "PythonInterpreter.hpp"
#include "PluginFsUtils.hpp"
#include "PluginAuditManager.hpp"
// Trampoline variants of pybind11's override macros. Every C++->Python plugin call
// crosses through a trampoline method, so this single boundary is where we (1) log the
// full Python traceback (to sys.stderr -> session log) and rethrow the exception intact,
// and (2) open the plugin's filesystem audit scope for the duration of the call.
// We catch ONLY error_already_set (a Python-side raise); other pybind11_fail/runtime_error
// like a pure-virtual-missing failure must keep their own path and are deliberately not
// caught here.
// Trampoline variants of pybind11's override macros. Every C++->Python plugin call crosses a
// trampoline method, so this single boundary is where we (1) log the full Python traceback and
// rethrow the exception intact, and (2) open the plugin's filesystem audit scope for the call.
// We catch ONLY error_already_set (a Python-side raise); other pybind11_fail/runtime_error, such as
// a pure-virtual-missing failure, must keep their own path and are deliberately not caught here.
// Logs (and rethrows) a Python exception from a pybind11 override call, preserving the
// traceback. Internal helper shared by the public macros below and by trampolines that
@@ -29,13 +30,13 @@
throw; \
}
// 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`.
// Opens the plugin's filesystem audit scope for the duration of a C++ -> Python call, and publishes
// the calling capability's cached name so host APIs invoked from Python can tell which capability
// they are serving. No-op without an audit plugin key. 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->name(), mode)
#define ORCA_PY_OVERRIDE_AUDITED(mode, audit_setup, override_macro, ret, base, name, ...) \
do { \
@@ -55,13 +56,65 @@ template<class Base> class PyPluginCommonTrampoline : public Base
public:
using Base::Base;
// get_name is required on all capabilities — Python subclass must implement it.
std::string get_name() const override
{
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, Base, get_name);
}
// All plugins may define their own on_load/unload functions.
// Config UI hooks. Available on every capability type, so they live here rather than in
// PyPluginInterfaceTrampoline. A Python exception is rethrown; the caller decides the fallback.
bool has_config_ui() const override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {},
PYBIND11_OVERRIDE,
bool,
Base,
has_config_ui);
}
std::string get_config_ui() const override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {},
PYBIND11_OVERRIDE,
std::string,
Base,
get_config_ui);
}
// Hand-rolled rather than PYBIND11_OVERRIDE: the macro casts the Python result to the return
// type, and nlohmann::json has no pybind caster (config crosses this boundary through the
// explicit py_to_json/json_to_py helpers). Otherwise identical — same audit scope, same rethrow.
//
// The hook is optional, and "not implemented" must mean an EMPTY config: no override, or an
// override returning None or any non-object (`def get_default_config(self): pass` is the easy
// mistake), both fall back to the base's empty object rather than writing `"cap_config": null`.
nlohmann::json get_default_config() const override
{
ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
try {
pybind11::gil_scoped_acquire gil;
pybind11::function override = pybind11::get_override(static_cast<const Base*>(this), "get_default_config");
if (!override)
return Base::get_default_config();
nlohmann::json config = ::Slic3r::py_to_json(override());
if (!config.is_object()) {
BOOST_LOG_TRIVIAL(warning)
<< "Plugin capability '" << this->name() << "' of plugin '" << this->audit_plugin_key()
<< "': get_default_config() returned " << config.type_name() << ", not an object; restoring an empty config";
return Base::get_default_config();
}
return config;
} catch (pybind11::error_already_set& err) {
::Slic3r::log_python_exception_keep(err);
throw;
}
}
void on_load() override
{
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_load);
@@ -83,8 +136,6 @@ class PyPluginInterfaceTrampoline : public PyPluginCommonTrampoline<PluginCapabi
public:
using PyPluginCommonTrampoline<PluginCapabilityInterface>::PyPluginCommonTrampoline;
// get_name is implemented in PyPluginCommonTrampoline (PYBIND11_OVERRIDE_PURE).
PluginCapabilityType get_type() const override
{
ORCA_PY_OVERRIDE_AUDITED(
+57 -1
View File
@@ -1,8 +1,11 @@
#include "PythonPluginBridge.hpp"
#include <boost/log/trivial.hpp>
#include <exception>
#include <memory>
#include <mutex>
#include <slic3r/plugin/PluginAuditManager.hpp>
#include <string>
#include <unordered_map>
#include <pybind11/embed.h>
@@ -10,6 +13,8 @@
#include <pybind11/stl.h>
#include "PythonInterpreter.hpp"
#include "PluginFsUtils.hpp"
#include "PluginConfig.hpp"
#include "host/PluginHost.hpp"
#include "PyPluginPackage.hpp"
#include "PyPluginTrampoline.hpp"
@@ -347,12 +352,63 @@ void bind_python_api(pybind11::module_& m)
.def_static("skipped", &ExecutionResult::skipped, py::arg("message") = std::string())
.def_static("failure", &ExecutionResult::failure, py::arg("status"), py::arg("message"), py::arg("data") = std::string());
// Config lives at the capability level, not as a global orca.* function: the host reads the
// owning (plugin_key, capability) straight off the instance the call arrived on, so a
// capability can only ever address its own config and never has to name itself.
// Registered on the base, so every capability type (script/gcode/printer-agent) inherits it.
py::class_<PluginCapabilityInterface, PyPluginInterfaceTrampoline, std::shared_ptr<PluginCapabilityInterface>>(m, "PythonPluginBase")
.def(py::init<>())
.def("get_name", &PluginCapabilityInterface::get_name)
.def("get_type", &PluginCapabilityInterface::get_type)
.def("on_load", &PluginCapabilityInterface::on_load)
.def("on_unload", &PluginCapabilityInterface::on_unload);
.def("on_unload", &PluginCapabilityInterface::on_unload)
.def("has_config_ui", &PluginCapabilityInterface::has_config_ui,
"Override to return True to replace the host's default JSON editor with your own HTML\n"
"UI, returned by get_config_ui(). Every capability is configurable and appears in the\n"
"Plugins dialog's Config tab regardless; this only chooses how its config is edited.")
.def("get_config_ui", &PluginCapabilityInterface::get_config_ui,
"Override to return the custom configuration UI as an HTML string. Only called when\n"
"has_config_ui() is True; an empty result falls back to the default JSON editor.\n"
"Inside the page, use window.orca.getConfig()/saveConfig() to reach this same config.")
.def(
"get_default_config",
[](const PluginCapabilityInterface& self) {
nlohmann::json config = self.get_default_config();
return config.dump();
},
"Override to return the config that the Config tab's \"Restore defaults\" action writes\n"
"back, as a dict. Optional: without it the action stores an empty config, which already\n"
"restores the defaults of a capability that keeps its stored config sparse and applies\n"
"its own defaults on read. Override it to write an explicit starting config instead.\n"
"Calling it returns that config as a JSON string.")
.def(
"get_config",
[](const PluginCapabilityInterface& self) {
nlohmann::json config = capability_get_config(self);
return config.dump();
},
"Return this capability's stored config as a JSON string — json.loads() it to use.\n"
"\"{}\" if it has never been saved, so the parsed result is always indexable.")
.def(
"get_config_version", [](const PluginCapabilityInterface& self) { return capability_get_config_version(self); },
"Return the plugin version that last wrote this capability's config, so a newer\n"
"release can spot a stale config and migrate it. Empty string if never saved.")
.def(
"save_config",
[](const PluginCapabilityInterface& self, const std::string& config_str) {
nlohmann::json config = nlohmann::json::parse(config_str, nullptr, /* allow_exceptions */ false);
if (config.is_discarded()) {
// Refused rather than stored: the caller gets False, and the previously stored
// config is left alone. Logged because False alone does not say why.
BOOST_LOG_TRIVIAL(error) << "save_config: capability '" << self.get_name() << "' passed a config that is not valid JSON";
return false;
}
return capability_save_config(self, config);
},
py::arg("config"),
"Persist this capability's config, given as a JSON string (e.g. json.dumps(cfg)).\n"
"The plugin key, capability name and version are supplied by the host. Returns False if\n"
"the string is not valid JSON, or if the config file could not be written.");
// Expose the package marker base as orca.base. @orca.plugin later verifies that the
// decorated class derives from this exact pybind-registered C++ type.
+47 -1
View File
@@ -7,12 +7,34 @@
#include <string_view>
#include <utility>
#include <nlohmann/json.hpp>
#include <pybind11/embed.h>
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) {
@@ -127,6 +149,20 @@ public:
virtual std::string get_name() const = 0; // required — overridden in Python
virtual PluginCapabilityType get_type() const { return PluginCapabilityType::Unknown; } // optional — typed bases override
// Every capability is configurable and always gets the host's default JSON editor over its
// stored config; this only says whether it supplies its own UI *instead of* that editor.
// get_config_ui() is called only when it returns true.
virtual bool has_config_ui() const { return false; }
// An HTML snippet for the custom configuration UI. An empty or throwing result is treated as
// "no custom UI" and falls back to the default JSON editor.
virtual std::string get_config_ui() const { return ""; }
// The config the "Restore defaults" action writes back. Not overridden -> an empty object, which
// is right for a capability that keeps its stored config sparse and applies its own defaults on
// read. Override it to write an explicit starting config instead (e.g. to seed a form UI with
// every field present). The host neither invents nor validates this value.
virtual nlohmann::json get_default_config() const { return nlohmann::json::object(); }
virtual void on_load() {}
virtual void on_unload() {}
virtual void on_cancelled() {}
@@ -137,9 +173,12 @@ public:
// exactly as long as the capability does, and are discarded with it on unload. Nothing about a
// capability outlives the capability — the durable record is the .install_state.json sidecar.
// Cached identity. Plain C++ reads, safe under any lock and after the interpreter is gone.
// Cached identity. Plain C++ reads, safe under any lock and after the interpreter is gone. Also
// doubles as the audited capability name (paired with audit_plugin_key()) so host APIs invoked
// 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);
@@ -152,6 +191,12 @@ public:
bool is_enabled() const { return m_enabled.load(std::memory_order_acquire); }
void set_enabled(bool enabled) { m_enabled.store(enabled, std::memory_order_release); }
// Whether this capability supplies its own config UI (has_config_ui()), resolved once at
// materialization under the GIL and cached here. Plain C++ read so the GUI can decide between
// the capability's custom UI and the host's default JSON editor without touching Python.
bool config_ui_available() const { return m_config_ui_available; }
void set_config_ui_available(bool available) { m_config_ui_available = available; }
// The owning package (PluginDescriptor::plugin_key), the canonical runtime id. Also scopes
// filesystem enforcement for trampoline calls.
void set_audit_plugin_key(std::string key) { m_audit_plugin_key = std::move(key); }
@@ -165,6 +210,7 @@ private:
std::string m_name;
PluginCapabilityType m_type = PluginCapabilityType::Unknown;
std::atomic<bool> m_enabled{true};
bool m_config_ui_available = false;
std::string m_audit_plugin_key;
mutable std::atomic<int> m_refs{0};
+1 -57
View File
@@ -2,6 +2,7 @@
#include "slic3r/plugin/PluginAuditManager.hpp"
#include "slic3r/plugin/PythonInterpreter.hpp" // PythonGILState
#include "slic3r/plugin/PluginFsUtils.hpp" // json_to_py / py_to_json
#include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/GUI/MainFrame.hpp>
@@ -34,63 +35,6 @@ using json = nlohmann::json;
namespace Slic3r {
namespace {
// --------------------------------------------------------------------------
// JSON <-> Python conversion (caller must hold the GIL).
// --------------------------------------------------------------------------
py::object json_to_py(const json& j)
{
switch (j.type()) {
case json::value_t::null: return py::none();
case json::value_t::boolean: return py::bool_(j.get<bool>());
case json::value_t::number_integer: return py::int_(j.get<std::int64_t>());
case json::value_t::number_unsigned: return py::int_(j.get<std::uint64_t>());
case json::value_t::number_float: return py::float_(j.get<double>());
case json::value_t::string: return py::str(j.get<std::string>());
case json::value_t::array: {
py::list lst;
for (const auto& e : j)
lst.append(json_to_py(e));
return lst;
}
case json::value_t::object: {
py::dict d;
for (auto it = j.begin(); it != j.end(); ++it)
d[py::str(it.key())] = json_to_py(it.value());
return d;
}
default: return py::none();
}
}
json py_to_json(const py::handle& o)
{
if (o.is_none())
return json(nullptr);
if (py::isinstance<py::bool_>(o)) // bool before int (bool subclasses int in Python)
return o.cast<bool>();
if (py::isinstance<py::int_>(o))
return o.cast<std::int64_t>();
if (py::isinstance<py::float_>(o))
return o.cast<double>();
if (py::isinstance<py::str>(o))
return o.cast<std::string>();
if (py::isinstance<py::bytes>(o))
return o.cast<std::string>();
if (py::isinstance<py::dict>(o)) {
json obj = json::object();
for (auto item : py::reinterpret_borrow<py::dict>(o))
obj[py::str(item.first).cast<std::string>()] = py_to_json(item.second);
return obj;
}
if (py::isinstance<py::list>(o) || py::isinstance<py::tuple>(o)) {
json arr = json::array();
for (auto e : o)
arr.push_back(py_to_json(e));
return arr;
}
return py::str(o).cast<std::string>(); // fallback: str()
}
// --------------------------------------------------------------------------
// GIL-safe holder for a Python callable. A std::function that captured a bare
// py::object could be destroyed on the main thread without the GIL (a dialog
@@ -2,7 +2,6 @@
#include "SlicingPipelinePluginCapabilityTrampoline.hpp"
#include "slic3r/plugin/PluginBindingUtils.hpp" // config_value_or_none
#include "libslic3r/libslic3r.h" // unscale<>, live SCALING_FACTOR
#include <pybind11/stl.h> // std::map<std::string,std::string> -> dict for ctx.params
namespace py = pybind11;
namespace Slic3r {
@@ -31,7 +30,7 @@ void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module, py::
// ctx.object are None; instead ctx.gcode_path / ctx.host / ctx.output_name are set and the plugin
// edits the file at ctx.gcode_path IN PLACE. May fire more than once per slice (file export and/or
// upload each fire once, on separate working copies) and its output is not reflected in the G-code
// preview (the viewer maps the pre-post-process file). ctx.config_value()/ctx.params still work.
// preview (the viewer maps the pre-post-process file). ctx.config_value() still works.
.value("psGCodePostProcess", SlicingPipelineStepPlugin::psGCodePostProcess)
.export_values();
@@ -48,9 +47,6 @@ void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module, py::
py::class_<SlicingPipelineContext>(slicing, "SlicingPipelineContext")
.def_readonly("orca_version", &SlicingPipelineContext::orca_version)
.def_readonly("step", &SlicingPipelineContext::step)
.def_readonly("params", &SlicingPipelineContext::params,
"read-only dict of this plugin's [tool.orcaslicer.plugin.settings] values "
"(string->string). Parse the values you need, e.g. float(ctx.params['rate']).")
.def_readonly("gcode_path", &SlicingPipelineContext::gcode_path,
"Path to the working G-code file, set ONLY at Step.psGCodePostProcess. Edit it in "
"place; empty at every other step.")
@@ -18,10 +18,6 @@ struct SlicingPipelineContext {
SlicingPipelineStepPlugin step { SlicingPipelineStepPlugin::posSlice };
Print* print { nullptr }; // present for in-pipeline steps; null at psGCodePostProcess
const PrintObject* object { nullptr }; // null for print-wide steps and psGCodePostProcess
// read-only per-plugin settings, populated by the dispatcher from the
// plugin's [tool.orcaslicer.plugin.settings] PEP-723 table. Exposed as
// ctx.params (dict of string->string).
std::map<std::string, std::string> params;
// Populated ONLY at Step.psGCodePostProcess (the GUI G-code export/post-process seam,
// PostProcessor.cpp). gcode_path is the working G-code file on disk that the plugin edits
// in place; host is the target ("File", "OctoPrint", ...); output_name mirrors