mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-05 01:02:08 +00:00
feat: Python Plugins
This commit is contained in:
347
src/slic3r/plugin/CloudPluginService.cpp
Normal file
347
src/slic3r/plugin/CloudPluginService.cpp
Normal file
@@ -0,0 +1,347 @@
|
||||
#include "CloudPluginService.hpp"
|
||||
|
||||
#include "OrcaCloudServiceAgent.hpp"
|
||||
#include "slic3r/Utils/Http.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <cstddef>
|
||||
#include <slic3r/plugin/PluginDescriptor.hpp>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void CloudPluginService::set_cloud_agent(std::shared_ptr<OrcaCloudServiceAgent> agent) { m_orca_agent = std::move(agent); }
|
||||
|
||||
std::shared_ptr<OrcaCloudServiceAgent> CloudPluginService::get_cloud_agent() const { return m_orca_agent; }
|
||||
|
||||
bool CloudPluginService::can_fetch_cloud_plugins() const
|
||||
{
|
||||
if (!m_orca_agent) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Orca service agent is null";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_orca_agent->is_user_login() || m_orca_agent->get_user_id().empty()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "User not logged in, no cloud directory";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CloudPluginService::fetch_manifests_into_descriptors(std::vector<PluginDescriptor>& descriptors,
|
||||
std::vector<std::string>& not_found,
|
||||
std::vector<std::string>& unauthorized) const
|
||||
{
|
||||
descriptors.clear();
|
||||
not_found.clear();
|
||||
unauthorized.clear();
|
||||
|
||||
if (m_orca_agent) {
|
||||
int ret = m_orca_agent->fetch_subscribed_manifests_into_descriptors(descriptors, not_found, unauthorized);
|
||||
|
||||
if (ret != 0) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to get subscribed plugins.";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<PluginDescriptor> mine_descriptors;
|
||||
|
||||
ret = m_orca_agent->fetch_mine_manifests_into_descriptors(mine_descriptors);
|
||||
|
||||
if (ret != 0) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to get owned plugins.";
|
||||
return false;
|
||||
}
|
||||
|
||||
descriptors.insert(descriptors.end(), mine_descriptors.begin(), mine_descriptors.end());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CloudPluginService::request_cloud_subscribe(const std::string& plugin_uuid, std::string& error) const
|
||||
{
|
||||
error.clear();
|
||||
if (!m_orca_agent) {
|
||||
error = "No cloud agent.";
|
||||
return false;
|
||||
}
|
||||
if (plugin_uuid.empty()) {
|
||||
error = "Cloud plugin key is missing UUID.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_orca_agent->subscribe_plugin(plugin_uuid) != 0) {
|
||||
error = "Failed to subscribe to cloud plugin, see logs for more info.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CloudPluginService::request_cloud_unsubscribe(const PluginDescriptor& plugin, std::string& error) const
|
||||
{
|
||||
if (!m_orca_agent) {
|
||||
error = "No cloud agent.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!plugin.is_cloud_plugin()) {
|
||||
error = "Only cloud plugins can be unsubscribed.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string cloud_uuid = plugin.cloud_uuid();
|
||||
if (cloud_uuid.empty()) {
|
||||
error = "Cloud plugin key is missing UUID.";
|
||||
return false;
|
||||
}
|
||||
|
||||
int result = m_orca_agent->unsubscribe_plugins({cloud_uuid});
|
||||
|
||||
if (result != 0) {
|
||||
error = "Failed to unsubscribe plugin, see logs for more info.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CloudPluginService::request_cloud_delete(const PluginDescriptor& plugin, std::string& error) const
|
||||
{
|
||||
if (!m_orca_agent) {
|
||||
error = "No cloud agent.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!plugin.is_cloud_plugin()) {
|
||||
error = "Only cloud plugins can be deleted.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string cloud_uuid = plugin.cloud_uuid();
|
||||
if (cloud_uuid.empty()) {
|
||||
error = "Cloud plugin key is missing UUID.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!plugin.cloud.has_value() || !plugin.cloud->is_mine) {
|
||||
error = "Only your own plugins can be deleted from the cloud.";
|
||||
return false;
|
||||
}
|
||||
|
||||
int result = m_orca_agent->delete_my_plugin(cloud_uuid);
|
||||
|
||||
if (result != 0) {
|
||||
error = "Failed to delete plugin from cloud, see logs for more info.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CloudPluginService::download_cloud_plugin(PluginDescriptor& entry,
|
||||
const std::string& requested_version,
|
||||
CloudPluginDownload& download,
|
||||
std::string& error) const
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
error.clear();
|
||||
download = {};
|
||||
|
||||
// Look up the cloud plugin metadata in the catalog.
|
||||
std::string download_url;
|
||||
if (!entry.is_cloud_plugin()) {
|
||||
error = "Plugin is not a cloud plugin";
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string entry_uuid = entry.cloud_uuid();
|
||||
|
||||
if (!m_orca_agent) {
|
||||
error = "Cloud service agent is null";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<PluginDownloadData> data;
|
||||
std::vector<PluginDownloadNotFound> not_found;
|
||||
std::vector<std::string> unauthorized;
|
||||
|
||||
int result = m_orca_agent->get_plugin_download_url(entry_uuid, requested_version, data, not_found, unauthorized);
|
||||
if (result != 0) {
|
||||
error = "Failed to fetch download_url, result =" + std::to_string(result);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const std::string& plugin_uuid : unauthorized) {
|
||||
if (plugin_uuid == entry_uuid) {
|
||||
error = "You are not authorized to download this cloud plugin.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const PluginDownloadNotFound& item : not_found) {
|
||||
if (item.id == entry_uuid) {
|
||||
if (item.reason == "requested os not found")
|
||||
error = "No plugin package is available for this operating system.";
|
||||
else if (item.reason == "requested version not found")
|
||||
error = requested_version.empty() ? "The selected plugin version was not found." :
|
||||
"Plugin version " + requested_version + " was not found.";
|
||||
else if (item.reason == "requested plugin not found")
|
||||
error = "Cloud plugin was not found.";
|
||||
else if (!item.reason.empty())
|
||||
error = "Cloud plugin download was not found: " + item.reason + ".";
|
||||
else
|
||||
error = "Cloud plugin download was not found.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const PluginDownloadData& item : data) {
|
||||
if (item.plugin_id == entry_uuid) {
|
||||
download_url = item.download_link;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (download_url.empty()) {
|
||||
error = "No download URL is available for this plugin. Please "
|
||||
"check the logs for errors.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Download the plugin package.
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "Downloading cloud plugin " << entry_uuid << " from " << download_url;
|
||||
|
||||
std::string body;
|
||||
unsigned http_status = 0;
|
||||
|
||||
Http http = Http::get(download_url);
|
||||
http.timeout_connect(30)
|
||||
.timeout_max(300)
|
||||
.on_complete([&body, &http_status](std::string response_body, unsigned status) {
|
||||
body = std::move(response_body);
|
||||
http_status = status;
|
||||
})
|
||||
.on_error([&error](std::string response_body, std::string err, unsigned status) {
|
||||
error = std::move(err);
|
||||
if (!response_body.empty())
|
||||
error += " — " + response_body;
|
||||
})
|
||||
.perform_sync();
|
||||
|
||||
if (!error.empty()) {
|
||||
error = "Failed to download plugin: " + error;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (http_status >= 400) {
|
||||
error = "Plugin download failed with HTTP status " + std::to_string(http_status);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (body.empty()) {
|
||||
error = "Plugin download returned empty data.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Detect file format from content: PK magic bytes = zip/wheel, otherwise .py.
|
||||
const bool is_wheel = (body.size() >= 4 && body[0] == 'P' && body[1] == 'K' && body[2] == '\x03' && body[3] == '\x04');
|
||||
const std::string ext = is_wheel ? ".whl" : ".py";
|
||||
|
||||
const fs::path tmp_path = fs::temp_directory_path() / (fs::unique_path("cloud_plugin-%%%%-%%%%-%%%%-%%%%%%%").string() + ext);
|
||||
{
|
||||
boost::nowide::ofstream file(tmp_path.string(), std::ios::binary | std::ios::trunc);
|
||||
if (!file) {
|
||||
error = "Failed to create temporary file for plugin download.";
|
||||
return false;
|
||||
}
|
||||
file.write(body.data(), body.size());
|
||||
if (!file) {
|
||||
error = "Failed to write plugin data to temporary file.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
download.package_path = tmp_path;
|
||||
|
||||
if (entry.cloud.has_value())
|
||||
entry.cloud->update_available = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CloudPluginService::fetch_plugin_changelog(const PluginDescriptor& descriptor,
|
||||
std::vector<PluginChangelog>& changelog,
|
||||
std::string& error) const
|
||||
{
|
||||
error.clear();
|
||||
changelog.clear();
|
||||
|
||||
std::string plugin_uuid = descriptor.cloud_uuid();
|
||||
std::vector<PluginDescriptor> descriptors{descriptor};
|
||||
std::unordered_map<std::string, std::vector<PluginChangelog>> changelogs;
|
||||
|
||||
bool result = fetch_plugin_changelog(descriptors, changelogs, error);
|
||||
|
||||
bool found_changelog = changelogs.find(plugin_uuid) != changelogs.end();
|
||||
|
||||
if (changelogs.empty() || !found_changelog) {
|
||||
if (!error.empty())
|
||||
return false;
|
||||
error = "Failed to fetch changelogs for plugin " + descriptor.cloud_uuid();
|
||||
return false;
|
||||
}
|
||||
|
||||
changelog = std::move(changelogs[plugin_uuid]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool CloudPluginService::fetch_plugin_changelog(const std::vector<PluginDescriptor>& descriptors,
|
||||
std::unordered_map<std::string, std::vector<PluginChangelog>>& changelog,
|
||||
std::string& error) const
|
||||
{
|
||||
error.clear();
|
||||
changelog.clear();
|
||||
|
||||
if (!m_orca_agent) {
|
||||
error = "Cloud service agent is null.";
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t descriptor_count = descriptors.size();
|
||||
|
||||
if (descriptor_count <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::string> uuids;
|
||||
uuids.reserve(descriptor_count);
|
||||
for (const PluginDescriptor& descriptor : descriptors) {
|
||||
const std::string uuid = descriptor.cloud_uuid();
|
||||
if (!uuid.empty())
|
||||
uuids.push_back(uuid);
|
||||
}
|
||||
|
||||
if (uuids.empty())
|
||||
return true;
|
||||
|
||||
int result = m_orca_agent->fetch_plugin_changelogs(uuids, changelog);
|
||||
|
||||
if (result != 0) {
|
||||
error = "Failed to fetch one or more plugin changelogs. result=" + std::to_string(result);
|
||||
BOOST_LOG_TRIVIAL(warning) << error;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
46
src/slic3r/plugin/CloudPluginService.hpp
Normal file
46
src/slic3r/plugin/CloudPluginService.hpp
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include "PluginDescriptor.hpp"
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class OrcaCloudServiceAgent;
|
||||
|
||||
struct CloudPluginDownload
|
||||
{
|
||||
boost::filesystem::path package_path;
|
||||
};
|
||||
|
||||
class CloudPluginService
|
||||
{
|
||||
public:
|
||||
void set_cloud_agent(std::shared_ptr<OrcaCloudServiceAgent> agent);
|
||||
std::shared_ptr<OrcaCloudServiceAgent> get_cloud_agent() const;
|
||||
bool can_fetch_cloud_plugins() const;
|
||||
bool fetch_manifests_into_descriptors(std::vector<PluginDescriptor>& descriptors,
|
||||
std::vector<std::string>& not_found,
|
||||
std::vector<std::string>& unauthorized) const;
|
||||
bool request_cloud_subscribe(const std::string& plugin_uuid, std::string& error) const;
|
||||
bool request_cloud_unsubscribe(const PluginDescriptor& plugin, std::string& error) const;
|
||||
bool request_cloud_delete(const PluginDescriptor& plugin, std::string& error) const;
|
||||
bool download_cloud_plugin(PluginDescriptor& entry,
|
||||
const std::string& requested_version,
|
||||
CloudPluginDownload& download,
|
||||
std::string& error) const;
|
||||
bool fetch_plugin_changelog(const PluginDescriptor& descriptor, std::vector<PluginChangelog>& changelog, std::string& error) const;
|
||||
bool fetch_plugin_changelog(const std::vector<PluginDescriptor>& descriptors,
|
||||
std::unordered_map<std::string, std::vector<PluginChangelog>>& changelog,
|
||||
std::string& error) const;
|
||||
|
||||
private:
|
||||
std::shared_ptr<OrcaCloudServiceAgent> m_orca_agent = nullptr;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
281
src/slic3r/plugin/PluginAuditManager.cpp
Normal file
281
src/slic3r/plugin/PluginAuditManager.cpp
Normal file
@@ -0,0 +1,281 @@
|
||||
#include "PluginAuditManager.hpp"
|
||||
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Path safety
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool is_inside_allowed_root(const std::filesystem::path& candidate, const std::filesystem::path& allowed_root)
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
std::error_code ec;
|
||||
|
||||
// Canonicalize both paths. weakly_canonical resolves symlinks but does
|
||||
// NOT require the path to exist — it canonicalizes the prefix that exists
|
||||
// and appends the non-existing tail lexically.
|
||||
fs::path canon_candidate = fs::weakly_canonical(candidate, ec);
|
||||
if (ec) {
|
||||
// Fall back to lexically_normal + absolute
|
||||
canon_candidate = fs::absolute(candidate, ec).lexically_normal();
|
||||
if (ec)
|
||||
canon_candidate = candidate;
|
||||
}
|
||||
|
||||
fs::path canon_root = fs::weakly_canonical(allowed_root, ec);
|
||||
if (ec) {
|
||||
canon_root = fs::absolute(allowed_root, ec).lexically_normal();
|
||||
if (ec)
|
||||
canon_root = allowed_root;
|
||||
}
|
||||
|
||||
// Component-wise comparison: the root must be a prefix of candidate,
|
||||
// and the next component must not be ".." or missing.
|
||||
auto cand_it = canon_candidate.begin();
|
||||
auto cand_end = canon_candidate.end();
|
||||
auto root_it = canon_root.begin();
|
||||
auto root_end = canon_root.end();
|
||||
|
||||
// Consume matching components
|
||||
while (root_it != root_end && cand_it != cand_end && *root_it == *cand_it) {
|
||||
++root_it;
|
||||
++cand_it;
|
||||
}
|
||||
|
||||
// If we didn't consume the entire root, candidate is not inside it.
|
||||
if (root_it != root_end)
|
||||
return false;
|
||||
|
||||
// The remaining path components must not traverse upward.
|
||||
for (auto it = cand_it; it != cand_end; ++it) {
|
||||
if (*it == "..")
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ScopedPluginAuditContext
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
thread_local std::string PluginAuditManager::m_current_plugin_key = "";
|
||||
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 bool PluginAuditManager::m_has_last_violation = false;
|
||||
thread_local AuditViolation PluginAuditManager::m_last_violation;
|
||||
|
||||
ScopedPluginAuditContext::ScopedPluginAuditContext(const std::string& plugin_key, PluginAuditManager::AuditMode mode)
|
||||
: m_previous_id(PluginAuditManager::instance().current_plugin())
|
||||
, 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_audit_mode(mode);
|
||||
PluginAuditManager::m_scoped_allowed_roots.clear();
|
||||
}
|
||||
|
||||
ScopedPluginAuditContext::~ScopedPluginAuditContext()
|
||||
{
|
||||
PluginAuditManager::instance().set_current_plugin(m_previous_id);
|
||||
PluginAuditManager::instance().set_audit_mode(m_previous_mode);
|
||||
PluginAuditManager::m_scoped_allowed_roots = std::move(m_previous_scoped_roots);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PluginAuditManager
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
PluginAuditManager& PluginAuditManager::instance()
|
||||
{
|
||||
static PluginAuditManager mgr;
|
||||
return mgr;
|
||||
}
|
||||
|
||||
void PluginAuditManager::set_current_plugin(const std::string& plugin_key) { m_current_plugin_key = plugin_key; }
|
||||
|
||||
std::string PluginAuditManager::current_plugin() const { return m_current_plugin_key; }
|
||||
|
||||
void PluginAuditManager::clear_current_plugin() { m_current_plugin_key.clear(); }
|
||||
|
||||
void PluginAuditManager::add_global_allowed_root(const std::filesystem::path& root)
|
||||
{
|
||||
if (root.empty())
|
||||
return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_global_allowed_roots.push_back(root);
|
||||
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Global allowed root: " << root.string();
|
||||
}
|
||||
|
||||
void PluginAuditManager::add_scoped_allowed_root(const std::filesystem::path& root)
|
||||
{
|
||||
if (root.empty())
|
||||
return;
|
||||
|
||||
m_scoped_allowed_roots.push_back(root);
|
||||
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Scoped allowed root for plugin " << current_plugin() << ": " << root.string();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Audit mode
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void PluginAuditManager::set_audit_mode(AuditMode mode) { m_audit_mode = mode; }
|
||||
|
||||
PluginAuditManager::AuditMode PluginAuditManager::audit_mode() const { return m_audit_mode; }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Policy checks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
AuditDecision PluginAuditManager::check_open(const std::string& path_str, const std::string& mode)
|
||||
{
|
||||
if (path_str.empty())
|
||||
return {true, ""};
|
||||
|
||||
std::string plugin_key = current_plugin();
|
||||
if (plugin_key.empty())
|
||||
return {true, ""}; // not running inside a plugin context
|
||||
|
||||
// During import/loading, only block writes. Python must be able to read
|
||||
// stdlib modules and the plugin file itself during import.
|
||||
if (m_audit_mode == AuditMode::Loading) {
|
||||
bool is_write = (mode.find('w') != std::string::npos || mode.find('a') != std::string::npos || mode.find('+') != std::string::npos);
|
||||
if (!is_write)
|
||||
return {true, ""};
|
||||
}
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
fs::path candidate(path_str);
|
||||
|
||||
// Resolve relative paths against the current working directory
|
||||
if (candidate.is_relative()) {
|
||||
std::error_code ec;
|
||||
candidate = fs::absolute(candidate, ec);
|
||||
if (ec)
|
||||
candidate = fs::path(path_str);
|
||||
}
|
||||
|
||||
for (const auto& root : m_scoped_allowed_roots) {
|
||||
if (is_inside_allowed_root(candidate, root)) {
|
||||
return {true, ""};
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (const auto& root : m_global_allowed_roots) {
|
||||
if (is_inside_allowed_root(candidate, root)) {
|
||||
return {true, ""};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << candidate.string() << " open_mode=" << mode
|
||||
<< " audit_mode=" << (m_audit_mode == AuditMode::Loading ? "Loading" : "Enforcing")
|
||||
<< " plugin=" << plugin_key;
|
||||
return {false, "outside allowed root"};
|
||||
}
|
||||
|
||||
void PluginAuditManager::report_violation(const AuditViolation& violation)
|
||||
{
|
||||
m_last_violation = violation;
|
||||
m_has_last_violation = true;
|
||||
|
||||
BOOST_LOG_TRIVIAL(warning) << "[AUDIT BLOCKED] plugin=" << violation.plugin_key << " event=" << violation.event_name
|
||||
<< " path=" << violation.path.string() << " reason=" << violation.reason;
|
||||
}
|
||||
|
||||
void PluginAuditManager::clear_last_violation()
|
||||
{
|
||||
m_has_last_violation = false;
|
||||
m_last_violation = AuditViolation{};
|
||||
}
|
||||
|
||||
bool PluginAuditManager::last_violation(AuditViolation& violation) const
|
||||
{
|
||||
if (!m_has_last_violation)
|
||||
return false;
|
||||
|
||||
violation = m_last_violation;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The C-level audit hook
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user_data)
|
||||
{
|
||||
auto* mgr = static_cast<PluginAuditManager*>(user_data);
|
||||
if (!mgr)
|
||||
return 0;
|
||||
|
||||
std::string event_name(event ? event : "");
|
||||
|
||||
// Verbose logging of every audit event (can be noisy)
|
||||
if (mgr->verbose_events) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "[AUDIT EVENT] " << event_name;
|
||||
}
|
||||
|
||||
// extensive list of audit events can be found at https://docs.python.org/3/library/audit_events.html
|
||||
|
||||
// --- open event ---
|
||||
if (event_name == "open") {
|
||||
const char* path_cstr = nullptr;
|
||||
const char* mode_cstr = nullptr;
|
||||
int flags = 0;
|
||||
|
||||
// open(path, mode, flags) — path may be str, bytes, or int fd
|
||||
if (!PyArg_ParseTuple(args, "s|si", &path_cstr, &mode_cstr, &flags)) {
|
||||
PyErr_Clear(); // couldn't parse; allow
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string path_str(path_cstr ? path_cstr : "");
|
||||
std::string mode_str(mode_cstr ? mode_cstr : "r");
|
||||
|
||||
AuditDecision decision = mgr->check_open(path_str, mode_str);
|
||||
if (!decision.allowed) {
|
||||
AuditViolation violation;
|
||||
violation.plugin_key = mgr->current_plugin();
|
||||
violation.event_name = event_name;
|
||||
violation.path = path_str;
|
||||
violation.reason = decision.reason;
|
||||
mgr->report_violation(violation);
|
||||
|
||||
PyErr_SetString(PyExc_PermissionError, "Plugin attempted to access a blocked file path");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Unknown event — allow by default
|
||||
return 0;
|
||||
}
|
||||
|
||||
void PluginAuditManager::install_hook()
|
||||
{
|
||||
if (PySys_AddAuditHook(audit_hook, this) < 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install CPython audit hook";
|
||||
return;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "[AUDIT] CPython audit hook installed successfully";
|
||||
|
||||
// data_dir() is the only globally-allowed root during enforced plugin execution.
|
||||
// The executable directory and resources directory are intentionally NOT allowed
|
||||
// here: plugins must not write outside data_dir() (G-code plugins additionally get
|
||||
// the temp G-code folder via a scoped root). Reads remain permissive in Loading mode.
|
||||
add_global_allowed_root(data_dir());
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
108
src/slic3r/plugin/PluginAuditManager.hpp
Normal file
108
src/slic3r/plugin/PluginAuditManager.hpp
Normal file
@@ -0,0 +1,108 @@
|
||||
#ifndef slic3r_PluginAuditManager_hpp_
|
||||
#define slic3r_PluginAuditManager_hpp_
|
||||
|
||||
#include <Python.h>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
struct AuditDecision {
|
||||
bool allowed = true;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct AuditViolation {
|
||||
std::string plugin_key;
|
||||
std::string event_name;
|
||||
std::filesystem::path path;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
// Returns true if candidate resolves to a path inside allowed_root.
|
||||
// Uses weakly_canonical and component-wise comparison to reject traversal attacks.
|
||||
bool is_inside_allowed_root(const std::filesystem::path& candidate,
|
||||
const std::filesystem::path& allowed_root);
|
||||
|
||||
class PluginAuditManager
|
||||
{
|
||||
public:
|
||||
static PluginAuditManager& instance();
|
||||
|
||||
// Call once after Py_Initialize to install the global audit hook.
|
||||
void install_hook();
|
||||
|
||||
// --- current-plugin context (thread_local) ---
|
||||
void set_current_plugin(const std::string& plugin_key);
|
||||
std::string current_plugin() const;
|
||||
void clear_current_plugin();
|
||||
|
||||
// --- allowed-roots registry ---
|
||||
void add_global_allowed_root(const std::filesystem::path& root);
|
||||
void add_scoped_allowed_root(const std::filesystem::path& root);
|
||||
|
||||
// --- enforcement mode ---
|
||||
enum class AuditMode {
|
||||
// Import/loading phase: allow reads anywhere, only block writes
|
||||
// outside allowed roots. Python needs to read stdlib modules
|
||||
// during import and those are not inside plugin directories.
|
||||
Loading,
|
||||
|
||||
// Execution phase: block both reads and writes outside allowed
|
||||
// roots, plus subprocess/socket/ctypes.
|
||||
Enforcing,
|
||||
};
|
||||
|
||||
void set_audit_mode(AuditMode mode);
|
||||
AuditMode audit_mode() const;
|
||||
|
||||
// --- policy checks ---
|
||||
AuditDecision check_open(const std::string& path, const std::string& mode);
|
||||
|
||||
void report_violation(const AuditViolation& violation);
|
||||
void clear_last_violation();
|
||||
bool last_violation(AuditViolation& violation) const;
|
||||
|
||||
bool verbose_events = true;
|
||||
|
||||
private:
|
||||
friend class ScopedPluginAuditContext;
|
||||
|
||||
PluginAuditManager() = default;
|
||||
|
||||
static int audit_hook(const char* event, PyObject* args, void* user_data);
|
||||
|
||||
static thread_local std::string m_current_plugin_key;
|
||||
static thread_local AuditMode m_audit_mode;
|
||||
static thread_local std::vector<std::filesystem::path> m_scoped_allowed_roots;
|
||||
static thread_local bool m_has_last_violation;
|
||||
static thread_local AuditViolation m_last_violation;
|
||||
|
||||
std::mutex m_mutex;
|
||||
std::vector<std::filesystem::path> m_global_allowed_roots;
|
||||
};
|
||||
|
||||
// RAII guard that sets the current plugin key and restores the previous one.
|
||||
class ScopedPluginAuditContext
|
||||
{
|
||||
public:
|
||||
explicit ScopedPluginAuditContext(
|
||||
const std::string& plugin_key,
|
||||
PluginAuditManager::AuditMode mode = PluginAuditManager::AuditMode::Loading);
|
||||
|
||||
~ScopedPluginAuditContext();
|
||||
|
||||
ScopedPluginAuditContext(const ScopedPluginAuditContext&) = delete;
|
||||
ScopedPluginAuditContext& operator=(const ScopedPluginAuditContext&) = delete;
|
||||
|
||||
private:
|
||||
std::string m_previous_id;
|
||||
PluginAuditManager::AuditMode m_previous_mode;
|
||||
std::vector<std::filesystem::path> m_previous_scoped_roots;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_PluginAuditManager_hpp_
|
||||
714
src/slic3r/plugin/PluginCatalog.cpp
Normal file
714
src/slic3r/plugin/PluginCatalog.cpp
Normal file
@@ -0,0 +1,714 @@
|
||||
#include "PluginCatalog.hpp"
|
||||
|
||||
#include "PluginFsUtils.hpp"
|
||||
#include "libslic3r/Semver.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "PythonFileUtils.hpp"
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <slic3r/plugin/PluginDescriptor.hpp>
|
||||
#include <thread>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
|
||||
const char* kCloudPluginNotFoundError = "Plugin was not found in the cloud.";
|
||||
|
||||
bool is_cloud_version_newer(const std::string& cloud_version, const std::string& local_version)
|
||||
{
|
||||
auto cloud_parsed = Semver::parse(cloud_version);
|
||||
auto local_parsed = Semver::parse(local_version);
|
||||
if (cloud_parsed && local_parsed)
|
||||
return *cloud_parsed > *local_parsed;
|
||||
// Fall back to string comparison if semver parsing fails for either version.
|
||||
return cloud_version != local_version;
|
||||
}
|
||||
|
||||
void remove_plugin_from_entries(std::vector<PluginDescriptor>& entries, const std::string& plugin_key)
|
||||
{
|
||||
entries.erase(std::remove_if(entries.begin(), entries.end(), [&plugin_key](const PluginDescriptor& entry) {
|
||||
return entry.plugin_key == plugin_key;
|
||||
}), entries.end());
|
||||
}
|
||||
|
||||
void clear_plugin_cloud_state_in_entries(std::vector<PluginDescriptor>& entries, const std::string& plugin_key)
|
||||
{
|
||||
for (auto& entry : entries) {
|
||||
if (entry.plugin_key == plugin_key) {
|
||||
entry.cloud.reset();
|
||||
if (entry.normalized_error() == kCloudPluginNotFoundError)
|
||||
entry.clear_error();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool set_plugin_error_in_entries(std::vector<PluginDescriptor>& entries, const std::string& plugin_key, const std::string& error)
|
||||
{
|
||||
for (auto& entry : entries) {
|
||||
if (entry.plugin_key == plugin_key) {
|
||||
entry.set_error(error);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void clear_plugin_errors_in_entries(std::vector<PluginDescriptor>& entries)
|
||||
{
|
||||
for (auto& entry : entries)
|
||||
entry.clear_error();
|
||||
}
|
||||
|
||||
// Derive a discovered descriptor's operational plugin_key: "<name>:<uuid>" for cloud
|
||||
// entries, otherwise the (escaped) stem of name_source (the entry file when one
|
||||
// exists, or the plugin directory when it does not). plugin_key is always derived,
|
||||
// never read back from the install-state sidecar.
|
||||
void assign_discovered_plugin_key(PluginDescriptor& descriptor, const boost::filesystem::path& name_source)
|
||||
{
|
||||
if (descriptor.is_cloud_plugin())
|
||||
descriptor.plugin_key = descriptor.cloud_uuid();
|
||||
else
|
||||
descriptor.plugin_key = make_local_plugin_key(name_source.stem().string());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void PluginCatalog::discover_plugins(bool async, bool clear)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_discovery_in_progress) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "Plugin discovery already running";
|
||||
return;
|
||||
}
|
||||
if (clear) {
|
||||
m_plugin_catalog.clear();
|
||||
m_invalid_plugins.clear();
|
||||
m_install_states.clear();
|
||||
}
|
||||
m_discovery_in_progress = true;
|
||||
m_discovery_complete = false;
|
||||
m_discovery_error.clear();
|
||||
}
|
||||
|
||||
run_discovery(async);
|
||||
}
|
||||
|
||||
bool PluginCatalog::is_discovery_complete() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_discovery_complete;
|
||||
}
|
||||
|
||||
bool PluginCatalog::is_discovery_in_progress() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_discovery_in_progress;
|
||||
}
|
||||
|
||||
std::string PluginCatalog::get_discovery_error() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_discovery_error;
|
||||
}
|
||||
|
||||
bool PluginCatalog::wait_for_discovery(std::chrono::milliseconds timeout, std::string& error) const
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_mutex);
|
||||
if (!m_discovery_in_progress)
|
||||
return true;
|
||||
|
||||
if (timeout == std::chrono::milliseconds::max()) {
|
||||
m_discovery_cv.wait(lock, [this]() { return !m_discovery_in_progress; });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!m_discovery_cv.wait_for(lock, timeout, [this]() { return !m_discovery_in_progress; })) {
|
||||
error = "Plugin discovery is still running";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::vector<PluginDescriptor>& PluginCatalog::get_plugin_catalog() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_plugin_catalog;
|
||||
}
|
||||
|
||||
std::vector<PluginDescriptor> PluginCatalog::get_all_plugin_descriptors() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_plugin_catalog;
|
||||
}
|
||||
|
||||
std::vector<PluginDescriptor> PluginCatalog::get_invalid_plugins() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_invalid_plugins;
|
||||
}
|
||||
|
||||
std::vector<PluginDescriptor> PluginCatalog::get_plugin_descriptors_by_type(const std::string& type) const
|
||||
{
|
||||
return get_plugin_descriptors_by_type(plugin_capability_type_from_string(type));
|
||||
}
|
||||
|
||||
std::vector<PluginDescriptor> PluginCatalog::get_plugin_descriptors_by_type(PluginCapabilityType type) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
std::vector<PluginDescriptor> result;
|
||||
for (const auto& entry : m_plugin_catalog) {
|
||||
if (entry.has_capability_type(type))
|
||||
result.push_back(entry);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const PluginDescriptor* PluginCatalog::find_valid_plugin_descriptor(const std::string& plugin_key) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
for (const auto& entry : m_plugin_catalog) {
|
||||
if (entry.plugin_key == plugin_key)
|
||||
return &entry;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool PluginCatalog::try_get_valid_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& descriptor) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
const auto it = std::find_if(m_plugin_catalog.begin(), m_plugin_catalog.end(), [&plugin_key](const PluginDescriptor& entry) {
|
||||
return entry.plugin_key == plugin_key;
|
||||
});
|
||||
|
||||
if (it == m_plugin_catalog.end())
|
||||
return false;
|
||||
|
||||
descriptor = *it;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginCatalog::try_get_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& descriptor) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
const auto find_by_key = [&plugin_key](const PluginDescriptor& entry) {
|
||||
return entry.plugin_key == plugin_key;
|
||||
};
|
||||
|
||||
auto catalog_it = std::find_if(m_plugin_catalog.begin(), m_plugin_catalog.end(), find_by_key);
|
||||
if (catalog_it != m_plugin_catalog.end()) {
|
||||
descriptor = *catalog_it;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto invalid_it = std::find_if(m_invalid_plugins.begin(), m_invalid_plugins.end(), find_by_key);
|
||||
if (invalid_it != m_invalid_plugins.end()) {
|
||||
descriptor = *invalid_it;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PluginCatalog::try_get_invalid_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& descriptor) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
const auto it = std::find_if(m_invalid_plugins.begin(), m_invalid_plugins.end(), [&plugin_key](const PluginDescriptor& entry) {
|
||||
return entry.plugin_key == plugin_key;
|
||||
});
|
||||
|
||||
if (it == m_invalid_plugins.end())
|
||||
return false;
|
||||
|
||||
descriptor = *it;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginCatalog::has_valid_plugin_descriptor(const std::string& plugin_key) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return std::any_of(m_plugin_catalog.begin(), m_plugin_catalog.end(), [&plugin_key](const PluginDescriptor& entry) {
|
||||
return entry.plugin_key == plugin_key;
|
||||
});
|
||||
}
|
||||
|
||||
void PluginCatalog::set_cloud_plugin_dir(const std::string& dir)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_cloud_plugin_dir = dir;
|
||||
}
|
||||
|
||||
std::vector<std::string> PluginCatalog::get_plugin_directories() const
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
std::vector<std::string> dirs;
|
||||
std::string cloud_plugin_dir_name;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
cloud_plugin_dir_name = m_cloud_plugin_dir;
|
||||
}
|
||||
|
||||
auto add_or_create_dir = [&dirs](const fs::path& path) {
|
||||
if (fs::exists(path) && fs::is_directory(path)) {
|
||||
dirs.push_back(path.string());
|
||||
} else {
|
||||
try {
|
||||
fs::create_directories(path);
|
||||
dirs.push_back(path.string());
|
||||
BOOST_LOG_TRIVIAL(info) << "Created plugin directory: " << path.string();
|
||||
} catch (const std::exception& ex) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Failed to create plugin directory: " << ex.what();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Local plugins: {data_dir}/orca_plugins/
|
||||
add_or_create_dir(fs::path(data_dir()) / "orca_plugins");
|
||||
|
||||
// Cloud plugins: {data_dir}/orca_plugins/_subscribed/{user_id}/
|
||||
if (!cloud_plugin_dir_name.empty())
|
||||
add_or_create_dir(fs::path(get_cloud_plugin_dir(cloud_plugin_dir_name)));
|
||||
|
||||
return dirs;
|
||||
}
|
||||
|
||||
void PluginCatalog::update_cloud_catalog(const std::vector<PluginDescriptor>& cloud_list)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
for (const auto& cloud_entry : cloud_list) {
|
||||
std::string cloud_uuid = cloud_entry.cloud_uuid();
|
||||
const std::string cloud_key = cloud_entry.plugin_key;
|
||||
if (cloud_uuid.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Skipping cloud plugin record without UUID";
|
||||
continue;
|
||||
}
|
||||
|
||||
auto matches_cloud_descriptor = [&cloud_key, &cloud_uuid](const PluginDescriptor& entry) {
|
||||
if (!cloud_key.empty() && entry.plugin_key == cloud_key)
|
||||
return true;
|
||||
return entry.is_cloud_plugin() && entry.cloud_uuid() == cloud_uuid;
|
||||
};
|
||||
|
||||
auto apply_cloud_state = [&cloud_entry, &cloud_uuid](PluginDescriptor& entry) {
|
||||
const PluginDescriptor local_entry = entry;
|
||||
const std::string installed_version = entry.installed_version;
|
||||
const std::string latest_version = cloud_entry.latest_available_version();
|
||||
const std::string local_plugin_root = entry.plugin_root;
|
||||
const std::string local_entry_path = entry.entry_path;
|
||||
const bool local_metadata_valid = entry.is_metadata_valid();
|
||||
const bool has_local_package = entry.has_local_package();
|
||||
const std::string previous_error = entry.error;
|
||||
|
||||
entry = cloud_entry;
|
||||
entry.plugin_root = local_plugin_root;
|
||||
entry.entry_path = local_entry_path;
|
||||
if (has_local_package)
|
||||
apply_plugin_metadata_fallbacks(entry, local_entry);
|
||||
if (entry.plugin_key.empty())
|
||||
entry.plugin_key = cloud_uuid;
|
||||
entry.metadata_valid = has_local_package ? local_metadata_valid : cloud_entry.metadata_valid;
|
||||
entry.error = previous_error;
|
||||
if (!entry.cloud.has_value())
|
||||
entry.cloud = CloudPluginState{cloud_uuid, has_local_package, false, false};
|
||||
else if (entry.cloud->uuid.empty())
|
||||
entry.cloud->uuid = cloud_uuid;
|
||||
|
||||
entry.cloud->installed = has_local_package;
|
||||
// The installed version is the source of truth read back from the install-state
|
||||
// sidecar (the version fetched from the cloud at install time), not the local
|
||||
// manifest/PEP723 header. The header may be stale — the cloud can bump the version
|
||||
// without the header changing — which would otherwise make an already-updated
|
||||
// plugin appear perpetually out of date.
|
||||
entry.installed_version = has_local_package ? installed_version : std::string{};
|
||||
entry.cloud->update_available = has_local_package && local_metadata_valid && !installed_version.empty() &&
|
||||
!latest_version.empty() && is_cloud_version_newer(latest_version, installed_version);
|
||||
if (entry.normalized_error() == kCloudPluginNotFoundError)
|
||||
entry.clear_error();
|
||||
};
|
||||
|
||||
auto catalog_it = std::find_if(m_plugin_catalog.begin(), m_plugin_catalog.end(), matches_cloud_descriptor);
|
||||
if (catalog_it != m_plugin_catalog.end()) {
|
||||
apply_cloud_state(*catalog_it);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto invalid_it = std::find_if(m_invalid_plugins.begin(), m_invalid_plugins.end(), matches_cloud_descriptor);
|
||||
if (invalid_it != m_invalid_plugins.end()) {
|
||||
apply_cloud_state(*invalid_it);
|
||||
continue;
|
||||
}
|
||||
|
||||
PluginDescriptor normalized_entry = cloud_entry;
|
||||
if (normalized_entry.plugin_key.empty())
|
||||
normalized_entry.plugin_key = cloud_uuid;
|
||||
if (!normalized_entry.cloud.has_value())
|
||||
normalized_entry.cloud = CloudPluginState{cloud_uuid, false, false, false};
|
||||
else if (normalized_entry.cloud->uuid.empty())
|
||||
normalized_entry.cloud->uuid = cloud_uuid;
|
||||
m_plugin_catalog.push_back(std::move(normalized_entry));
|
||||
}
|
||||
}
|
||||
|
||||
void PluginCatalog::mark_cloud_plugin_unauthorized(const std::string& cloud_uuid)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
auto mark_unauthorized = [&cloud_uuid](std::vector<PluginDescriptor>& entries) {
|
||||
for (auto& entry : entries) {
|
||||
if (entry.is_cloud_plugin() && entry.cloud_uuid() == cloud_uuid) {
|
||||
entry.set_unauthorized(true);
|
||||
if (entry.cloud.has_value())
|
||||
entry.cloud->update_available = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (mark_unauthorized(m_plugin_catalog))
|
||||
return;
|
||||
|
||||
mark_unauthorized(m_invalid_plugins);
|
||||
}
|
||||
|
||||
void PluginCatalog::mark_cloud_plugin_not_found(const std::string& cloud_uuid)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
auto mark_not_found = [&cloud_uuid](std::vector<PluginDescriptor>& entries) {
|
||||
for (auto& entry : entries) {
|
||||
if (entry.is_cloud_plugin() && entry.cloud_uuid() == cloud_uuid) {
|
||||
if (!entry.has_local_package())
|
||||
entry.set_error(kCloudPluginNotFoundError);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (mark_not_found(m_plugin_catalog))
|
||||
return;
|
||||
|
||||
mark_not_found(m_invalid_plugins);
|
||||
}
|
||||
|
||||
void PluginCatalog::clear_cloud_plugin_unauthorized()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
auto clear_unauthorized = [](std::vector<PluginDescriptor>& entries) {
|
||||
for (auto& entry : entries) {
|
||||
if (entry.is_cloud_plugin())
|
||||
entry.set_unauthorized(false);
|
||||
}
|
||||
};
|
||||
|
||||
clear_unauthorized(m_plugin_catalog);
|
||||
clear_unauthorized(m_invalid_plugins);
|
||||
}
|
||||
|
||||
void PluginCatalog::clear_cloud_plugin_not_found_errors()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
auto clear_not_found_errors = [](std::vector<PluginDescriptor>& entries) {
|
||||
for (auto& entry : entries) {
|
||||
if (entry.is_cloud_plugin() && entry.normalized_error() == kCloudPluginNotFoundError)
|
||||
entry.clear_error();
|
||||
}
|
||||
};
|
||||
|
||||
clear_not_found_errors(m_plugin_catalog);
|
||||
clear_not_found_errors(m_invalid_plugins);
|
||||
}
|
||||
|
||||
void PluginCatalog::clear_cloud_plugin_catalog()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
auto clear_entries = [](std::vector<PluginDescriptor>& entries) {
|
||||
entries.erase(std::remove_if(entries.begin(), entries.end(), [](const PluginDescriptor& entry) {
|
||||
return entry.is_cloud_plugin() && !entry.has_local_package();
|
||||
}), entries.end());
|
||||
|
||||
for (auto& entry : entries) {
|
||||
if (entry.is_cloud_plugin()) {
|
||||
entry.cloud->update_available = false;
|
||||
entry.cloud->unauthorized = false;
|
||||
entry.cloud->is_mine = false;
|
||||
if (!entry.plugin_root.empty() || !entry.entry_path.empty())
|
||||
entry.cloud->installed = true;
|
||||
}
|
||||
if (entry.normalized_error() == kCloudPluginNotFoundError)
|
||||
entry.clear_error();
|
||||
}
|
||||
};
|
||||
|
||||
clear_entries(m_plugin_catalog);
|
||||
clear_entries(m_invalid_plugins);
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Cleared cloud plugin catalog entries";
|
||||
}
|
||||
|
||||
void PluginCatalog::remove_plugin(const std::string& plugin_key)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
remove_plugin_from_entries(m_plugin_catalog, plugin_key);
|
||||
remove_plugin_from_entries(m_invalid_plugins, plugin_key);
|
||||
}
|
||||
|
||||
void PluginCatalog::clear_plugin_cloud_state(const std::string& plugin_key)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
clear_plugin_cloud_state_in_entries(m_plugin_catalog, plugin_key);
|
||||
clear_plugin_cloud_state_in_entries(m_invalid_plugins, plugin_key);
|
||||
}
|
||||
|
||||
bool PluginCatalog::set_plugin_error(const std::string& plugin_key, const std::string& error)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (set_plugin_error_in_entries(m_plugin_catalog, plugin_key, error))
|
||||
return true;
|
||||
|
||||
return set_plugin_error_in_entries(m_invalid_plugins, plugin_key, error);
|
||||
}
|
||||
|
||||
bool PluginCatalog::clear_plugin_error(const std::string& plugin_key)
|
||||
{
|
||||
return set_plugin_error(plugin_key, "");
|
||||
}
|
||||
|
||||
void PluginCatalog::clear_all_plugin_errors()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
clear_plugin_errors_in_entries(m_plugin_catalog);
|
||||
clear_plugin_errors_in_entries(m_invalid_plugins);
|
||||
}
|
||||
|
||||
void PluginCatalog::discover_plugins_impl()
|
||||
{
|
||||
const auto start_time = std::chrono::steady_clock::now();
|
||||
|
||||
try {
|
||||
const std::vector<std::string> plugin_dirs = get_plugin_directories();
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Scanning " << plugin_dirs.size() << " plugin directories...";
|
||||
|
||||
for (const auto& dir : plugin_dirs)
|
||||
scan_directory(dir);
|
||||
|
||||
std::size_t plugin_count = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_discovery_complete = true;
|
||||
plugin_count = m_plugin_catalog.size();
|
||||
}
|
||||
|
||||
const auto end_time = std::chrono::steady_clock::now();
|
||||
const auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
|
||||
BOOST_LOG_TRIVIAL(info) << "Plugin discovery completed in " << duration.count() << "ms. Found " << plugin_count
|
||||
<< " plugin manifests";
|
||||
} catch (const std::exception& ex) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_discovery_error = std::string("Plugin discovery failed: ") + ex.what();
|
||||
m_discovery_complete = true;
|
||||
BOOST_LOG_TRIVIAL(error) << m_discovery_error;
|
||||
}
|
||||
}
|
||||
|
||||
void PluginCatalog::scan_directory(const std::string& dir_path)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
if (!fs::exists(dir_path) || !fs::is_directory(dir_path))
|
||||
return;
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug) << "Scanning plugin directory: " << dir_path;
|
||||
|
||||
try {
|
||||
for (fs::directory_iterator it(dir_path); it != fs::directory_iterator(); ++it) {
|
||||
if (!fs::is_directory(it->status()))
|
||||
continue;
|
||||
|
||||
const fs::path plugin_dir = it->path();
|
||||
if (is_ignored_plugin_directory(plugin_dir))
|
||||
continue;
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
descriptor.plugin_root = plugin_dir.string();
|
||||
|
||||
std::string entry_error;
|
||||
const fs::path entry_path = find_installed_plugin_entry(plugin_dir, entry_error);
|
||||
|
||||
if (entry_path.empty()) {
|
||||
descriptor.set_error(entry_error);
|
||||
read_install_state(plugin_dir, descriptor);
|
||||
assign_discovered_plugin_key(descriptor, plugin_dir);
|
||||
PluginInstallState install_state;
|
||||
const bool have_install_state = read_install_state(plugin_dir, install_state);
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (have_install_state)
|
||||
m_install_states[descriptor.plugin_key] = std::move(install_state);
|
||||
m_invalid_plugins.push_back(std::move(descriptor));
|
||||
BOOST_LOG_TRIVIAL(warning) << "Invalid plugin package: " << plugin_dir.string() << " - " << m_invalid_plugins.back().error;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse local file metadata for dependencies and loading details.
|
||||
std::string meta_error;
|
||||
if (entry_path.extension() == ".whl") {
|
||||
if (!read_wheel_plugin_metadata(entry_path, descriptor, meta_error)) {
|
||||
descriptor.set_error(meta_error);
|
||||
read_install_state(plugin_dir, descriptor);
|
||||
assign_discovered_plugin_key(descriptor, entry_path);
|
||||
PluginInstallState install_state;
|
||||
const bool have_install_state = read_install_state(plugin_dir, install_state);
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (have_install_state)
|
||||
m_install_states[descriptor.plugin_key] = std::move(install_state);
|
||||
m_invalid_plugins.push_back(std::move(descriptor));
|
||||
BOOST_LOG_TRIVIAL(warning) << "Invalid wheel plugin: " << plugin_dir.string() << " - "
|
||||
<< m_invalid_plugins.back().error;
|
||||
continue;
|
||||
}
|
||||
descriptor.entry_path = entry_path.string();
|
||||
} else {
|
||||
if (!read_python_plugin_metadata(entry_path, descriptor, meta_error)) {
|
||||
descriptor.set_error(meta_error);
|
||||
read_install_state(plugin_dir, descriptor);
|
||||
assign_discovered_plugin_key(descriptor, entry_path);
|
||||
PluginInstallState install_state;
|
||||
const bool have_install_state = read_install_state(plugin_dir, install_state);
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (have_install_state)
|
||||
m_install_states[descriptor.plugin_key] = std::move(install_state);
|
||||
m_invalid_plugins.push_back(std::move(descriptor));
|
||||
BOOST_LOG_TRIVIAL(warning) << "Invalid .py plugin: " << plugin_dir.string() << " - " << m_invalid_plugins.back().error;
|
||||
continue;
|
||||
}
|
||||
descriptor.entry_path = entry_path.string();
|
||||
}
|
||||
|
||||
descriptor.set_metadata_valid(true);
|
||||
descriptor.clear_error();
|
||||
|
||||
// Read cloud identity (uuid) from sidecar; plugin_key is always derived.
|
||||
read_install_state(plugin_dir, descriptor);
|
||||
assign_discovered_plugin_key(descriptor, entry_path);
|
||||
|
||||
PluginInstallState install_state;
|
||||
const bool have_install_state = read_install_state(plugin_dir, install_state);
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
if (have_install_state)
|
||||
m_install_states[descriptor.plugin_key] = std::move(install_state);
|
||||
m_plugin_catalog.push_back(std::move(descriptor));
|
||||
BOOST_LOG_TRIVIAL(info) << "Discovered plugin: " << m_plugin_catalog.back().name
|
||||
<< " (type: " << m_plugin_catalog.back().type_label() << ", version: " << m_plugin_catalog.back().version
|
||||
<< ")";
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Error scanning directory " << dir_path << ": " << ex.what();
|
||||
}
|
||||
}
|
||||
|
||||
void PluginCatalog::run_discovery(bool async)
|
||||
{
|
||||
auto task = [this]() { run_discovery_task(); };
|
||||
|
||||
if (async)
|
||||
std::thread(std::move(task)).detach();
|
||||
else
|
||||
task();
|
||||
}
|
||||
|
||||
void PluginCatalog::run_discovery_task()
|
||||
{
|
||||
try {
|
||||
discover_plugins_impl();
|
||||
} catch (const std::exception& ex) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_discovery_error = std::string("Plugin discovery failed: ") + ex.what();
|
||||
m_discovery_complete = true;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(error) << m_discovery_error;
|
||||
} catch (...) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_discovery_error = "Plugin discovery failed: unknown error";
|
||||
m_discovery_complete = true;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(error) << m_discovery_error;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_discovery_in_progress = false;
|
||||
}
|
||||
m_discovery_cv.notify_all();
|
||||
}
|
||||
|
||||
bool PluginCatalog::update_plugin_descriptor(const std::string& plugin_key, const PluginDescriptor& descriptor)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
const auto find_by_key = [&plugin_key](const PluginDescriptor& entry) {
|
||||
return entry.plugin_key == plugin_key;
|
||||
};
|
||||
|
||||
auto catalog_it = std::find_if(m_plugin_catalog.begin(), m_plugin_catalog.end(), find_by_key);
|
||||
if (catalog_it != m_plugin_catalog.end()) {
|
||||
*catalog_it = std::move(descriptor);
|
||||
return true;
|
||||
}
|
||||
|
||||
auto invalid_it = std::find_if(m_invalid_plugins.begin(), m_invalid_plugins.end(), find_by_key);
|
||||
if (invalid_it != m_invalid_plugins.end()) {
|
||||
*invalid_it = std::move(descriptor);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PluginCatalog::try_get_install_state(const std::string& plugin_key, PluginInstallState& out) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto it = m_install_states.find(plugin_key);
|
||||
if (it == m_install_states.end())
|
||||
return false;
|
||||
out = it->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::string> PluginCatalog::get_enabled_plugin_keys() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::vector<std::string> keys;
|
||||
for (const auto& [plugin_key, state] : m_install_states) {
|
||||
if (state.enabled)
|
||||
keys.push_back(plugin_key);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
76
src/slic3r/plugin/PluginCatalog.hpp
Normal file
76
src/slic3r/plugin/PluginCatalog.hpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include "PluginDescriptor.hpp"
|
||||
#include "PythonFileUtils.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class PluginCatalog
|
||||
{
|
||||
public:
|
||||
void discover_plugins(bool async = false, bool clear = false);
|
||||
|
||||
bool is_discovery_complete() const;
|
||||
bool is_discovery_in_progress() const;
|
||||
std::string get_discovery_error() const;
|
||||
bool wait_for_discovery(std::chrono::milliseconds timeout, std::string& error) const;
|
||||
|
||||
const std::vector<PluginDescriptor>& get_plugin_catalog() const;
|
||||
std::vector<PluginDescriptor> get_all_plugin_descriptors() const;
|
||||
std::vector<PluginDescriptor> get_invalid_plugins() const;
|
||||
std::vector<PluginDescriptor> get_plugin_descriptors_by_type(const std::string& type) const;
|
||||
std::vector<PluginDescriptor> get_plugin_descriptors_by_type(PluginCapabilityType type) const;
|
||||
const PluginDescriptor* find_valid_plugin_descriptor(const std::string& plugin_key) const;
|
||||
bool try_get_valid_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& descriptor) const;
|
||||
bool try_get_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& descriptor) const;
|
||||
bool try_get_invalid_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& descriptor) const;
|
||||
bool has_valid_plugin_descriptor(const std::string& plugin_key) const;
|
||||
|
||||
void set_cloud_plugin_dir(const std::string& dir);
|
||||
std::vector<std::string> get_plugin_directories() const;
|
||||
|
||||
void update_cloud_catalog(const std::vector<PluginDescriptor>& cloud_list);
|
||||
void mark_cloud_plugin_unauthorized(const std::string& cloud_uuid);
|
||||
void mark_cloud_plugin_not_found(const std::string& cloud_uuid);
|
||||
void clear_cloud_plugin_unauthorized();
|
||||
void clear_cloud_plugin_not_found_errors();
|
||||
void clear_cloud_plugin_catalog();
|
||||
void remove_plugin(const std::string& plugin_key);
|
||||
void clear_plugin_cloud_state(const std::string& plugin_key);
|
||||
bool set_plugin_error(const std::string& plugin_key, const std::string& error);
|
||||
bool clear_plugin_error(const std::string& plugin_key);
|
||||
void clear_all_plugin_errors();
|
||||
|
||||
bool update_plugin_descriptor(const std::string& plugin_key, const PluginDescriptor& descriptor);
|
||||
|
||||
// Cached install state, populated from each plugin's .install_state.json during discovery.
|
||||
bool try_get_install_state(const std::string& plugin_key, PluginInstallState& out) const;
|
||||
std::vector<std::string> get_enabled_plugin_keys() const;
|
||||
|
||||
private:
|
||||
void discover_plugins_impl();
|
||||
void scan_directory(const std::string& dir_path);
|
||||
void run_discovery(bool async);
|
||||
void run_discovery_task();
|
||||
|
||||
bool m_discovery_complete = false;
|
||||
std::string m_discovery_error;
|
||||
bool m_discovery_in_progress = false;
|
||||
|
||||
std::vector<PluginDescriptor> m_plugin_catalog;
|
||||
std::vector<PluginDescriptor> m_invalid_plugins;
|
||||
std::unordered_map<std::string, PluginInstallState> m_install_states;
|
||||
std::string m_cloud_plugin_dir;
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
mutable std::condition_variable m_discovery_cv;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
222
src/slic3r/plugin/PluginDescriptor.hpp
Normal file
222
src/slic3r/plugin/PluginDescriptor.hpp
Normal file
@@ -0,0 +1,222 @@
|
||||
#pragma once
|
||||
|
||||
#include "PythonPluginInterface.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Cloud overlay on a PluginDescriptor; presence means the descriptor is cloud-backed.
|
||||
struct CloudPluginState
|
||||
{
|
||||
std::string uuid; // Cloud service UUID without the cloud: key prefix.
|
||||
bool installed = false; // Cloud package exists locally and can be loaded.
|
||||
bool update_available = false; // Cloud version > the local package version.
|
||||
bool unauthorized = false; // Cloud plugin is valid locally, but cannot receive cloud updates.
|
||||
bool is_mine = false; // Plugin was created (and uploaded) by the current user.
|
||||
};
|
||||
|
||||
enum class PluginUpdateStatus
|
||||
{
|
||||
Normal,
|
||||
UpdateAvailable,
|
||||
Unauthorized,
|
||||
};
|
||||
|
||||
struct PluginChangelog {
|
||||
std::string changelog_id;
|
||||
std::string plugin_uuid;
|
||||
std::string version;
|
||||
std::string changelog;
|
||||
long long created_time = 0;
|
||||
};
|
||||
|
||||
inline void sort_plugin_changelog(std::vector<PluginChangelog>& changelog)
|
||||
{
|
||||
std::sort(changelog.begin(), changelog.end(), [](const PluginChangelog& lhs, const PluginChangelog& rhs) {
|
||||
if (lhs.created_time != rhs.created_time)
|
||||
return lhs.created_time > rhs.created_time;
|
||||
return lhs.version > rhs.version;
|
||||
});
|
||||
}
|
||||
|
||||
// Canonical plugin runtime/catalog representation used by Orca.
|
||||
struct PluginDescriptor
|
||||
{
|
||||
std::string plugin_key; // OrcaSlicer-generated operational identity
|
||||
std::string name; // Display name
|
||||
std::string description; // Plugin description
|
||||
std::string author; // Plugin author from manifest, if available
|
||||
std::string version; // Selected plugin version
|
||||
std::string latest_version; // Latest available cloud version fallback when changelog is unavailable.
|
||||
std::string installed_version; // Locally installed package version. Preserved across cloud merges, which overwrite `version` with the latest cloud version. Empty when not installed.
|
||||
std::vector<PluginCapabilityType> capability_types; // Capability types this package materializes (one package → N capabilities)
|
||||
std::vector<std::string> display_types; // Display-only "compatibility" labels (cloud: raw service labels; local: from real capabilities). Never used for dispatch.
|
||||
std::string plugin_root; // Installed plugin directory, even when entry_path is invalid or ambiguous.
|
||||
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::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.
|
||||
std::optional<CloudPluginState> cloud; // Extra cloud state layered on top of a normal plugin descriptor.
|
||||
bool metadata_valid = false; // Manifest/package validity stays separate from the user-facing error field.
|
||||
std::string sharing_token; // Use BASE_URL/p/SHARING_TOKEN to open relevant plugin in browser.
|
||||
std::string thumbnail_url; // Cloud main_image pre-signed (access_url) thumbnail; empty for local plugins. Display-only.
|
||||
|
||||
bool is_cloud_plugin() const { return cloud.has_value(); }
|
||||
std::string cloud_uuid() const { return cloud.has_value() ? cloud->uuid : std::string{}; }
|
||||
bool has_local_package() const { return !is_cloud_plugin() || cloud->installed || !plugin_root.empty() || !entry_path.empty(); }
|
||||
bool is_metadata_valid() const { return metadata_valid; }
|
||||
|
||||
// Capability-type helpers. A package may materialize several capability types;
|
||||
// these accessors give callers that still reason about a single "type" (catalog
|
||||
// display, cloud overlay, dispatch — finalized in later tasks) a stable view.
|
||||
bool has_capability_type(PluginCapabilityType t) const
|
||||
{
|
||||
return std::find(capability_types.begin(), capability_types.end(), t) != capability_types.end();
|
||||
}
|
||||
PluginCapabilityType primary_capability_type() const
|
||||
{
|
||||
return capability_types.empty() ? PluginCapabilityType::Unknown : capability_types.front();
|
||||
}
|
||||
// Set the package to a single capability type (metadata/cloud sources currently
|
||||
// declare one type; multi-type discovery is finalized in later tasks).
|
||||
void set_capability_type(PluginCapabilityType t) { capability_types.assign(1, t); }
|
||||
// Canonical label derived from the primary type for UI / config matching.
|
||||
std::string type_label() const { return plugin_capability_type_to_string(primary_capability_type()); }
|
||||
|
||||
std::string normalized_error() const
|
||||
{
|
||||
auto begin = std::find_if_not(error.begin(), error.end(), [](unsigned char ch) { return std::isspace(ch) != 0; });
|
||||
if (begin == error.end())
|
||||
return {};
|
||||
|
||||
auto end = std::find_if_not(error.rbegin(), error.rend(), [](unsigned char ch) { return std::isspace(ch) != 0; }).base();
|
||||
return std::string(begin, end);
|
||||
}
|
||||
|
||||
bool has_error() const { return !normalized_error().empty(); }
|
||||
|
||||
PluginUpdateStatus get_update_status() const
|
||||
{
|
||||
if (!cloud.has_value())
|
||||
return PluginUpdateStatus::Normal;
|
||||
if (cloud->unauthorized)
|
||||
return PluginUpdateStatus::Unauthorized;
|
||||
if (cloud->update_available)
|
||||
return PluginUpdateStatus::UpdateAvailable;
|
||||
return PluginUpdateStatus::Normal;
|
||||
}
|
||||
|
||||
bool has_update_available() const { return get_update_status() == PluginUpdateStatus::UpdateAvailable; }
|
||||
bool is_unauthorized() const { return get_update_status() == PluginUpdateStatus::Unauthorized; }
|
||||
std::string latest_available_version() const
|
||||
{
|
||||
for (const PluginChangelog& entry : changelog) {
|
||||
if (!entry.version.empty())
|
||||
return entry.version;
|
||||
}
|
||||
if (!latest_version.empty())
|
||||
return latest_version;
|
||||
return version;
|
||||
}
|
||||
|
||||
void set_metadata_valid(bool is_valid)
|
||||
{
|
||||
metadata_valid = is_valid;
|
||||
if (!is_valid && !has_error())
|
||||
error = "Plugin metadata is invalid.";
|
||||
}
|
||||
|
||||
void set_unauthorized(bool unauthorized)
|
||||
{
|
||||
if (!cloud.has_value())
|
||||
return;
|
||||
cloud->unauthorized = unauthorized;
|
||||
if (unauthorized)
|
||||
cloud->update_available = false;
|
||||
}
|
||||
|
||||
void clear_error() { error.clear(); }
|
||||
void set_error(std::string message) { error = std::move(message); }
|
||||
};
|
||||
|
||||
inline void apply_plugin_metadata_fallbacks(PluginDescriptor& target, const PluginDescriptor& fallback)
|
||||
{
|
||||
if (target.name.empty())
|
||||
target.name = fallback.name;
|
||||
if (target.description.empty())
|
||||
target.description = fallback.description;
|
||||
if (target.author.empty())
|
||||
target.author = fallback.author;
|
||||
if (target.version.empty())
|
||||
target.version = fallback.version;
|
||||
if (target.capability_types.empty())
|
||||
target.capability_types = fallback.capability_types;
|
||||
if (target.entry_package.empty())
|
||||
target.entry_package = fallback.entry_package;
|
||||
if (target.dependencies.empty())
|
||||
target.dependencies = fallback.dependencies;
|
||||
}
|
||||
|
||||
// Sanitize a value for use as a filesystem name and as a local plugin_key:
|
||||
// keeps [A-Za-z0-9_-.], collapses any other run into a single '_'.
|
||||
inline std::string filesystem_safe_escape(const std::string& value)
|
||||
{
|
||||
std::string escaped;
|
||||
escaped.reserve(value.size());
|
||||
for (unsigned char ch : value) {
|
||||
if (std::isalnum(ch) || ch == '_' || ch == '-' || ch == '.') {
|
||||
escaped += static_cast<char>(ch);
|
||||
continue;
|
||||
}
|
||||
if (escaped.empty() || escaped.back() != '_')
|
||||
escaped += '_';
|
||||
}
|
||||
return escaped.empty() ? "path" : escaped;
|
||||
}
|
||||
|
||||
// Plugin display names are serialized into ';'-delimited config/preset strings
|
||||
// (see escape_strings_cstyle in libslic3r/Config.cpp), so a ';' in a name would
|
||||
// corrupt that encoding. Replace any ';' with '_' and otherwise leave the name
|
||||
// untouched. Capability names are validated more strictly elsewhere — a ';' there
|
||||
// is treated as an error because those names drive preset dispatch, not display.
|
||||
inline std::string sanitize_plugin_name(std::string name)
|
||||
{
|
||||
std::replace(name.begin(), name.end(), ';', '_');
|
||||
return name;
|
||||
}
|
||||
|
||||
// True when s is a canonical 8-4-4-12 hex UUID, e.g. 550e8400-e29b-41d4-a716-446655440000.
|
||||
inline bool is_uuid(const std::string& s)
|
||||
{
|
||||
if (s.size() != 36)
|
||||
return false;
|
||||
for (size_t i = 0; i < s.size(); ++i) {
|
||||
const char ch = s[i];
|
||||
if (i == 8 || i == 13 || i == 18 || i == 23) {
|
||||
if (ch != '-')
|
||||
return false;
|
||||
} else if (std::isxdigit(static_cast<unsigned char>(ch)) == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Key generation helpers.
|
||||
// Local key = filesystem-safe plugin file stem (filename without extension), e.g. "Slow_Load_Plugin".
|
||||
// Cloud key = the cloud UUID. Local vs cloud is determined from the descriptor's cloud
|
||||
// state, never by parsing the key. Plugin keys are matched by plain equality.
|
||||
inline std::string make_local_plugin_key(const std::string& stem)
|
||||
{
|
||||
return filesystem_safe_escape(stem);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
110
src/slic3r/plugin/PluginFsUtils.cpp
Normal file
110
src/slic3r/plugin/PluginFsUtils.cpp
Normal file
@@ -0,0 +1,110 @@
|
||||
#include "PluginFsUtils.hpp"
|
||||
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include "PluginAuditManager.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
const char* const INSTALL_STATE_FILE = ".install_state.json";
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
boost::filesystem::path resolve_plugin_root_from_descriptor(const PluginDescriptor& descriptor)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
if (!descriptor.plugin_root.empty())
|
||||
return fs::path(descriptor.plugin_root);
|
||||
if (!descriptor.entry_path.empty())
|
||||
return fs::path(descriptor.entry_path).parent_path();
|
||||
return {};
|
||||
}
|
||||
|
||||
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);
|
||||
if (ec) {
|
||||
ec.clear();
|
||||
resolved_root = boost::filesystem::absolute(candidate_root, ec);
|
||||
}
|
||||
|
||||
if (ec || resolved_root.empty())
|
||||
return false;
|
||||
|
||||
for (const auto& allowed_dir : allowed_dirs) {
|
||||
if (is_inside_allowed_root(std::filesystem::path(resolved_root.string()),
|
||||
std::filesystem::path(allowed_dir)))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool resolve_allowed_plugin_root(const PluginDescriptor& descriptor,
|
||||
const std::vector<std::string>& allowed_dirs,
|
||||
const std::string& out_of_scope_error,
|
||||
boost::filesystem::path& resolved_root,
|
||||
std::string& error)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
const fs::path plugin_root = resolve_plugin_root_from_descriptor(descriptor);
|
||||
if (plugin_root.empty()) {
|
||||
error = "Plugin folder could not be determined.";
|
||||
return false;
|
||||
}
|
||||
|
||||
boost::system::error_code ec;
|
||||
resolved_root = fs::weakly_canonical(plugin_root, ec);
|
||||
if (ec) {
|
||||
ec.clear();
|
||||
resolved_root = fs::absolute(plugin_root, ec);
|
||||
}
|
||||
if (ec || resolved_root.empty()) {
|
||||
error = "Failed to resolve plugin folder: " + plugin_root.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_plugin_root_allowed(plugin_root, allowed_dirs)) {
|
||||
error = out_of_scope_error;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool delete_plugin_root(const boost::filesystem::path& resolved_root,
|
||||
const std::string& plugin_id,
|
||||
std::string& error)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
boost::system::error_code ec;
|
||||
const auto removed_count = fs::remove_all(resolved_root, ec);
|
||||
if (ec) {
|
||||
error = "Failed to delete plugin folder " + resolved_root.string() + ": " + ec.message();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (removed_count == 0) {
|
||||
error = "Plugin folder was not found: " + resolved_root.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Deleted plugin: " << plugin_id << " from " << resolved_root.string();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
35
src/slic3r/plugin/PluginFsUtils.hpp
Normal file
35
src/slic3r/plugin/PluginFsUtils.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "PluginDescriptor.hpp"
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#define PLUGIN_SUBSCRIBED_DIR "_subscribed"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
extern const char* const INSTALL_STATE_FILE;
|
||||
|
||||
// Returns the cloud plugin install/scan directory for a given user_id.
|
||||
// Path: {data_dir}/orca_plugins/_subscribed/{user_id}/
|
||||
std::string get_cloud_plugin_dir(const std::string& user_id);
|
||||
|
||||
boost::filesystem::path resolve_plugin_root_from_descriptor(const PluginDescriptor& descriptor);
|
||||
|
||||
bool is_plugin_root_allowed(const boost::filesystem::path& candidate_root,
|
||||
const std::vector<std::string>& allowed_dirs);
|
||||
|
||||
bool resolve_allowed_plugin_root(const PluginDescriptor& descriptor,
|
||||
const std::vector<std::string>& allowed_dirs,
|
||||
const std::string& out_of_scope_error,
|
||||
boost::filesystem::path& resolved_root,
|
||||
std::string& error);
|
||||
|
||||
bool delete_plugin_root(const boost::filesystem::path& resolved_root,
|
||||
const std::string& plugin_id,
|
||||
std::string& error);
|
||||
|
||||
} // namespace Slic3r
|
||||
513
src/slic3r/plugin/PluginHostApi.cpp
Normal file
513
src/slic3r/plugin/PluginHostApi.cpp
Normal file
@@ -0,0 +1,513 @@
|
||||
#include "PluginHostApi.hpp"
|
||||
#include "PluginHostUi.hpp"
|
||||
|
||||
#include <libslic3r/BoundingBox.hpp>
|
||||
#include <libslic3r/Model.hpp>
|
||||
#include <libslic3r/Preset.hpp>
|
||||
#include <libslic3r/PresetBundle.hpp>
|
||||
#include <libslic3r/TriangleMesh.hpp>
|
||||
#include <slic3r/GUI/GUI_App.hpp>
|
||||
#include <slic3r/GUI/Plater.hpp>
|
||||
|
||||
#include <pybind11/numpy.h>
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
|
||||
GUI::Plater* current_plater()
|
||||
{
|
||||
if (wxTheApp == nullptr)
|
||||
throw std::runtime_error("OrcaSlicer application is not initialized");
|
||||
|
||||
GUI::Plater* plater = GUI::wxGetApp().plater();
|
||||
if (plater == nullptr)
|
||||
throw std::runtime_error("Plater is not available");
|
||||
|
||||
return plater;
|
||||
}
|
||||
|
||||
PresetBundle* current_preset_bundle()
|
||||
{
|
||||
if (wxTheApp == nullptr)
|
||||
throw std::runtime_error("OrcaSlicer application is not initialized");
|
||||
|
||||
PresetBundle* preset_bundle = GUI::wxGetApp().preset_bundle;
|
||||
if (preset_bundle == nullptr)
|
||||
throw std::runtime_error("Preset bundle is not available");
|
||||
|
||||
return preset_bundle;
|
||||
}
|
||||
|
||||
py::object config_value_or_none(const DynamicPrintConfig& config, const std::string& key)
|
||||
{
|
||||
if (!config.has(key))
|
||||
return py::none();
|
||||
return py::cast(config.opt_serialize(key));
|
||||
}
|
||||
|
||||
// Plugins receive 3D vectors as plain Python tuples (x, y, z) so the API stays
|
||||
// Pythonic and free of an Eigen/numpy runtime dependency.
|
||||
py::tuple vec3_to_tuple(const Vec3d& v)
|
||||
{
|
||||
return py::make_tuple(v.x(), v.y(), v.z());
|
||||
}
|
||||
|
||||
// Build a BoundingBoxf3 from precomputed (float) triangle-mesh stats min/max.
|
||||
BoundingBoxf3 bbox_from_stats(const TriangleMeshStats& stats)
|
||||
{
|
||||
if (stats.number_of_facets == 0)
|
||||
return BoundingBoxf3();
|
||||
return BoundingBoxf3(stats.min.cast<double>(), stats.max.cast<double>());
|
||||
}
|
||||
|
||||
// --- Mesh geometry helpers -------------------------------------------------
|
||||
|
||||
// Zero-copy export of its.vertices / its.indices relies on these Eigen
|
||||
// row-vectors being tightly packed (no padding between the 3 components).
|
||||
static_assert(sizeof(stl_vertex) == 3 * sizeof(float),
|
||||
"stl_vertex must be a packed float[3] for zero-copy numpy export");
|
||||
static_assert(sizeof(stl_triangle_vertex_indices) == 3 * sizeof(std::int32_t),
|
||||
"triangle index must be a packed int32[3] for zero-copy numpy export");
|
||||
|
||||
// Immutable snapshot of a ModelVolume's mesh. Holding a strong reference to the
|
||||
// const mesh keeps any zero-copy numpy views valid even if the volume's mesh is
|
||||
// later replaced on the main thread.
|
||||
struct HostTriangleMesh
|
||||
{
|
||||
std::shared_ptr<const TriangleMesh> mesh;
|
||||
const indexed_triangle_set& its() const { return mesh->its; }
|
||||
};
|
||||
|
||||
// Run a builder that constructs numpy objects, translating the "numpy missing"
|
||||
// ImportError into an actionable message (plugins must declare numpy as a dep).
|
||||
template<typename Builder>
|
||||
py::object with_numpy(Builder&& build)
|
||||
{
|
||||
try {
|
||||
return std::forward<Builder>(build)();
|
||||
} catch (py::error_already_set& err) {
|
||||
if (err.matches(PyExc_ImportError))
|
||||
throw py::import_error("numpy is required to access mesh arrays/matrices; "
|
||||
"add dependencies = [\"numpy\"] to your plugin metadata");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Read-only, zero-copy (rows, 3) numpy view over a packed T[rows][3] buffer.
|
||||
// The array owns a capsule that pins `mesh` alive for the view's lifetime.
|
||||
template<typename T>
|
||||
py::array make_readonly_rows3(const std::shared_ptr<const TriangleMesh>& mesh,
|
||||
const T* data, py::ssize_t rows)
|
||||
{
|
||||
if (rows == 0 || data == nullptr)
|
||||
return py::array_t<T>(std::vector<py::ssize_t>{0, 3});
|
||||
|
||||
auto* owner = new std::shared_ptr<const TriangleMesh>(mesh);
|
||||
py::capsule base(owner, [](void* p) {
|
||||
delete reinterpret_cast<std::shared_ptr<const TriangleMesh>*>(p);
|
||||
});
|
||||
|
||||
py::array_t<T> array(
|
||||
{ rows, py::ssize_t(3) },
|
||||
{ py::ssize_t(3 * sizeof(T)), py::ssize_t(sizeof(T)) },
|
||||
data,
|
||||
base);
|
||||
// A capsule-based array is writable by default in pybind11; the underlying
|
||||
// mesh is const, so force the view read-only.
|
||||
array.attr("setflags")(py::arg("write") = false);
|
||||
return array;
|
||||
}
|
||||
|
||||
// 4x4 row-major float64 copy of an affine transform. Eigen stores column-major,
|
||||
// so fill element-wise to produce correct C-order data.
|
||||
py::object mat4_to_numpy(const Transform3d& transform)
|
||||
{
|
||||
return with_numpy([&] {
|
||||
py::array_t<double> array({ py::ssize_t(4), py::ssize_t(4) });
|
||||
auto view = array.mutable_unchecked<2>();
|
||||
const auto& matrix = transform.matrix();
|
||||
for (int i = 0; i < 4; ++i)
|
||||
for (int j = 0; j < 4; ++j)
|
||||
view(i, j) = matrix(i, j);
|
||||
return py::object(std::move(array));
|
||||
});
|
||||
}
|
||||
|
||||
py::list current_filament_presets(PresetBundle& bundle)
|
||||
{
|
||||
py::list presets;
|
||||
for (const std::string& preset_name : bundle.filament_presets) {
|
||||
Preset* preset = bundle.filaments.find_preset(preset_name);
|
||||
if (preset == nullptr)
|
||||
presets.append(py::none());
|
||||
else
|
||||
presets.append(py::cast(preset, py::return_value_policy::reference));
|
||||
}
|
||||
return presets;
|
||||
}
|
||||
|
||||
PresetCollection& printer_presets(PresetBundle& bundle)
|
||||
{
|
||||
return static_cast<PresetCollection&>(bundle.printers);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void PluginHostApi::RegisterBindings(pybind11::module_& module)
|
||||
{
|
||||
auto host = module.def_submodule("host", "Host application API");
|
||||
|
||||
py::enum_<Preset::Type>(host, "PresetType")
|
||||
.value("Invalid", Preset::TYPE_INVALID)
|
||||
.value("Print", Preset::TYPE_PRINT)
|
||||
.value("SlaPrint", Preset::TYPE_SLA_PRINT)
|
||||
.value("Filament", Preset::TYPE_FILAMENT)
|
||||
.value("SlaMaterial", Preset::TYPE_SLA_MATERIAL)
|
||||
.value("Printer", Preset::TYPE_PRINTER)
|
||||
.value("PhysicalPrinter", Preset::TYPE_PHYSICAL_PRINTER)
|
||||
.value("Plate", Preset::TYPE_PLATE)
|
||||
.value("Model", Preset::TYPE_MODEL);
|
||||
|
||||
py::class_<Preset, std::unique_ptr<Preset, py::nodelete>>(host, "Preset")
|
||||
.def_readonly("type", &Preset::type)
|
||||
.def_readonly("name", &Preset::name)
|
||||
.def_readonly("alias", &Preset::alias)
|
||||
.def_readonly("file", &Preset::file)
|
||||
.def_readonly("is_default", &Preset::is_default)
|
||||
.def_readonly("is_external", &Preset::is_external)
|
||||
.def_readonly("is_system", &Preset::is_system)
|
||||
.def_readonly("is_visible", &Preset::is_visible)
|
||||
.def_readonly("is_dirty", &Preset::is_dirty)
|
||||
.def_readonly("is_compatible", &Preset::is_compatible)
|
||||
.def_readonly("is_project_embedded", &Preset::is_project_embedded)
|
||||
.def_readonly("bundle_id", &Preset::bundle_id)
|
||||
.def("is_user", &Preset::is_user)
|
||||
.def("is_from_bundle", &Preset::is_from_bundle)
|
||||
.def("label", &Preset::label, py::arg("no_alias") = false)
|
||||
.def("config_keys", [](const Preset& preset) { return preset.config.keys(); })
|
||||
.def("config_value", [](const Preset& preset, const std::string& key) {
|
||||
return config_value_or_none(preset.config, key);
|
||||
});
|
||||
|
||||
py::class_<PresetCollection, std::unique_ptr<PresetCollection, py::nodelete>>(host, "PresetCollection")
|
||||
.def("size", &PresetCollection::size)
|
||||
.def("get_selected_preset", [](PresetCollection& collection) -> Preset& {
|
||||
return collection.get_selected_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("selected_preset", [](PresetCollection& collection) -> Preset& {
|
||||
return collection.get_selected_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("get_selected_preset_name", &PresetCollection::get_selected_preset_name)
|
||||
.def("selected_preset_name", &PresetCollection::get_selected_preset_name)
|
||||
.def("get_edited_preset", [](PresetCollection& collection) -> Preset& {
|
||||
return collection.get_edited_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("edited_preset", [](PresetCollection& collection) -> Preset& {
|
||||
return collection.get_edited_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("preset", [](PresetCollection& collection, size_t index) -> Preset& {
|
||||
if (index >= collection.size())
|
||||
throw py::index_error("preset index out of range");
|
||||
return collection.preset(index);
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("find_preset", [](PresetCollection& collection, const std::string& name) -> Preset* {
|
||||
return collection.find_preset(name);
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("preset_names", [](const PresetCollection& collection) {
|
||||
std::vector<std::string> names;
|
||||
names.reserve(collection.get_presets().size());
|
||||
for (const Preset& preset : collection.get_presets())
|
||||
names.push_back(preset.name);
|
||||
return names;
|
||||
});
|
||||
|
||||
py::class_<PresetBundle, std::unique_ptr<PresetBundle, py::nodelete>>(host, "PresetBundle")
|
||||
.def_property_readonly("prints", [](PresetBundle& bundle) -> PresetCollection& {
|
||||
return bundle.prints;
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def_property_readonly("printers", &printer_presets, py::return_value_policy::reference_internal)
|
||||
.def_property_readonly("filaments", [](PresetBundle& bundle) -> PresetCollection& {
|
||||
return bundle.filaments;
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def_property_readonly("sla_prints", [](PresetBundle& bundle) -> PresetCollection& {
|
||||
return bundle.sla_prints;
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def_property_readonly("sla_materials", [](PresetBundle& bundle) -> PresetCollection& {
|
||||
return bundle.sla_materials;
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("current_process_preset", [](PresetBundle& bundle) -> Preset& {
|
||||
return bundle.prints.get_edited_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("current_print_preset", [](PresetBundle& bundle) -> Preset& {
|
||||
return bundle.prints.get_edited_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("current_printer_preset", [](PresetBundle& bundle) -> Preset& {
|
||||
return bundle.printers.get_edited_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("current_filament_preset_names", [](PresetBundle& bundle) {
|
||||
return bundle.filament_presets;
|
||||
})
|
||||
.def("current_filament_presets", ¤t_filament_presets)
|
||||
.def("full_config_keys", [](const PresetBundle& bundle) {
|
||||
return bundle.full_config().keys();
|
||||
})
|
||||
.def("full_config_value", [](const PresetBundle& bundle, const std::string& key) {
|
||||
return config_value_or_none(bundle.full_config(), key);
|
||||
});
|
||||
|
||||
// Axis-aligned bounding box, returned by value (a copy) so its lifetime is
|
||||
// independent of the model object it was computed from. Coordinates are in mm.
|
||||
py::class_<BoundingBoxf3>(host, "BoundingBox", "Axis-aligned bounding box in millimetres")
|
||||
.def_property_readonly("defined", [](const BoundingBoxf3& bb) { return bb.defined; })
|
||||
.def_property_readonly("min", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.min); })
|
||||
.def_property_readonly("max", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.max); })
|
||||
.def_property_readonly("size", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.size()); })
|
||||
.def_property_readonly("center", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.center()); })
|
||||
.def_property_readonly("radius", [](const BoundingBoxf3& bb) { return bb.radius(); });
|
||||
|
||||
py::class_<HostTriangleMesh>(host, "TriangleMesh",
|
||||
"Immutable snapshot of a ModelVolume's mesh in local (untransformed) coordinates, mm.")
|
||||
.def("vertex_count", [](const HostTriangleMesh& mesh) { return mesh.its().vertices.size(); })
|
||||
.def("triangle_count", [](const HostTriangleMesh& mesh) { return mesh.its().indices.size(); })
|
||||
.def("facets_count", [](const HostTriangleMesh& mesh) { return mesh.its().indices.size(); })
|
||||
.def("is_empty", [](const HostTriangleMesh& mesh) { return mesh.its().indices.empty(); })
|
||||
// Read-only, zero-copy (N, 3) float32 view of vertex positions. Requires numpy.
|
||||
.def("vertices", [](const HostTriangleMesh& mesh) {
|
||||
return with_numpy([&] {
|
||||
const indexed_triangle_set& its = mesh.its();
|
||||
return make_readonly_rows3<float>(
|
||||
mesh.mesh,
|
||||
its.vertices.empty() ? nullptr : its.vertices.front().data(),
|
||||
static_cast<py::ssize_t>(its.vertices.size()));
|
||||
});
|
||||
}, "Read-only zero-copy (N, 3) float32 ndarray of vertex positions (local mm). Requires numpy.")
|
||||
// Read-only, zero-copy (M, 3) int32 view of triangle vertex indices. Requires numpy.
|
||||
.def("triangles", [](const HostTriangleMesh& mesh) {
|
||||
return with_numpy([&] {
|
||||
const indexed_triangle_set& its = mesh.its();
|
||||
return make_readonly_rows3<std::int32_t>(
|
||||
mesh.mesh,
|
||||
its.indices.empty() ? nullptr : its.indices.front().data(),
|
||||
static_cast<py::ssize_t>(its.indices.size()));
|
||||
});
|
||||
}, "Read-only zero-copy (M, 3) int32 ndarray of triangle vertex indices. Requires numpy.")
|
||||
// One normalized normal per triangle as an (M, 3) float32 copy. Requires numpy.
|
||||
.def("face_normals", [](const HostTriangleMesh& mesh) {
|
||||
return with_numpy([&] {
|
||||
std::vector<Vec3f> normals = its_face_normals(mesh.its());
|
||||
py::array_t<float> array({ static_cast<py::ssize_t>(normals.size()), py::ssize_t(3) });
|
||||
if (!normals.empty()) {
|
||||
auto view = array.mutable_unchecked<2>();
|
||||
for (size_t i = 0; i < normals.size(); ++i) {
|
||||
view(i, 0) = normals[i].x();
|
||||
view(i, 1) = normals[i].y();
|
||||
view(i, 2) = normals[i].z();
|
||||
}
|
||||
}
|
||||
return py::object(std::move(array));
|
||||
});
|
||||
}, "Per-triangle normalized normals as an (M, 3) float32 ndarray (copy). Requires numpy.")
|
||||
// numpy-free element access, bounds-checked.
|
||||
.def("vertex", [](const HostTriangleMesh& mesh, size_t index) {
|
||||
const std::vector<stl_vertex>& vertices = mesh.its().vertices;
|
||||
if (index >= vertices.size())
|
||||
throw py::index_error("vertex index out of range");
|
||||
const stl_vertex& vertex = vertices[index];
|
||||
return py::make_tuple(vertex.x(), vertex.y(), vertex.z());
|
||||
})
|
||||
.def("triangle", [](const HostTriangleMesh& mesh, size_t index) {
|
||||
const std::vector<stl_triangle_vertex_indices>& indices = mesh.its().indices;
|
||||
if (index >= indices.size())
|
||||
throw py::index_error("triangle index out of range");
|
||||
const stl_triangle_vertex_indices& triangle = indices[index];
|
||||
return py::make_tuple(triangle[0], triangle[1], triangle[2]);
|
||||
})
|
||||
.def("volume", [](const HostTriangleMesh& mesh) { return mesh.mesh->stats().volume; })
|
||||
.def("bounding_box", [](const HostTriangleMesh& mesh) { return bbox_from_stats(mesh.mesh->stats()); })
|
||||
.def("is_manifold", [](const HostTriangleMesh& mesh) { return mesh.mesh->stats().manifold(); });
|
||||
|
||||
py::enum_<ModelVolumeType>(host, "ModelVolumeType")
|
||||
.value("Invalid", ModelVolumeType::INVALID)
|
||||
.value("ModelPart", ModelVolumeType::MODEL_PART)
|
||||
.value("NegativeVolume", ModelVolumeType::NEGATIVE_VOLUME)
|
||||
.value("ParameterModifier", ModelVolumeType::PARAMETER_MODIFIER)
|
||||
.value("SupportBlocker", ModelVolumeType::SUPPORT_BLOCKER)
|
||||
.value("SupportEnforcer", ModelVolumeType::SUPPORT_ENFORCER);
|
||||
|
||||
py::class_<ModelVolume, std::unique_ptr<ModelVolume, py::nodelete>>(host, "ModelVolume")
|
||||
.def("id", [](const ModelVolume& volume) { return volume.id().id; })
|
||||
.def_readonly("name", &ModelVolume::name)
|
||||
.def("type", &ModelVolume::type)
|
||||
.def("is_model_part", &ModelVolume::is_model_part)
|
||||
.def("is_modifier", &ModelVolume::is_modifier)
|
||||
.def("is_negative_volume", &ModelVolume::is_negative_volume)
|
||||
.def("is_support_enforcer", &ModelVolume::is_support_enforcer)
|
||||
.def("is_support_blocker", &ModelVolume::is_support_blocker)
|
||||
.def("is_support_modifier", &ModelVolume::is_support_modifier)
|
||||
// Extruder ID is 1-based for FFF, -1 for SLA or support volumes.
|
||||
.def("extruder_id", &ModelVolume::extruder_id)
|
||||
.def("offset", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_offset()); })
|
||||
.def("rotation", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_rotation()); })
|
||||
.def("scaling_factor", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_scaling_factor()); })
|
||||
.def("mirror", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_mirror()); })
|
||||
// 4x4 float64 affine matrix mapping this volume into its parent object frame. Requires numpy.
|
||||
.def("matrix", [](const ModelVolume& volume) { return mat4_to_numpy(volume.get_matrix()); },
|
||||
"Volume-to-object 4x4 float64 affine matrix (copy). Requires numpy.")
|
||||
.def("facets_count", [](const ModelVolume& volume) { return volume.mesh().facets_count(); })
|
||||
// Raw (untransformed) mesh volume in mm^3; -1 if it was never computed.
|
||||
.def("volume", [](const ModelVolume& volume) { return volume.mesh().stats().volume; })
|
||||
// Bounding box of the raw (untransformed) mesh, in the volume's local frame.
|
||||
.def("bounding_box", [](const ModelVolume& volume) { return bbox_from_stats(volume.mesh().stats()); })
|
||||
.def("is_manifold", [](const ModelVolume& volume) { return volume.mesh().stats().manifold(); })
|
||||
// Full mesh geometry (vertices/triangles) as an immutable snapshot.
|
||||
.def("mesh", [](const ModelVolume& volume) {
|
||||
return HostTriangleMesh{ volume.get_mesh_shared_ptr() };
|
||||
}, "Return the volume's TriangleMesh (local coordinates) for vertex/triangle access.")
|
||||
.def("mesh_errors_count", [](const ModelVolume& volume) { return volume.get_repaired_errors_count(); })
|
||||
.def("is_fdm_support_painted", &ModelVolume::is_fdm_support_painted)
|
||||
.def("is_seam_painted", &ModelVolume::is_seam_painted)
|
||||
.def("is_mm_painted", &ModelVolume::is_mm_painted)
|
||||
.def("is_fuzzy_skin_painted", &ModelVolume::is_fuzzy_skin_painted)
|
||||
.def("config_keys", [](const ModelVolume& volume) { return volume.config.keys(); })
|
||||
.def("config_value", [](const ModelVolume& volume, const std::string& key) {
|
||||
return config_value_or_none(volume.config.get(), key);
|
||||
});
|
||||
|
||||
py::class_<ModelInstance, std::unique_ptr<ModelInstance, py::nodelete>>(host, "ModelInstance")
|
||||
.def("id", [](const ModelInstance& instance) { return instance.id().id; })
|
||||
.def_readonly("printable", &ModelInstance::printable)
|
||||
// True only if the object is printable, this instance is printable and it
|
||||
// currently sits fully inside the print volume (set during slicing).
|
||||
.def("is_printable", &ModelInstance::is_printable)
|
||||
.def("offset", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_offset()); })
|
||||
.def("rotation", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_rotation()); })
|
||||
.def("scaling_factor", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_scaling_factor()); })
|
||||
.def("mirror", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_mirror()); })
|
||||
// 4x4 float64 affine matrix mapping the object into world space. Requires numpy.
|
||||
// World vertices = instance.matrix() @ volume.matrix() applied to mesh vertices.
|
||||
.def("matrix", [](const ModelInstance& instance) { return mat4_to_numpy(instance.get_matrix()); },
|
||||
"Object-to-world 4x4 float64 affine matrix (copy). Requires numpy.")
|
||||
.def("is_left_handed", &ModelInstance::is_left_handed)
|
||||
// World-space bounding box of this instance.
|
||||
.def("bounding_box", [](ModelInstance& instance) {
|
||||
const ModelObject* object = instance.get_object();
|
||||
if (object == nullptr)
|
||||
return BoundingBoxf3();
|
||||
return object->instance_bounding_box(instance);
|
||||
});
|
||||
|
||||
py::class_<ModelObject, std::unique_ptr<ModelObject, py::nodelete>>(host, "ModelObject")
|
||||
.def("id", [](const ModelObject& object) { return object.id().id; })
|
||||
.def_readonly("name", &ModelObject::name)
|
||||
.def_readonly("module_name", &ModelObject::module_name)
|
||||
.def_readonly("input_file", &ModelObject::input_file)
|
||||
.def_readonly("printable", &ModelObject::printable)
|
||||
.def("instance_count", [](const ModelObject& object) {
|
||||
return object.instances.size();
|
||||
})
|
||||
.def("volume_count", [](const ModelObject& object) {
|
||||
return object.volumes.size();
|
||||
})
|
||||
.def("instances", [](ModelObject& object) {
|
||||
py::list instances;
|
||||
for (ModelInstance* instance : object.instances)
|
||||
instances.append(py::cast(instance, py::return_value_policy::reference));
|
||||
return instances;
|
||||
})
|
||||
.def("instance", [](ModelObject& object, size_t index) -> ModelInstance* {
|
||||
if (index >= object.instances.size())
|
||||
throw py::index_error("instance index out of range");
|
||||
return object.instances[index];
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("volumes", [](ModelObject& object) {
|
||||
py::list volumes;
|
||||
for (ModelVolume* volume : object.volumes)
|
||||
volumes.append(py::cast(volume, py::return_value_policy::reference));
|
||||
return volumes;
|
||||
})
|
||||
.def("volume", [](ModelObject& object, size_t index) -> ModelVolume* {
|
||||
if (index >= object.volumes.size())
|
||||
throw py::index_error("volume index out of range");
|
||||
return object.volumes[index];
|
||||
}, py::return_value_policy::reference_internal)
|
||||
// World-space bounding box over all instances of this object.
|
||||
.def("bounding_box", [](const ModelObject& object) { return object.bounding_box_exact(); })
|
||||
// Bounding box of the object's raw (untransformed) part meshes — its intrinsic size.
|
||||
.def("raw_mesh_bounding_box", [](const ModelObject& object) { return object.raw_mesh_bounding_box(); })
|
||||
.def("min_z", &ModelObject::min_z)
|
||||
.def("max_z", &ModelObject::max_z)
|
||||
.def("facets_count", [](const ModelObject& object) { return object.facets_count(); })
|
||||
.def("parts_count", [](const ModelObject& object) { return object.parts_count(); })
|
||||
.def("materials_count", [](const ModelObject& object) { return object.materials_count(); })
|
||||
.def("mesh_errors_count", [](const ModelObject& object) { return object.get_repaired_errors_count(); })
|
||||
.def("is_multiparts", &ModelObject::is_multiparts)
|
||||
.def("is_cut", &ModelObject::is_cut)
|
||||
.def("has_custom_layering", &ModelObject::has_custom_layering)
|
||||
.def("is_fdm_support_painted", &ModelObject::is_fdm_support_painted)
|
||||
.def("is_seam_painted", &ModelObject::is_seam_painted)
|
||||
.def("is_mm_painted", &ModelObject::is_mm_painted)
|
||||
.def("is_fuzzy_skin_painted", &ModelObject::is_fuzzy_skin_painted)
|
||||
.def("config_keys", [](const ModelObject& object) {
|
||||
return object.config.keys();
|
||||
})
|
||||
.def("config_value", [](const ModelObject& object, const std::string& key) {
|
||||
return config_value_or_none(object.config.get(), key);
|
||||
});
|
||||
|
||||
py::class_<Model, std::unique_ptr<Model, py::nodelete>>(host, "Model")
|
||||
.def("id", [](const Model& model) { return model.id().id; })
|
||||
.def("object_count", [](const Model& model) {
|
||||
return model.objects.size();
|
||||
})
|
||||
.def("object", [](Model& model, size_t index) -> ModelObject* {
|
||||
if (index >= model.objects.size())
|
||||
throw py::index_error("model object index out of range");
|
||||
return model.objects[index];
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("objects", [](Model& model) {
|
||||
py::list objects;
|
||||
for (ModelObject* object : model.objects)
|
||||
objects.append(py::cast(object, py::return_value_policy::reference));
|
||||
return objects;
|
||||
})
|
||||
// World-space bounding box of the whole model. bounding_box() is exact;
|
||||
// bounding_box_approx() is faster and cached.
|
||||
.def("bounding_box", [](const Model& model) { return model.bounding_box_exact(); })
|
||||
.def("bounding_box_approx", [](const Model& model) { return model.bounding_box_approx(); })
|
||||
.def("max_z", &Model::max_z)
|
||||
.def("material_count", [](const Model& model) { return model.materials.size(); })
|
||||
.def("is_fdm_support_painted", &Model::is_fdm_support_painted)
|
||||
.def("is_seam_painted", &Model::is_seam_painted)
|
||||
.def("is_mm_painted", &Model::is_mm_painted)
|
||||
.def("is_fuzzy_skin_painted", &Model::is_fuzzy_skin_painted)
|
||||
.def("current_plate_index", [](const Model& model) { return model.curr_plate_index; })
|
||||
.def("designer", [](const Model& model) {
|
||||
return model.design_info ? model.design_info->Designer : std::string();
|
||||
})
|
||||
.def("design_id", [](const Model& model) { return model.stl_design_id; });
|
||||
|
||||
py::class_<GUI::Plater, std::unique_ptr<GUI::Plater, py::nodelete>>(host, "Plater")
|
||||
.def("model", static_cast<Model& (GUI::Plater::*)()>(&GUI::Plater::model), py::return_value_policy::reference_internal)
|
||||
.def("is_project_dirty", &GUI::Plater::is_project_dirty)
|
||||
.def("is_presets_dirty", &GUI::Plater::is_presets_dirty)
|
||||
.def("inside_snapshot_capture", &GUI::Plater::inside_snapshot_capture);
|
||||
|
||||
host.def("plater", ¤t_plater, py::return_value_policy::reference);
|
||||
host.def("model", []() -> Model& {
|
||||
return current_plater()->model();
|
||||
}, py::return_value_policy::reference);
|
||||
host.def("preset_bundle", ¤t_preset_bundle, py::return_value_policy::reference);
|
||||
|
||||
// UI: native dialogs and interactive HTML windows for plugins.
|
||||
PluginHostUi::RegisterBindings(host);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
13
src/slic3r/plugin/PluginHostApi.hpp
Normal file
13
src/slic3r/plugin/PluginHostApi.hpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class PluginHostApi
|
||||
{
|
||||
public:
|
||||
static void RegisterBindings(pybind11::module_& module);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
562
src/slic3r/plugin/PluginHostUi.cpp
Normal file
562
src/slic3r/plugin/PluginHostUi.cpp
Normal file
@@ -0,0 +1,562 @@
|
||||
#include "PluginHostUi.hpp"
|
||||
|
||||
#include "PluginAuditManager.hpp"
|
||||
#include "PythonInterpreter.hpp" // PythonGILState
|
||||
|
||||
#include <slic3r/GUI/GUI_App.hpp>
|
||||
#include <slic3r/GUI/MainFrame.hpp>
|
||||
#include <slic3r/GUI/MsgDialog.hpp>
|
||||
#include <slic3r/GUI/PluginProgressDialog.hpp>
|
||||
#include <slic3r/GUI/PluginWebDialog.hpp>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include <wx/app.h>
|
||||
#include <wx/defs.h>
|
||||
#include <wx/window.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace py = pybind11;
|
||||
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
|
||||
// teardown), which would Py_DECREF unsafely. Wrapping the callable here means
|
||||
// the GIL is acquired exactly when the last reference is released, on any thread.
|
||||
// --------------------------------------------------------------------------
|
||||
struct GilSafeCallable
|
||||
{
|
||||
py::object fn;
|
||||
explicit GilSafeCallable(py::object f) : fn(std::move(f)) {}
|
||||
~GilSafeCallable()
|
||||
{
|
||||
if (fn) {
|
||||
PythonGILState gil;
|
||||
fn = py::object();
|
||||
}
|
||||
}
|
||||
};
|
||||
using CallablePtr = std::shared_ptr<GilSafeCallable>;
|
||||
|
||||
CallablePtr make_holder(py::object obj)
|
||||
{
|
||||
if (!obj || obj.is_none())
|
||||
return nullptr;
|
||||
return std::make_shared<GilSafeCallable>(std::move(obj));
|
||||
}
|
||||
|
||||
// Adapt a Python callable to a GUI message handler that acquires the GIL and
|
||||
// swallows/logs exceptions (a raising handler must not escape into wx events).
|
||||
GUI::PluginWebDialog::MessageHandler make_message_adapter(py::object on_message)
|
||||
{
|
||||
CallablePtr holder = make_holder(std::move(on_message));
|
||||
if (!holder)
|
||||
return nullptr;
|
||||
return [holder](const json& data) {
|
||||
PythonGILState gil;
|
||||
try {
|
||||
holder->fn(json_to_py(data));
|
||||
} catch (py::error_already_set& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "orca.host.ui on_message handler raised: " << e.what();
|
||||
PyErr_Clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Registry of live plugin UI resources. Keyed by an opaque id; tracks the
|
||||
// owning plugin so all of a plugin's UI can be torn down on unload.
|
||||
// --------------------------------------------------------------------------
|
||||
class UiRegistry
|
||||
{
|
||||
public:
|
||||
static UiRegistry& instance()
|
||||
{
|
||||
static UiRegistry r;
|
||||
return r;
|
||||
}
|
||||
|
||||
int reserve_id()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mtx);
|
||||
return m_next_id++;
|
||||
}
|
||||
void bind(int id, wxWindow* window, const std::string& plugin_key)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mtx);
|
||||
m_resources[id] = window;
|
||||
m_owners[id] = plugin_key;
|
||||
}
|
||||
void remove(int id)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mtx);
|
||||
m_resources.erase(id);
|
||||
m_owners.erase(id);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T* get_as(int id)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mtx);
|
||||
auto it = m_resources.find(id);
|
||||
if (it == m_resources.end())
|
||||
return nullptr;
|
||||
return dynamic_cast<T*>(it->second);
|
||||
}
|
||||
bool is_open(int id)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mtx);
|
||||
return m_resources.count(id) > 0; // presence only; no pointer deref -> thread-safe
|
||||
}
|
||||
std::vector<wxWindow*> take_for_plugin(const std::string& plugin_key)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mtx);
|
||||
std::vector<wxWindow*> out;
|
||||
for (auto it = m_owners.begin(); it != m_owners.end();) {
|
||||
if (it->second == plugin_key) {
|
||||
auto rit = m_resources.find(it->first);
|
||||
if (rit != m_resources.end()) {
|
||||
out.push_back(rit->second);
|
||||
m_resources.erase(rit);
|
||||
}
|
||||
it = m_owners.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex m_mtx;
|
||||
std::unordered_map<int, wxWindow*> m_resources;
|
||||
std::unordered_map<int, std::string> m_owners;
|
||||
int m_next_id{1};
|
||||
};
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Run a (pure C++/wx) callable on the main/UI thread, blocking the caller until
|
||||
// it completes, with the GIL released across the wait. If already on the main
|
||||
// thread, run inline (also with the GIL released so other Python threads run).
|
||||
// --------------------------------------------------------------------------
|
||||
template<typename Fn>
|
||||
auto run_on_ui_blocking(Fn&& fn) -> std::invoke_result_t<Fn&>
|
||||
{
|
||||
using R = std::invoke_result_t<Fn&>;
|
||||
if (wxTheApp == nullptr)
|
||||
throw std::runtime_error("OrcaSlicer application is not initialized");
|
||||
|
||||
if (wxIsMainThread()) {
|
||||
py::gil_scoped_release nogil;
|
||||
return fn();
|
||||
}
|
||||
|
||||
std::promise<R> prom;
|
||||
std::future<R> fut = prom.get_future();
|
||||
|
||||
py::gil_scoped_release nogil;
|
||||
GUI::wxGetApp().CallAfter([&prom, &fn]() {
|
||||
try {
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
fn();
|
||||
prom.set_value();
|
||||
} else {
|
||||
prom.set_value(fn());
|
||||
}
|
||||
} catch (...) {
|
||||
prom.set_exception(std::current_exception());
|
||||
}
|
||||
});
|
||||
return fut.get();
|
||||
}
|
||||
|
||||
wxWindow* ui_parent()
|
||||
{
|
||||
return wxTheApp == nullptr ? nullptr : dynamic_cast<wxWindow*>(GUI::wxGetApp().mainframe);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// orca.host.ui.message
|
||||
// --------------------------------------------------------------------------
|
||||
long message_style(const std::string& buttons, const std::string& icon)
|
||||
{
|
||||
long style = wxOK;
|
||||
if (buttons == "ok_cancel")
|
||||
style = wxOK | wxCANCEL;
|
||||
else if (buttons == "yes_no")
|
||||
style = wxYES_NO;
|
||||
else if (buttons == "yes_no_cancel")
|
||||
style = wxYES_NO | wxCANCEL;
|
||||
|
||||
if (icon == "warning")
|
||||
style |= wxICON_WARNING;
|
||||
else if (icon == "error")
|
||||
style |= wxICON_ERROR;
|
||||
else if (icon == "question")
|
||||
style |= wxICON_QUESTION;
|
||||
else
|
||||
style |= wxICON_INFORMATION;
|
||||
return style;
|
||||
}
|
||||
|
||||
std::string button_to_string(int rc)
|
||||
{
|
||||
switch (rc) {
|
||||
case wxID_OK: return "ok";
|
||||
case wxID_YES: return "yes";
|
||||
case wxID_NO: return "no";
|
||||
default: return "cancel";
|
||||
}
|
||||
}
|
||||
|
||||
std::string ui_message(const std::string& text, const std::string& title,
|
||||
const std::string& buttons, const std::string& icon)
|
||||
{
|
||||
const long style = message_style(buttons, icon);
|
||||
return run_on_ui_blocking([&]() -> std::string {
|
||||
GUI::MessageDialog dlg(nullptr, wxString::FromUTF8(text), wxString::FromUTF8(title), style);
|
||||
return button_to_string(dlg.ShowModal());
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// orca.host.ui.show_dialog (modal)
|
||||
// --------------------------------------------------------------------------
|
||||
py::object ui_show_dialog(const std::string& html, const std::string& title,
|
||||
int width, int height, py::object on_message)
|
||||
{
|
||||
auto handler = make_message_adapter(std::move(on_message));
|
||||
const int w = width > 0 ? width : 820;
|
||||
const int h = height > 0 ? height : 600;
|
||||
|
||||
std::optional<json> result = run_on_ui_blocking([&]() -> std::optional<json> {
|
||||
return GUI::PluginWebDialog::show_modal_dialog(ui_parent(), wxString::FromUTF8(title), html, wxSize(w, h),
|
||||
std::move(handler));
|
||||
});
|
||||
|
||||
if (!result.has_value())
|
||||
return py::none();
|
||||
return json_to_py(*result); // GIL held in the binding body
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// orca.host.ui.create_window (non-modal) + UiWindow handle
|
||||
// --------------------------------------------------------------------------
|
||||
struct UiWindowHandle
|
||||
{
|
||||
int id{0};
|
||||
};
|
||||
|
||||
struct UiProgressHandle
|
||||
{
|
||||
int id{0};
|
||||
};
|
||||
|
||||
py::object ui_create_window(const std::string& html, const std::string& title, int width, int height,
|
||||
py::object on_message, py::object on_close)
|
||||
{
|
||||
auto msg_adapter = make_message_adapter(std::move(on_message));
|
||||
CallablePtr close_holder = make_holder(std::move(on_close));
|
||||
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
|
||||
const int w = width > 0 ? width : 820;
|
||||
const int h = height > 0 ? height : 600;
|
||||
|
||||
const int id = run_on_ui_blocking([&]() -> int {
|
||||
const int new_id = UiRegistry::instance().reserve_id();
|
||||
|
||||
// Plugin's on_close: fired only on a user/JS-initiated close (not forced
|
||||
// teardown), while the dialog is alive. Empty if the plugin passed None.
|
||||
GUI::PluginWebDialog::CloseHandler on_close;
|
||||
if (close_holder) {
|
||||
on_close = [close_holder]() {
|
||||
PythonGILState gil;
|
||||
try {
|
||||
close_holder->fn();
|
||||
} catch (py::error_already_set& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "orca.host.ui on_close handler raised: " << e.what();
|
||||
PyErr_Clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
// Registry cleanup: GIL-free, runs from the dialog destructor on every path.
|
||||
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
|
||||
|
||||
auto* dlg = GUI::PluginWebDialog::create_modeless_dialog(ui_parent(), wxString::FromUTF8(title), html,
|
||||
wxSize(w, h), std::move(msg_adapter),
|
||||
std::move(on_close), std::move(on_destroyed));
|
||||
UiRegistry::instance().bind(new_id, dlg, plugin_key);
|
||||
GUI::PluginWebDialog::show_modeless_dialog(dlg);
|
||||
return new_id;
|
||||
});
|
||||
|
||||
return py::cast(UiWindowHandle{id});
|
||||
}
|
||||
|
||||
void handle_post(int id, py::object data)
|
||||
{
|
||||
if (wxTheApp == nullptr)
|
||||
return;
|
||||
json j = py_to_json(data); // GIL held (binding body)
|
||||
GUI::wxGetApp().CallAfter([id, j = std::move(j)]() {
|
||||
auto* d = UiRegistry::instance().get_as<GUI::PluginWebDialog>(id);
|
||||
GUI::PluginWebDialog::post_message(d, j);
|
||||
});
|
||||
}
|
||||
|
||||
void handle_close(int id)
|
||||
{
|
||||
if (wxTheApp == nullptr)
|
||||
return;
|
||||
GUI::wxGetApp().CallAfter([id]() {
|
||||
auto* d = UiRegistry::instance().get_as<GUI::PluginWebDialog>(id);
|
||||
GUI::PluginWebDialog::request_close(d);
|
||||
});
|
||||
}
|
||||
|
||||
UiProgressHandle ui_create_progress_dialog(const std::string& title, const std::string& message, int maximum, int style)
|
||||
{
|
||||
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
|
||||
const int max_value = maximum > 0 ? maximum : 100;
|
||||
|
||||
return run_on_ui_blocking([&]() -> UiProgressHandle {
|
||||
const int new_id = UiRegistry::instance().reserve_id();
|
||||
|
||||
// Registry cleanup: GIL-free, runs from the dialog destructor on every path.
|
||||
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
|
||||
|
||||
auto* dlg = GUI::PluginProgressDialog::create_dialog(ui_parent(), wxString::FromUTF8(title),
|
||||
wxString::FromUTF8(message), max_value, style,
|
||||
std::move(on_destroyed));
|
||||
UiRegistry::instance().bind(new_id, dlg, plugin_key);
|
||||
return UiProgressHandle{new_id};
|
||||
});
|
||||
}
|
||||
|
||||
UiProgressHandle* new_progress_dialog(const std::string& title, const std::string& message, int maximum, int style)
|
||||
{
|
||||
return new UiProgressHandle(ui_create_progress_dialog(title, message, maximum, style));
|
||||
}
|
||||
|
||||
bool progress_is_open(int id)
|
||||
{
|
||||
return UiRegistry::instance().get_as<GUI::PluginProgressDialog>(id) != nullptr;
|
||||
}
|
||||
|
||||
bool progress_pulse(int id, const std::string& message)
|
||||
{
|
||||
return run_on_ui_blocking([&]() {
|
||||
auto* d = UiRegistry::instance().get_as<GUI::PluginProgressDialog>(id);
|
||||
return GUI::PluginProgressDialog::pulse(d, wxString::FromUTF8(message));
|
||||
});
|
||||
}
|
||||
|
||||
bool progress_update(int id, int value, const std::string& message)
|
||||
{
|
||||
return run_on_ui_blocking([&]() {
|
||||
auto* d = UiRegistry::instance().get_as<GUI::PluginProgressDialog>(id);
|
||||
return GUI::PluginProgressDialog::update(d, value, wxString::FromUTF8(message));
|
||||
});
|
||||
}
|
||||
|
||||
void progress_start_pulse(int id, int interval_ms, const std::string& message)
|
||||
{
|
||||
run_on_ui_blocking([&]() {
|
||||
auto* d = UiRegistry::instance().get_as<GUI::PluginProgressDialog>(id);
|
||||
GUI::PluginProgressDialog::start_pulse(d, interval_ms, wxString::FromUTF8(message));
|
||||
});
|
||||
}
|
||||
|
||||
void progress_stop_pulse(int id)
|
||||
{
|
||||
run_on_ui_blocking([&]() {
|
||||
auto* d = UiRegistry::instance().get_as<GUI::PluginProgressDialog>(id);
|
||||
GUI::PluginProgressDialog::stop_pulse(d);
|
||||
});
|
||||
}
|
||||
|
||||
void progress_close(int id)
|
||||
{
|
||||
run_on_ui_blocking([&]() {
|
||||
auto* d = UiRegistry::instance().get_as<GUI::PluginProgressDialog>(id);
|
||||
GUI::PluginProgressDialog::request_close(d);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void PluginHostUi::RegisterBindings(pybind11::module_& host)
|
||||
{
|
||||
auto ui = host.def_submodule(
|
||||
"ui",
|
||||
"Host UI: native dialogs and interactive HTML windows. Calls run on the main/UI "
|
||||
"thread (marshaled from the plugin thread). See the plugin docs for the window.orca bridge.");
|
||||
|
||||
ui.def("message", &ui_message, py::arg("text"), py::arg("title") = "OrcaSlicer", py::arg("buttons") = "ok",
|
||||
py::arg("icon") = "info",
|
||||
"Show a native modal message box; returns the clicked button id "
|
||||
"(\"ok\"/\"cancel\"/\"yes\"/\"no\"). buttons: \"ok\"|\"ok_cancel\"|\"yes_no\"|\"yes_no_cancel\"; "
|
||||
"icon: \"info\"|\"warning\"|\"error\"|\"question\".");
|
||||
|
||||
ui.def("show_dialog", &ui_show_dialog, py::arg("html"), py::arg("title") = "OrcaSlicer", py::arg("width") = 820,
|
||||
py::arg("height") = 600, py::arg("on_message") = py::none(),
|
||||
"Show a modal dialog rendering the given raw HTML. The page talks to the plugin via "
|
||||
"window.orca (postMessage/onMessage/submit/close). Blocks until closed; returns the "
|
||||
"orca.submit() payload as a dict, or None.");
|
||||
|
||||
ui.attr("PD_APP_MODAL") = py::int_(wxPD_APP_MODAL);
|
||||
ui.attr("PD_AUTO_HIDE") = py::int_(wxPD_AUTO_HIDE);
|
||||
ui.attr("PD_CAN_ABORT") = py::int_(wxPD_CAN_ABORT);
|
||||
ui.attr("PD_CAN_SKIP") = py::int_(wxPD_CAN_SKIP);
|
||||
ui.attr("PD_ELAPSED_TIME") = py::int_(wxPD_ELAPSED_TIME);
|
||||
ui.attr("PD_ESTIMATED_TIME") = py::int_(wxPD_ESTIMATED_TIME);
|
||||
ui.attr("PD_REMAINING_TIME") = py::int_(wxPD_REMAINING_TIME);
|
||||
|
||||
py::class_<UiWindowHandle>(ui, "UiWindow", "Handle to a non-modal plugin window created by create_window().")
|
||||
.def_property_readonly("id", [](const UiWindowHandle& h) { return h.id; })
|
||||
.def(
|
||||
"post", [](const UiWindowHandle& h, py::object data) { handle_post(h.id, std::move(data)); },
|
||||
py::arg("data"), "Send a payload to the page (delivered to window.orca.onMessage handlers).")
|
||||
.def(
|
||||
"close", [](const UiWindowHandle& h) { handle_close(h.id); }, "Close the window (fires on_close).")
|
||||
.def(
|
||||
"is_open", [](const UiWindowHandle& h) { return UiRegistry::instance().is_open(h.id); },
|
||||
"Return True while the window is open.");
|
||||
|
||||
ui.def("create_window", &ui_create_window, py::arg("html"), py::arg("title") = "OrcaSlicer", py::arg("width") = 820,
|
||||
py::arg("height") = 600, py::arg("on_message") = py::none(), py::arg("on_close") = py::none(),
|
||||
"Open a non-modal, persistent HTML window and return a UiWindow. on_message(data) is called on "
|
||||
"the UI thread when the page posts; offload heavy work to a thread and push results back with "
|
||||
"window.post().");
|
||||
|
||||
py::class_<UiProgressHandle>(ui, "ProgressDialog", "Handle to a native progress dialog.")
|
||||
.def(py::init(&new_progress_dialog), py::arg("title"), py::arg("message"), py::arg("maximum") = 100,
|
||||
py::arg("style") = wxPD_APP_MODAL | wxPD_AUTO_HIDE)
|
||||
.def_property_readonly("id", [](const UiProgressHandle& h) { return h.id; })
|
||||
.def(
|
||||
"pulse", [](const UiProgressHandle& h, const std::string& message) { return progress_pulse(h.id, message); },
|
||||
py::arg("message") = "", "Pulse the dialog gauge; returns False if the dialog is closed or cancelled.")
|
||||
.def(
|
||||
"update",
|
||||
[](const UiProgressHandle& h, int value, const std::string& message) {
|
||||
return progress_update(h.id, value, message);
|
||||
},
|
||||
py::arg("value"), py::arg("message") = "",
|
||||
"Set the dialog progress value; returns False if the dialog is closed or cancelled.")
|
||||
.def(
|
||||
"start_pulse",
|
||||
[](const UiProgressHandle& h, int interval_ms, const std::string& message) {
|
||||
progress_start_pulse(h.id, interval_ms, message);
|
||||
},
|
||||
py::arg("interval_ms") = 100, py::arg("message") = "", "Start periodic pulsing.")
|
||||
.def(
|
||||
"stop_pulse", [](const UiProgressHandle& h) { progress_stop_pulse(h.id); }, "Stop periodic pulsing.")
|
||||
.def(
|
||||
"close", [](const UiProgressHandle& h) { progress_close(h.id); }, "Close the progress dialog.")
|
||||
.def(
|
||||
"is_open", [](const UiProgressHandle& h) { return progress_is_open(h.id); },
|
||||
"Return True while this progress dialog is registered.")
|
||||
.def("__enter__", [](UiProgressHandle& h) -> UiProgressHandle& { return h; }, py::return_value_policy::reference_internal)
|
||||
.def("__exit__", [](const UiProgressHandle& h, py::object, py::object, py::object) {
|
||||
progress_close(h.id);
|
||||
return false;
|
||||
});
|
||||
|
||||
ui.def("create_progress_dialog", &ui_create_progress_dialog, py::arg("title"), py::arg("message"),
|
||||
py::arg("maximum") = 100, py::arg("style") = wxPD_APP_MODAL | wxPD_AUTO_HIDE,
|
||||
"Create a native progress dialog and return a ProgressDialog handle.");
|
||||
}
|
||||
|
||||
void PluginHostUi::close_windows_for_plugin(const std::string& plugin_key)
|
||||
{
|
||||
if (wxTheApp == nullptr)
|
||||
return;
|
||||
|
||||
auto teardown = [plugin_key]() {
|
||||
// Destroy() bypasses wxEVT_CLOSE, so the plugin's on_close is not fired on
|
||||
// forced teardown (intended); the resource destructor still cleans the registry.
|
||||
for (auto* window : UiRegistry::instance().take_for_plugin(plugin_key)) {
|
||||
if (window != nullptr)
|
||||
window->Destroy();
|
||||
}
|
||||
};
|
||||
|
||||
if (wxIsMainThread())
|
||||
teardown();
|
||||
else
|
||||
GUI::wxGetApp().CallAfter(teardown);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
24
src/slic3r/plugin/PluginHostUi.hpp
Normal file
24
src/slic3r/plugin/PluginHostUi.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Binds the `orca.host.ui` submodule: native message boxes, progress dialogs,
|
||||
// and interactive HTML windows for plugins. All calls run on the main/UI thread
|
||||
// (marshaled from the plugin worker thread) and the host owns every window.
|
||||
class PluginHostUi
|
||||
{
|
||||
public:
|
||||
static void RegisterBindings(pybind11::module_& host);
|
||||
|
||||
// Lifecycle hook: close and tear down every UI window owned by a plugin.
|
||||
// Registered via PluginLoader::subscribe_on_unload_callback so UI windows
|
||||
// are destroyed on plugin unload/reload and at app shutdown (before the
|
||||
// Python interpreter is finalized). Matches PluginLifecycleCompleteFn.
|
||||
static void close_windows_for_plugin(const std::string& plugin_key);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
1506
src/slic3r/plugin/PluginLoader.cpp
Normal file
1506
src/slic3r/plugin/PluginLoader.cpp
Normal file
File diff suppressed because it is too large
Load Diff
212
src/slic3r/plugin/PluginLoader.hpp
Normal file
212
src/slic3r/plugin/PluginLoader.hpp
Normal file
@@ -0,0 +1,212 @@
|
||||
#pragma once
|
||||
|
||||
#include "PluginDescriptor.hpp"
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include <condition_variable>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
|
||||
#include <pybind11/embed.h>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class PluginCatalog;
|
||||
|
||||
// A single capability materialized from a plugin package. The owning LoadedPlugin
|
||||
// holds the descriptor and the Python module; capabilities only back-reference the
|
||||
// package by plugin_key and cache their resolved name.
|
||||
struct LoadedPluginCapability
|
||||
{
|
||||
std::shared_ptr<PluginCapabilityInterface> instance; // Materialized capability instance
|
||||
std::string name; // Cached from instance->get_name() at load time
|
||||
std::string plugin_key; // Owning package
|
||||
PluginCapabilityType type = PluginCapabilityType::Unknown; // cached from instance->get_type() at load (GUI reads this without the GIL)
|
||||
std::atomic<bool> enabled{true}; // logical enable/disable; disabled capabilities are skipped by consumers but stay loaded
|
||||
};
|
||||
|
||||
struct PluginCapabilityIdentifier
|
||||
{
|
||||
PluginCapabilityType type = PluginCapabilityType::Unknown;
|
||||
std::string name;
|
||||
std::string plugin_key; // owning package — makes the identity globally unique
|
||||
|
||||
bool operator==(const PluginCapabilityIdentifier& o) const
|
||||
{ return type == o.type && name == o.name && plugin_key == o.plugin_key; }
|
||||
};
|
||||
|
||||
// A loaded plugin package: one .py/.whl file → one descriptor + one module + N capabilities.
|
||||
struct LoadedPlugin
|
||||
{
|
||||
PluginDescriptor descriptor;
|
||||
PyObject* module = nullptr; // Python module object, shared by all capabilities
|
||||
std::vector<PluginCapabilityIdentifier> capabilities;
|
||||
|
||||
LoadedPlugin() = default;
|
||||
LoadedPlugin(const LoadedPlugin&) = delete;
|
||||
LoadedPlugin& operator=(const LoadedPlugin&) = delete;
|
||||
// Move transfers module ownership; the moved-from package must not Py_DECREF it.
|
||||
LoadedPlugin(LoadedPlugin&& other) noexcept
|
||||
: descriptor(std::move(other.descriptor)), module(other.module), capabilities(std::move(other.capabilities))
|
||||
{ other.module = nullptr; }
|
||||
LoadedPlugin& operator=(LoadedPlugin&& other) noexcept = delete;
|
||||
|
||||
~LoadedPlugin();
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
template<> struct std::hash<Slic3r::PluginCapabilityIdentifier>
|
||||
{
|
||||
std::size_t operator()(const Slic3r::PluginCapabilityIdentifier& id) const noexcept
|
||||
{
|
||||
std::size_t h = std::hash<std::size_t>{}(static_cast<std::size_t>(id.type));
|
||||
auto mix = [&h](std::size_t v) { h ^= v + 0x9e3779b97f4a7c15ULL + (h << 6) + (h >> 2); };
|
||||
mix(std::hash<std::string>{}(id.name));
|
||||
mix(std::hash<std::string>{}(id.plugin_key));
|
||||
return h;
|
||||
}
|
||||
};
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class PluginLoader
|
||||
{
|
||||
public:
|
||||
enum CallbackType {
|
||||
Load,
|
||||
Unload,
|
||||
};
|
||||
|
||||
using PluginLoadInProgress = std::unordered_set<std::string>;
|
||||
using PluginLoadErrors = std::unordered_map<std::string, std::string>;
|
||||
|
||||
using PluginLifecycleCompleteFn = std::function<void(const std::string&)>;
|
||||
|
||||
using PluginChangedCallbacks = std::unordered_map<PluginCapabilityType, std::vector<std::function<void(const std::vector<PluginDescriptor>&)>>>;
|
||||
|
||||
bool is_idle_and_empty() const;
|
||||
bool is_plugin_loaded(const std::string& plugin_key) const;
|
||||
bool is_plugin_load_in_progress(const std::string& plugin_key) const;
|
||||
void wait_for_all_plugin_loads() const;
|
||||
bool wait_for_all_plugin_loads(std::chrono::milliseconds timeout) const; // bounded; true if all finished, false on timeout
|
||||
bool wait_for_plugin_load(const std::string& plugin_key,
|
||||
std::chrono::milliseconds timeout,
|
||||
std::string& error) const;
|
||||
std::vector<PluginDescriptor> get_all_loaded_plugin_descriptors() const;
|
||||
|
||||
|
||||
// Package descriptor accessor; returns nullptr when the package is not loaded.
|
||||
std::vector<std::shared_ptr<LoadedPluginCapability>> get_plugin_capabilities_by_type(const std::string& plugin_type) const;
|
||||
std::vector<std::shared_ptr<LoadedPluginCapability>> get_plugin_capabilities_by_type(PluginCapabilityType type) const;
|
||||
std::vector<std::shared_ptr<LoadedPluginCapability>> get_plugin_capabilities_by_type(
|
||||
const std::string& plugin_key, PluginCapabilityType type) const;
|
||||
// Resolve a capability by its owning package + type + name. plugin_key is matched by equality.
|
||||
std::shared_ptr<LoadedPluginCapability> get_plugin_capability_by_name(
|
||||
const std::string& plugin_key, PluginCapabilityType type, const std::string& name) const;
|
||||
std::shared_ptr<LoadedPluginCapability> try_get_plugin_capability_by_name_and_type(const std::string& capability_name, PluginCapabilityType type) const;
|
||||
std::shared_ptr<LoadedPluginCapability> get_plugin_capability_by_name(const PluginCapabilityIdentifier& identifier) const;
|
||||
std::vector<std::shared_ptr<LoadedPluginCapability>> get_loaded_plugin_capabilities(const std::string& plugin_key) const;
|
||||
|
||||
std::string get_plugin_load_error(const std::string& plugin_key) const;
|
||||
bool cancel_plugin_load(const std::string& plugin_key);
|
||||
bool cancel_plugin_unload(const std::string& plugin_key);
|
||||
|
||||
bool install_packages(const std::vector<std::string>& pkgs, std::string& error) const;
|
||||
void unload_all_plugins();
|
||||
bool unload_plugin(const std::string& plugin_key,
|
||||
PluginCapabilityType type);
|
||||
bool unload_plugin(const std::string& plugin_key);
|
||||
|
||||
void load_plugin(PluginCatalog& catalog,
|
||||
const std::string& plugin_key,
|
||||
bool skip_deps = false,
|
||||
std::vector<std::string> capabilities_to_enable = std::vector<std::string>());
|
||||
|
||||
void enable_capability(const std::string& plugin_key, const std::string& capability_name, PluginCapabilityType type);
|
||||
void disable_capability(const std::string& plugin_key, const std::string& capability_name, PluginCapabilityType type);
|
||||
|
||||
// Writes the .install_state.json sidecar for a currently-loaded plugin (enabled=true plus
|
||||
// the current per-capability enabled flags). Source of truth for auto-load on next startup.
|
||||
void write_loaded_plugin_install_state(const std::string& plugin_key);
|
||||
|
||||
bool inspect_local_plugin_package(const boost::filesystem::path& filepath,
|
||||
PluginDescriptor& plugin_descriptor,
|
||||
bool& existing_installation,
|
||||
std::string& error) const;
|
||||
bool install_plugin(const boost::filesystem::path& filepath, std::string& error);
|
||||
bool install_plugin(const boost::filesystem::path& filepath,
|
||||
PluginDescriptor& plugin_descriptor, std::string& error);
|
||||
void clear_loaded_plugin_cloud_state(const std::string& plugin_key);
|
||||
void update_loaded_plugin_key(const std::string& old_key, const std::string& new_key);
|
||||
|
||||
void set_cloud_user_id(const std::string& user_id) { m_cloud_user_id = user_id; }
|
||||
void set_shutting_down() { m_shutting_down.store(true, std::memory_order_release); }
|
||||
|
||||
void unload_cloud_plugins();
|
||||
|
||||
void subscribe_on_load_callback(PluginLifecycleCompleteFn fn);
|
||||
void subscribe_on_unload_callback(PluginLifecycleCompleteFn fn);
|
||||
|
||||
// Capability-level lifecycle callbacks, mirroring the package-level load/unload callbacks
|
||||
// above but carrying the full capability identity. Fired for logical enable/disable and
|
||||
// loaded-capability key migration; no Python module interaction.
|
||||
using CapabilityLifecycleFn = std::function<void(const PluginCapabilityIdentifier&)>;
|
||||
void subscribe_on_capability_load_callback(CapabilityLifecycleFn fn);
|
||||
void subscribe_on_capability_unload_callback(CapabilityLifecycleFn fn);
|
||||
|
||||
private:
|
||||
void load_plugin_impl(PluginCatalog& catalog,
|
||||
const std::string& plugin_key,
|
||||
bool skip_deps,
|
||||
std::vector<std::string> capabilities_to_enable = std::vector<std::string>());
|
||||
|
||||
// Caller holds m_mutex. Removes only the exact typed identifiers owned by plugin.
|
||||
std::vector<std::shared_ptr<LoadedPluginCapability>> extract_plugin_capabilities_locked(const LoadedPlugin& plugin);
|
||||
void teardown_capabilities(std::vector<std::shared_ptr<LoadedPluginCapability>>& capabilities,
|
||||
std::size_t lifecycle_count) const;
|
||||
|
||||
bool cancel_plugin_load_locked(const std::string& plugin_key);
|
||||
bool is_plugin_load_cancelled_locked(const std::string& plugin_key) const;
|
||||
void notify_plugin_load_state_changed(bool changed);
|
||||
void run_on_load_callbacks(const std::string& plugin_key);
|
||||
void run_on_unload_callbacks(const std::string& plugin_key);
|
||||
void run_on_capability_load_callbacks(const PluginCapabilityIdentifier& id);
|
||||
void run_on_capability_unload_callbacks(const PluginCapabilityIdentifier& id);
|
||||
|
||||
// Package store keyed by plugin_key. Capability wrappers live in the typed registry;
|
||||
// packages retain their registration order through exact typed identifiers.
|
||||
std::unordered_map<std::string /*plugin_key*/, LoadedPlugin> m_plugins;
|
||||
using PluginCapabilityMap = std::unordered_map<PluginCapabilityIdentifier, std::shared_ptr<LoadedPluginCapability>>;
|
||||
std::unordered_map<PluginCapabilityType, PluginCapabilityMap> m_plugin_capabilities;
|
||||
|
||||
PluginLoadInProgress m_plugin_load_in_progress;
|
||||
PluginLoadErrors m_plugin_load_errors;
|
||||
std::string m_cloud_user_id;
|
||||
std::atomic<bool> m_shutting_down{false};
|
||||
mutable std::mutex m_mutex;
|
||||
mutable std::condition_variable m_plugin_load_cv;
|
||||
|
||||
std::unordered_map<CallbackType, std::vector<PluginLifecycleCompleteFn>> m_callbacks{};
|
||||
std::unordered_map<CallbackType, std::vector<CapabilityLifecycleFn>> m_capability_callbacks{};
|
||||
|
||||
/*
|
||||
callbacks:
|
||||
on plugin load/unload
|
||||
plugin discovery should always be blocking (with dialog)
|
||||
all executions should be blocking (with dialog)
|
||||
Currently, only script plugins should be cancellable.
|
||||
*/
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
732
src/slic3r/plugin/PluginManager.cpp
Normal file
732
src/slic3r/plugin/PluginManager.cpp
Normal file
@@ -0,0 +1,732 @@
|
||||
#include "PluginManager.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
|
||||
#include <pybind11/embed.h>
|
||||
namespace py = pybind11;
|
||||
|
||||
#include "PythonPluginBridge.hpp"
|
||||
#include "PythonPluginInterface.hpp"
|
||||
#include "PluginFsUtils.hpp"
|
||||
#include "PythonFileUtils.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <mutex>
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <chrono>
|
||||
#include <slic3r/GUI/NotificationManager.hpp>
|
||||
#include <slic3r/plugin/PluginDescriptor.hpp>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
#include "PythonInterpreter.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
|
||||
#include "OrcaCloudServiceAgent.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
|
||||
bool wait_for_plugin_catalog(const PluginCatalog& catalog, std::string& error)
|
||||
{
|
||||
error.clear();
|
||||
if (!catalog.wait_for_discovery(std::chrono::milliseconds::max(), error)) {
|
||||
if (error.empty())
|
||||
error = "Plugin discovery is still running";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool find_plugin_descriptor_by_key(const std::vector<PluginDescriptor>& catalog,
|
||||
const std::vector<PluginDescriptor>& invalid_plugins,
|
||||
const std::string& plugin_key,
|
||||
PluginDescriptor& descriptor)
|
||||
{
|
||||
auto find_by_key = [&plugin_key](const PluginDescriptor& entry) { return entry.plugin_key == plugin_key; };
|
||||
|
||||
auto catalog_it = std::find_if(catalog.begin(), catalog.end(), find_by_key);
|
||||
if (catalog_it != catalog.end()) {
|
||||
descriptor = *catalog_it;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto invalid_it = std::find_if(invalid_plugins.begin(), invalid_plugins.end(), find_by_key);
|
||||
if (invalid_it != invalid_plugins.end()) {
|
||||
descriptor = *invalid_it;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void remove_plugin_from_entries(std::vector<PluginDescriptor>& entries, const std::string& plugin_key)
|
||||
{
|
||||
entries.erase(std::remove_if(entries.begin(), entries.end(),
|
||||
[&plugin_key](const PluginDescriptor& entry) { return entry.plugin_key == plugin_key; }),
|
||||
entries.end());
|
||||
}
|
||||
|
||||
void clear_plugin_cloud_state(std::vector<PluginDescriptor>& entries, const std::string& plugin_key)
|
||||
{
|
||||
for (auto& entry : entries) {
|
||||
if (entry.plugin_key == plugin_key) {
|
||||
entry.cloud.reset();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LoadedPlugin::~LoadedPlugin()
|
||||
{
|
||||
if (!PythonInterpreter::instance().is_initialized()) {
|
||||
module = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
PythonGILState gil;
|
||||
if (module != nullptr) {
|
||||
Py_DECREF(module);
|
||||
module = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
PluginManager& PluginManager::instance()
|
||||
{
|
||||
PythonInterpreter::instance();
|
||||
static PluginManager inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
PluginManager::~PluginManager() { shutdown(); }
|
||||
|
||||
bool PluginManager::initialize()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
if (m_initialized)
|
||||
return true;
|
||||
|
||||
// Initialize the Python interpreter eagerly on the main thread.
|
||||
// CPython must be initialized from the main thread; calling
|
||||
// Py_InitializeFromConfig from a background thread (e.g. the
|
||||
// load_plugin worker) causes heap corruption in CPython internals.
|
||||
PythonInterpreter& interpreter = PythonInterpreter::instance();
|
||||
if (!interpreter.is_initialized() && !interpreter.initialize()) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Failed to initialize Python interpreter: " << interpreter.last_error();
|
||||
return false;
|
||||
}
|
||||
|
||||
m_initialized = true;
|
||||
|
||||
// Persist auto-load / capability state to each plugin's .install_state.json sidecar.
|
||||
// On load: write enabled=true plus current capability flags. On unload: flip enabled=false.
|
||||
// The on-unload callback is skipped during shutdown (run_on_unload_callbacks is gated by
|
||||
// !m_shutting_down), so app exit does not wipe the auto-load list.
|
||||
m_loader.subscribe_on_load_callback([this](const std::string& key) {
|
||||
m_loader.write_loaded_plugin_install_state(key);
|
||||
});
|
||||
m_loader.subscribe_on_unload_callback([this](const std::string& key) {
|
||||
PluginDescriptor descriptor;
|
||||
if (!m_catalog.try_get_plugin_descriptor(key, descriptor) || descriptor.plugin_root.empty())
|
||||
return;
|
||||
const boost::filesystem::path root(descriptor.plugin_root);
|
||||
PluginInstallState st;
|
||||
if (read_install_state(root, st)) {
|
||||
st.enabled = false;
|
||||
write_install_state(root, st);
|
||||
}
|
||||
});
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Plugin manager initialized";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PluginManager::shutdown()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (!m_initialized && !m_catalog.is_discovery_in_progress() && m_loader.is_idle_and_empty())
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": PluginManager shutdown enter";
|
||||
|
||||
// Signal the loader to reject new plugin loads before we drain.
|
||||
m_loader.set_shutting_down();
|
||||
|
||||
std::string wait_error;
|
||||
if (!m_catalog.wait_for_discovery(std::chrono::milliseconds::max(), wait_error) && !wait_error.empty())
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Plugin discovery did not finish cleanly during shutdown: " << wait_error;
|
||||
|
||||
// Wait for any in-progress plugin loads.
|
||||
m_loader.wait_for_all_plugin_loads();
|
||||
|
||||
m_loader.unload_all_plugins();
|
||||
PythonPluginBridge::instance().clear_pending_captures();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_initialized = false;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": PluginManager shutdown exit";
|
||||
}
|
||||
|
||||
void PluginManager::discover_plugins(bool async, bool clear)
|
||||
{
|
||||
if (!initialize())
|
||||
return;
|
||||
|
||||
if (clear)
|
||||
m_catalog.clear_all_plugin_errors();
|
||||
|
||||
m_catalog.discover_plugins(async, clear);
|
||||
}
|
||||
|
||||
void PluginManager::rescan_plugins()
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Rescanning plugins...";
|
||||
|
||||
std::string wait_error;
|
||||
m_catalog.wait_for_discovery(std::chrono::milliseconds::max(), wait_error);
|
||||
|
||||
if (!initialize())
|
||||
return;
|
||||
|
||||
m_catalog.clear_all_plugin_errors();
|
||||
m_catalog.discover_plugins(false, true);
|
||||
}
|
||||
|
||||
bool PluginManager::install_plugin(const boost::filesystem::path& filepath, std::string& error)
|
||||
{
|
||||
error.clear();
|
||||
|
||||
std::string wait_error;
|
||||
if (!wait_for_plugin_catalog(m_catalog, wait_error)) {
|
||||
error = wait_error;
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin installation failed while waiting for discovery: " << wait_error;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_loader.install_plugin(filepath, error))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginManager::install_plugin(const boost::filesystem::path& filepath, PluginDescriptor& plugin_descriptor, std::string& error)
|
||||
{
|
||||
error.clear();
|
||||
|
||||
std::string wait_error;
|
||||
if (!wait_for_plugin_catalog(m_catalog, wait_error)) {
|
||||
error = wait_error;
|
||||
if (!plugin_descriptor.plugin_key.empty())
|
||||
m_catalog.set_plugin_error(plugin_descriptor.plugin_key, error);
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin installation failed while waiting for discovery: " << wait_error;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_loader.install_plugin(filepath, plugin_descriptor, error)) {
|
||||
if (!plugin_descriptor.plugin_key.empty())
|
||||
m_catalog.set_plugin_error(plugin_descriptor.plugin_key, error);
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": " << error;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!plugin_descriptor.plugin_key.empty())
|
||||
m_catalog.clear_plugin_error(plugin_descriptor.plugin_key);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginManager::set_plugin_error(const std::string& plugin_key, std::string error)
|
||||
{
|
||||
std::string wait_error;
|
||||
if (!wait_for_plugin_catalog(m_catalog, wait_error))
|
||||
return false;
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!m_catalog.try_get_plugin_descriptor(plugin_key, descriptor))
|
||||
return false;
|
||||
|
||||
descriptor.set_error(std::move(error));
|
||||
return m_catalog.update_plugin_descriptor(plugin_key, descriptor);
|
||||
}
|
||||
|
||||
bool PluginManager::clear_plugin_error(const std::string& plugin_key)
|
||||
{
|
||||
std::string wait_error;
|
||||
if (!wait_for_plugin_catalog(m_catalog, wait_error))
|
||||
return false;
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!m_catalog.try_get_plugin_descriptor(plugin_key, descriptor))
|
||||
return false;
|
||||
if (!descriptor.is_metadata_valid())
|
||||
return false;
|
||||
|
||||
descriptor.clear_error();
|
||||
return m_catalog.update_plugin_descriptor(plugin_key, descriptor);
|
||||
}
|
||||
|
||||
bool PluginManager::delete_plugin(const std::string& plugin_key, std::string& error)
|
||||
{
|
||||
if (!wait_for_plugin_catalog(m_catalog, error))
|
||||
return false;
|
||||
|
||||
error.clear();
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!m_catalog.try_get_plugin_descriptor(plugin_key, descriptor)) {
|
||||
error = "Plugin not found: " + plugin_key;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!delete_installed_plugin_package(descriptor, error)) {
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Deleted plugin: " << plugin_key;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginManager::unsubscribe_cloud_plugin(const std::string& plugin_key, std::string& error)
|
||||
{
|
||||
if (!wait_for_plugin_catalog(m_catalog, error))
|
||||
return false;
|
||||
|
||||
error.clear();
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!m_catalog.try_get_plugin_descriptor(plugin_key, descriptor)) {
|
||||
error = "Plugin not found: " + plugin_key;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!descriptor.is_cloud_plugin()) {
|
||||
error = "Only cloud plugins can be unsubscribed.";
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (descriptor.cloud && descriptor.cloud->is_mine) {
|
||||
error = "Cannot unsubscribe your own plugins. Use Delete from Cloud instead.";
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_cloud_service.request_cloud_unsubscribe(descriptor, error)) {
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return finalize_cloud_plugin_removal(descriptor, true, error);
|
||||
}
|
||||
|
||||
bool PluginManager::delete_and_unsubscribe_cloud_plugin(const std::string& plugin_key, std::string& error)
|
||||
{
|
||||
if (!wait_for_plugin_catalog(m_catalog, error))
|
||||
return false;
|
||||
|
||||
error.clear();
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!m_catalog.try_get_plugin_descriptor(plugin_key, descriptor)) {
|
||||
error = "Plugin not found: " + plugin_key;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!descriptor.is_cloud_plugin()) {
|
||||
error = "Only cloud plugins can be deleted and unsubscribed.";
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (descriptor.cloud->is_mine) {
|
||||
error = "Use Delete local and cloud for owned plugins.";
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_cloud_service.request_cloud_unsubscribe(descriptor, error)) {
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return finalize_cloud_plugin_removal(descriptor, false, error);
|
||||
}
|
||||
|
||||
bool PluginManager::delete_mine_plugin_from_cloud(const std::string& plugin_key, std::string& error)
|
||||
{
|
||||
if (!wait_for_plugin_catalog(m_catalog, error))
|
||||
return false;
|
||||
|
||||
error.clear();
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!m_catalog.try_get_plugin_descriptor(plugin_key, descriptor)) {
|
||||
error = "Plugin not found: " + plugin_key;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!descriptor.is_cloud_plugin()) {
|
||||
error = "Only owned cloud plugins can be deleted from the cloud.";
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!descriptor.cloud->is_mine) {
|
||||
error = "Only your own plugins can be deleted from the cloud.";
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_cloud_service.request_cloud_delete(descriptor, error)) {
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return finalize_cloud_plugin_removal(descriptor, true, error);
|
||||
}
|
||||
|
||||
bool PluginManager::delete_mine_local_and_cloud_plugin(const std::string& plugin_key, std::string& error)
|
||||
{
|
||||
if (!wait_for_plugin_catalog(m_catalog, error))
|
||||
return false;
|
||||
|
||||
error.clear();
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!m_catalog.try_get_plugin_descriptor(plugin_key, descriptor)) {
|
||||
error = "Plugin not found: " + plugin_key;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!descriptor.is_cloud_plugin()) {
|
||||
error = "Only owned cloud plugins can be deleted from local and cloud.";
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!descriptor.cloud->is_mine) {
|
||||
error = "Only your own plugins can be deleted from local and cloud.";
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_cloud_service.request_cloud_delete(descriptor, error)) {
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
return false;
|
||||
}
|
||||
return finalize_cloud_plugin_removal(descriptor, false, error);
|
||||
}
|
||||
|
||||
void PluginManager::fetch_plugins_from_cloud(std::vector<std::string>* out_not_found, std::vector<std::string>* out_unauthorized)
|
||||
{
|
||||
if (!m_cloud_service.can_fetch_cloud_plugins())
|
||||
return;
|
||||
|
||||
std::vector<PluginDescriptor> cloud_list{};
|
||||
std::vector<std::string> not_found{}, unauthorized{};
|
||||
bool result = m_cloud_service.fetch_manifests_into_descriptors(cloud_list, not_found, unauthorized);
|
||||
if (!result) {
|
||||
if (GUI::wxGetApp().plater() != nullptr && GUI::wxGetApp().imgui()->display_initialized()) {
|
||||
GUI::wxGetApp()
|
||||
.plater()
|
||||
->get_notification_manager()
|
||||
->push_notification(GUI::NotificationType::CustomNotification,
|
||||
GUI::NotificationManager::NotificationLevel::WarningNotificationLevel,
|
||||
"Failed to fetch plugins from the cloud. See logs for details.");
|
||||
}
|
||||
}
|
||||
|
||||
m_catalog.update_cloud_catalog(cloud_list);
|
||||
m_catalog.clear_cloud_plugin_unauthorized();
|
||||
m_catalog.clear_cloud_plugin_not_found_errors();
|
||||
|
||||
// Mark cloud issues in the catalog so app UIs can reflect their shared state.
|
||||
for (const auto& uuid : not_found)
|
||||
m_catalog.mark_cloud_plugin_not_found(uuid);
|
||||
for (const auto& uuid : unauthorized)
|
||||
m_catalog.mark_cloud_plugin_unauthorized(uuid);
|
||||
|
||||
// Return the vectors to callers that need them (e.g. for notifications).
|
||||
if (out_not_found)
|
||||
*out_not_found = std::move(not_found);
|
||||
if (out_unauthorized)
|
||||
*out_unauthorized = std::move(unauthorized);
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Cloud plugin fetch: " << cloud_list.size() << " plugins returned";
|
||||
}
|
||||
|
||||
bool PluginManager::subscribe_and_install_cloud_plugin(const std::string& plugin_key, std::string& error)
|
||||
{
|
||||
error.clear();
|
||||
|
||||
const std::string plugin_uuid = is_uuid(plugin_key) ? plugin_key : std::string{};
|
||||
if (plugin_uuid.empty()) {
|
||||
error = "Cloud plugin key is missing UUID.";
|
||||
return false;
|
||||
}
|
||||
if (!m_cloud_service.can_fetch_cloud_plugins()) {
|
||||
error = "Sign in to OrcaCloud to install this plugin.";
|
||||
return false;
|
||||
}
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
bool found = m_catalog.try_get_plugin_descriptor(plugin_key, descriptor) && descriptor.is_cloud_plugin();
|
||||
if (!found) {
|
||||
// The plugin may already be subscribed or owned while the local catalog is stale.
|
||||
fetch_plugins_from_cloud();
|
||||
found = m_catalog.try_get_plugin_descriptor(plugin_key, descriptor) && descriptor.is_cloud_plugin();
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
if (!m_cloud_service.request_cloud_subscribe(plugin_uuid, error))
|
||||
return false;
|
||||
|
||||
fetch_plugins_from_cloud();
|
||||
found = m_catalog.try_get_plugin_descriptor(plugin_key, descriptor) && descriptor.is_cloud_plugin();
|
||||
if (!found) {
|
||||
error = "Subscribed cloud plugin was not returned by OrcaCloud.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (descriptor.has_local_package())
|
||||
return true;
|
||||
|
||||
return download_and_install_cloud_plugin(descriptor.plugin_key, descriptor.version, error);
|
||||
}
|
||||
|
||||
bool PluginManager::download_and_install_cloud_plugin(const std::string& plugin_key, const std::string& version, std::string& error)
|
||||
{
|
||||
error.clear();
|
||||
|
||||
CloudPluginDownload download;
|
||||
PluginDescriptor descriptor;
|
||||
if (!m_catalog.try_get_plugin_descriptor(plugin_key, descriptor)) {
|
||||
error = "Plugin not found: " + plugin_key;
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to find plugin key " << plugin_key;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_catalog.clear_plugin_error(plugin_key);
|
||||
|
||||
const std::string requested_version = version.empty() ? descriptor.version : version;
|
||||
if (!m_cloud_service.download_cloud_plugin(descriptor, requested_version, download, error)) {
|
||||
if (error.empty())
|
||||
error = "Failed to download cloud plugin.";
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": " << error;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure cloud plugins install to _subscribed/<user_id>/
|
||||
if (auto agent = m_cloud_service.get_cloud_agent()) {
|
||||
const std::string user_id = agent->get_user_id();
|
||||
if (!user_id.empty())
|
||||
m_loader.set_cloud_user_id(user_id);
|
||||
}
|
||||
|
||||
// Record the version we just fetched from the cloud so install_plugin persists it to the
|
||||
// install-state sidecar (the source of truth for the installed cloud version) instead of
|
||||
// the local manifest/PEP723 header version.
|
||||
descriptor.installed_version = requested_version;
|
||||
|
||||
if (!install_plugin(download.package_path, descriptor, error)) {
|
||||
if (error.empty())
|
||||
error = "Failed to install cloud plugin.";
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::remove(download.package_path, ec);
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": " << error;
|
||||
return false;
|
||||
}
|
||||
|
||||
PluginDescriptor updated_descriptor = descriptor;
|
||||
if (!requested_version.empty())
|
||||
updated_descriptor.version = requested_version;
|
||||
// The version just downloaded and installed is now the locally installed version.
|
||||
updated_descriptor.installed_version = requested_version.empty() ? descriptor.version : requested_version;
|
||||
if (updated_descriptor.cloud.has_value()) {
|
||||
updated_descriptor.cloud->installed = true;
|
||||
updated_descriptor.cloud->update_available = false;
|
||||
updated_descriptor.cloud->unauthorized = false;
|
||||
}
|
||||
updated_descriptor.clear_error();
|
||||
updated_descriptor.set_unauthorized(false);
|
||||
|
||||
if (!m_catalog.update_plugin_descriptor(plugin_key, updated_descriptor)) {
|
||||
error = "Plugin Manifest not found.";
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Cloud plugin " << plugin_key
|
||||
<< " downloaded successfully but failed to update plugin manifest. Manifest not found in catalog.";
|
||||
return false;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Cloud plugin " << plugin_key << " installed successfully";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginManager::finalize_cloud_plugin_removal(const PluginDescriptor& plugin, bool keep_local, std::string& error)
|
||||
{
|
||||
// Shared by all four cloud-removal entrypoints after the cloud-side request
|
||||
// succeeds. Handles the common local follow-up of keeping a detached local
|
||||
// copy, deleting local files, or dropping a cloud-only row from the catalog.
|
||||
if (keep_local && plugin.has_local_package()) {
|
||||
if (!keep_installed_plugin_as_local(plugin, error))
|
||||
return false;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Removed cloud tracking, kept local copy: " << plugin.plugin_key;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (plugin.has_local_package()) {
|
||||
if (!delete_installed_plugin_package(plugin, error))
|
||||
return false;
|
||||
// Re-sync the cloud catalog so observers/UI see the updated cloud list
|
||||
// after the local package has been removed.
|
||||
fetch_plugins_from_cloud();
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Deleted local package after cloud removal: " << plugin.plugin_key;
|
||||
return true;
|
||||
}
|
||||
|
||||
m_catalog.remove_plugin(plugin.plugin_key);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Removed cloud-only plugin from catalog: " << plugin.plugin_key;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginManager::delete_installed_plugin_package(const PluginDescriptor& plugin, std::string& error)
|
||||
{
|
||||
boost::filesystem::path resolved_root;
|
||||
if (!resolve_allowed_plugin_root(plugin, m_catalog.get_plugin_directories(),
|
||||
"Refusing to delete a plugin outside the known plugin directories.", resolved_root, error))
|
||||
return false;
|
||||
|
||||
m_loader.unload_plugin(plugin.plugin_key, plugin.primary_capability_type());
|
||||
|
||||
if (!delete_plugin_root(resolved_root, plugin.plugin_key, error))
|
||||
return false;
|
||||
|
||||
m_catalog.remove_plugin(plugin.plugin_key);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginManager::keep_installed_plugin_as_local(const PluginDescriptor& plugin_descriptor, std::string& error)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
boost::filesystem::path resolved_root;
|
||||
if (!resolve_allowed_plugin_root(plugin_descriptor, m_catalog.get_plugin_directories(),
|
||||
"Refusing to update a plugin outside the known plugin directories.", resolved_root, error))
|
||||
return false;
|
||||
|
||||
const std::string old_key = plugin_descriptor.plugin_key;
|
||||
|
||||
// Generate new local key from the entry file stem (cloud → local conversion).
|
||||
const std::string entry_stem = fs::path(plugin_descriptor.entry_path).stem().string();
|
||||
const std::string new_key = make_local_plugin_key(
|
||||
!entry_stem.empty() ? entry_stem : resolved_root.stem().string());
|
||||
|
||||
// Update metadata.
|
||||
PluginDescriptor local_descriptor = plugin_descriptor;
|
||||
local_descriptor.plugin_key = new_key;
|
||||
local_descriptor.cloud = std::nullopt;
|
||||
local_descriptor.clear_error();
|
||||
|
||||
if (!write_install_state(resolved_root, local_descriptor)) {
|
||||
error = "Failed to update plugin install state: " + (resolved_root / INSTALL_STATE_FILE).string();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update catalog entry key in-memory.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
auto& catalog_entries = const_cast<std::vector<PluginDescriptor>&>(m_catalog.get_plugin_catalog());
|
||||
for (auto& entry : catalog_entries) {
|
||||
if (entry.plugin_key == old_key) {
|
||||
entry.plugin_key = new_key;
|
||||
entry.cloud.reset();
|
||||
entry.clear_error();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update loaded plugin manifest key if currently loaded.
|
||||
m_loader.update_loaded_plugin_key(old_key, new_key);
|
||||
|
||||
// Auto-load state for the new local key is carried by the .install_state.json sidecar
|
||||
// written above with the new local descriptor.
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Transitioned plugin from " << old_key << " to " << new_key;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginManager::update_cloud_plugin(const std::string& plugin_key, std::string& error, std::string version)
|
||||
{
|
||||
error.clear();
|
||||
|
||||
// delete local plugin file and download newer version
|
||||
PluginDescriptor descriptor;
|
||||
if (!m_catalog.try_get_plugin_descriptor(plugin_key, descriptor)) {
|
||||
error = "Plugin not found: " + plugin_key;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!descriptor.is_cloud_plugin()) {
|
||||
error = "Only cloud plugins can be updated.";
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Empty version should represent that we are trying to update the current plugin.
|
||||
// So if the version is empty but there isn't an update available, the intention is unclear.
|
||||
if (version.empty()) {
|
||||
if (descriptor.cloud->update_available)
|
||||
version = descriptor.latest_available_version();
|
||||
else {
|
||||
error = "Trying to update plugin with no available update. Version is empty.";
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
m_catalog.clear_plugin_error(plugin_key);
|
||||
|
||||
if (descriptor.has_local_package()) {
|
||||
boost::filesystem::path resolved_root;
|
||||
if (!resolve_allowed_plugin_root(descriptor, m_catalog.get_plugin_directories(),
|
||||
"Refusing to delete a plugin outside the known plugin directories.", resolved_root, error)) {
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_loader.unload_plugin(plugin_key, descriptor.primary_capability_type());
|
||||
|
||||
if (!delete_plugin_root(resolved_root, plugin_key, error)) {
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!download_and_install_cloud_plugin(plugin_key, version, error)) {
|
||||
m_catalog.set_plugin_error(plugin_key, error);
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": " << error;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_catalog.clear_plugin_error(plugin_key);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
163
src/slic3r/plugin/PluginManager.hpp
Normal file
163
src/slic3r/plugin/PluginManager.hpp
Normal file
@@ -0,0 +1,163 @@
|
||||
#ifndef slic3r_PluginManager_hpp_
|
||||
#define slic3r_PluginManager_hpp_
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <libslic3r/Config.hpp>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "CloudPluginService.hpp"
|
||||
#include "PluginCatalog.hpp"
|
||||
#include "PluginLoader.hpp"
|
||||
#include "PluginDescriptor.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class OrcaCloudServiceAgent;
|
||||
|
||||
class PluginManager
|
||||
{
|
||||
public:
|
||||
static PluginManager& instance();
|
||||
|
||||
~PluginManager();
|
||||
|
||||
// Initialize plugin system (no longer initializes Python — that happens lazily on first load_plugin)
|
||||
bool initialize();
|
||||
|
||||
// Stop discovery and unload Python plugin objects before Python finalizes.
|
||||
void shutdown();
|
||||
|
||||
// Discover and scan plugins from standard directories (manifest-only, no Python loading).
|
||||
// Runs on a worker thread when async=true.
|
||||
void discover_plugins(bool async = false, bool clear = false);
|
||||
|
||||
// fetches plugins from the cloud
|
||||
void fetch_plugins_from_cloud(std::vector<std::string>* out_not_found = nullptr,
|
||||
std::vector<std::string>* out_unauthorized = nullptr);
|
||||
|
||||
// Download and install a cloud plugin from its download_url.
|
||||
// Returns an error string on failure (empty URL, network down, etc.).
|
||||
// On success returns an empty string.
|
||||
bool download_and_install_cloud_plugin(const std::string& plugin_key, const std::string& version, std::string& error);
|
||||
bool subscribe_and_install_cloud_plugin(const std::string& plugin_key, std::string& error);
|
||||
|
||||
// Manually trigger manifest-only rescan of plugins. Blocks until discovery is complete.
|
||||
void rescan_plugins();
|
||||
|
||||
PluginCatalog& get_catalog() { return m_catalog; }
|
||||
const PluginCatalog& get_catalog() const { return m_catalog; }
|
||||
PluginLoader& get_loader() { return m_loader; }
|
||||
const PluginLoader& get_loader() const { return m_loader; }
|
||||
|
||||
void set_cloud_agent(std::shared_ptr<OrcaCloudServiceAgent> agent) { m_cloud_service.set_cloud_agent(std::move(agent)); }
|
||||
|
||||
bool install_plugin(const boost::filesystem::path& filepath, std::string& error);
|
||||
bool install_plugin(const boost::filesystem::path& filepath, PluginDescriptor& plugin_descriptor, std::string& error);
|
||||
bool set_plugin_error(const std::string& plugin_key, std::string error);
|
||||
bool clear_plugin_error(const std::string& plugin_key);
|
||||
|
||||
// If the version is empty, take the latest version.
|
||||
bool update_cloud_plugin(const std::string& plugin_key, std::string& error, std::string version = "");
|
||||
|
||||
bool delete_plugin(const std::string& plugin_key, std::string& error);
|
||||
bool unsubscribe_cloud_plugin(const std::string& plugin_key, std::string& error);
|
||||
bool delete_and_unsubscribe_cloud_plugin(const std::string& plugin_key, std::string& error);
|
||||
bool delete_mine_plugin_from_cloud(const std::string& plugin_key, std::string& error);
|
||||
bool delete_mine_local_and_cloud_plugin(const std::string& plugin_key, std::string& error);
|
||||
|
||||
private:
|
||||
PluginManager() = default;
|
||||
PluginManager(const PluginManager&) = delete;
|
||||
PluginManager& operator=(const PluginManager&) = delete;
|
||||
|
||||
bool finalize_cloud_plugin_removal(const PluginDescriptor& plugin, bool keep_local, std::string& error);
|
||||
bool delete_installed_plugin_package(const PluginDescriptor& plugin, std::string& error);
|
||||
bool keep_installed_plugin_as_local(const PluginDescriptor& plugin_descriptor, std::string& error);
|
||||
|
||||
bool m_initialized = false;
|
||||
CloudPluginService m_cloud_service;
|
||||
PluginCatalog m_catalog;
|
||||
PluginLoader m_loader;
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
|
||||
std::unordered_map<PluginCapabilityType, std::vector<std::function<void(const std::vector<PluginDescriptor>&)>>> m_loaded_plugin_changed_callbacks;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
void execute_capabilities_from_refs(const ConfigOptionStrings& capabilities,
|
||||
const ConfigOptionStrings* plugins,
|
||||
PluginCapabilityType type,
|
||||
std::function<void(std::shared_ptr<T>, const PluginCapabilityRef&)> execute)
|
||||
{
|
||||
PluginManager& plugin_mgr = PluginManager::instance();
|
||||
|
||||
const bool has_any = std::any_of(capabilities.values.begin(), capabilities.values.end(),
|
||||
[](const std::string& s) { return !s.empty(); });
|
||||
if (has_any && !plugin_mgr.get_loader().wait_for_all_plugin_loads(std::chrono::seconds(10))) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Post-process: timed out waiting for plugin loads; unresolved capabilities will be skipped";
|
||||
}
|
||||
|
||||
for (const std::string& capability : capabilities.values) {
|
||||
if (capability.empty())
|
||||
continue;
|
||||
|
||||
std::shared_ptr<LoadedPluginCapability> cap;
|
||||
std::string cap_name, plugin_key;
|
||||
|
||||
std::optional<PluginCapabilityRef> ref;
|
||||
if (plugins != nullptr) {
|
||||
for (const std::string& plugin_ref : plugins->values) {
|
||||
auto parsed = Slic3r::parse_capability_ref(plugin_ref);
|
||||
if (parsed && parsed->capability_name == capability) {
|
||||
ref = std::move(parsed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ref) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Post-processing: no plugin reference found for capability '" << capability << "'; skipping";
|
||||
continue;
|
||||
}
|
||||
|
||||
cap_name = ref->capability_name;
|
||||
plugin_key = ref->uuid.empty() ? ref->name : ref->uuid;
|
||||
cap = plugin_mgr.get_loader().get_plugin_capability_by_name(plugin_key, type, cap_name);
|
||||
|
||||
if (!cap) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Post-processing: no loaded capability '" << cap_name
|
||||
<< "' for plugin '" << plugin_key << "'; skipping";
|
||||
continue;
|
||||
}
|
||||
if (!cap->enabled) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Post-processing: capability '" << cap_name
|
||||
<< "' for plugin '" << plugin_key << "' is disabled; skipping";
|
||||
continue;
|
||||
}
|
||||
|
||||
auto plugin_capability = std::dynamic_pointer_cast<T>(cap->instance);
|
||||
if (!plugin_capability) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Post-processing: capability '" << cap_name
|
||||
<< "' (plugin_key=" << cap->plugin_key
|
||||
<< ") is not a " << plugin_capability_type_to_string(type) << "; skipping";
|
||||
continue;
|
||||
}
|
||||
|
||||
execute(plugin_capability, ref.value());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif /* slic3r_PluginManager_hpp_ */
|
||||
401
src/slic3r/plugin/PluginResolver.cpp
Normal file
401
src/slic3r/plugin/PluginResolver.cpp
Normal file
@@ -0,0 +1,401 @@
|
||||
#include "PluginResolver.hpp"
|
||||
|
||||
#include "PluginManager.hpp"
|
||||
#include "../Utils/Http.hpp"
|
||||
#include "../Utils/OrcaCloudServiceAgent.hpp"
|
||||
#include "../GUI/GUI.hpp"
|
||||
#include "../GUI/GUI_App.hpp"
|
||||
#include "../GUI/I18N.hpp"
|
||||
#include "../GUI/Plater.hpp"
|
||||
#include "../GUI/NotificationManager.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <libslic3r/Config.hpp>
|
||||
#include <libslic3r/PresetBundle.hpp>
|
||||
#include <vector>
|
||||
#include <wx/utils.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#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)
|
||||
{
|
||||
if (type != Preset::TYPE_PRINT && type != Preset::TYPE_PRINTER && type != Preset::TYPE_FILAMENT)
|
||||
return {};
|
||||
|
||||
// Plugin-bearing options opt in via ConfigOptionDef::support_plugin, 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`.
|
||||
const ConfigDef* def = preset.config.def();
|
||||
if (def == nullptr)
|
||||
return {};
|
||||
|
||||
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->support_plugin)
|
||||
continue;
|
||||
|
||||
const ConfigOption* option = preset.config.option(field);
|
||||
if (option == nullptr)
|
||||
continue;
|
||||
|
||||
if (const auto* string_option = dynamic_cast<const ConfigOptionString*>(option)) {
|
||||
if (string_option->value == ref.capability_name)
|
||||
return field;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (const auto* vector_option = dynamic_cast<const ConfigOptionVectorBase*>(option)) {
|
||||
const std::vector<std::string> values = vector_option->vserialize();
|
||||
if (std::any_of(values.begin(), values.end(), matches_ref))
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
// 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"))
|
||||
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.
|
||||
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.
|
||||
static std::map<Preset::Type, std::unordered_map<std::string, MissingPlugin>> s_inactive;
|
||||
// Installed+loaded but capability absent; not resolvable by activation. Both share s_missing_mutex.
|
||||
static std::map<Preset::Type, std::unordered_map<std::string, MissingPlugin>> s_broken;
|
||||
|
||||
static bool is_tracked_type(Preset::Type type)
|
||||
{
|
||||
return type == Preset::TYPE_PRINT || type == Preset::TYPE_PRINTER || type == Preset::TYPE_FILAMENT;
|
||||
}
|
||||
|
||||
static std::string resolve_cloud_base_url()
|
||||
{
|
||||
std::string cloud_base_url = "https://cloud.orcaslicer.com";
|
||||
if (auto agent = GUI::wxGetApp().getAgent()) {
|
||||
if (auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(agent->get_cloud_agent())) {
|
||||
if (!orca_agent->get_cloud_base_url().empty())
|
||||
cloud_base_url = orca_agent->get_cloud_base_url();
|
||||
}
|
||||
}
|
||||
return cloud_base_url;
|
||||
}
|
||||
|
||||
std::string create_full_ref(const PluginCapabilityRef& ref) { return ref.name + ';' + ref.uuid + ';' + ref.capability_name; }
|
||||
|
||||
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.
|
||||
static std::pair<bool, bool> loaded_capability_state(const std::string& plugin_key, const PluginCapabilityRef& ref)
|
||||
{
|
||||
bool present = false, enabled = false;
|
||||
PluginLoader& loader = PluginManager::instance().get_loader();
|
||||
for (const auto& capability : loader.get_loaded_plugin_capabilities(plugin_key))
|
||||
if (capability && capability->name == ref.capability_name) {
|
||||
present = true;
|
||||
if (capability->enabled)
|
||||
enabled = true;
|
||||
}
|
||||
return {present, enabled};
|
||||
}
|
||||
|
||||
void refresh_missing_plugins(const PresetBundle& preset_bundle)
|
||||
{
|
||||
const auto manifest_of = [](const Preset& preset) { return dynamic_cast<const ConfigOptionStrings*>(preset.config.option("plugins")); };
|
||||
const Preset& print_preset = preset_bundle.prints.get_edited_preset();
|
||||
refresh_missing_plugins(Preset::TYPE_PRINT, manifest_of(print_preset), &print_preset);
|
||||
const Preset& printer_preset = preset_bundle.printers.get_edited_preset();
|
||||
refresh_missing_plugins(Preset::TYPE_PRINTER, manifest_of(printer_preset), &printer_preset);
|
||||
|
||||
// Filament plugins (if any) are the union over all selected filament presets.
|
||||
ConfigOptionStrings filament_manifest;
|
||||
for (const std::string& filament_name : preset_bundle.filament_presets) {
|
||||
const Preset* filament = preset_bundle.filaments.find_preset(filament_name);
|
||||
if (!filament)
|
||||
continue;
|
||||
if (const auto* opt = dynamic_cast<const ConfigOptionStrings*>(filament->config.option("plugins")))
|
||||
filament_manifest.values.insert(filament_manifest.values.end(), opt->values.begin(), opt->values.end());
|
||||
}
|
||||
refresh_missing_plugins(Preset::TYPE_FILAMENT, &filament_manifest);
|
||||
}
|
||||
|
||||
void refresh_missing_plugins(Preset::Type type, const ConfigOptionStrings* manifest, const Preset* preset)
|
||||
{
|
||||
if (!is_tracked_type(type))
|
||||
return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(s_missing_mutex);
|
||||
auto& missing_set = s_missing[type];
|
||||
auto& inactive_set = s_inactive[type];
|
||||
auto& broken_set = s_broken[type];
|
||||
missing_set.clear();
|
||||
inactive_set.clear();
|
||||
broken_set.clear();
|
||||
if (manifest == nullptr)
|
||||
return;
|
||||
|
||||
PluginLoader& loader = PluginManager::instance().get_loader();
|
||||
const PluginCatalog& catalog = PluginManager::instance().get_catalog();
|
||||
for (const std::string& entry : manifest->values) {
|
||||
const auto ref = parse_capability_ref(entry);
|
||||
if (!ref)
|
||||
continue;
|
||||
|
||||
// Cloud plugins resolve by UUID, local plugins by plugin_key (the first field).
|
||||
const std::string key = ref->uuid.empty() ? ref->name : ref->uuid;
|
||||
if (key.empty())
|
||||
continue;
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
const bool in_catalog = catalog.try_get_plugin_descriptor(key, descriptor);
|
||||
const bool installed = in_catalog && descriptor.has_local_package();
|
||||
const bool loaded = installed && loader.is_plugin_loaded(descriptor.plugin_key);
|
||||
const auto cap_state = installed ? loaded_capability_state(descriptor.plugin_key, *ref)
|
||||
: std::pair<bool, bool>{false, false};
|
||||
const bool cap_present = cap_state.first;
|
||||
const bool cap_enabled = cap_state.second;
|
||||
if (cap_enabled)
|
||||
continue; // active and enabled — nothing to resolve
|
||||
|
||||
std::string opt = preset != nullptr ? find_option_for_capability(type, *preset, *ref) : std::string();
|
||||
if (opt.empty())
|
||||
continue;
|
||||
|
||||
if (!installed) {
|
||||
// Not on disk — needs download/install (existing behavior).
|
||||
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) {
|
||||
// Installed but not loaded yet (optimistic), or loaded but capability disabled — activatable.
|
||||
inactive_set.emplace(entry, MissingPlugin{*ref, std::string(), std::move(opt), type, PluginCapabilityType::Unknown});
|
||||
} else {
|
||||
// Loaded but the capability is absent — activation cannot fix it; offer a browse/update link.
|
||||
broken_set.emplace(entry, MissingPlugin{*ref, resolve_recovery_url(*ref), std::move(opt), type, PluginCapabilityType::Unknown});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<MissingPlugin> get_missing_cloud_plugins()
|
||||
{
|
||||
std::vector<MissingPlugin> out;
|
||||
std::lock_guard<std::mutex> lock(s_missing_mutex);
|
||||
for (const auto& [type, set] : s_missing)
|
||||
for (const auto& [ref_str, missing] : set)
|
||||
if (!missing.ref.uuid.empty())
|
||||
out.push_back(missing);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<MissingPlugin> get_missing_local_plugins()
|
||||
{
|
||||
std::vector<MissingPlugin> out;
|
||||
std::lock_guard<std::mutex> lock(s_missing_mutex);
|
||||
for (const auto& [type, set] : s_missing)
|
||||
for (const auto& [ref_str, missing] : set)
|
||||
if (missing.ref.uuid.empty())
|
||||
out.push_back(missing);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool has_missing_plugins()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_missing_mutex);
|
||||
for (const auto& [type, set] : s_missing)
|
||||
if (!set.empty())
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<MissingPlugin> get_inactive_plugins()
|
||||
{
|
||||
std::vector<MissingPlugin> out;
|
||||
std::lock_guard<std::mutex> lock(s_missing_mutex);
|
||||
for (const auto& [type, set] : s_inactive)
|
||||
for (const auto& [ref_str, plugin] : set)
|
||||
out.push_back(plugin);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool has_inactive_plugins()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_missing_mutex);
|
||||
for (const auto& [type, set] : s_inactive)
|
||||
if (!set.empty())
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<MissingPlugin> get_broken_plugins()
|
||||
{
|
||||
std::vector<MissingPlugin> out;
|
||||
std::lock_guard<std::mutex> lock(s_missing_mutex);
|
||||
for (const auto& [type, set] : s_broken)
|
||||
for (const auto& [ref_str, plugin] : set)
|
||||
out.push_back(plugin);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool has_broken_plugins()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_missing_mutex);
|
||||
for (const auto& [type, set] : s_broken)
|
||||
if (!set.empty())
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static void report_install_failure(const std::string& message)
|
||||
{
|
||||
GUI::wxGetApp().CallAfter([message]() {
|
||||
if (GUI::Plater* plater = GUI::wxGetApp().plater())
|
||||
plater->get_notification_manager()->push_notification(GUI::NotificationType::OrcaCloudAPIError,
|
||||
GUI::NotificationManager::NotificationLevel::ErrorNotificationLevel,
|
||||
_u8L("Plugin installation failed") + ": " + 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);
|
||||
if (ref && !ref->uuid.empty() && std::find(uuids.begin(), uuids.end(), ref->uuid) == uuids.end())
|
||||
uuids.push_back(ref->uuid);
|
||||
}
|
||||
if (uuids.empty()) {
|
||||
if (progress.on_finished)
|
||||
progress.on_finished();
|
||||
return;
|
||||
}
|
||||
|
||||
// subscribe_and_install_cloud_plugin blocks on network + load, so run off the UI thread. On
|
||||
// success the capability-load callback re-validates the plate and clears the notification.
|
||||
std::thread worker([uuids, progress = std::move(progress)]() {
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
const std::size_t total = uuids.size();
|
||||
for (std::size_t i = 0; i < total; ++i) {
|
||||
if (progress.is_cancelled && progress.is_cancelled())
|
||||
break;
|
||||
|
||||
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.get_catalog().try_get_plugin_descriptor(uuid, known) && !known.name.empty())
|
||||
display_name = known.name;
|
||||
if (progress.on_plugin_begin)
|
||||
progress.on_plugin_begin(display_name, i, total);
|
||||
|
||||
std::string error;
|
||||
if (!mgr.subscribe_and_install_cloud_plugin(uuid, error)) {
|
||||
report_install_failure(error.empty() ? uuid : (uuid + ": " + error));
|
||||
continue;
|
||||
}
|
||||
PluginDescriptor descriptor;
|
||||
if (!mgr.get_catalog().try_get_plugin_descriptor(uuid, descriptor)) {
|
||||
report_install_failure(uuid + ": installed plugin was not found in the catalog.");
|
||||
continue;
|
||||
}
|
||||
PluginLoader& loader = mgr.get_loader();
|
||||
loader.load_plugin(mgr.get_catalog(), descriptor.plugin_key, false);
|
||||
if (!loader.wait_for_plugin_load(descriptor.plugin_key, std::chrono::minutes(5), error) ||
|
||||
!loader.is_plugin_loaded(descriptor.plugin_key)) {
|
||||
report_install_failure(descriptor.name + ": " + (error.empty() ? "plugin failed to load." : error));
|
||||
}
|
||||
}
|
||||
if (progress.on_finished)
|
||||
progress.on_finished();
|
||||
});
|
||||
worker.detach();
|
||||
}
|
||||
|
||||
void resolve_inactive_plugins(const std::vector<std::string>& refs)
|
||||
{
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
PluginCatalog& catalog = mgr.get_catalog();
|
||||
|
||||
// Group the requested capabilities 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);
|
||||
if (!ref)
|
||||
continue;
|
||||
const std::string key = ref->uuid.empty() ? ref->name : ref->uuid;
|
||||
PluginDescriptor descriptor;
|
||||
if (!catalog.try_get_plugin_descriptor(key, descriptor) || !descriptor.has_local_package())
|
||||
continue;
|
||||
by_plugin[descriptor.plugin_key].push_back(ref->capability_name);
|
||||
}
|
||||
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.
|
||||
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();
|
||||
PluginLoader& loader = mgr.get_loader();
|
||||
PluginCatalog& catalog = mgr.get_catalog();
|
||||
for (auto& [plugin_key, capabilities] : work) {
|
||||
loader.load_plugin(catalog, plugin_key, /*skip_deps=*/false, capabilities);
|
||||
std::string error;
|
||||
loader.wait_for_plugin_load(plugin_key, std::chrono::minutes(5), error);
|
||||
}
|
||||
GUI::wxGetApp().CallAfter([]() {
|
||||
if (GUI::Plater* plater = GUI::wxGetApp().plater())
|
||||
plater->revalidate_current_plate_if_plugins_missing();
|
||||
});
|
||||
}).detach();
|
||||
}
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
}
|
||||
wxLaunchDefaultBrowser(GUI::from_u8(resolve_cloud_base_url() + "/app/plugins/plugin-hub"), wxBROWSER_NEW_WINDOW);
|
||||
}
|
||||
|
||||
bool check_capability_in_use(const std::string &capability_refs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
88
src/slic3r/plugin/PluginResolver.hpp
Normal file
88
src/slic3r/plugin/PluginResolver.hpp
Normal file
@@ -0,0 +1,88 @@
|
||||
#ifndef slic3r_PluginResolver_hpp_
|
||||
#define slic3r_PluginResolver_hpp_
|
||||
|
||||
#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 <cstddef>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// A plugin capability referenced by an active preset but not currently available (installed and
|
||||
// loadable) on this machine.
|
||||
struct MissingPlugin
|
||||
{
|
||||
PluginCapabilityRef ref; // parsed "name;uuid;capability"
|
||||
std::string recovery_url; // OrcaCloud search URL (local plugins only)
|
||||
std::string opt;
|
||||
Preset::Type opt_type;
|
||||
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.
|
||||
void refresh_missing_plugins(Preset::Type type, const ConfigOptionStrings* manifest, const Preset* preset = nullptr);
|
||||
void refresh_missing_plugins(const PresetBundle& preset_bundle);
|
||||
|
||||
// Aggregate queries across all tracked preset types. Each entry is a full "name;uuid;capability"
|
||||
// reference. Cloud refs carry a non-empty UUID; local refs do not.
|
||||
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.
|
||||
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.
|
||||
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
|
||||
{
|
||||
// Fired before each plugin's install begins. `index` is 0-based; `total` is the plugin count.
|
||||
std::function<void(const std::string& name, std::size_t index, std::size_t total)> on_plugin_begin;
|
||||
// Polled between plugins; returning true stops the loop before the next plugin starts.
|
||||
std::function<bool()> is_cancelled;
|
||||
// Fired exactly once when the loop ends (all installed, failed, or cancelled).
|
||||
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.
|
||||
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).
|
||||
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).
|
||||
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);
|
||||
|
||||
bool check_capability_in_use(const std::string& capability_refs);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
27
src/slic3r/plugin/PyPluginPackage.hpp
Normal file
27
src/slic3r/plugin/PyPluginPackage.hpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#ifndef slic3r_PyPluginPackage_hpp_
|
||||
#define slic3r_PyPluginPackage_hpp_
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Package base class: a plugin file subclasses orca.base and overrides
|
||||
// register_capabilities() to call orca.register_capability() for each capability.
|
||||
class PyPluginPackage
|
||||
{
|
||||
public:
|
||||
virtual ~PyPluginPackage() = default;
|
||||
virtual void register_capabilities() {}
|
||||
};
|
||||
|
||||
class PyPluginPackageTrampoline : public PyPluginPackage
|
||||
{
|
||||
public:
|
||||
using PyPluginPackage::PyPluginPackage;
|
||||
|
||||
void register_capabilities() override { PYBIND11_OVERRIDE(void, PyPluginPackage, register_capabilities); }
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
110
src/slic3r/plugin/PyPluginTrampoline.hpp
Normal file
110
src/slic3r/plugin/PyPluginTrampoline.hpp
Normal file
@@ -0,0 +1,110 @@
|
||||
#ifndef slic3r_PyPluginTrampoline_hpp_
|
||||
#define slic3r_PyPluginTrampoline_hpp_
|
||||
|
||||
#include <pybind11/embed.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "PythonPluginInterface.hpp"
|
||||
#include "PythonInterpreter.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.
|
||||
|
||||
// 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
|
||||
// manage their own audit scope (e.g. the G-code plugin).
|
||||
#define ORCA_PY_LOGGED_OVERRIDE_BODY(override_call) \
|
||||
try { \
|
||||
override_call; \
|
||||
} catch (pybind11::error_already_set & err) { \
|
||||
::Slic3r::log_python_exception_keep(err); \
|
||||
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`.
|
||||
#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)
|
||||
|
||||
#define ORCA_PY_OVERRIDE_AUDITED(mode, audit_setup, override_macro, ret, base, name, ...) \
|
||||
do { \
|
||||
ORCA_PY_AUDIT_SCOPE(mode); \
|
||||
if (_orca_audit_scope) \
|
||||
audit_setup(); \
|
||||
ORCA_PY_LOGGED_OVERRIDE_BODY(override_macro(ret, base, name, ##__VA_ARGS__)); \
|
||||
} while (0)
|
||||
|
||||
namespace Slic3r {
|
||||
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.
|
||||
void on_load() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||||
[] {},
|
||||
PYBIND11_OVERRIDE,
|
||||
void,
|
||||
Base,
|
||||
on_load);
|
||||
}
|
||||
|
||||
void on_unload() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||||
[] {},
|
||||
PYBIND11_OVERRIDE,
|
||||
void,
|
||||
Base,
|
||||
on_unload);
|
||||
}
|
||||
};
|
||||
|
||||
class PyPluginInterfaceTrampoline : public PyPluginCommonTrampoline<PluginCapabilityInterface>
|
||||
{
|
||||
public:
|
||||
using PyPluginCommonTrampoline<PluginCapabilityInterface>::PyPluginCommonTrampoline;
|
||||
|
||||
// get_name is implemented in PyPluginCommonTrampoline (PYBIND11_OVERRIDE_PURE).
|
||||
|
||||
PluginCapabilityType get_type() const override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||||
[] {},
|
||||
PYBIND11_OVERRIDE,
|
||||
PluginCapabilityType,
|
||||
PluginCapabilityInterface,
|
||||
get_type);
|
||||
}
|
||||
};
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
955
src/slic3r/plugin/PythonFileUtils.cpp
Normal file
955
src/slic3r/plugin/PythonFileUtils.cpp
Normal file
@@ -0,0 +1,955 @@
|
||||
#include "PythonFileUtils.hpp"
|
||||
|
||||
#include "PluginFsUtils.hpp"
|
||||
#include "PythonInterpreter.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "PluginDescriptor.hpp"
|
||||
#include "libslic3r/miniz_extension.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <sstream>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#ifdef WIN32
|
||||
#include <boost/locale/encoding_utf.hpp>
|
||||
#endif
|
||||
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
|
||||
bool is_safe_archive_entry_path(const boost::filesystem::path& path)
|
||||
{
|
||||
if (!is_safe_relative_path(path))
|
||||
return false;
|
||||
|
||||
for (const auto& part : path) {
|
||||
const std::string token = part.string();
|
||||
if (token.empty() || token == "." || token == "..")
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string decode_zip_entry_extra_path(const std::string& extra, const std::string& fallback)
|
||||
{
|
||||
const char* p = extra.data();
|
||||
const char* e = p + extra.length();
|
||||
while (p + 4 <= e) {
|
||||
const auto len = static_cast<std::uint16_t>(static_cast<unsigned char>(p[2])) |
|
||||
static_cast<std::uint16_t>(static_cast<unsigned char>(p[3]) << 8);
|
||||
if (p[0] == '\x75' && p[1] == '\x70' && len >= 5 && p + 4 + len <= e && p[4] == '\x01')
|
||||
return std::string(p + 9, p + 4 + len);
|
||||
p += 4 + len;
|
||||
}
|
||||
|
||||
return decode_path(fallback.c_str());
|
||||
}
|
||||
|
||||
std::string zip_entry_name(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat)
|
||||
{
|
||||
if (stat.m_is_utf8)
|
||||
return stat.m_filename;
|
||||
|
||||
std::string extra(1024, 0);
|
||||
const size_t n = mz_zip_reader_get_extra(&archive, stat.m_file_index, extra.data(), extra.size());
|
||||
return decode_zip_entry_extra_path(extra.substr(0, n), stat.m_filename);
|
||||
}
|
||||
|
||||
std::string normalize_zip_entry_name(std::string entry_name)
|
||||
{
|
||||
std::replace(entry_name.begin(), entry_name.end(), '\\', '/');
|
||||
while (!entry_name.empty() && entry_name.back() == '/')
|
||||
entry_name.pop_back();
|
||||
return entry_name;
|
||||
}
|
||||
|
||||
struct ZipReaderGuard
|
||||
{
|
||||
mz_zip_archive archive;
|
||||
bool opened = false;
|
||||
|
||||
ZipReaderGuard() { mz_zip_zero_struct(&archive); }
|
||||
|
||||
~ZipReaderGuard()
|
||||
{
|
||||
if (opened)
|
||||
close_zip_reader(&archive);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool is_valid_plugin_id(const std::string& id)
|
||||
{
|
||||
if (id.empty())
|
||||
return false;
|
||||
if (id == "." || id == ".." || id[0] == '.' || id.rfind("__", 0) == 0)
|
||||
return false;
|
||||
|
||||
for (unsigned char ch : id) {
|
||||
if (std::isalnum(ch) || ch == '_' || ch == '-' || ch == '.')
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// RAII helper to free heap-allocated memory from miniz.
|
||||
struct MzHeapFree {
|
||||
void* ptr = nullptr;
|
||||
~MzHeapFree() { if (ptr) std::free(ptr); }
|
||||
};
|
||||
|
||||
// Read a text file from within a zip archive into a string.
|
||||
// Returns true on success, false if the file is not found or cannot be read.
|
||||
bool read_zip_text_file(mz_zip_archive& archive, const char* filename, std::string& out, std::string& error)
|
||||
{
|
||||
size_t size = 0;
|
||||
void* data = mz_zip_reader_extract_file_to_heap(&archive, filename, &size, 0);
|
||||
if (!data) {
|
||||
error = std::string("Wheel does not contain ") + filename;
|
||||
return false;
|
||||
}
|
||||
MzHeapFree guard{data};
|
||||
out.assign(static_cast<const char*>(data), size);
|
||||
return true;
|
||||
}
|
||||
|
||||
// TOML section parsing states.
|
||||
enum class TomlSection { Root, OrcaPlugin, InDepsArray };
|
||||
|
||||
// Strip a quoted string value: "foo" → foo, 'foo' → foo.
|
||||
// Returns the unquoted value or the input unchanged if not quoted.
|
||||
std::string unquote_toml_string(const std::string& val)
|
||||
{
|
||||
if (val.size() >= 2 && ((val.front() == '"' && val.back() == '"') || (val.front() == '\'' && val.back() == '\'')))
|
||||
return val.substr(1, val.size() - 2);
|
||||
return val;
|
||||
}
|
||||
|
||||
// Split a TOML inline array: ["a", "b"] → {"a", "b"}.
|
||||
// Handles trailing commas and single-line format.
|
||||
std::vector<std::string> parse_toml_inline_array(const std::string& val)
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
std::string inner = val;
|
||||
// Strip outer brackets.
|
||||
if (!inner.empty() && inner.front() == '[')
|
||||
inner.erase(0, 1);
|
||||
if (!inner.empty() && inner.back() == ']')
|
||||
inner.pop_back();
|
||||
|
||||
// Simple split by comma, strip quotes and whitespace.
|
||||
std::istringstream ss(inner);
|
||||
std::string item;
|
||||
while (std::getline(ss, item, ',')) {
|
||||
// Trim whitespace.
|
||||
size_t s = 0, e = item.size();
|
||||
while (s < e && (item[s] == ' ' || item[s] == '\t')) ++s;
|
||||
while (e > s && (item[e - 1] == ' ' || item[e - 1] == '\t')) --e;
|
||||
item = item.substr(s, e - s);
|
||||
if (!item.empty())
|
||||
result.push_back(unquote_toml_string(item));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse PEP 723 TOML subset for dependencies, requires-python, and
|
||||
// [tool.orcaslicer.plugin] identity fields.
|
||||
//
|
||||
// requires-python = ">=3.12"
|
||||
// dependencies = ["pkg>=1.0", ]
|
||||
//
|
||||
// [tool.orcaslicer.plugin]
|
||||
// id = "my-plugin"
|
||||
// name = "My Plugin"
|
||||
// description = "Does things."
|
||||
// author = "Author"
|
||||
// version = "1.0.0"
|
||||
//
|
||||
// Returns false only on parse errors; missing block is not an error.
|
||||
bool parse_pep723_toml(const std::string& toml_content,
|
||||
std::vector<std::string>& out_deps,
|
||||
std::string& out_requires_python,
|
||||
std::string& out_name,
|
||||
std::string& out_description,
|
||||
std::string& out_author,
|
||||
std::string& out_version,
|
||||
std::string& error)
|
||||
{
|
||||
out_deps.clear();
|
||||
out_requires_python.clear();
|
||||
out_name.clear();
|
||||
out_description.clear();
|
||||
out_author.clear();
|
||||
out_version.clear();
|
||||
|
||||
TomlSection section = TomlSection::Root;
|
||||
|
||||
std::istringstream stream(toml_content);
|
||||
std::string line;
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
// Trim leading/trailing whitespace.
|
||||
size_t start = 0;
|
||||
while (start < line.size() && (line[start] == ' ' || line[start] == '\t'))
|
||||
++start;
|
||||
size_t end = line.size();
|
||||
while (end > start && (line[end - 1] == ' ' || line[end - 1] == '\t'))
|
||||
--end;
|
||||
std::string trimmed = line.substr(start, end - start);
|
||||
|
||||
if (trimmed.empty() || trimmed[0] == '#')
|
||||
continue;
|
||||
|
||||
// TOML section header.
|
||||
if (trimmed[0] == '[') {
|
||||
if (trimmed == "[tool.orcaslicer.plugin]") {
|
||||
section = TomlSection::OrcaPlugin;
|
||||
} else {
|
||||
section = TomlSection::Root; // Unknown section — skip.
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (section == TomlSection::InDepsArray) {
|
||||
if (trimmed == "]") {
|
||||
section = TomlSection::Root;
|
||||
continue;
|
||||
}
|
||||
std::string val = trimmed;
|
||||
if (!val.empty() && val.back() == ',')
|
||||
val.pop_back();
|
||||
val = unquote_toml_string(val);
|
||||
if (!val.empty())
|
||||
out_deps.push_back(val);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Look for key = value.
|
||||
size_t eq = trimmed.find('=');
|
||||
if (eq == std::string::npos)
|
||||
continue;
|
||||
|
||||
std::string key = trimmed.substr(0, eq);
|
||||
while (!key.empty() && (key.back() == ' ' || key.back() == '\t'))
|
||||
key.pop_back();
|
||||
|
||||
std::string val = trimmed.substr(eq + 1);
|
||||
while (!val.empty() && (val.front() == ' ' || val.front() == '\t'))
|
||||
val.erase(0, 1);
|
||||
// Trim trailing.
|
||||
while (!val.empty() && (val.back() == ' ' || val.back() == '\t'))
|
||||
val.pop_back();
|
||||
|
||||
if (section == TomlSection::Root) {
|
||||
if (key == "requires-python") {
|
||||
out_requires_python = unquote_toml_string(val);
|
||||
} else if (key == "dependencies") {
|
||||
if (val == "[") {
|
||||
section = TomlSection::InDepsArray;
|
||||
} else {
|
||||
// Inline array: dependencies = ["a", "b"]
|
||||
out_deps = parse_toml_inline_array(val);
|
||||
}
|
||||
}
|
||||
} else if (section == TomlSection::OrcaPlugin) {
|
||||
if (key == "name") out_name = unquote_toml_string(val);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for unclosed arrays.
|
||||
if (section == TomlSection::InDepsArray) {
|
||||
error = "PEP 723 metadata: unclosed dependencies array";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Normalize a distribution name to a Python import package name.
|
||||
// Converts hyphens to underscores and lowercases.
|
||||
std::string normalize_package_name(const std::string& name)
|
||||
{
|
||||
std::string result;
|
||||
result.reserve(name.size());
|
||||
for (unsigned char ch : name) {
|
||||
if (ch == '-' || ch == '.')
|
||||
result += '_';
|
||||
else
|
||||
result += static_cast<char>(std::tolower(ch));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse METADATA (RFC 822 style) into a flat multimap.
|
||||
// https://packaging.python.org/en/latest/specifications/core-metadata/
|
||||
void parse_metadata_rfc822(const std::string& content,
|
||||
std::string& out_name,
|
||||
std::string& out_version,
|
||||
std::string& out_summary,
|
||||
std::string& out_author,
|
||||
std::string& out_requires_python,
|
||||
std::string& out_import_name,
|
||||
std::vector<std::string>& out_requires_dist,
|
||||
std::string& error)
|
||||
{
|
||||
std::istringstream stream(content);
|
||||
std::string line;
|
||||
std::string current_header;
|
||||
std::string current_value;
|
||||
|
||||
auto flush = [&]() {
|
||||
if (current_header.empty())
|
||||
return;
|
||||
std::string lower = current_header;
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
|
||||
if (lower == "name")
|
||||
out_name = current_value;
|
||||
else if (lower == "version")
|
||||
out_version = current_value;
|
||||
else if (lower == "summary")
|
||||
out_summary = current_value;
|
||||
else if (lower == "author")
|
||||
out_author = current_value;
|
||||
else if (lower == "requires-python")
|
||||
out_requires_python = current_value;
|
||||
else if (lower == "import-name")
|
||||
out_import_name = current_value;
|
||||
else if (lower == "requires-dist")
|
||||
out_requires_dist.push_back(current_value);
|
||||
|
||||
current_header.clear();
|
||||
current_value.clear();
|
||||
};
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
// Blank line after headers marks the start of the body.
|
||||
if (line.empty() && current_header.empty())
|
||||
continue;
|
||||
if (line.empty()) {
|
||||
flush();
|
||||
// Remaining content is the body (description); stop parsing headers.
|
||||
break;
|
||||
}
|
||||
|
||||
// Continuation line.
|
||||
if (line[0] == ' ' || line[0] == '\t') {
|
||||
if (!current_value.empty())
|
||||
current_value += '\n';
|
||||
size_t pos = line.find_first_not_of(" \t");
|
||||
current_value += (pos != std::string::npos) ? line.substr(pos) : "";
|
||||
continue;
|
||||
}
|
||||
|
||||
flush();
|
||||
|
||||
size_t colon = line.find(':');
|
||||
if (colon == std::string::npos)
|
||||
continue;
|
||||
|
||||
current_header = line.substr(0, colon);
|
||||
size_t val_start = colon + 1;
|
||||
while (val_start < line.size() && (line[val_start] == ' ' || line[val_start] == '\t'))
|
||||
++val_start;
|
||||
current_value = line.substr(val_start);
|
||||
}
|
||||
|
||||
flush();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool is_ignored_plugin_directory(const boost::filesystem::path& path)
|
||||
{
|
||||
const std::string name = path.filename().string();
|
||||
return name.empty() || name[0] == '.' || name.rfind("__", 0) == 0 || name == PLUGIN_SUBSCRIBED_DIR;
|
||||
}
|
||||
|
||||
bool is_safe_relative_path(const boost::filesystem::path& path)
|
||||
{
|
||||
if (path.empty() || path.is_absolute() || path.has_root_directory() || path.has_root_name())
|
||||
return false;
|
||||
|
||||
for (const auto& part : path) {
|
||||
const std::string token = part.string();
|
||||
if (token == "..")
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boost::filesystem::path& destination, std::string& error)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
boost::system::error_code ec;
|
||||
fs::create_directories(destination, ec);
|
||||
if (ec) {
|
||||
error = "Failed to create plugin staging directory: " + ec.message();
|
||||
return false;
|
||||
}
|
||||
|
||||
ZipReaderGuard reader;
|
||||
if (!open_zip_reader(&reader.archive, zip_path.string())) {
|
||||
error = "Failed to open plugin zip: " + MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
|
||||
return false;
|
||||
}
|
||||
reader.opened = true;
|
||||
|
||||
std::unordered_set<std::string> extracted_entries;
|
||||
const mz_uint num_entries = mz_zip_reader_get_num_files(&reader.archive);
|
||||
mz_zip_archive_file_stat stat;
|
||||
for (mz_uint i = 0; i < num_entries; ++i) {
|
||||
if (!mz_zip_reader_file_stat(&reader.archive, i, &stat)) {
|
||||
error = "Failed to read plugin zip entry metadata: " + MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string entry_name = normalize_zip_entry_name(zip_entry_name(reader.archive, stat));
|
||||
if (entry_name.empty())
|
||||
continue;
|
||||
if (entry_name.find(':') != std::string::npos) {
|
||||
error = "Plugin zip entry contains an invalid path: " + entry_name;
|
||||
return false;
|
||||
}
|
||||
|
||||
const fs::path relative_path(entry_name);
|
||||
if (!is_safe_archive_entry_path(relative_path)) {
|
||||
error = "Plugin zip entry escapes the plugin package: " + entry_name;
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string relative_key = relative_path.generic_string();
|
||||
if (!extracted_entries.insert(relative_key).second) {
|
||||
error = "Plugin zip contains duplicate entry: " + relative_key;
|
||||
return false;
|
||||
}
|
||||
|
||||
const fs::path output_path = destination / relative_path;
|
||||
if (stat.m_is_directory || mz_zip_reader_is_file_a_directory(&reader.archive, stat.m_file_index)) {
|
||||
fs::create_directories(output_path, ec);
|
||||
if (ec) {
|
||||
error = "Failed to create plugin zip directory " + output_path.string() + ": " + ec.message();
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
fs::create_directories(output_path.parent_path(), ec);
|
||||
if (ec) {
|
||||
error = "Failed to create plugin zip parent directory " + output_path.parent_path().string() + ": " + ec.message();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fs::exists(output_path, ec) && fs::is_directory(output_path, ec)) {
|
||||
error = "Plugin zip file conflicts with an existing directory: " + output_path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string encoded_output_path = encode_path(output_path.string().c_str());
|
||||
mz_bool extracted = mz_zip_reader_extract_to_file(&reader.archive, stat.m_file_index, encoded_output_path.c_str(), 0);
|
||||
#ifdef WIN32
|
||||
if (!extracted) {
|
||||
const std::wstring wide_output_path = boost::locale::conv::utf_to_utf<wchar_t>(output_path.generic_string());
|
||||
extracted = mz_zip_reader_extract_to_file_w(&reader.archive, stat.m_file_index, wide_output_path.c_str(), 0);
|
||||
}
|
||||
#endif
|
||||
if (!extracted) {
|
||||
error = "Failed to extract plugin zip entry " + relative_key + ": " +
|
||||
MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void read_install_state(const boost::filesystem::path& plugin_dir, PluginDescriptor& entry)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
const fs::path sidecar_path = plugin_dir / ".install_state.json";
|
||||
|
||||
if (!fs::exists(sidecar_path) || !fs::is_regular_file(sidecar_path))
|
||||
return;
|
||||
|
||||
boost::nowide::ifstream f(sidecar_path.string());
|
||||
if (!f)
|
||||
return;
|
||||
|
||||
try {
|
||||
nlohmann::json state = nlohmann::json::parse(f, nullptr, false, true);
|
||||
if (state.is_discarded() || !state.is_object())
|
||||
return;
|
||||
|
||||
// The cloud identity and the persisted installed version are read back. plugin_key
|
||||
// is always derived by the catalog scan (filename for local, the cloud uuid for
|
||||
// cloud), so it is not read from the sidecar. installed_version is the source of
|
||||
// truth for a cloud plugin's installed version: it records the version fetched from
|
||||
// the cloud at install time, independent of the (possibly stale) manifest/PEP723
|
||||
// header that scan_directory parses into entry.version.
|
||||
if (state.contains("installed_version") && state["installed_version"].is_string())
|
||||
entry.installed_version = state["installed_version"].get<std::string>();
|
||||
if (state.contains("cloud_uuid") && state["cloud_uuid"].is_string()) {
|
||||
const std::string cloud_uuid = state["cloud_uuid"].get<std::string>();
|
||||
if (!cloud_uuid.empty())
|
||||
entry.cloud = CloudPluginState{cloud_uuid, true, false, false};
|
||||
}
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstallState& out)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
const fs::path sidecar_path = plugin_dir / ".install_state.json";
|
||||
|
||||
if (!fs::exists(sidecar_path) || !fs::is_regular_file(sidecar_path))
|
||||
return false;
|
||||
|
||||
boost::nowide::ifstream f(sidecar_path.string());
|
||||
if (!f)
|
||||
return false;
|
||||
|
||||
try {
|
||||
nlohmann::json state = nlohmann::json::parse(f, nullptr, false, true);
|
||||
if (state.is_discarded() || !state.is_object())
|
||||
return false;
|
||||
|
||||
PluginInstallState parsed;
|
||||
if (state.contains("installed_from") && state["installed_from"].is_string())
|
||||
parsed.installed_from = state["installed_from"].get<std::string>();
|
||||
if (state.contains("installed_version") && state["installed_version"].is_string())
|
||||
parsed.installed_version = state["installed_version"].get<std::string>();
|
||||
if (state.contains("plugin_name") && state["plugin_name"].is_string())
|
||||
parsed.plugin_name = state["plugin_name"].get<std::string>();
|
||||
if (state.contains("cloud_uuid") && state["cloud_uuid"].is_string())
|
||||
parsed.cloud_uuid = state["cloud_uuid"].get<std::string>();
|
||||
if (state.contains("enabled") && state["enabled"].is_boolean())
|
||||
parsed.enabled = state["enabled"].get<bool>();
|
||||
|
||||
// capabilities is a JSON array of single-key objects {<cap_name>: <bool>}.
|
||||
if (state.contains("capabilities") && state["capabilities"].is_array()) {
|
||||
for (const auto& item : state["capabilities"]) {
|
||||
if (!item.is_object())
|
||||
continue;
|
||||
for (auto it = item.begin(); it != item.end(); ++it) {
|
||||
if (it.value().is_boolean())
|
||||
parsed.capabilities.emplace_back(it.key(), it.value().get<bool>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out = std::move(parsed);
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginInstallState& state)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
const fs::path sidecar_path = plugin_dir / ".install_state.json";
|
||||
|
||||
nlohmann::json json;
|
||||
json["installed_from"] = state.installed_from;
|
||||
json["installed_version"] = state.installed_version;
|
||||
json["plugin_name"] = state.plugin_name;
|
||||
json["enabled"] = state.enabled;
|
||||
if (!state.cloud_uuid.empty())
|
||||
json["cloud_uuid"] = state.cloud_uuid;
|
||||
|
||||
nlohmann::json capabilities = nlohmann::json::array();
|
||||
for (const auto& [name, enabled] : state.capabilities)
|
||||
capabilities.push_back(nlohmann::json{{name, enabled}});
|
||||
json["capabilities"] = std::move(capabilities);
|
||||
|
||||
boost::nowide::ofstream f(sidecar_path.string());
|
||||
if (!f)
|
||||
return false;
|
||||
|
||||
f << json.dump(2);
|
||||
return static_cast<bool>(f);
|
||||
}
|
||||
|
||||
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry, bool enabled,
|
||||
const std::vector<std::pair<std::string, bool>>& capabilities)
|
||||
{
|
||||
PluginInstallState state;
|
||||
state.installed_from = entry.is_cloud_plugin() ? "cloud" : "local";
|
||||
// Prefer the descriptor's recorded installed_version (the version fetched from the cloud
|
||||
// at install time, preserved across sidecar re-writes) so a stale manifest/PEP723 header
|
||||
// never overwrites the source-of-truth version. Fall back to the manifest version for
|
||||
// first-time/local installs where installed_version is not yet populated.
|
||||
state.installed_version = !entry.installed_version.empty() ? entry.installed_version : entry.version;
|
||||
state.plugin_name = entry.name;
|
||||
state.cloud_uuid = entry.cloud_uuid();
|
||||
state.enabled = enabled;
|
||||
state.capabilities = capabilities;
|
||||
return write_install_state(plugin_dir, state);
|
||||
}
|
||||
|
||||
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry)
|
||||
{
|
||||
return write_install_state(plugin_dir, entry, true, {});
|
||||
}
|
||||
|
||||
bool read_python_plugin_metadata(const boost::filesystem::path& py_path, PluginDescriptor& descriptor, std::string& error)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
if (!fs::exists(py_path) || !fs::is_regular_file(py_path)) {
|
||||
error = "Python plugin file does not exist: " + py_path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
boost::nowide::ifstream f(py_path.string());
|
||||
if (!f) {
|
||||
error = "Failed to open Python plugin file: " + py_path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Scan for PEP 723 inline script metadata block.
|
||||
// The block is delimited by:
|
||||
// # /// script
|
||||
// # <toml content>
|
||||
// # ///
|
||||
std::string pep723_content;
|
||||
bool in_block = false;
|
||||
std::string line;
|
||||
|
||||
while (std::getline(f, line)) {
|
||||
// Strip trailing carriage return (Windows line endings).
|
||||
if (!line.empty() && line.back() == '\r')
|
||||
line.pop_back();
|
||||
|
||||
if (!in_block) {
|
||||
// Look for opening delimiter.
|
||||
if (line == "# /// script")
|
||||
in_block = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line == "# ///") {
|
||||
in_block = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract TOML content from the comment line.
|
||||
// Lines must start with "# " or "#\t" per PEP 723.
|
||||
if (line.size() >= 2 && line[0] == '#' && (line[1] == ' ' || line[1] == '\t'))
|
||||
pep723_content += line.substr(2) + "\n";
|
||||
else if (line == "#")
|
||||
pep723_content += "\n";
|
||||
// If line doesn't start with "# ", it's still part of the block content
|
||||
// but we skip it as it doesn't follow the spec.
|
||||
}
|
||||
|
||||
if (!pep723_content.empty()) {
|
||||
std::string pep723_error;
|
||||
std::string requires_python;
|
||||
std::string pep_name, pep_desc, pep_author, pep_version;
|
||||
if (!parse_pep723_toml(pep723_content,
|
||||
descriptor.dependencies,
|
||||
requires_python,
|
||||
pep_name,
|
||||
pep_desc,
|
||||
pep_author,
|
||||
pep_version,
|
||||
pep723_error)) {
|
||||
error = "Failed to parse PEP 723 metadata: " + pep723_error;
|
||||
return false;
|
||||
}
|
||||
// requires-python is stored but not validated against the bundled Python here.
|
||||
(void) requires_python;
|
||||
|
||||
// Populate identity fields from the PEP 723 [tool.orcaslicer.plugin] section.
|
||||
// Cloud metadata overrides these when available; they serve as the local
|
||||
// source of truth for side-loaded .py plugins and as fallback values.
|
||||
if (!pep_name.empty()) descriptor.name = sanitize_plugin_name(pep_name);
|
||||
if (!pep_desc.empty()) descriptor.description = pep_desc;
|
||||
if (!pep_author.empty()) descriptor.author = pep_author;
|
||||
if (!pep_version.empty()) descriptor.version = pep_version;
|
||||
}
|
||||
|
||||
// Validate that required identity fields are present (either from PEP 723 or
|
||||
// from cloud metadata already set on the manifest by the caller).
|
||||
// Validation is deferred to the install/discovery layer so cloud metadata
|
||||
// can fill in gaps.
|
||||
return true;
|
||||
}
|
||||
|
||||
bool read_wheel_plugin_metadata(const boost::filesystem::path& whl_path, PluginDescriptor& descriptor, std::string& error)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
if (!fs::exists(whl_path) || !fs::is_regular_file(whl_path)) {
|
||||
error = "Wheel plugin file does not exist: " + whl_path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
ZipReaderGuard reader;
|
||||
if (!open_zip_reader(&reader.archive, whl_path.string())) {
|
||||
error = "Failed to open wheel as zip: " + MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
|
||||
return false;
|
||||
}
|
||||
reader.opened = true;
|
||||
|
||||
// Find the single .dist-info directory.
|
||||
// Scan ALL entries, not just directory entries — some zip writers omit
|
||||
// explicit directory entries. normalize_zip_entry_name strips trailing
|
||||
// slashes, so we match ".dist-info" as the last path component of a
|
||||
// directory entry, and ".dist-info/" embedded in a file path.
|
||||
const mz_uint num_entries = mz_zip_reader_get_num_files(&reader.archive);
|
||||
std::string dist_info_dir;
|
||||
mz_zip_archive_file_stat stat;
|
||||
|
||||
for (mz_uint i = 0; i < num_entries; ++i) {
|
||||
if (!mz_zip_reader_file_stat(&reader.archive, i, &stat))
|
||||
continue;
|
||||
|
||||
std::string entry_name = normalize_zip_entry_name(zip_entry_name(reader.archive, stat));
|
||||
if (entry_name.empty())
|
||||
continue;
|
||||
|
||||
// Find .dist-info as a path component.
|
||||
size_t pos = entry_name.find(".dist-info");
|
||||
if (pos == std::string::npos)
|
||||
continue;
|
||||
|
||||
std::string candidate;
|
||||
if (pos + 10 == entry_name.size()) {
|
||||
// Directory entry itself (trailing / stripped by normalize).
|
||||
candidate = entry_name + "/";
|
||||
} else if (pos + 10 < entry_name.size() && entry_name[pos + 10] == '/') {
|
||||
// File inside .dist-info/: name.dist-info/METADATA
|
||||
candidate = entry_name.substr(0, pos + 11); // include trailing /
|
||||
} else {
|
||||
continue; // .dist-info mid-name, not a path component.
|
||||
}
|
||||
|
||||
if (!dist_info_dir.empty() && candidate != dist_info_dir) {
|
||||
error = "Wheel contains multiple .dist-info directories: " + dist_info_dir + " and " + candidate;
|
||||
return false;
|
||||
}
|
||||
dist_info_dir = candidate;
|
||||
}
|
||||
|
||||
if (dist_info_dir.empty()) {
|
||||
error = "Wheel does not contain a .dist-info directory";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read METADATA.
|
||||
const std::string metadata_path = dist_info_dir + "METADATA";
|
||||
std::string meta_content;
|
||||
if (!read_zip_text_file(reader.archive, metadata_path.c_str(), meta_content, error))
|
||||
return false;
|
||||
|
||||
std::string meta_name, meta_version, meta_summary, meta_author, meta_requires_python, meta_import_name;
|
||||
std::vector<std::string> requires_dist;
|
||||
std::string meta_error;
|
||||
parse_metadata_rfc822(meta_content, meta_name, meta_version, meta_summary, meta_author,
|
||||
meta_requires_python, meta_import_name, requires_dist, meta_error);
|
||||
|
||||
if (meta_name.empty()) {
|
||||
error = "Wheel METADATA missing required Name field";
|
||||
return false;
|
||||
}
|
||||
if (meta_version.empty()) {
|
||||
error = "Wheel METADATA missing required Version field";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read WHEEL (verify existence and Wheel-Version).
|
||||
const std::string wheel_path = dist_info_dir + "WHEEL";
|
||||
std::string wheel_content;
|
||||
if (!read_zip_text_file(reader.archive, wheel_path.c_str(), wheel_content, error))
|
||||
return false;
|
||||
// Verify there's at least a Wheel-Version header line.
|
||||
if (wheel_content.find("Wheel-Version:") == std::string::npos) {
|
||||
error = "Wheel WHEEL file missing Wheel-Version header";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse and validate wheel platform tags.
|
||||
{
|
||||
std::vector<std::string> wheel_tags;
|
||||
std::istringstream wstream(wheel_content);
|
||||
std::string wline;
|
||||
while (std::getline(wstream, wline)) {
|
||||
while (!wline.empty() && (wline.back() == '\r' || wline.back() == '\n'))
|
||||
wline.pop_back();
|
||||
if (wline.rfind("Tag:", 0) == 0) {
|
||||
std::string tag = wline.substr(4);
|
||||
size_t s = 0, e = tag.size();
|
||||
while (s < e && (tag[s] == ' ' || tag[s] == '\t')) ++s;
|
||||
while (e > s && (tag[e - 1] == ' ' || tag[e - 1] == '\t')) --e;
|
||||
wheel_tags.push_back(tag.substr(s, e - s));
|
||||
}
|
||||
}
|
||||
|
||||
if (!wheel_tags.empty()) {
|
||||
bool compatible = false;
|
||||
const std::string abi_tag = PythonInterpreter::python_abi_tag();
|
||||
for (const auto& tag : wheel_tags) {
|
||||
// Pure Python wheel: py3-none-any or cp312-none-any
|
||||
if (tag.find("-none-any") != std::string::npos) {
|
||||
compatible = true;
|
||||
break;
|
||||
}
|
||||
// Platform-specific: check ABI tag matches.
|
||||
if (tag.find(abi_tag) == 0) {
|
||||
// Accept if the platform tag matches the current OS.
|
||||
#ifdef _WIN32
|
||||
if (tag.find("-win") != std::string::npos)
|
||||
compatible = true;
|
||||
#elif __APPLE__
|
||||
if (tag.find("-macosx") != std::string::npos)
|
||||
compatible = true;
|
||||
#else
|
||||
if (tag.find("-linux") != std::string::npos || tag.find("-manylinux") != std::string::npos)
|
||||
compatible = true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (!compatible) {
|
||||
error = "Wheel is incompatible with this platform. Tags: ";
|
||||
for (size_t i = 0; i < wheel_tags.size(); ++i) {
|
||||
if (i > 0) error += ", ";
|
||||
error += wheel_tags[i];
|
||||
}
|
||||
error += "; expected ABI: " + abi_tag;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read RECORD (verify existence).
|
||||
const std::string record_path = dist_info_dir + "RECORD";
|
||||
std::string record_content;
|
||||
if (!read_zip_text_file(reader.archive, record_path.c_str(), record_content, error))
|
||||
return false;
|
||||
if (record_content.empty()) {
|
||||
error = "Wheel RECORD file is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse top_level.txt if present.
|
||||
std::string top_level;
|
||||
const std::string top_level_path = dist_info_dir + "top_level.txt";
|
||||
std::string top_level_content;
|
||||
if (read_zip_text_file(reader.archive, top_level_path.c_str(), top_level_content, error)) {
|
||||
// top_level.txt contains one package name per line.
|
||||
std::istringstream tl_stream(top_level_content);
|
||||
std::string tl_line;
|
||||
std::vector<std::string> top_levels;
|
||||
while (std::getline(tl_stream, tl_line)) {
|
||||
while (!tl_line.empty() && (tl_line.back() == '\r' || tl_line.back() == '\n'))
|
||||
tl_line.pop_back();
|
||||
if (!tl_line.empty())
|
||||
top_levels.push_back(tl_line);
|
||||
}
|
||||
if (top_levels.size() == 1)
|
||||
top_level = top_levels[0];
|
||||
else if (top_levels.size() > 1) {
|
||||
// Ambiguous: multiple top-level packages. Fall through to Name-based fallback.
|
||||
}
|
||||
// Zero entries: leave top_level empty.
|
||||
}
|
||||
// If top_level.txt is not found, that's OK — it's optional per the wheel spec.
|
||||
|
||||
// Determine the entry package in priority order.
|
||||
// 1. Cloud/catalog metadata — handled by caller, not here.
|
||||
// 2. Core Metadata Import-Name.
|
||||
// 3. top_level.txt if unambiguous.
|
||||
// 4. Normalized Name as fallback.
|
||||
if (!meta_import_name.empty()) {
|
||||
descriptor.entry_package = meta_import_name;
|
||||
} else if (!top_level.empty()) {
|
||||
descriptor.entry_package = top_level;
|
||||
} else {
|
||||
descriptor.entry_package = normalize_package_name(meta_name);
|
||||
}
|
||||
|
||||
descriptor.dependencies = std::move(requires_dist);
|
||||
|
||||
// Populate local identity fallbacks from wheel metadata.
|
||||
// Cloud metadata will override these when available.
|
||||
descriptor.name = sanitize_plugin_name(meta_name);
|
||||
descriptor.version = meta_version;
|
||||
descriptor.description = meta_summary;
|
||||
descriptor.author = meta_author;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::path& plugin_dir, std::string& error)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
if (!fs::exists(plugin_dir) || !fs::is_directory(plugin_dir)) {
|
||||
error = "Plugin directory does not exist: " + plugin_dir.string();
|
||||
return {};
|
||||
}
|
||||
|
||||
fs::path py_entry;
|
||||
fs::path whl_entry;
|
||||
|
||||
for (fs::directory_iterator it(plugin_dir); it != fs::directory_iterator(); ++it) {
|
||||
if (is_ignored_plugin_directory(it->path()))
|
||||
continue;
|
||||
if (!fs::is_regular_file(it->status()))
|
||||
continue;
|
||||
|
||||
const fs::path ext = it->path().extension();
|
||||
if (ext == ".py") {
|
||||
if (!py_entry.empty()) {
|
||||
error = "Plugin directory contains multiple .py files: " + py_entry.filename().string() +
|
||||
" and " + it->path().filename().string();
|
||||
return {};
|
||||
}
|
||||
py_entry = it->path();
|
||||
} else if (ext == ".whl") {
|
||||
if (!whl_entry.empty()) {
|
||||
error = "Plugin directory contains multiple .whl files: " + whl_entry.filename().string() +
|
||||
" and " + it->path().filename().string();
|
||||
return {};
|
||||
}
|
||||
whl_entry = it->path();
|
||||
}
|
||||
}
|
||||
|
||||
if (!py_entry.empty() && !whl_entry.empty()) {
|
||||
error = "Plugin directory contains both .py and .whl entry files";
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!py_entry.empty())
|
||||
return py_entry;
|
||||
|
||||
if (!whl_entry.empty())
|
||||
return whl_entry;
|
||||
|
||||
error = "Plugin directory does not contain a .py or .whl entry file";
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
61
src/slic3r/plugin/PythonFileUtils.hpp
Normal file
61
src/slic3r/plugin/PythonFileUtils.hpp
Normal file
@@ -0,0 +1,61 @@
|
||||
#ifndef slic3r_PythonFileUtils_hpp_
|
||||
#define slic3r_PythonFileUtils_hpp_
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
struct PluginDescriptor;
|
||||
|
||||
// Persisted per-plugin install/auto-load state, stored in the .install_state.json sidecar.
|
||||
// This replaces app_config as the source of truth for auto-load and capability enable state.
|
||||
struct PluginInstallState {
|
||||
std::string installed_from; // "local" | "cloud"
|
||||
std::string installed_version;
|
||||
std::string plugin_name;
|
||||
std::string cloud_uuid; // empty for local
|
||||
bool enabled = true;
|
||||
std::vector<std::pair<std::string, bool>> capabilities; // name -> enabled, ordered
|
||||
};
|
||||
|
||||
bool is_ignored_plugin_directory(const boost::filesystem::path& path);
|
||||
bool is_safe_relative_path(const boost::filesystem::path& path);
|
||||
bool is_valid_plugin_id(const std::string& id);
|
||||
bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boost::filesystem::path& destination, std::string& error);
|
||||
|
||||
// Read PEP 723 inline script metadata from a .py plugin file.
|
||||
// Populates metadata.dependencies and local identity fallbacks.
|
||||
// Returns true on success (including when no PEP 723 block is found — deps will be empty).
|
||||
bool read_python_plugin_metadata(const boost::filesystem::path& py_path, PluginDescriptor& descriptor, std::string& error);
|
||||
|
||||
// Read wheel metadata from a .whl plugin file (zip archive).
|
||||
// Reads METADATA, WHEEL, RECORD, and top_level.txt from the .dist-info directory.
|
||||
// Populates metadata.entry_package, metadata.dependencies, and local identity fallbacks.
|
||||
// Returns true on success.
|
||||
bool read_wheel_plugin_metadata(const boost::filesystem::path& whl_path, PluginDescriptor& descriptor, std::string& error);
|
||||
|
||||
// Find the single plugin entry file (.py or .whl) in a directory.
|
||||
// Ignores __whl_extracted__ and hidden files/dirs.
|
||||
// Returns the path to the entry file, or an empty path with error set if zero or multiple candidates.
|
||||
boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::path& plugin_dir, std::string& error);
|
||||
|
||||
// Canonical writer: emits the .install_state.json schema for the given state.
|
||||
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginInstallState& state);
|
||||
// Builds a PluginInstallState from the descriptor and delegates to the canonical writer.
|
||||
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry, bool enabled,
|
||||
const std::vector<std::pair<std::string, bool>>& capabilities);
|
||||
// Convenience overload: write(dir, entry, /*enabled=*/true, /*capabilities=*/{}).
|
||||
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry);
|
||||
|
||||
// Reads only the cloud identity (uuid) back into the descriptor; plugin_key is always derived.
|
||||
void read_install_state(const boost::filesystem::path& plugin_dir, PluginDescriptor& entry);
|
||||
// Full read of the sidecar; returns false if there is no/invalid sidecar.
|
||||
bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstallState& out);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_PythonFileUtils_hpp_
|
||||
996
src/slic3r/plugin/PythonInterpreter.cpp
Normal file
996
src/slic3r/plugin/PythonInterpreter.cpp
Normal file
@@ -0,0 +1,996 @@
|
||||
#include "PythonInterpreter.hpp"
|
||||
#include "GeneratedConfig.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "PluginAuditManager.hpp"
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <pytypedefs.h>
|
||||
#include "PythonFileUtils.hpp"
|
||||
|
||||
#include <pybind11/embed.h>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/dll/runtime_symbol_info.hpp>
|
||||
#include <boost/nowide/convert.hpp>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void log_python_exception_keep(pybind11::error_already_set& err)
|
||||
{
|
||||
// The GIL may already be released here: the macro's gil_scoped_acquire is a
|
||||
// local destroyed by stack unwinding before this catch runs. Touching Python
|
||||
// state below needs the GIL. PyGILState_Ensure is reentrant — harmless if held.
|
||||
PythonGILState gil;
|
||||
|
||||
// Non-destructive: print the traceback to sys.stderr (tee'd to the session log)
|
||||
// WITHOUT consuming err, so the caller can rethrow it intact. For example, downstream C++
|
||||
// catchers can still read err.what() for the user-facing dialog. We must NOT use
|
||||
// restore()+PyErr_Print() here as those empty err.
|
||||
try {
|
||||
namespace py = pybind11;
|
||||
py::module_ tb = py::module_::import("traceback");
|
||||
py::module_ sys = py::module_::import("sys");
|
||||
tb.attr("print_exception")(err.type(), err.value(), err.trace(), py::none(), sys.attr("stderr"));
|
||||
} catch (...) {
|
||||
// Fallback: at least get the formatted message out. err.what() includes the
|
||||
// traceback in recent pybind11 and does not consume err.
|
||||
try {
|
||||
pybind11::module_::import("sys").attr("stderr").attr("write")(std::string(err.what()) + "\n");
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::string format_python_error(PyObject* ptype, PyObject* pvalue, PyObject* ptraceback)
|
||||
{
|
||||
std::string result;
|
||||
|
||||
if (ptype) {
|
||||
PyObject* type_name = PyObject_GetAttrString(ptype, "__name__");
|
||||
if (type_name) {
|
||||
const char* s = PyUnicode_AsUTF8(type_name);
|
||||
if (s) result += s;
|
||||
Py_DECREF(type_name);
|
||||
}
|
||||
}
|
||||
|
||||
if (pvalue) {
|
||||
if (!result.empty()) result += ": ";
|
||||
PyObject* s = PyObject_Str(pvalue);
|
||||
if (s) {
|
||||
const char* cstr = PyUnicode_AsUTF8(s);
|
||||
if (cstr) result += cstr;
|
||||
Py_DECREF(s);
|
||||
}
|
||||
}
|
||||
|
||||
if (ptraceback) {
|
||||
result += "\nTraceback (most recent call last):";
|
||||
PyTracebackObject* tb = reinterpret_cast<PyTracebackObject*>(ptraceback);
|
||||
while (tb) {
|
||||
PyFrameObject* frame = tb->tb_frame;
|
||||
int line = PyFrame_GetLineNumber(frame);
|
||||
PyCodeObject* code = PyFrame_GetCode(frame); // returns a NEW strong reference; must be released
|
||||
const char* filename = code ? PyUnicode_AsUTF8(code->co_filename) : nullptr;
|
||||
const char* funcname = code ? PyUnicode_AsUTF8(code->co_name) : nullptr;
|
||||
result += "\n File \"" + std::string(filename ? filename : "?") +
|
||||
"\", line " + std::to_string(line) +
|
||||
", in " + std::string(funcname ? funcname : "?");
|
||||
Py_XDECREF(code);
|
||||
tb = tb->tb_next;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
constexpr const char* PYTHON_DLL = "python312.dll";
|
||||
constexpr const char* PYTHON_DEBUG_DLL = "python312_d.dll";
|
||||
#else
|
||||
constexpr const char* PYTHON_STDLIB_DIR = "python3.12";
|
||||
constexpr const char* PYTHON_EXECUTABLE = "python3.12";
|
||||
#endif
|
||||
|
||||
std::string executable_name(const char* base)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return std::string(base) + ".exe";
|
||||
#else
|
||||
return base;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool add_sys_path_entry(const boost::filesystem::path& path, std::string& error)
|
||||
{
|
||||
PyObject* sys_path = PySys_GetObject("path");
|
||||
if (!sys_path || !PyList_Check(sys_path)) {
|
||||
error = "Python sys.path is not available";
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string path_str = path.string();
|
||||
PyObjectPtr py_path(PyUnicode_DecodeFSDefault(path_str.c_str()));
|
||||
if (!py_path) {
|
||||
error = "Failed to decode path for Python sys.path: " + path_str;
|
||||
PyErr_Clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
const int contains = PySequence_Contains(sys_path, py_path.get());
|
||||
if (contains == 1)
|
||||
return true;
|
||||
if (contains < 0)
|
||||
PyErr_Clear();
|
||||
|
||||
if (PyList_Insert(sys_path, 0, py_path.get()) != 0) {
|
||||
error = "Failed to add path to Python sys.path: " + path_str;
|
||||
PyErr_Clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void log_python_stream(PyObject* sys, const char* name)
|
||||
{
|
||||
PyObject* stream = PyObject_GetAttrString(sys, name);
|
||||
if (!stream) {
|
||||
PyErr_Clear();
|
||||
BOOST_LOG_TRIVIAL(info) << "Python shutdown: sys." << name << " is not set";
|
||||
return;
|
||||
}
|
||||
|
||||
PyObject* original = nullptr;
|
||||
std::string original_name = std::string("__") + name + "__";
|
||||
original = PyObject_GetAttrString(sys, original_name.c_str());
|
||||
if (!original)
|
||||
PyErr_Clear();
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Python shutdown: sys." << name << " type=" << Py_TYPE(stream)->tp_name << " ptr=" << stream
|
||||
<< " original_ptr=" << original << " is_original=" << (stream == original);
|
||||
|
||||
Py_XDECREF(original);
|
||||
Py_DECREF(stream);
|
||||
}
|
||||
|
||||
void restore_python_stream(PyObject* sys, const char* name)
|
||||
{
|
||||
std::string original_name = std::string("__") + name + "__";
|
||||
PyObject* original = PyObject_GetAttrString(sys, original_name.c_str());
|
||||
if (!original) {
|
||||
PyErr_Clear();
|
||||
original = Py_NewRef(Py_None);
|
||||
}
|
||||
|
||||
if (PyObject_SetAttrString(sys, name, original) < 0) {
|
||||
PyErr_Clear();
|
||||
BOOST_LOG_TRIVIAL(warning) << "Python shutdown: failed to restore sys." << name;
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(info) << "Python shutdown: restored sys." << name << " to type=" << Py_TYPE(original)->tp_name
|
||||
<< " ptr=" << original;
|
||||
}
|
||||
|
||||
Py_DECREF(original);
|
||||
}
|
||||
|
||||
// Tee Python sys.stderr to <data_dir>/log/python_*.log so plugin errors are
|
||||
// persisted. Uncaught exceptions in plugin-spawned threads never cross the
|
||||
// pybind11 boundary back into C++ — CPython's default threading.excepthook
|
||||
// prints them to sys.stderr and lets the thread die — so capturing stderr is
|
||||
// the one place all of them can be observed.
|
||||
void install_python_stderr_redirect()
|
||||
{
|
||||
// Mirror the session filename convention from GUI_App: python_<weekday>_<mon>_<day>_<HH>_<MM>_<SS>_<pid>.log
|
||||
std::time_t t = std::time(nullptr);
|
||||
std::tm* now = std::localtime(&t);
|
||||
std::ostringstream name;
|
||||
name << std::put_time(now, "python_%a_%b_%d_%H_%M_%S_") << get_current_pid() << ".log";
|
||||
const std::string log_path = (boost::filesystem::path(data_dir()) / "log" / name.str()).generic_string();
|
||||
|
||||
const std::string redirect_script =
|
||||
"import io, os, sys, threading\n"
|
||||
"_ORCA_PYTHON_LOG = \"" + log_path + "\"\n"
|
||||
+ std::string(R"REDIRECT_SCRIPT(
|
||||
class _OrcaTeeStderr(io.TextIOBase):
|
||||
def __init__(self, original, path):
|
||||
self._original = original
|
||||
self._path = path
|
||||
self._lock = threading.Lock()
|
||||
def writable(self):
|
||||
return True
|
||||
def write(self, s):
|
||||
try:
|
||||
if self._original is not None:
|
||||
self._original.write(s)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
with self._lock, open(self._path, "a", encoding="utf-8", errors="replace") as f:
|
||||
f.write(s)
|
||||
except Exception:
|
||||
pass
|
||||
return len(s)
|
||||
def flush(self):
|
||||
try:
|
||||
if self._original is not None:
|
||||
self._original.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
os.makedirs(os.path.dirname(_ORCA_PYTHON_LOG), exist_ok=True)
|
||||
sys.stderr = _OrcaTeeStderr(sys.__stderr__, _ORCA_PYTHON_LOG)
|
||||
del _OrcaTeeStderr
|
||||
)REDIRECT_SCRIPT");
|
||||
|
||||
if (PyRun_SimpleString(redirect_script.c_str()) != 0) {
|
||||
PyErr_Clear();
|
||||
BOOST_LOG_TRIVIAL(warning) << "Failed to install Python stderr redirect to " << log_path;
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(info) << "Python stderr redirected to " << log_path;
|
||||
}
|
||||
}
|
||||
|
||||
void log_and_restore_python_stdio()
|
||||
{
|
||||
PyObject* sys = PyImport_ImportModule("sys");
|
||||
if (!sys) {
|
||||
PyErr_Clear();
|
||||
BOOST_LOG_TRIVIAL(warning) << "Python shutdown: failed to import sys for stdio diagnostics";
|
||||
return;
|
||||
}
|
||||
|
||||
log_python_stream(sys, "stdout");
|
||||
log_python_stream(sys, "stderr");
|
||||
restore_python_stream(sys, "stdout");
|
||||
restore_python_stream(sys, "stderr");
|
||||
|
||||
Py_DECREF(sys);
|
||||
}
|
||||
|
||||
bool valid_python_home(const boost::filesystem::path& candidate)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return boost::filesystem::exists(candidate / "Lib" / "encodings") &&
|
||||
(boost::filesystem::exists(candidate / PYTHON_DLL) || boost::filesystem::exists(candidate / PYTHON_DEBUG_DLL));
|
||||
#else
|
||||
return boost::filesystem::exists(candidate / "lib" / PYTHON_STDLIB_DIR / "encodings");
|
||||
#endif
|
||||
}
|
||||
|
||||
boost::filesystem::path find_bundled_python_home()
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
#ifdef __APPLE__
|
||||
fs::path bundle_python = fs::path(resources_dir()).parent_path() / "MacOS" / "python";
|
||||
if (valid_python_home(bundle_python))
|
||||
return bundle_python;
|
||||
#elif defined(_WIN32)
|
||||
fs::path exe_python = boost::dll::program_location().parent_path() / "python";
|
||||
if (valid_python_home(exe_python))
|
||||
return exe_python;
|
||||
#else
|
||||
fs::path linux_python = fs::path(resources_dir()).parent_path() / "lib" / "python";
|
||||
if (valid_python_home(linux_python))
|
||||
return linux_python;
|
||||
#endif
|
||||
|
||||
fs::path configured_python = ORCA_BUNDLED_PYTHON_ROOT;
|
||||
if (!configured_python.empty() && valid_python_home(configured_python))
|
||||
return configured_python;
|
||||
|
||||
const char* prefix_path = std::getenv("CMAKE_PREFIX_PATH");
|
||||
if (prefix_path && std::strlen(prefix_path) > 0) {
|
||||
fs::path libpython = fs::path(prefix_path) / "libpython";
|
||||
if (valid_python_home(libpython))
|
||||
return libpython;
|
||||
}
|
||||
|
||||
fs::path res_python = fs::path(resources_dir()) / "python";
|
||||
if (valid_python_home(res_python))
|
||||
return res_python;
|
||||
|
||||
#ifndef _WIN32
|
||||
fs::path data_python = fs::path(data_dir()) / "python";
|
||||
if (valid_python_home(data_python))
|
||||
return data_python;
|
||||
#endif
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
boost::filesystem::path find_python_executable(const boost::filesystem::path& python_home)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
#ifdef _WIN32
|
||||
const std::vector<fs::path> candidates = {
|
||||
python_home / "python.exe",
|
||||
python_home / "python_d.exe",
|
||||
};
|
||||
#else
|
||||
const std::vector<fs::path> candidates = {
|
||||
python_home / "bin" / PYTHON_EXECUTABLE,
|
||||
python_home / "bin" / "python3",
|
||||
python_home / "bin" / "python",
|
||||
};
|
||||
#endif
|
||||
|
||||
for (const fs::path& candidate : candidates) {
|
||||
if (fs::exists(candidate) && fs::is_regular_file(candidate))
|
||||
return candidate;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string PythonInterpreter::python_abi_tag()
|
||||
{
|
||||
return std::string("cp") + std::to_string(PY_MAJOR_VERSION) + std::to_string(PY_MINOR_VERSION);
|
||||
}
|
||||
|
||||
PythonInterpreter& PythonInterpreter::instance()
|
||||
{
|
||||
static PythonInterpreter inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
std::string PythonInterpreter::shared_packages_dir()
|
||||
{
|
||||
return (boost::filesystem::path(data_dir()) / "python" / "packages" / PythonInterpreter::python_abi_tag()).string();
|
||||
}
|
||||
|
||||
std::string PythonInterpreter::bundled_python_executable()
|
||||
{
|
||||
const boost::filesystem::path python_home = find_bundled_python_home();
|
||||
if (python_home.empty())
|
||||
return {};
|
||||
|
||||
const boost::filesystem::path executable = find_python_executable(python_home);
|
||||
return executable.empty() ? std::string{} : executable.string();
|
||||
}
|
||||
|
||||
std::string PythonInterpreter::bundled_uv_path()
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
const fs::path configured_uv = ORCA_BUNDLED_UV_EXECUTABLE;
|
||||
if (!configured_uv.empty() && fs::exists(configured_uv) && fs::is_regular_file(configured_uv))
|
||||
return configured_uv.string();
|
||||
|
||||
// <binary dir>/tools/uv covers the macOS bundle (Contents/MacOS/tools/uv)
|
||||
// and build-tree runs; <resources>/tools/uv covers the install() and
|
||||
// AppImage layouts.
|
||||
const std::string uv_exe = executable_name("uv");
|
||||
const std::vector<fs::path> candidates = {
|
||||
fs::path(resources_dir()) / "tools" / "uv" / uv_exe,
|
||||
boost::dll::program_location().parent_path() / "tools" / "uv" / uv_exe,
|
||||
};
|
||||
|
||||
for (const fs::path& candidate : candidates) {
|
||||
if (fs::exists(candidate) && fs::is_regular_file(candidate))
|
||||
return candidate.string();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
bool PythonInterpreter::initialize()
|
||||
{
|
||||
if (m_initialized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
m_last_error.clear();
|
||||
|
||||
try {
|
||||
// Set Python home to the bundled Python installation
|
||||
// This is critical for finding the standard library (encodings module, etc.)
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
std::string python_home;
|
||||
const auto valid_python_home = [](const fs::path& candidate) {
|
||||
#ifdef _WIN32
|
||||
return fs::exists(candidate / "Lib" / "encodings") &&
|
||||
(fs::exists(candidate / PYTHON_DLL) || fs::exists(candidate / PYTHON_DEBUG_DLL));
|
||||
#else
|
||||
return fs::exists(candidate / "lib" / PYTHON_STDLIB_DIR / "encodings");
|
||||
#endif
|
||||
};
|
||||
|
||||
// Determine Python home based on application structure
|
||||
// Python is bundled at different locations depending on platform and build type
|
||||
|
||||
// Strategy 1: Platform-specific bundled locations (highest priority)
|
||||
#ifdef __APPLE__
|
||||
// macOS app bundle: OrcaSlicer.app/Contents/MacOS/python
|
||||
// (resources_dir is Contents/Resources, so go up and into MacOS)
|
||||
fs::path bundle_python = fs::path(resources_dir()).parent_path() / "MacOS" / "python";
|
||||
if (valid_python_home(bundle_python)) {
|
||||
python_home = bundle_python.string();
|
||||
BOOST_LOG_TRIVIAL(info) << "Found Python in macOS app bundle: " << python_home;
|
||||
}
|
||||
#elif defined(_WIN32)
|
||||
fs::path exe_python = boost::dll::program_location().parent_path() / "python";
|
||||
if (valid_python_home(exe_python)) {
|
||||
python_home = exe_python.string();
|
||||
BOOST_LOG_TRIVIAL(info) << "Found Python next to Windows executable: " << python_home;
|
||||
}
|
||||
#else
|
||||
// Linux: typically in ../lib or ../share relative to binary
|
||||
fs::path linux_python = fs::path(resources_dir()).parent_path() / "lib" / "python";
|
||||
if (valid_python_home(linux_python)) {
|
||||
python_home = linux_python.string();
|
||||
BOOST_LOG_TRIVIAL(info) << "Found Python in Linux install: " << python_home;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Strategy 2: Configured development dependency directory.
|
||||
if (python_home.empty()) {
|
||||
fs::path configured_python = ORCA_BUNDLED_PYTHON_ROOT;
|
||||
if (!configured_python.empty() && valid_python_home(configured_python)) {
|
||||
python_home = configured_python.string();
|
||||
BOOST_LOG_TRIVIAL(info) << "Found Python in configured bundled path: " << python_home;
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: Development build directory from runtime environment.
|
||||
if (python_home.empty()) {
|
||||
const char* prefix_path = std::getenv("CMAKE_PREFIX_PATH");
|
||||
if (prefix_path && std::strlen(prefix_path) > 0) {
|
||||
fs::path libpython = fs::path(prefix_path) / "libpython";
|
||||
if (valid_python_home(libpython)) {
|
||||
python_home = libpython.string();
|
||||
BOOST_LOG_TRIVIAL(info) << "Found Python in CMAKE_PREFIX_PATH: " << python_home;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: Check resources directory (alternate bundling location)
|
||||
if (python_home.empty()) {
|
||||
fs::path res_python = fs::path(resources_dir()) / "python";
|
||||
if (valid_python_home(res_python)) {
|
||||
python_home = res_python.string();
|
||||
BOOST_LOG_TRIVIAL(info) << "Found Python in resources directory: " << python_home;
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 4: Check data_dir (user configuration directory)
|
||||
#ifndef _WIN32
|
||||
if (python_home.empty()) {
|
||||
fs::path data_python = fs::path(data_dir()) / "python";
|
||||
if (valid_python_home(data_python)) {
|
||||
python_home = data_python.string();
|
||||
BOOST_LOG_TRIVIAL(info) << "Found Python in data directory: " << python_home;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (python_home.empty()) {
|
||||
m_last_error = "Could not locate bundled Python installation";
|
||||
BOOST_LOG_TRIVIAL(error) << "Could not locate bundled Python installation";
|
||||
BOOST_LOG_TRIVIAL(error) << "Configured bundled Python root: " << ORCA_BUNDLED_PYTHON_ROOT;
|
||||
#ifdef _WIN32
|
||||
BOOST_LOG_TRIVIAL(error) << "Searched next to executable: "
|
||||
<< (boost::dll::program_location().parent_path() / "python").string();
|
||||
#endif
|
||||
BOOST_LOG_TRIVIAL(error) << "Searched in resources_dir: " << resources_dir();
|
||||
#ifndef _WIN32
|
||||
BOOST_LOG_TRIVIAL(error) << "Searched in data_dir: " << data_dir();
|
||||
#endif
|
||||
BOOST_LOG_TRIVIAL(error) << "CMAKE_PREFIX_PATH: "
|
||||
<< (std::getenv("CMAKE_PREFIX_PATH") ? std::getenv("CMAKE_PREFIX_PATH") : "not set");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify Python standard library directory exists
|
||||
#ifdef _WIN32
|
||||
fs::path python_lib = fs::path(python_home) / "Lib";
|
||||
#else
|
||||
fs::path python_lib = fs::path(python_home) / "lib" / PYTHON_STDLIB_DIR;
|
||||
#endif
|
||||
if (!fs::exists(python_lib)) {
|
||||
m_last_error = "Python standard library directory not found at: " + python_lib.string();
|
||||
BOOST_LOG_TRIVIAL(error) << "Python standard library directory not found at: " << python_lib.string();
|
||||
BOOST_LOG_TRIVIAL(error) << "Please build Python dependencies or check installation";
|
||||
return false;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Python standard library found at: " << python_lib.string();
|
||||
|
||||
#ifdef _WIN32
|
||||
fs::path python_dll = fs::exists(fs::path(python_home) / PYTHON_DLL) ? fs::path(python_home) / PYTHON_DLL :
|
||||
fs::path(python_home) / PYTHON_DEBUG_DLL;
|
||||
if (!fs::exists(python_dll)) {
|
||||
m_last_error = "Python DLL not found in: " + python_home;
|
||||
BOOST_LOG_TRIVIAL(error) << "Python DLL not found in: " << python_home;
|
||||
return false;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Python DLL found at: " << python_dll.string();
|
||||
if (!fs::exists(fs::path(python_home) / "DLLs")) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Python DLLs directory not found at: " << (fs::path(python_home) / "DLLs").string();
|
||||
}
|
||||
#endif
|
||||
|
||||
// Log the exact paths being used for debugging
|
||||
BOOST_LOG_TRIVIAL(info) << "Setting Python home to: " << python_home;
|
||||
BOOST_LOG_TRIVIAL(info) << "Python 3.12 stdlib path: " << python_lib.string();
|
||||
|
||||
// Verify encodings module exists
|
||||
fs::path encodings_path = python_lib / "encodings";
|
||||
if (fs::exists(encodings_path)) {
|
||||
BOOST_LOG_TRIVIAL(info) << "Encodings module found at: " << encodings_path.string();
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Encodings module NOT found at: " << encodings_path.string();
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Using Python 3.12 PyConfig initialization API";
|
||||
|
||||
// Set Python home - this is the prefix where Python libraries are located
|
||||
PyConfig config;
|
||||
PyConfig_InitPythonConfig(&config);
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug) << "Calling PyConfig_SetBytesString with home=" << python_home;
|
||||
PyStatus status = PyConfig_SetBytesString(&config, &config.home, python_home.c_str());
|
||||
if (PyStatus_Exception(status)) {
|
||||
m_last_error = status.err_msg ? status.err_msg : "Failed to set Python home";
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to set Python home to: " << python_home << ": " << m_last_error;
|
||||
PyConfig_Clear(&config);
|
||||
return false;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(debug) << "Python home set successfully";
|
||||
|
||||
// Set program name
|
||||
status = PyConfig_SetBytesString(&config, &config.program_name, "OrcaSlicer");
|
||||
if (PyStatus_Exception(status)) {
|
||||
m_last_error = status.err_msg ? status.err_msg : "Failed to set program name";
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to set program name: " << m_last_error;
|
||||
PyConfig_Clear(&config);
|
||||
return false;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(debug) << "Program name set successfully";
|
||||
|
||||
// Use pybind11's scoped_interpreter which properly initializes
|
||||
// pybind11 internals for multi-threaded use — raw Py_InitializeFromConfig
|
||||
// does not set up thread state tracking that pybind11 requires.
|
||||
BOOST_LOG_TRIVIAL(debug) << "Creating py::scoped_interpreter with PyConfig...";
|
||||
try {
|
||||
m_interpreter = std::make_unique<pybind11::scoped_interpreter>(&config);
|
||||
} catch (const std::exception& ex) {
|
||||
m_last_error = std::string("Python initialization failed: ") + ex.what();
|
||||
BOOST_LOG_TRIVIAL(error) << m_last_error;
|
||||
PyConfig_Clear(&config);
|
||||
return false;
|
||||
}
|
||||
// PyConfig is cleared by pybind11's initialize_interpreter() internally.
|
||||
BOOST_LOG_TRIVIAL(debug) << "py::scoped_interpreter initialized successfully";
|
||||
|
||||
if (!Py_IsInitialized()) {
|
||||
m_last_error = "Python interpreter not initialized";
|
||||
BOOST_LOG_TRIVIAL(error) << "Python interpreter not initialized";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Log Python paths for debugging
|
||||
PyObject* sys = PyImport_ImportModule("sys");
|
||||
if (sys) {
|
||||
PyObject* path = PyObject_GetAttrString(sys, "path");
|
||||
if (path) {
|
||||
PyObject* path_str = PyObject_Str(path);
|
||||
if (path_str) {
|
||||
const char* path_cstr = PyUnicode_AsUTF8(path_str);
|
||||
if (path_cstr) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "Python sys.path: " << path_cstr;
|
||||
}
|
||||
Py_DECREF(path_str);
|
||||
}
|
||||
Py_DECREF(path);
|
||||
}
|
||||
Py_DECREF(sys);
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Python " << Py_GetVersion() << " initialized successfully";
|
||||
|
||||
const fs::path shared_packages = shared_packages_dir();
|
||||
boost::system::error_code ec;
|
||||
fs::create_directories(shared_packages, ec);
|
||||
if (ec) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Failed to create Python shared package directory: " << shared_packages.string() << ": "
|
||||
<< ec.message();
|
||||
} else {
|
||||
std::string path_error;
|
||||
if (add_sys_path_entry(shared_packages, path_error))
|
||||
BOOST_LOG_TRIVIAL(info) << "Added Python shared package directory to sys.path: " << shared_packages.string();
|
||||
else
|
||||
BOOST_LOG_TRIVIAL(warning) << path_error;
|
||||
}
|
||||
|
||||
const std::string uv_path = bundled_uv_path();
|
||||
if (!uv_path.empty())
|
||||
BOOST_LOG_TRIVIAL(info) << "Bundled uv executable found at: " << uv_path;
|
||||
else
|
||||
BOOST_LOG_TRIVIAL(info) << "Bundled uv executable not found";
|
||||
|
||||
// Install the CPython audit hook for plugin policy enforcement.
|
||||
// This is defense-in-depth: it monitors file/subprocess/socket/ctypes
|
||||
// access from plugin code. It is NOT a full security sandbox.
|
||||
PluginAuditManager::instance().install_hook();
|
||||
|
||||
// Persist Python stderr (plugin tracebacks, including uncaught
|
||||
// background-thread exceptions) to <data_dir>/log/python_*.log.
|
||||
install_python_stderr_redirect();
|
||||
|
||||
m_initialized = true;
|
||||
|
||||
// Release the GIL so other threads can acquire it via PyGILState_Ensure.
|
||||
// Without this, calls from background threads will block trying to acquire the GIL.
|
||||
m_main_thread_state = PyEval_SaveThread();
|
||||
BOOST_LOG_TRIVIAL(debug) << "Main thread released Python GIL after initialization";
|
||||
return true;
|
||||
|
||||
} catch (const std::exception& ex) {
|
||||
m_last_error = std::string("Exception initializing Python: ") + ex.what();
|
||||
BOOST_LOG_TRIVIAL(error) << "Exception initializing Python: " << ex.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
PythonInterpreter::~PythonInterpreter() { shutdown(); }
|
||||
|
||||
void PythonInterpreter::shutdown()
|
||||
{
|
||||
if (!m_initialized)
|
||||
return;
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Python interpreter shutdown enter";
|
||||
|
||||
// Reacquire the GIL using the saved thread state before finalizing.
|
||||
if (m_main_thread_state) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "Restoring Python main thread state before shutdown";
|
||||
PyEval_RestoreThread(m_main_thread_state);
|
||||
m_main_thread_state = nullptr;
|
||||
}
|
||||
|
||||
log_and_restore_python_stdio();
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Finalizing Python interpreter";
|
||||
m_interpreter.reset();
|
||||
BOOST_LOG_TRIVIAL(info) << "Python interpreter finalized";
|
||||
|
||||
m_initialized = false;
|
||||
}
|
||||
|
||||
bool PythonInterpreter::add_sys_path(const std::string& path, std::string& error)
|
||||
{
|
||||
if (!m_initialized) {
|
||||
error = "Python interpreter not initialized";
|
||||
return false;
|
||||
}
|
||||
|
||||
PythonGILState gil;
|
||||
|
||||
PyObject* sys_path = PySys_GetObject("path");
|
||||
if (!sys_path || !PyList_Check(sys_path)) {
|
||||
error = "Python sys.path is not available";
|
||||
return false;
|
||||
}
|
||||
|
||||
PyObjectPtr py_path(PyUnicode_DecodeFSDefault(path.c_str()));
|
||||
if (!py_path) {
|
||||
error = "Failed to decode path for Python sys.path: " + path;
|
||||
PyErr_Clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
const int contains = PySequence_Contains(sys_path, py_path.get());
|
||||
if (contains == 1)
|
||||
return true;
|
||||
if (contains < 0)
|
||||
PyErr_Clear();
|
||||
|
||||
if (PyList_Insert(sys_path, 0, py_path.get()) != 0) {
|
||||
error = "Failed to append path to Python sys.path: " + path;
|
||||
PyErr_Clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PythonInterpreter::execute_string(const std::string& code, std::string& error)
|
||||
{
|
||||
if (!m_initialized) {
|
||||
error = "Python interpreter not initialized";
|
||||
return false;
|
||||
}
|
||||
|
||||
PythonGILState gil;
|
||||
|
||||
PyObject* main_module = PyImport_AddModule("__main__");
|
||||
if (!main_module) {
|
||||
error = "Failed to get __main__ module";
|
||||
return false;
|
||||
}
|
||||
|
||||
PyObject* global_dict = PyModule_GetDict(main_module);
|
||||
PyObjectPtr result(PyRun_String(code.c_str(), Py_file_input, global_dict, global_dict));
|
||||
|
||||
if (!result) {
|
||||
PyObject *ptype, *pvalue, *ptraceback;
|
||||
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
|
||||
error = format_python_error(ptype, pvalue, ptraceback);
|
||||
Py_XDECREF(ptype);
|
||||
Py_XDECREF(pvalue);
|
||||
Py_XDECREF(ptraceback);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
PyObject* PythonInterpreter::load_module_from_file(const std::string& file_path, std::string& error)
|
||||
{
|
||||
if (!m_initialized) {
|
||||
error = "Python interpreter not initialized";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
fs::path path(file_path);
|
||||
|
||||
if (!fs::exists(path)) {
|
||||
error = "File does not exist: " + file_path;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PythonGILState gil;
|
||||
|
||||
// Add the directory to sys.path
|
||||
fs::path dir_path = path.parent_path();
|
||||
std::string module_name = path.stem().string();
|
||||
|
||||
PyObjectPtr sys(PyImport_ImportModule("sys"));
|
||||
if (!sys) {
|
||||
error = "Failed to import sys module";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyObject* sys_path = PyObject_GetAttrString(sys.get(), "path");
|
||||
if (!sys_path) {
|
||||
error = "Failed to get sys.path";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyObjectPtr dir_str(PyUnicode_FromString(dir_path.string().c_str()));
|
||||
if (!dir_str) {
|
||||
Py_DECREF(sys_path);
|
||||
error = "Failed to create directory string";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (PyList_Insert(sys_path, 0, dir_str.get()) < 0) {
|
||||
Py_DECREF(sys_path);
|
||||
error = "Failed to add directory to sys.path";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Py_DECREF(sys_path);
|
||||
|
||||
// Ensure module is re-imported fresh by removing any cached instance.
|
||||
if (PyObject* modules = PyImport_GetModuleDict()) {
|
||||
if (PyDict_GetItemString(modules, module_name.c_str())) {
|
||||
if (PyDict_DelItemString(modules, module_name.c_str()) != 0) {
|
||||
PyErr_Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Import the module
|
||||
PyObject* module = PyImport_ImportModule(module_name.c_str());
|
||||
if (!module) {
|
||||
PyObject *ptype, *pvalue, *ptraceback;
|
||||
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
|
||||
error = "Failed to import module: " + format_python_error(ptype, pvalue, ptraceback);
|
||||
Py_XDECREF(ptype);
|
||||
Py_XDECREF(pvalue);
|
||||
Py_XDECREF(ptraceback);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
PyObject* PythonInterpreter::load_module_from_directory(const std::string& dir_path, const std::string& pkg_name, std::string& error)
|
||||
{
|
||||
if (!m_initialized) {
|
||||
error = "Python interpreter not initialized";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
fs::path dir(dir_path);
|
||||
|
||||
if (!fs::exists(dir) || !fs::is_directory(dir)) {
|
||||
error = "Directory does not exist: " + dir_path;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PythonGILState gil;
|
||||
|
||||
PyObjectPtr sys(PyImport_ImportModule("sys"));
|
||||
if (!sys) {
|
||||
error = "Failed to import sys module";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyObject* sys_path = PyObject_GetAttrString(sys.get(), "path");
|
||||
if (!sys_path) {
|
||||
error = "Failed to get sys.path";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyObjectPtr dir_str(PyUnicode_FromString(dir.string().c_str()));
|
||||
if (!dir_str) {
|
||||
Py_DECREF(sys_path);
|
||||
error = "Failed to create directory string";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (PyList_Insert(sys_path, 0, dir_str.get()) < 0) {
|
||||
Py_DECREF(sys_path);
|
||||
error = "Failed to add directory to sys.path";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Py_DECREF(sys_path);
|
||||
|
||||
if (PyObject* modules = PyImport_GetModuleDict()) {
|
||||
if (PyDict_GetItemString(modules, pkg_name.c_str())) {
|
||||
if (PyDict_DelItemString(modules, pkg_name.c_str()) != 0) {
|
||||
PyErr_Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PyObject* module = PyImport_ImportModule(pkg_name.c_str());
|
||||
if (!module) {
|
||||
PyObject *ptype, *pvalue, *ptraceback;
|
||||
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
|
||||
error = "Failed to import module: " + format_python_error(ptype, pvalue, ptraceback);
|
||||
Py_XDECREF(ptype);
|
||||
Py_XDECREF(pvalue);
|
||||
Py_XDECREF(ptraceback);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
PyObject* PythonInterpreter::load_module_from_whl(const std::string& file_path, const std::string& pkg_name, std::string& error)
|
||||
{
|
||||
if (!m_initialized) {
|
||||
error = "Python interpreter not initialized";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
fs::path whl_path(file_path);
|
||||
|
||||
if (!fs::exists(whl_path)) {
|
||||
error = "File does not exist: " + file_path;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
fs::path extract_dir = whl_path.parent_path() / "__whl_extracted__" / pkg_name;
|
||||
|
||||
if (!fs::exists(extract_dir)) {
|
||||
if (!extract_zip_to_directory(whl_path, extract_dir, error))
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return load_module_from_directory(extract_dir.string(), pkg_name, error);
|
||||
}
|
||||
|
||||
bool PythonInterpreter::call_function(
|
||||
PyObject* module, const std::string& function_name, const std::string& arg, std::string& result, std::string& error)
|
||||
{
|
||||
if (!m_initialized || !module) {
|
||||
error = "Python interpreter not initialized or module is null";
|
||||
return false;
|
||||
}
|
||||
|
||||
PythonGILState gil;
|
||||
|
||||
PyObject* func = PyObject_GetAttrString(module, function_name.c_str());
|
||||
if (!func || !PyCallable_Check(func)) {
|
||||
Py_XDECREF(func);
|
||||
error = "Function '" + function_name + "' not found or not callable";
|
||||
return false;
|
||||
}
|
||||
|
||||
PyObjectPtr args(PyTuple_New(1));
|
||||
PyObjectPtr arg_str(PyUnicode_FromString(arg.c_str()));
|
||||
PyTuple_SetItem(args.get(), 0, arg_str.release());
|
||||
|
||||
PyObjectPtr py_result(PyObject_CallObject(func, args.get()));
|
||||
Py_DECREF(func);
|
||||
|
||||
if (!py_result) {
|
||||
PyObject *ptype, *pvalue, *ptraceback;
|
||||
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
|
||||
error = "Function call failed: " + format_python_error(ptype, pvalue, ptraceback);
|
||||
Py_XDECREF(ptype);
|
||||
Py_XDECREF(pvalue);
|
||||
Py_XDECREF(ptraceback);
|
||||
return false;
|
||||
}
|
||||
|
||||
result = py_object_to_string(py_result.get());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PythonInterpreter::call_function_no_args(PyObject* module, const std::string& function_name, std::string& result, std::string& error)
|
||||
{
|
||||
if (!m_initialized || !module) {
|
||||
error = "Python interpreter not initialized or module is null";
|
||||
return false;
|
||||
}
|
||||
|
||||
PythonGILState gil;
|
||||
|
||||
PyObject* func = PyObject_GetAttrString(module, function_name.c_str());
|
||||
if (!func || !PyCallable_Check(func)) {
|
||||
Py_XDECREF(func);
|
||||
error = "Function '" + function_name + "' not found or not callable";
|
||||
return false;
|
||||
}
|
||||
|
||||
PyObjectPtr py_result(PyObject_CallObject(func, nullptr));
|
||||
Py_DECREF(func);
|
||||
|
||||
if (!py_result) {
|
||||
PyObject *ptype, *pvalue, *ptraceback;
|
||||
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
|
||||
error = "Function call failed: " + format_python_error(ptype, pvalue, ptraceback);
|
||||
Py_XDECREF(ptype);
|
||||
Py_XDECREF(pvalue);
|
||||
Py_XDECREF(ptraceback);
|
||||
return false;
|
||||
}
|
||||
|
||||
result = py_object_to_string(py_result.get());
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string PythonInterpreter::py_object_to_string(PyObject* obj)
|
||||
{
|
||||
if (!obj) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (PyUnicode_Check(obj)) {
|
||||
const char* str = PyUnicode_AsUTF8(obj);
|
||||
return str ? std::string(str) : "";
|
||||
}
|
||||
|
||||
PyObjectPtr str_obj(PyObject_Str(obj));
|
||||
if (str_obj) {
|
||||
const char* str = PyUnicode_AsUTF8(str_obj.get());
|
||||
return str ? std::string(str) : "";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
148
src/slic3r/plugin/PythonInterpreter.hpp
Normal file
148
src/slic3r/plugin/PythonInterpreter.hpp
Normal file
@@ -0,0 +1,148 @@
|
||||
#ifndef slic3r_PythonInterpreter_hpp_
|
||||
#define slic3r_PythonInterpreter_hpp_
|
||||
|
||||
#include <Python.h>
|
||||
#include <pytypedefs.h>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
namespace pybind11 {
|
||||
class scoped_interpreter;
|
||||
class error_already_set;
|
||||
}
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Print a Python exception's full traceback to sys.stderr (tee'd to the session
|
||||
// log) WITHOUT consuming err.
|
||||
//
|
||||
// Used by the ORCA_PY_OVERRIDE_AUDITED trampoline macro: the
|
||||
// traceback is logged centrally at the C++<->Python boundary, then the original
|
||||
// error_already_set is rethrown intact so downstream C++ catchers can still build
|
||||
// the user-facing dialog from err.what(). Because it is non-destructive it does
|
||||
// NOT call restore()/PyErr_Print() (those will empty err); it prints via Python's
|
||||
// traceback module instead.
|
||||
void log_python_exception_keep(pybind11::error_already_set& err);
|
||||
|
||||
|
||||
// RAII wrapper for Python interpreter initialization/finalization
|
||||
class PythonInterpreter
|
||||
{
|
||||
public:
|
||||
static PythonInterpreter& instance();
|
||||
|
||||
// Initialize the Python interpreter
|
||||
bool initialize();
|
||||
|
||||
// Check if interpreter is initialized
|
||||
bool is_initialized() const { return m_initialized; }
|
||||
|
||||
const std::string& last_error() const { return m_last_error; }
|
||||
|
||||
// Shared user-writable package directory added to sys.path for plugins.
|
||||
static std::string shared_packages_dir();
|
||||
|
||||
// Bundled Python executable path, or empty when no executable is found.
|
||||
static std::string bundled_python_executable();
|
||||
|
||||
// Bundled uv executable path, or empty when uv is not bundled/found.
|
||||
static std::string bundled_uv_path();
|
||||
|
||||
// Python ABI tag for the bundled interpreter, e.g. "cp312".
|
||||
static std::string python_abi_tag();
|
||||
|
||||
// Finalize the Python interpreter.
|
||||
void shutdown();
|
||||
|
||||
// Add a filesystem path to sys.path if not already present.
|
||||
bool add_sys_path(const std::string& path, std::string& error);
|
||||
|
||||
// Execute a Python string and return result
|
||||
bool execute_string(const std::string& code, std::string& error);
|
||||
|
||||
// Load a Python module from file path
|
||||
PyObject* load_module_from_file(const std::string& file_path, std::string& error);
|
||||
PyObject* load_module_from_whl(const std::string& whl_path, const std::string& pkg_name, std::string& error);
|
||||
PyObject* load_module_from_directory(const std::string& dir_path, const std::string& pkg_name, std::string& error);
|
||||
|
||||
// Call a Python function with string argument, return string result
|
||||
bool call_function(PyObject* module, const std::string& function_name,
|
||||
const std::string& arg, std::string& result, std::string& error);
|
||||
|
||||
// Call a Python function with no arguments, return string result
|
||||
bool call_function_no_args(PyObject* module, const std::string& function_name,
|
||||
std::string& result, std::string& error);
|
||||
|
||||
// Helper to get string from Python object
|
||||
static std::string py_object_to_string(PyObject* obj);
|
||||
|
||||
// Destructor finalizes Python if shutdown() was not called explicitly.
|
||||
~PythonInterpreter();
|
||||
|
||||
private:
|
||||
PythonInterpreter() = default;
|
||||
PythonInterpreter(const PythonInterpreter&) = delete;
|
||||
PythonInterpreter& operator=(const PythonInterpreter&) = delete;
|
||||
|
||||
bool m_initialized = false;
|
||||
PyThreadState* m_main_thread_state = nullptr; // thread state saved after releasing GIL post-initialize
|
||||
std::unique_ptr<pybind11::scoped_interpreter> m_interpreter;
|
||||
std::string m_last_error;
|
||||
};
|
||||
|
||||
// RAII helper for Python GIL (Global Interpreter Lock)
|
||||
class PythonGILState
|
||||
{
|
||||
public:
|
||||
PythonGILState() {
|
||||
m_state = PyGILState_Ensure();
|
||||
}
|
||||
|
||||
~PythonGILState() {
|
||||
PyGILState_Release(m_state);
|
||||
}
|
||||
|
||||
private:
|
||||
PyGILState_STATE m_state;
|
||||
};
|
||||
|
||||
// RAII helper for Python object references
|
||||
class PyObjectPtr
|
||||
{
|
||||
public:
|
||||
explicit PyObjectPtr(PyObject* obj = nullptr) : m_obj(obj) {}
|
||||
|
||||
~PyObjectPtr() {
|
||||
if (m_obj) {
|
||||
Py_DECREF(m_obj);
|
||||
}
|
||||
}
|
||||
|
||||
PyObject* get() const { return m_obj; }
|
||||
PyObject* release() {
|
||||
PyObject* temp = m_obj;
|
||||
m_obj = nullptr;
|
||||
return temp;
|
||||
}
|
||||
|
||||
void reset(PyObject* obj = nullptr) {
|
||||
if (m_obj) {
|
||||
Py_DECREF(m_obj);
|
||||
}
|
||||
m_obj = obj;
|
||||
}
|
||||
|
||||
operator bool() const { return m_obj != nullptr; }
|
||||
|
||||
private:
|
||||
PyObject* m_obj;
|
||||
|
||||
PyObjectPtr(const PyObjectPtr&) = delete;
|
||||
PyObjectPtr& operator=(const PyObjectPtr&) = delete;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif /* slic3r_PythonInterpreter_hpp_ */
|
||||
398
src/slic3r/plugin/PythonPluginBridge.cpp
Normal file
398
src/slic3r/plugin/PythonPluginBridge.cpp
Normal file
@@ -0,0 +1,398 @@
|
||||
#include "PythonPluginBridge.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <pybind11/embed.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
#include "PythonInterpreter.hpp"
|
||||
#include "PluginHostApi.hpp"
|
||||
#include "PyPluginPackage.hpp"
|
||||
#include "PyPluginTrampoline.hpp"
|
||||
#include "pluginTypes/gcode/GCodePluginCapability.hpp"
|
||||
#include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp"
|
||||
#include "pluginTypes/script/ScriptPluginCapability.hpp"
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
|
||||
// Python plugin discovery is a two-step capture:
|
||||
// 1) PluginLoader sets an active plugin key and imports the Python module.
|
||||
// 2) Python decorators/API calls enter these pybind callbacks without receiving the
|
||||
// C++ PluginDescriptor, so the callbacks use the active key to attach Python classes
|
||||
// to the plugin currently being loaded.
|
||||
//
|
||||
// The pending maps hold Python class objects, not plugin instances. Instances are created
|
||||
// only after the package class has had a chance to register every capability.
|
||||
thread_local std::string g_active_plugin_key;
|
||||
std::mutex g_registry_mutex;
|
||||
std::unordered_map<std::string, std::vector<py::object>> g_pending_capabilities;
|
||||
std::unordered_map<std::string, py::object> g_pending_package;
|
||||
struct PluginInstanceHandle
|
||||
{
|
||||
// The C++ plugin interface points into a Python object. Keep both alive through one
|
||||
// shared control block; CapturedCapability later exposes an aliasing shared_ptr to plugin.
|
||||
std::shared_ptr<PluginCapabilityInterface> plugin;
|
||||
py::object keep_alive;
|
||||
|
||||
~PluginInstanceHandle()
|
||||
{
|
||||
if (keep_alive) {
|
||||
if (Py_IsInitialized()) {
|
||||
// Dropping a py::object decrefs the Python object, so reacquire the GIL.
|
||||
PythonGILState gil;
|
||||
keep_alive = py::object();
|
||||
} else {
|
||||
// During interpreter shutdown it is no longer safe to decref Python objects.
|
||||
// release() forgets the wrapper ownership without touching Python runtime state.
|
||||
(void) keep_alive.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
PythonPluginBridge& PythonPluginBridge::instance()
|
||||
{
|
||||
static PythonPluginBridge bridge;
|
||||
return bridge;
|
||||
}
|
||||
|
||||
void PythonPluginBridge::begin_plugin_capture(const std::string& plugin_key)
|
||||
{
|
||||
PythonGILState gil;
|
||||
BOOST_LOG_TRIVIAL(info) << "Beginning Python plugin capture for key " << plugin_key;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
// Start from a clean slot in case a previous failed load left pending Python classes
|
||||
// for this same entry path.
|
||||
g_pending_capabilities.erase(plugin_key);
|
||||
g_pending_package.erase(plugin_key);
|
||||
}
|
||||
// From now until finalize/cancel, @orca.plugin and register_capability() calls made by
|
||||
// Python code on this thread are attributed to this plugin.
|
||||
g_active_plugin_key = plugin_key;
|
||||
}
|
||||
|
||||
std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(const std::string& plugin_key, std::string& error)
|
||||
{
|
||||
PythonGILState gil;
|
||||
BOOST_LOG_TRIVIAL(info) << "Finalizing Python plugin capture for key " << plugin_key;
|
||||
|
||||
// Phase 1: run the package class's register_capabilities() while the active key is
|
||||
// still set. That method is expected to call orca.register_capability() once per
|
||||
// capability class, and register_capability() needs g_active_plugin_key to know which
|
||||
// pending bucket to append to.
|
||||
{
|
||||
auto clear_active_key = [&plugin_key]() {
|
||||
if (g_active_plugin_key == plugin_key)
|
||||
g_active_plugin_key.clear();
|
||||
};
|
||||
auto discard_pending_for_key = [&plugin_key]() {
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
g_pending_capabilities.erase(plugin_key);
|
||||
g_pending_package.erase(plugin_key);
|
||||
};
|
||||
|
||||
try {
|
||||
// The @orca.plugin decorator records the package class during module import.
|
||||
// Move it into a local py::object and remove it from the pending map so the
|
||||
// registry no longer owns it once finalization starts.
|
||||
py::object package_cls;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
auto it = g_pending_package.find(plugin_key);
|
||||
if (it != g_pending_package.end()) {
|
||||
package_cls = it->second;
|
||||
g_pending_package.erase(it);
|
||||
}
|
||||
}
|
||||
if (!package_cls) {
|
||||
error = "Plugin did not register a package class; decorate it with @orca.plugin";
|
||||
BOOST_LOG_TRIVIAL(error) << error << " for key " << plugin_key;
|
||||
discard_pending_for_key();
|
||||
clear_active_key();
|
||||
return {};
|
||||
}
|
||||
|
||||
// The package instance is only a registration coordinator. It is not returned
|
||||
// to the rest of the plugin system; only the capability classes it registers
|
||||
// are kept.
|
||||
py::object package = package_cls();
|
||||
package.attr("register_capabilities")();
|
||||
} catch (py::error_already_set& err) {
|
||||
log_python_exception_keep(err);
|
||||
error = err.what();
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin register_capabilities raised Python exception for key " << plugin_key
|
||||
<< " error=" << error;
|
||||
discard_pending_for_key();
|
||||
clear_active_key();
|
||||
return {};
|
||||
} catch (const std::exception& ex) {
|
||||
error = ex.what();
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin register_capabilities raised exception for key " << plugin_key
|
||||
<< " error=" << error;
|
||||
discard_pending_for_key();
|
||||
clear_active_key();
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: move the capability classes that register_capabilities() appended into a
|
||||
// local vector. From this point the pending registry no longer owns these py::objects.
|
||||
std::vector<py::object> classes;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
auto it = g_pending_capabilities.find(plugin_key);
|
||||
if (it != g_pending_capabilities.end()) {
|
||||
classes = std::move(it->second);
|
||||
g_pending_capabilities.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
// Registration is complete. Later register_capability() calls should fail instead of
|
||||
// accidentally attaching themselves to this plugin.
|
||||
if (g_active_plugin_key == plugin_key)
|
||||
g_active_plugin_key.clear();
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Collected " << classes.size() << " registered capability class(es) for key " << plugin_key;
|
||||
|
||||
std::vector<CapturedCapability> capabilities;
|
||||
capabilities.reserve(classes.size());
|
||||
|
||||
// Phase 3: instantiate each registered capability class and convert it to the common
|
||||
// C++ interface used by the rest of OrcaSlicer.
|
||||
for (auto& cls : classes) {
|
||||
try {
|
||||
py::object instance = cls();
|
||||
if (!py::isinstance<PluginCapabilityInterface>(instance)) {
|
||||
error = "Registered capability must inherit from a PluginCapability base";
|
||||
BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed type check for key " << plugin_key
|
||||
<< " error=" << error;
|
||||
return {};
|
||||
}
|
||||
|
||||
auto capability_iface = instance.cast<std::shared_ptr<PluginCapabilityInterface>>();
|
||||
if (!capability_iface) {
|
||||
error = "Failed to cast Python capability to PluginCapabilityInterface";
|
||||
BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed cast for key " << plugin_key
|
||||
<< " error=" << error;
|
||||
return {};
|
||||
}
|
||||
|
||||
// This is a registered capability, not the transient orca.base package.
|
||||
// get_name() is required on capabilities and is cached for preset lookup.
|
||||
std::string name = capability_iface->get_name();
|
||||
|
||||
// Capability names feed ';'-delimited config/preset serialization and drive
|
||||
// dispatch, so unlike display names they cannot be silently rewritten — a ';'
|
||||
// here is a hard error that rejects the whole plugin capture.
|
||||
if (name.find(';') != std::string::npos) {
|
||||
error = "Capability name must not contain ';': " + name;
|
||||
BOOST_LOG_TRIVIAL(error) << "Python plugin capture rejected capability for key " << plugin_key
|
||||
<< " error=" << error;
|
||||
return {};
|
||||
}
|
||||
|
||||
auto handle = std::make_shared<PluginInstanceHandle>();
|
||||
handle->keep_alive = instance;
|
||||
handle->plugin = std::move(capability_iface);
|
||||
|
||||
CapturedCapability captured;
|
||||
// Return a shared_ptr<PluginCapabilityInterface> while keeping PluginInstanceHandle
|
||||
// as the owner, so the Python instance stays alive as long as the C++ interface does.
|
||||
captured.instance = std::shared_ptr<PluginCapabilityInterface>(handle, handle->plugin.get());
|
||||
captured.name = std::move(name);
|
||||
capabilities.emplace_back(std::move(captured));
|
||||
} catch (py::error_already_set& err) {
|
||||
// Direct Python call (cls() / get_name() above), not a trampoline override —
|
||||
// log the traceback here. GIL is held for the duration of finalize_plugin_capture.
|
||||
log_python_exception_keep(err);
|
||||
error = err.what();
|
||||
BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised Python exception for key " << plugin_key
|
||||
<< " error=" << error;
|
||||
return {};
|
||||
} catch (const std::exception& ex) {
|
||||
error = ex.what();
|
||||
BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised exception for key " << plugin_key
|
||||
<< " error=" << error;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Instantiated " << capabilities.size() << " Python capability instance(s) for key " << plugin_key;
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
void PythonPluginBridge::cancel_plugin_capture(const std::string& plugin_key)
|
||||
{
|
||||
PythonGILState gil;
|
||||
BOOST_LOG_TRIVIAL(warning) << "Cancelling Python plugin capture for key " << plugin_key;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
// Import or dependency setup failed before finalization. Drop anything the module
|
||||
// may already have registered under this key.
|
||||
g_pending_capabilities.erase(plugin_key);
|
||||
g_pending_package.erase(plugin_key);
|
||||
}
|
||||
|
||||
if (g_active_plugin_key == plugin_key)
|
||||
g_active_plugin_key.clear();
|
||||
}
|
||||
|
||||
void PythonPluginBridge::clear_pending_captures()
|
||||
{
|
||||
if (!Py_IsInitialized()) {
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size()
|
||||
<< " pending Python plugin capture(s) without Python interpreter";
|
||||
// py::object destruction would decref Python objects. If the interpreter is already
|
||||
// gone, release the wrappers instead and intentionally skip decref.
|
||||
for (auto& [plugin_key, plugins] : g_pending_capabilities) {
|
||||
(void) plugin_key;
|
||||
for (py::object& plugin : plugins)
|
||||
(void) plugin.release();
|
||||
}
|
||||
g_pending_capabilities.clear();
|
||||
for (auto& [plugin_key, pkg] : g_pending_package) {
|
||||
(void) plugin_key;
|
||||
(void) pkg.release();
|
||||
}
|
||||
g_pending_package.clear();
|
||||
g_active_plugin_key.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal shutdown path: hold the GIL and let py::object destructors decref cleanly.
|
||||
PythonGILState gil;
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size() << " pending Python plugin capture(s)";
|
||||
g_pending_capabilities.clear();
|
||||
g_pending_package.clear();
|
||||
g_active_plugin_key.clear();
|
||||
}
|
||||
|
||||
void bind_python_api(pybind11::module_& m)
|
||||
{
|
||||
m.doc() = "OrcaSlicer plugin API";
|
||||
|
||||
auto pluginTypes = py::enum_<PluginCapabilityType>(m, "PluginType", "Available plugin capability groups")
|
||||
.value("PostProcessing", PluginCapabilityType::PostProcessing)
|
||||
.value("PrinterConnection", PluginCapabilityType::PrinterConnection)
|
||||
.value("Automation", PluginCapabilityType::Automation)
|
||||
.value("Analysis", PluginCapabilityType::Analysis)
|
||||
.value("Importer", PluginCapabilityType::Importer)
|
||||
.value("Exporter", PluginCapabilityType::Exporter)
|
||||
.value("Visualization", PluginCapabilityType::Visualization)
|
||||
.value("Script", PluginCapabilityType::Script)
|
||||
.value("Unknown", PluginCapabilityType::Unknown)
|
||||
.export_values();
|
||||
|
||||
py::enum_<PluginResult>(m, "PluginResult", "Execution summary code")
|
||||
.value("Success", PluginResult::Success)
|
||||
.value("Skipped", PluginResult::Skipped)
|
||||
.value("RecoverableError", PluginResult::RecoverableError)
|
||||
.value("FatalError", PluginResult::FatalError)
|
||||
.export_values();
|
||||
|
||||
py::class_<PluginContext>(m, "PluginContext", "Context shared with plugin entry points")
|
||||
.def(py::init<>())
|
||||
.def_readwrite("orca_version", &PluginContext::orca_version);
|
||||
|
||||
py::class_<ExecutionResult>(m, "ExecutionResult", "Structured execution outcome")
|
||||
.def(py::init<>())
|
||||
.def(py::init<PluginResult, std::string, std::string>())
|
||||
.def_readwrite("status", &ExecutionResult::status)
|
||||
.def_readwrite("message", &ExecutionResult::message)
|
||||
.def_readwrite("data", &ExecutionResult::data)
|
||||
.def_static("success", &ExecutionResult::success, py::arg("message") = std::string(), py::arg("data") = std::string())
|
||||
.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());
|
||||
|
||||
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);
|
||||
|
||||
// Expose the package marker base as orca.base. @orca.plugin later verifies that the
|
||||
// decorated class derives from this exact pybind-registered C++ type.
|
||||
py::class_<PyPluginPackage, PyPluginPackageTrampoline>(m, "base")
|
||||
.def(py::init<>())
|
||||
.def("register_capabilities", &PyPluginPackage::register_capabilities);
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug) << "Registering embedded Python plugin type bindings";
|
||||
|
||||
// Make sure you register your bindings here
|
||||
GCodePluginCapability::RegisterBindings(m, pluginTypes);
|
||||
PrinterAgentPluginCapability::RegisterBindings(m, pluginTypes);
|
||||
ScriptPluginCapability::RegisterBindings(m, pluginTypes);
|
||||
PluginHostApi::RegisterBindings(m);
|
||||
BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings";
|
||||
|
||||
m.def(
|
||||
"register_capability",
|
||||
[](py::object plugin_cls) {
|
||||
if (g_active_plugin_key.empty()) {
|
||||
throw py::value_error("register_capability() called outside plugin discovery context");
|
||||
}
|
||||
|
||||
// Store capability classes only, not instances. Finalization instantiates them
|
||||
// after the package has registered the full set for this plugin.
|
||||
py::handle base = py::type::of<PluginCapabilityInterface>();
|
||||
const int is_subclass = PyObject_IsSubclass(plugin_cls.ptr(), base.ptr());
|
||||
if (is_subclass != 1) {
|
||||
if (is_subclass < 0)
|
||||
PyErr_Clear();
|
||||
throw py::value_error("Registered class must inherit from a PluginCapability base");
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
g_pending_capabilities[g_active_plugin_key].push_back(std::move(plugin_cls));
|
||||
BOOST_LOG_TRIVIAL(debug) << "Registered Python plugin capability class for key " << g_active_plugin_key;
|
||||
},
|
||||
R"pbdoc(Register a PluginCapability subclass while OrcaSlicer loads your module.)pbdoc");
|
||||
|
||||
m.def("plugin", [](py::object cls) {
|
||||
if (g_active_plugin_key.empty())
|
||||
throw py::value_error("@orca.plugin used outside plugin discovery context");
|
||||
if (!PyType_Check(cls.ptr()))
|
||||
throw py::value_error("@orca.plugin must decorate a class");
|
||||
// The decorator is only a marker/capture hook. It records the package class now;
|
||||
// finalize_plugin_capture() instantiates it later and calls register_capabilities().
|
||||
py::handle base = py::type::of<PyPluginPackage>();
|
||||
const int is_subclass = PyObject_IsSubclass(cls.ptr(), base.ptr());
|
||||
if (is_subclass != 1) {
|
||||
if (is_subclass < 0)
|
||||
PyErr_Clear();
|
||||
throw py::value_error("@orca.plugin must decorate a subclass of orca.base");
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
auto& slot = g_pending_package[g_active_plugin_key];
|
||||
if (slot)
|
||||
throw py::value_error("multiple @orca.plugin classes registered; exactly one is allowed per plugin");
|
||||
slot = cls;
|
||||
}
|
||||
return cls; // decorator returns the class unchanged
|
||||
}, R"pbdoc(Mark the single plugin package class (the orca.base subclass) for this file.)pbdoc");
|
||||
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#ifdef ORCA_PYTHON_STUBGEN_MODULE
|
||||
PYBIND11_MODULE(orca, m) { Slic3r::bind_python_api(m); }
|
||||
#else
|
||||
PYBIND11_EMBEDDED_MODULE(orca, m) { Slic3r::bind_python_api(m); }
|
||||
#endif
|
||||
50
src/slic3r/plugin/PythonPluginBridge.hpp
Normal file
50
src/slic3r/plugin/PythonPluginBridge.hpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#ifndef slic3r_PythonPluginBridge_hpp_
|
||||
#define slic3r_PythonPluginBridge_hpp_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "PythonPluginInterface.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// One materialized capability returned from finalize_plugin_capture: the C++ instance
|
||||
// and its resolved get_name() (cached while the GIL was held).
|
||||
struct CapturedCapability
|
||||
{
|
||||
std::shared_ptr<PluginCapabilityInterface> instance;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
class PythonPluginBridge
|
||||
{
|
||||
public:
|
||||
static PythonPluginBridge& instance();
|
||||
|
||||
// Mark the beginning of a plugin registration capture for the provided key (usually file path).
|
||||
void begin_plugin_capture(const std::string& plugin_key);
|
||||
|
||||
// Finalize capture: the plugin class was recorded by the @orca.plugin decorator during
|
||||
// import; run register_capabilities() (which registers each capability class), then
|
||||
// instantiate every registered capability and cache its get_name().
|
||||
// Returns one CapturedCapability per capability, or an empty vector on failure
|
||||
// (error message populated).
|
||||
std::vector<CapturedCapability> finalize_plugin_capture(
|
||||
const std::string& plugin_key, std::string& error);
|
||||
|
||||
// Clear any pending registrations for the key. Safe to call when import fails.
|
||||
void cancel_plugin_capture(const std::string& plugin_key);
|
||||
|
||||
// Clear all pending registrations before interpreter shutdown.
|
||||
void clear_pending_captures();
|
||||
|
||||
private:
|
||||
PythonPluginBridge() = default;
|
||||
PythonPluginBridge(const PythonPluginBridge&) = delete;
|
||||
PythonPluginBridge& operator=(const PythonPluginBridge&) = delete;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif /* slic3r_PythonPluginBridge_hpp_ */
|
||||
119
src/slic3r/plugin/PythonPluginInterface.hpp
Normal file
119
src/slic3r/plugin/PythonPluginInterface.hpp
Normal file
@@ -0,0 +1,119 @@
|
||||
#ifndef slic3r_PythonPluginInterface_hpp_
|
||||
#define slic3r_PythonPluginInterface_hpp_
|
||||
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
#include <pybind11/embed.h>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
enum class PluginCapabilityType { PostProcessing = 0, PrinterConnection, Automation, Analysis, Importer, Exporter, Visualization, Script, Unknown };
|
||||
|
||||
inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
|
||||
{
|
||||
switch (type) {
|
||||
case PluginCapabilityType::PostProcessing: return "post-processing";
|
||||
case PluginCapabilityType::PrinterConnection: return "printer-connection";
|
||||
case PluginCapabilityType::Automation: return "automation";
|
||||
case PluginCapabilityType::Analysis: return "analysis";
|
||||
case PluginCapabilityType::Importer: return "importer";
|
||||
case PluginCapabilityType::Exporter: return "exporter";
|
||||
case PluginCapabilityType::Visualization: return "visualization";
|
||||
case PluginCapabilityType::Script: return "script";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string plugin_capability_type_display_name(PluginCapabilityType type)
|
||||
{
|
||||
switch (type) {
|
||||
case PluginCapabilityType::PostProcessing: return "Post-processing";
|
||||
case PluginCapabilityType::PrinterConnection: return "Printer connection";
|
||||
case PluginCapabilityType::Automation: return "Automation";
|
||||
case PluginCapabilityType::Analysis: return "Analysis";
|
||||
case PluginCapabilityType::Importer: return "Importer";
|
||||
case PluginCapabilityType::Exporter: return "Exporter";
|
||||
case PluginCapabilityType::Visualization: return "Visualization";
|
||||
case PluginCapabilityType::Script: return "Script";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
inline PluginCapabilityType plugin_capability_type_from_string(std::string_view value)
|
||||
{
|
||||
auto to_lower = [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); };
|
||||
std::string lowered;
|
||||
lowered.reserve(value.size());
|
||||
for (unsigned char ch : value) {
|
||||
lowered.push_back(to_lower(ch));
|
||||
}
|
||||
|
||||
if (lowered == "post-processing")
|
||||
return PluginCapabilityType::PostProcessing;
|
||||
if (lowered == "printer-connection")
|
||||
return PluginCapabilityType::PrinterConnection;
|
||||
if (lowered == "automation")
|
||||
return PluginCapabilityType::Automation;
|
||||
if (lowered == "analysis")
|
||||
return PluginCapabilityType::Analysis;
|
||||
if (lowered == "importer")
|
||||
return PluginCapabilityType::Importer;
|
||||
if (lowered == "exporter")
|
||||
return PluginCapabilityType::Exporter;
|
||||
if (lowered == "visualization")
|
||||
return PluginCapabilityType::Visualization;
|
||||
if (lowered == "script")
|
||||
return PluginCapabilityType::Script;
|
||||
return PluginCapabilityType::Unknown;
|
||||
}
|
||||
|
||||
struct PluginContext
|
||||
{ std::string orca_version; };
|
||||
|
||||
enum class PluginResult { Success, Skipped, RecoverableError, FatalError };
|
||||
|
||||
struct ExecutionResult
|
||||
{
|
||||
PluginResult status = PluginResult::Success;
|
||||
std::string message;
|
||||
std::string data;
|
||||
|
||||
static ExecutionResult success(std::string message = {}, std::string data = {})
|
||||
{ return {PluginResult::Success, std::move(message), std::move(data)}; }
|
||||
|
||||
static ExecutionResult skipped(std::string message = {})
|
||||
{
|
||||
return {PluginResult::Skipped, std::move(message), {}};
|
||||
}
|
||||
|
||||
static ExecutionResult failure(PluginResult status, std::string message, std::string data = {})
|
||||
{ return {status, std::move(message), std::move(data)}; }
|
||||
};
|
||||
|
||||
class PluginCapabilityInterface
|
||||
{
|
||||
public:
|
||||
virtual ~PluginCapabilityInterface() = default;
|
||||
|
||||
virtual std::string get_name() const = 0; // required — overridden in Python
|
||||
virtual PluginCapabilityType get_type() const { return PluginCapabilityType::Unknown; } // optional — typed bases override
|
||||
|
||||
virtual void on_load() {}
|
||||
virtual void on_unload() {}
|
||||
|
||||
// C++-only audit identity (never exposed to Python). Set by PluginLoader after
|
||||
// plugin capture so trampoline calls can scope filesystem enforcement to this
|
||||
// plugin. This is PluginDescriptor::plugin_key, the canonical runtime id.
|
||||
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; }
|
||||
|
||||
private:
|
||||
std::string m_audit_plugin_key;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif /* slic3r_PythonPluginInterface_hpp_ */
|
||||
@@ -0,0 +1,30 @@
|
||||
#include "GCodePluginCapability.hpp"
|
||||
|
||||
#include "GCodePluginCapabilityTrampoline.hpp"
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void GCodePluginCapability::RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes)
|
||||
{
|
||||
(void) pluginTypes;
|
||||
|
||||
auto gcode = module.def_submodule("gcode", "G-code API");
|
||||
|
||||
py::class_<GCodePluginContext, PluginContext>(gcode, "GCodePluginContext", "Context shared with G-code plugins")
|
||||
.def(py::init<>())
|
||||
.def_readwrite("gcode_path", &GCodePluginContext::gcode_path)
|
||||
.def_readwrite("host", &GCodePluginContext::host)
|
||||
.def_readwrite("output_name", &GCodePluginContext::output_name);
|
||||
|
||||
py::class_<GCodePluginCapability, PluginCapabilityInterface, PyGCodePluginCapabilityTrampoline, std::shared_ptr<GCodePluginCapability>>(gcode, "GCodePluginCapabilityBase")
|
||||
.def(py::init<>())
|
||||
.def("get_type", &GCodePluginCapability::get_type)
|
||||
.def("execute", &GCodePluginCapability::execute);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef slic3r_GCodePluginCapability_hpp_
|
||||
#define slic3r_GCodePluginCapability_hpp_
|
||||
|
||||
#include "../../PythonPluginInterface.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
struct GCodePluginContext : public PluginContext {
|
||||
std::string gcode_path;
|
||||
std::string host;
|
||||
std::string output_name;
|
||||
};
|
||||
|
||||
class GCodePluginCapability : public PluginCapabilityInterface
|
||||
{
|
||||
public:
|
||||
PluginCapabilityType get_type() const override { return PluginCapabilityType::PostProcessing; }
|
||||
|
||||
virtual ExecutionResult execute(const GCodePluginContext& ctx) = 0;
|
||||
|
||||
static void RegisterBindings(pybind11::module_ &module,
|
||||
pybind11::enum_<PluginCapabilityType> &pluginTypes);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif /* slic3r_GCodePluginCapability_hpp_ */
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef slic3r_GCodePluginCapabilityTrampoline_hpp_
|
||||
#define slic3r_GCodePluginCapabilityTrampoline_hpp_
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include "../../PyPluginTrampoline.hpp"
|
||||
#include "../../PluginAuditManager.hpp"
|
||||
#include "GCodePluginCapability.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
class PyGCodePluginCapabilityTrampoline : public PyPluginCommonTrampoline<GCodePluginCapability>
|
||||
{
|
||||
public:
|
||||
using PyPluginCommonTrampoline<GCodePluginCapability>::PyPluginCommonTrampoline;
|
||||
|
||||
ExecutionResult execute(const GCodePluginContext& ctx) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||||
[&] {
|
||||
// G-code post-processing plugins may also write into the folder holding the
|
||||
// current temp G-code file, in addition to the globally-allowed data_dir().
|
||||
// The setup callback runs AFTER the context is constructed so the scoped root
|
||||
// is not cleared by ScopedPluginAuditContext's constructor.
|
||||
|
||||
if (!ctx.gcode_path.empty())
|
||||
::Slic3r::PluginAuditManager::instance().add_scoped_allowed_root(
|
||||
std::filesystem::path(ctx.gcode_path).parent_path());
|
||||
},
|
||||
PYBIND11_OVERRIDE_PURE, ExecutionResult, GCodePluginCapability, execute, ctx);
|
||||
}
|
||||
};
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,121 @@
|
||||
#include "PrinterAgentPluginCapability.hpp"
|
||||
#include "PrinterAgentPluginCapabilityTrampoline.hpp"
|
||||
|
||||
#include "IPrinterAgent.hpp"
|
||||
|
||||
#include <pybind11/functional.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes)
|
||||
{
|
||||
(void) pluginTypes;
|
||||
|
||||
auto printer_agent_module = module.def_submodule("printer_agent", "Printer Agent API");
|
||||
|
||||
py::enum_<FilamentSyncMode>(printer_agent_module, "FilamentSyncMode")
|
||||
.value("None_", FilamentSyncMode::none)
|
||||
.value("Subscription", FilamentSyncMode::subscription)
|
||||
.value("Pull", FilamentSyncMode::pull)
|
||||
.export_values();
|
||||
|
||||
py::class_<AgentInfo>(printer_agent_module, "AgentInfo")
|
||||
.def(py::init<>())
|
||||
.def(py::init([](std::string id, std::string name, std::string version, std::string description) {
|
||||
return AgentInfo{std::move(id), std::move(name), std::move(version), std::move(description)};
|
||||
}),
|
||||
py::arg("id"), py::arg("name"), py::arg("version"), py::arg("description"))
|
||||
.def_readwrite("id", &AgentInfo::id)
|
||||
.def_readwrite("name", &AgentInfo::name)
|
||||
.def_readwrite("version", &AgentInfo::version)
|
||||
.def_readwrite("description", &AgentInfo::description);
|
||||
|
||||
py::class_<detectResult>(printer_agent_module, "DetectResult")
|
||||
.def(py::init<>())
|
||||
.def_readwrite("result_msg", &detectResult::result_msg)
|
||||
.def_readwrite("command", &detectResult::command)
|
||||
.def_readwrite("dev_id", &detectResult::dev_id)
|
||||
.def_readwrite("model_id", &detectResult::model_id)
|
||||
.def_readwrite("dev_name", &detectResult::dev_name)
|
||||
.def_readwrite("version", &detectResult::version)
|
||||
.def_readwrite("bind_state", &detectResult::bind_state)
|
||||
.def_readwrite("connect_type", &detectResult::connect_type);
|
||||
|
||||
py::class_<PrintParams>(printer_agent_module, "PrintParams")
|
||||
.def(py::init<>())
|
||||
.def_readwrite("dev_id", &PrintParams::dev_id)
|
||||
.def_readwrite("task_name", &PrintParams::task_name)
|
||||
.def_readwrite("project_name", &PrintParams::project_name)
|
||||
.def_readwrite("preset_name", &PrintParams::preset_name)
|
||||
.def_readwrite("filename", &PrintParams::filename)
|
||||
.def_readwrite("config_filename", &PrintParams::config_filename)
|
||||
.def_readwrite("plate_index", &PrintParams::plate_index)
|
||||
.def_readwrite("ftp_folder", &PrintParams::ftp_folder)
|
||||
.def_readwrite("ftp_file", &PrintParams::ftp_file)
|
||||
.def_readwrite("ftp_file_md5", &PrintParams::ftp_file_md5)
|
||||
.def_readwrite("nozzle_mapping", &PrintParams::nozzle_mapping)
|
||||
.def_readwrite("ams_mapping", &PrintParams::ams_mapping)
|
||||
.def_readwrite("ams_mapping2", &PrintParams::ams_mapping2)
|
||||
.def_readwrite("ams_mapping_info", &PrintParams::ams_mapping_info)
|
||||
.def_readwrite("nozzles_info", &PrintParams::nozzles_info)
|
||||
.def_readwrite("connection_type", &PrintParams::connection_type)
|
||||
.def_readwrite("comments", &PrintParams::comments)
|
||||
.def_readwrite("origin_profile_id", &PrintParams::origin_profile_id)
|
||||
.def_readwrite("stl_design_id", &PrintParams::stl_design_id)
|
||||
.def_readwrite("origin_model_id", &PrintParams::origin_model_id)
|
||||
.def_readwrite("print_type", &PrintParams::print_type)
|
||||
.def_readwrite("dst_file", &PrintParams::dst_file)
|
||||
.def_readwrite("dev_name", &PrintParams::dev_name)
|
||||
.def_readwrite("dev_ip", &PrintParams::dev_ip)
|
||||
.def_readwrite("use_ssl_for_ftp", &PrintParams::use_ssl_for_ftp)
|
||||
.def_readwrite("use_ssl_for_mqtt", &PrintParams::use_ssl_for_mqtt)
|
||||
.def_readwrite("username", &PrintParams::username)
|
||||
.def_readwrite("password", &PrintParams::password)
|
||||
.def_readwrite("task_bed_leveling", &PrintParams::task_bed_leveling)
|
||||
.def_readwrite("task_flow_cali", &PrintParams::task_flow_cali)
|
||||
.def_readwrite("task_vibration_cali", &PrintParams::task_vibration_cali)
|
||||
.def_readwrite("task_layer_inspect", &PrintParams::task_layer_inspect)
|
||||
.def_readwrite("task_record_timelapse", &PrintParams::task_record_timelapse)
|
||||
.def_readwrite("task_use_ams", &PrintParams::task_use_ams)
|
||||
.def_readwrite("task_bed_type", &PrintParams::task_bed_type)
|
||||
.def_readwrite("extra_options", &PrintParams::extra_options)
|
||||
.def_readwrite("auto_bed_leveling", &PrintParams::auto_bed_leveling)
|
||||
.def_readwrite("auto_flow_cali", &PrintParams::auto_flow_cali)
|
||||
.def_readwrite("auto_offset_cali", &PrintParams::auto_offset_cali)
|
||||
.def_readwrite("task_ext_change_assist", &PrintParams::task_ext_change_assist)
|
||||
.def_readwrite("try_emmc_print", &PrintParams::try_emmc_print);
|
||||
|
||||
py::class_<PrinterAgentPluginCapability, PluginCapabilityInterface, PyPrinterAgentPluginCapabilityTrampoline, std::shared_ptr<PrinterAgentPluginCapability>>(
|
||||
printer_agent_module, "PrinterAgentBase")
|
||||
.def(py::init<>())
|
||||
.def("get_type", &PrinterAgentPluginCapability::get_type)
|
||||
.def("get_agent_info", &PrinterAgentPluginCapability::get_agent_info)
|
||||
.def("connect_printer", &PrinterAgentPluginCapability::connect_printer)
|
||||
.def("disconnect_printer", &PrinterAgentPluginCapability::disconnect_printer)
|
||||
.def("send_message", &PrinterAgentPluginCapability::send_message)
|
||||
.def("send_message_to_printer", &PrinterAgentPluginCapability::send_message_to_printer)
|
||||
.def("start_discovery", &PrinterAgentPluginCapability::start_discovery)
|
||||
.def("bind_detect", &PrinterAgentPluginCapability::bind_detect)
|
||||
.def("get_user_selected_machine", &PrinterAgentPluginCapability::get_user_selected_machine)
|
||||
.def("set_user_selected_machine", &PrinterAgentPluginCapability::set_user_selected_machine)
|
||||
.def("start_send_gcode_to_sdcard", &PrinterAgentPluginCapability::start_send_gcode_to_sdcard)
|
||||
.def("start_local_print", &PrinterAgentPluginCapability::start_local_print)
|
||||
.def("get_filament_sync_mode", &PrinterAgentPluginCapability::get_filament_sync_mode)
|
||||
.def("fetch_filament_info", &PrinterAgentPluginCapability::fetch_filament_info)
|
||||
.def("check_cert", &PrinterAgentPluginCapability::check_cert)
|
||||
.def("install_device_cert", &PrinterAgentPluginCapability::install_device_cert)
|
||||
.def("ping_bind", &PrinterAgentPluginCapability::ping_bind)
|
||||
.def("bind", &PrinterAgentPluginCapability::bind)
|
||||
.def("unbind", &PrinterAgentPluginCapability::unbind)
|
||||
.def("start_print", &PrinterAgentPluginCapability::start_print)
|
||||
.def("start_local_print_with_record", &PrinterAgentPluginCapability::start_local_print_with_record)
|
||||
.def("start_sdcard_print", &PrinterAgentPluginCapability::start_sdcard_print);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,83 @@
|
||||
#ifndef slic3r_PrinterAgentPluginCapability_hpp_
|
||||
#define slic3r_PrinterAgentPluginCapability_hpp_
|
||||
|
||||
#include "../../PythonPluginInterface.hpp"
|
||||
|
||||
#include "IPrinterAgent.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// A printer-agent plugin capability implements IPrinterAgent directly: the host
|
||||
// drives it through the native IPrinterAgent surface and the Python plugin
|
||||
// overrides the individual operations. The capability is registered with the
|
||||
// NetworkAgentFactory and handed out as the live IPrinterAgent for the selected
|
||||
// printer agent.
|
||||
class PrinterAgentPluginCapability : public PluginCapabilityInterface, public IPrinterAgent
|
||||
{
|
||||
public:
|
||||
static void RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes);
|
||||
|
||||
PluginCapabilityType get_type() const override { return PluginCapabilityType::PrinterConnection; }
|
||||
|
||||
// set_cloud_agent is the host-managed dependency injection point — the host hands the
|
||||
// capability its ICloudServiceAgent — so it is the one operation kept native here. Every
|
||||
// other IPrinterAgent operation is pure: the Python plugin must implement all of them.
|
||||
void set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud) final override { (void) cloud; }
|
||||
|
||||
AgentInfo get_agent_info() override = 0;
|
||||
|
||||
int connect_printer(
|
||||
std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override = 0;
|
||||
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override = 0;
|
||||
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override = 0;
|
||||
bool start_discovery(bool start, bool sending) override = 0;
|
||||
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override = 0;
|
||||
std::string get_user_selected_machine() override = 0;
|
||||
int set_user_selected_machine(std::string dev_id) override = 0;
|
||||
int start_send_gcode_to_sdcard(PrintParams params,
|
||||
OnUpdateStatusFn update_fn,
|
||||
WasCancelledFn cancel_fn,
|
||||
OnWaitFn wait_fn) override = 0;
|
||||
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0;
|
||||
FilamentSyncMode get_filament_sync_mode() const override = 0;
|
||||
bool fetch_filament_info(std::string dev_id) override = 0;
|
||||
|
||||
int check_cert() override = 0;
|
||||
void install_device_cert(std::string dev_id, bool lan_only) override = 0;
|
||||
int ping_bind(std::string ping_code) override = 0;
|
||||
int bind(std::string dev_ip,
|
||||
std::string dev_id,
|
||||
std::string sec_link,
|
||||
std::string timezone,
|
||||
bool improved,
|
||||
OnUpdateStatusFn update_fn) override = 0;
|
||||
int unbind(std::string dev_id) override = 0;
|
||||
// request_bind_ticket has a std::string* out-param that cannot round-trip through a
|
||||
// pybind11 override directly; the trampoline wraps it (the Python plugin returns a
|
||||
// (result, ticket) tuple), so it stays pure here like the rest.
|
||||
int request_bind_ticket(std::string* ticket) override = 0;
|
||||
int set_server_callback(OnServerErrFn fn) override = 0;
|
||||
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override = 0;
|
||||
int start_local_print_with_record(PrintParams params,
|
||||
OnUpdateStatusFn update_fn,
|
||||
WasCancelledFn cancel_fn,
|
||||
OnWaitFn wait_fn) override = 0;
|
||||
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0;
|
||||
|
||||
int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override = 0;
|
||||
int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override = 0;
|
||||
int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override = 0;
|
||||
int set_on_message_fn(OnMessageFn fn) override = 0;
|
||||
int set_on_user_message_fn(OnMessageFn fn) override = 0;
|
||||
int set_on_local_connect_fn(OnLocalConnectedFn fn) override = 0;
|
||||
int set_on_local_message_fn(OnMessageFn fn) override = 0;
|
||||
int set_queue_on_main_fn(QueueOnMainFn fn) override = 0;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif /* slic3r_PrinterAgentPluginCapability_hpp_ */
|
||||
@@ -0,0 +1,237 @@
|
||||
#ifndef slic3r_PrinterAgentPluginCapabilityTrampoline_hpp_
|
||||
#define slic3r_PrinterAgentPluginCapabilityTrampoline_hpp_
|
||||
|
||||
#include "PrinterAgentPluginCapability.hpp"
|
||||
#include "../../PyPluginTrampoline.hpp"
|
||||
|
||||
#include "IPrinterAgent.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
class PyPrinterAgentPluginCapabilityTrampoline : public PyPluginCommonTrampoline<PrinterAgentPluginCapability>
|
||||
{
|
||||
public:
|
||||
using PyPluginCommonTrampoline<PrinterAgentPluginCapability>::PyPluginCommonTrampoline;
|
||||
|
||||
AgentInfo get_agent_info() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, AgentInfo, PrinterAgentPluginCapability,
|
||||
get_agent_info);
|
||||
}
|
||||
|
||||
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, connect_printer, dev_id,
|
||||
dev_ip, username, password, use_ssl);
|
||||
}
|
||||
|
||||
int disconnect_printer() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, disconnect_printer);
|
||||
}
|
||||
|
||||
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message, dev_id,
|
||||
json_str, qos, flag);
|
||||
}
|
||||
|
||||
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message_to_printer,
|
||||
dev_id, json_str, qos, flag);
|
||||
}
|
||||
|
||||
bool start_discovery(bool start, bool sending) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, start_discovery, start,
|
||||
sending);
|
||||
}
|
||||
|
||||
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind_detect, dev_ip,
|
||||
sec_link, detect);
|
||||
}
|
||||
|
||||
std::string get_user_selected_machine() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, PrinterAgentPluginCapability,
|
||||
get_user_selected_machine);
|
||||
}
|
||||
|
||||
int set_user_selected_machine(std::string dev_id) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
|
||||
set_user_selected_machine, dev_id);
|
||||
}
|
||||
|
||||
int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
|
||||
start_send_gcode_to_sdcard, params, update_fn, cancel_fn, wait_fn);
|
||||
}
|
||||
|
||||
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_local_print,
|
||||
params, update_fn, cancel_fn);
|
||||
}
|
||||
|
||||
FilamentSyncMode get_filament_sync_mode() const override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, FilamentSyncMode, PrinterAgentPluginCapability,
|
||||
get_filament_sync_mode);
|
||||
}
|
||||
|
||||
bool fetch_filament_info(std::string dev_id) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, fetch_filament_info, dev_id);
|
||||
}
|
||||
|
||||
int check_cert() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, check_cert);
|
||||
}
|
||||
|
||||
void install_device_cert(std::string dev_id, bool lan_only) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, void, PrinterAgentPluginCapability, install_device_cert, dev_id,
|
||||
lan_only);
|
||||
}
|
||||
|
||||
int ping_bind(std::string ping_code) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ping_bind, ping_code);
|
||||
}
|
||||
|
||||
int bind(std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind, dev_ip, dev_id,
|
||||
sec_link, timezone, improved, update_fn);
|
||||
}
|
||||
|
||||
int unbind(std::string dev_id) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, unbind, dev_id);
|
||||
}
|
||||
|
||||
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_print, params,
|
||||
update_fn, cancel_fn, wait_fn);
|
||||
}
|
||||
|
||||
int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
|
||||
start_local_print_with_record, params, update_fn, cancel_fn, wait_fn);
|
||||
}
|
||||
|
||||
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_sdcard_print, params,
|
||||
update_fn, cancel_fn);
|
||||
}
|
||||
|
||||
int set_server_callback(OnServerErrFn fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_server_callback, fn);
|
||||
}
|
||||
|
||||
int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_ssdp_msg_fn, fn);
|
||||
}
|
||||
|
||||
int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_printer_connected_fn,
|
||||
fn);
|
||||
}
|
||||
|
||||
int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_subscribe_failure_fn,
|
||||
fn);
|
||||
}
|
||||
|
||||
int set_on_message_fn(OnMessageFn fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_message_fn, fn);
|
||||
}
|
||||
|
||||
int set_on_user_message_fn(OnMessageFn fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_user_message_fn, fn);
|
||||
}
|
||||
|
||||
int set_on_local_connect_fn(OnLocalConnectedFn fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_connect_fn, fn);
|
||||
}
|
||||
|
||||
int set_on_local_message_fn(OnMessageFn fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_message_fn, fn);
|
||||
}
|
||||
|
||||
int set_queue_on_main_fn(QueueOnMainFn fn) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_queue_on_main_fn, fn);
|
||||
}
|
||||
|
||||
// request_bind_ticket returns its ticket through a std::string* out-param, which pybind11
|
||||
// cannot marshal back through a plain override. We dispatch manually: the Python plugin
|
||||
// returns a (result, ticket) tuple, which we unpack into the int result and the out-param.
|
||||
int request_bind_ticket(std::string* ticket) override
|
||||
{
|
||||
ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
|
||||
pybind11::gil_scoped_acquire gil;
|
||||
pybind11::function override =
|
||||
pybind11::get_override(static_cast<const PrinterAgentPluginCapability*>(this), "request_bind_ticket");
|
||||
if (!override)
|
||||
pybind11::pybind11_fail("Tried to call pure virtual function \"PrinterAgentPluginCapability::request_bind_ticket\"");
|
||||
try {
|
||||
pybind11::tuple result = override().cast<pybind11::tuple>();
|
||||
if (ticket)
|
||||
*ticket = result[1].cast<std::string>();
|
||||
return result[0].cast<int>();
|
||||
} catch (pybind11::error_already_set& err) {
|
||||
::Slic3r::log_python_exception_keep(err);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
};
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif /* slic3r_PrinterAgentPluginCapabilityTrampoline_hpp_ */
|
||||
@@ -0,0 +1,24 @@
|
||||
#include "ScriptPluginCapability.hpp"
|
||||
|
||||
#include "ScriptPluginCapabilityTrampoline.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
namespace Slic3r {
|
||||
void ScriptPluginCapability::RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes)
|
||||
{
|
||||
(void) pluginTypes;
|
||||
BOOST_LOG_TRIVIAL(debug) << "Registering orca.script bindings";
|
||||
|
||||
auto script = module.def_submodule("script", "Script Plugins API");
|
||||
|
||||
py::class_<ScriptPluginCapability, PluginCapabilityInterface, PyScriptPluginCapabilityTrampoline, std::shared_ptr<ScriptPluginCapability>>(script, "ScriptPluginCapabilityBase")
|
||||
.def(py::init<>())
|
||||
.def("get_type", &ScriptPluginCapability::get_type)
|
||||
.def("execute", &ScriptPluginCapability::execute);
|
||||
}
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,19 @@
|
||||
#ifndef slic3r_ScriptPluginCapability_hpp_
|
||||
#define slic3r_ScriptPluginCapability_hpp_
|
||||
|
||||
#include "../../PythonPluginInterface.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
class ScriptPluginCapability : public PluginCapabilityInterface
|
||||
{
|
||||
public:
|
||||
PluginCapabilityType get_type() const override { return PluginCapabilityType::Script; }
|
||||
|
||||
virtual ExecutionResult execute() = 0;
|
||||
|
||||
static void RegisterBindings(pybind11::module_ &module,
|
||||
pybind11::enum_<PluginCapabilityType> &pluginTypes);
|
||||
};
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif /* slic3r_ScriptPluginCapability_hpp_ */
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef slic3r_ScriptPluginCapabilityTrampoline_hpp_
|
||||
#define slic3r_ScriptPluginCapabilityTrampoline_hpp_
|
||||
|
||||
#include "ScriptPluginCapability.hpp"
|
||||
#include "../../PyPluginTrampoline.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
class PyScriptPluginCapabilityTrampoline : public PyPluginCommonTrampoline<ScriptPluginCapability>
|
||||
{
|
||||
public:
|
||||
using PyPluginCommonTrampoline<ScriptPluginCapability>::PyPluginCommonTrampoline;
|
||||
|
||||
ExecutionResult execute() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||||
[] {},
|
||||
PYBIND11_OVERRIDE_PURE,
|
||||
ExecutionResult,
|
||||
ScriptPluginCapability,
|
||||
execute);
|
||||
}
|
||||
};
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user