mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 10:51:22 +00:00
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:
@@ -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())
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user