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