mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-15 15:03:49 +00:00
Fix/impl refactor (#14776)
* fix: impl refactor * fix: unload/load python module, race conditions, freezes * remove dead code * remove extra hook * remove more dead code * fix gil run script
This commit is contained in:
@@ -617,8 +617,6 @@ set(SLIC3R_GUI_SOURCES
|
||||
plugin/CloudPluginService.hpp
|
||||
plugin/PluginFsUtils.cpp
|
||||
plugin/PluginFsUtils.hpp
|
||||
plugin/PluginCatalog.cpp
|
||||
plugin/PluginCatalog.hpp
|
||||
plugin/PluginLoader.cpp
|
||||
plugin/PluginLoader.hpp
|
||||
plugin/PluginDescriptor.hpp
|
||||
|
||||
@@ -1121,7 +1121,7 @@ void GUI_App::shutdown()
|
||||
if (m_is_recreating_gui) return;
|
||||
stop_http_server();
|
||||
set_closing(true);
|
||||
Slic3r::PluginManager::instance().get_loader().set_shutting_down();
|
||||
Slic3r::PluginManager::instance().set_shutting_down();
|
||||
|
||||
if (m_agent)
|
||||
m_agent->set_printer_agent(nullptr);
|
||||
@@ -2723,12 +2723,13 @@ void GUI_App::init_plugin_gui_wiring()
|
||||
});
|
||||
};
|
||||
|
||||
plugin_mgr.get_loader().subscribe_on_load_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
|
||||
plugin_mgr.get_loader().subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
|
||||
plugin_mgr.get_loader().subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin);
|
||||
plugin_mgr.get_loader().subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin);
|
||||
plugin_mgr.get_loader().subscribe_on_capability_load_callback(
|
||||
[refresh_plugins_dialog](const PluginCapabilityIdentifier& capability) {
|
||||
plugin_mgr.subscribe_on_unload_callback(PluginHostUi::close_windows_for_plugin);
|
||||
plugin_mgr.subscribe_on_load_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
|
||||
plugin_mgr.subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
|
||||
plugin_mgr.subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin);
|
||||
plugin_mgr.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin);
|
||||
plugin_mgr.subscribe_on_capability_load_callback(
|
||||
[refresh_plugins_dialog](const PluginCapabilityId& capability) {
|
||||
if (capability.type == PluginCapabilityType::PrinterConnection)
|
||||
NetworkAgentFactory::register_python_printer_agent(capability.plugin_key, capability.name);
|
||||
refresh_plugins_dialog();
|
||||
@@ -2740,8 +2741,8 @@ void GUI_App::init_plugin_gui_wiring()
|
||||
plater->revalidate_current_plate_if_plugins_missing();
|
||||
});
|
||||
});
|
||||
plugin_mgr.get_loader().subscribe_on_capability_unload_callback(
|
||||
[refresh_plugins_dialog](const PluginCapabilityIdentifier& capability) {
|
||||
plugin_mgr.subscribe_on_capability_unload_callback(
|
||||
[refresh_plugins_dialog](const PluginCapabilityId& capability) {
|
||||
if (capability.type == PluginCapabilityType::PrinterConnection)
|
||||
NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name);
|
||||
refresh_plugins_dialog();
|
||||
@@ -3162,17 +3163,16 @@ bool GUI_App::on_init_inner()
|
||||
// plugins are discovered even before the network agent is ready.
|
||||
const std::string preset_folder = app_config->get("preset_folder");
|
||||
if (!preset_folder.empty()) {
|
||||
plugin_mgr.get_catalog().set_cloud_plugin_dir(preset_folder);
|
||||
plugin_mgr.get_loader().set_cloud_user_id(preset_folder);
|
||||
plugin_mgr.set_cloud_user(preset_folder);
|
||||
}
|
||||
|
||||
plugin_mgr.discover_plugins(false, true);
|
||||
|
||||
init_plugin_gui_wiring();
|
||||
|
||||
for (const std::string& plugin_key : plugin_mgr.get_catalog().get_enabled_plugin_keys()) {
|
||||
if (!plugin_mgr.get_loader().is_plugin_loaded(plugin_key)) {
|
||||
plugin_mgr.get_loader().load_plugin(plugin_mgr.get_catalog(), plugin_key, false);
|
||||
for (const std::string& plugin_key : plugin_mgr.get_enabled_plugin_keys()) {
|
||||
if (!plugin_mgr.is_plugin_loaded(plugin_key)) {
|
||||
plugin_mgr.load_plugin(plugin_key, false);
|
||||
BOOST_LOG_TRIVIAL(info) << "Auto-loading plugin on startup: " << plugin_key;
|
||||
}
|
||||
}
|
||||
@@ -3182,8 +3182,7 @@ bool GUI_App::on_init_inner()
|
||||
|
||||
if (m_agent && m_agent->is_user_login()) {
|
||||
enable_user_preset_folder(true);
|
||||
plugin_mgr.get_catalog().set_cloud_plugin_dir(m_agent->get_user_id());
|
||||
plugin_mgr.get_loader().set_cloud_user_id(m_agent->get_user_id());
|
||||
plugin_mgr.set_cloud_user(m_agent->get_user_id());
|
||||
// If there is a user logged in we do an immediate sync.
|
||||
std::vector<std::string> not_found, unauthorized;
|
||||
plugin_mgr.fetch_plugins_from_cloud(¬_found, &unauthorized);
|
||||
@@ -3203,8 +3202,7 @@ bool GUI_App::on_init_inner()
|
||||
}
|
||||
} else {
|
||||
enable_user_preset_folder(false);
|
||||
plugin_mgr.get_catalog().set_cloud_plugin_dir("");
|
||||
plugin_mgr.get_loader().set_cloud_user_id("");
|
||||
plugin_mgr.set_cloud_user("");
|
||||
}
|
||||
|
||||
// BBS if load user preset failed
|
||||
@@ -4776,10 +4774,9 @@ void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PRO
|
||||
|
||||
remove_user_presets();
|
||||
enable_user_preset_folder(false);
|
||||
Slic3r::PluginManager::instance().get_loader().unload_cloud_plugins();
|
||||
Slic3r::PluginManager::instance().get_catalog().clear_cloud_plugin_catalog();
|
||||
Slic3r::PluginManager::instance().get_catalog().set_cloud_plugin_dir("");
|
||||
Slic3r::PluginManager::instance().get_loader().set_cloud_user_id("");
|
||||
Slic3r::PluginManager::instance().unload_cloud_plugins();
|
||||
Slic3r::PluginManager::instance().clear_cloud_plugin_catalog();
|
||||
Slic3r::PluginManager::instance().set_cloud_user("");
|
||||
preset_bundle->load_user_presets(DEFAULT_USER_FOLDER_NAME, ForwardCompatibilitySubstitutionRule::Enable);
|
||||
mainframe->update_side_preset_ui();
|
||||
|
||||
@@ -5325,14 +5322,12 @@ void GUI_App::enable_user_preset_folder(bool enable)
|
||||
std::string user_id = m_agent->get_user_id();
|
||||
app_config->set("preset_folder", user_id);
|
||||
GUI::wxGetApp().preset_bundle->update_user_presets_directory(user_id);
|
||||
PluginManager::instance().get_catalog().set_cloud_plugin_dir(user_id);
|
||||
PluginManager::instance().get_loader().set_cloud_user_id(user_id);
|
||||
PluginManager::instance().set_cloud_user(user_id);
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(info) << "preset_folder: set to empty";
|
||||
app_config->set("preset_folder", "");
|
||||
GUI::wxGetApp().preset_bundle->update_user_presets_directory(DEFAULT_USER_FOLDER_NAME);
|
||||
PluginManager::instance().get_catalog().set_cloud_plugin_dir("");
|
||||
PluginManager::instance().get_loader().set_cloud_user_id("");
|
||||
PluginManager::instance().set_cloud_user("");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8230,7 +8225,7 @@ void GUI_App::open_plugins_dialog(size_t open_on_tab, const std::string& highlig
|
||||
}
|
||||
|
||||
try {
|
||||
m_plugins_dlg = new PluginsDialog(mainframe, open_on_tab, highlight_option);
|
||||
m_plugins_dlg = new PluginsDialog(mainframe, wxID_ANY, _L("Plugins"));
|
||||
m_plugins_dlg->set_open_terminal_dlg_fn();
|
||||
m_plugins_dlg->Bind(wxEVT_DESTROY, [this](wxWindowDestroyEvent& event) {
|
||||
if (event.GetEventObject() == m_plugins_dlg)
|
||||
|
||||
@@ -683,8 +683,13 @@ std::string OptionsGroup::pick_plugin(const ConfigOptionDef& opt)
|
||||
return {};
|
||||
}
|
||||
|
||||
auto caps = manager.get_loader().get_plugin_capabilities_by_type(plugin_type);
|
||||
caps.erase(std::remove_if(caps.begin(), caps.end(), [](const auto& cap) { return !cap || !cap->enabled; }), caps.end());
|
||||
struct CapabilityRef { std::string plugin_key; std::string name; };
|
||||
std::vector<CapabilityRef> caps;
|
||||
for (const auto& capability : manager.get_plugin_capabilities(/*plugin_key=*/"", plugin_type, /*only_enabled=*/true))
|
||||
caps.push_back({capability->audit_plugin_key(), capability->name()});
|
||||
std::sort(caps.begin(), caps.end(), [](const CapabilityRef& lhs, const CapabilityRef& rhs) {
|
||||
return lhs.name == rhs.name ? lhs.plugin_key < rhs.plugin_key : lhs.name < rhs.name;
|
||||
});
|
||||
|
||||
if (caps.empty()) {
|
||||
wxMessageBox(_L("No plugins capabilities available for this type.\nEnable or install some to use."), _L("Plugin Selection"), wxOK | wxICON_INFORMATION, m_parent);
|
||||
@@ -695,10 +700,10 @@ std::string OptionsGroup::pick_plugin(const ConfigOptionDef& opt)
|
||||
entries.reserve(caps.size());
|
||||
for (const auto& cap : caps) {
|
||||
Slic3r::PluginDescriptor descriptor;
|
||||
wxString package_name = manager.get_catalog().try_get_plugin_descriptor(cap->plugin_key, descriptor)
|
||||
? from_u8(descriptor.name) : from_u8(cap->plugin_key);
|
||||
wxString package_name = manager.try_get_plugin_descriptor(cap.plugin_key, descriptor)
|
||||
? from_u8(descriptor.name) : from_u8(cap.plugin_key);
|
||||
entries.push_back({
|
||||
cap->plugin_key, cap->name, from_u8(cap->name) + from_u8(" \xE2\x80\x94 ") + package_name, package_name
|
||||
cap.plugin_key, cap.name, from_u8(cap.name) + from_u8(" \xE2\x80\x94 ") + package_name, package_name
|
||||
});
|
||||
}
|
||||
|
||||
@@ -713,7 +718,7 @@ std::string OptionsGroup::pick_plugin(const ConfigOptionDef& opt)
|
||||
// name is stored in the option; the full "name;uuid;capability" reference is derived lazily
|
||||
// when the preset is serialized (see ConfigBase::save_plugin_collection).
|
||||
Slic3r::PluginDescriptor descriptor;
|
||||
if (!manager.get_catalog().try_get_plugin_descriptor(selection.plugin_key, descriptor) || descriptor.name.empty())
|
||||
if (!manager.try_get_plugin_descriptor(selection.plugin_key, descriptor) || descriptor.name.empty())
|
||||
return {};
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Picked plugin capability: " << selection.name;
|
||||
|
||||
@@ -40,8 +40,6 @@ public:
|
||||
// Returns the {plugin_key, name} of the selected capability (capability path).
|
||||
CapabilityEntry selected_capability() const;
|
||||
|
||||
void update_plugin_list(const std::vector<PluginDescriptor>& updated_plugins) { m_plugins = updated_plugins; }
|
||||
|
||||
private:
|
||||
void build_ui(const wxString& plugin_type_label);
|
||||
void build_capability_ui(const wxString& plugin_type_label);
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
#include <wx/dialog.h>
|
||||
@@ -123,19 +124,19 @@ struct PluginDialogItem
|
||||
constexpr bool kFetchCloudCatalog = true;
|
||||
constexpr bool kUseCurrentCloudCatalog = false;
|
||||
|
||||
// The type of a package's first loaded capability. A package that is not loaded has no type.
|
||||
PluginCapabilityType primary_capability_type_of(PluginManager& manager, const std::string& plugin_key)
|
||||
{
|
||||
const auto capabilities = manager.get_plugin_capabilities(plugin_key, PluginCapabilityType::Unknown, /*only_enabled=*/false);
|
||||
return capabilities.empty() ? PluginCapabilityType::Unknown : capabilities.front()->type();
|
||||
}
|
||||
|
||||
std::vector<PluginDescriptor> current_cloud_catalog_snapshot()
|
||||
{
|
||||
const auto& catalog = PluginManager::instance().get_catalog();
|
||||
|
||||
std::vector<PluginDescriptor> cloud_entries;
|
||||
auto append_cloud_entries = [&cloud_entries](const std::vector<PluginDescriptor>& entries) {
|
||||
for (const PluginDescriptor& entry : entries)
|
||||
if (entry.is_cloud_plugin())
|
||||
cloud_entries.push_back(entry);
|
||||
};
|
||||
|
||||
append_cloud_entries(catalog.get_all_plugin_descriptors());
|
||||
append_cloud_entries(catalog.get_invalid_plugins());
|
||||
for (const PluginDescriptor& entry : PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true))
|
||||
if (entry.is_cloud_plugin())
|
||||
cloud_entries.push_back(entry);
|
||||
return cloud_entries;
|
||||
}
|
||||
|
||||
@@ -165,26 +166,30 @@ void refresh_plugin_catalog_blocking(bool fetch_cloud)
|
||||
manager.rescan_plugins();
|
||||
|
||||
if (!fetch_cloud) {
|
||||
manager.get_catalog().update_cloud_catalog(current_cloud_catalog);
|
||||
manager.update_cloud_catalog(current_cloud_catalog);
|
||||
return;
|
||||
}
|
||||
|
||||
manager.fetch_plugins_from_cloud(¬_found, &unauthorized);
|
||||
|
||||
auto plater = wxGetApp().plater();
|
||||
wxGetApp().CallAfter([not_found = std::move(not_found), unauthorized = std::move(unauthorized)]() {
|
||||
if (wxGetApp().is_closing())
|
||||
return;
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater == nullptr)
|
||||
return;
|
||||
|
||||
if (plater) {
|
||||
for (const auto& uuid : not_found) {
|
||||
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
format(_L("Plugin %s is no longer available."), uuid));
|
||||
}
|
||||
for (const auto& uuid : unauthorized) {
|
||||
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
format(_L("Plugin %s access is unauthorized."), uuid));
|
||||
}
|
||||
}
|
||||
for (const auto& uuid : not_found)
|
||||
plater->get_notification_manager()->push_notification(
|
||||
NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
format(_L("Plugin %s is no longer available."), uuid));
|
||||
for (const auto& uuid : unauthorized)
|
||||
plater->get_notification_manager()->push_notification(
|
||||
NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
format(_L("Plugin %s access is unauthorized."), uuid));
|
||||
});
|
||||
}
|
||||
|
||||
std::string to_string(PluginUpdateStatus status);
|
||||
@@ -305,7 +310,7 @@ PluginAvailableActions evaluate_action_policy(const PluginDialogItem& item)
|
||||
PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
|
||||
{
|
||||
PluginDialogItem item;
|
||||
const auto& loader = PluginManager::instance().get_loader();
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
|
||||
item.plugin_key = descriptor.plugin_key;
|
||||
item.display_name = descriptor.name;
|
||||
@@ -323,18 +328,25 @@ PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
|
||||
// why: sort by the same version the row displays (GetDisplayVersion in index.js) - installed when
|
||||
// installed, otherwise latest - so the Version sort matches what the user sees.
|
||||
item.sort_version = item.installed_version.empty() ? item.latest_version : item.installed_version;
|
||||
item.type_label = descriptor.type_label();
|
||||
item.type_key = plugin_capability_type_to_string(descriptor.primary_capability_type());
|
||||
// A package that is not loaded has no capabilities, so its row contributes none and does not
|
||||
// expand.
|
||||
const std::vector<std::shared_ptr<PluginCapabilityInterface>> capabilities =
|
||||
manager.get_plugin_capabilities(descriptor.plugin_key, PluginCapabilityType::Unknown, /*only_enabled=*/false);
|
||||
|
||||
const PluginCapabilityType primary_type = capabilities.empty() ? PluginCapabilityType::Unknown :
|
||||
capabilities.front()->type();
|
||||
item.type_label = plugin_capability_type_to_string(primary_type);
|
||||
item.type_key = plugin_capability_type_to_string(primary_type);
|
||||
// "types" is the display-only compatibility list. Cloud plugins show the raw labels the
|
||||
// service returned (which may not map to real capability types); local plugins derive them
|
||||
// from the capabilities actually discovered/loaded.
|
||||
// from the capabilities actually loaded.
|
||||
if (descriptor.is_cloud_plugin() && !descriptor.display_types.empty()) {
|
||||
for (const std::string& label : descriptor.display_types)
|
||||
if (std::find(item.type_labels.begin(), item.type_labels.end(), label) == item.type_labels.end())
|
||||
item.type_labels.push_back(label);
|
||||
} else {
|
||||
for (PluginCapabilityType type : descriptor.capability_types) {
|
||||
const std::string label = plugin_capability_type_display_name(type);
|
||||
for (const auto& capability : capabilities) {
|
||||
const std::string label = plugin_capability_type_display_name(capability->type());
|
||||
if (std::find(item.type_labels.begin(), item.type_labels.end(), label) == item.type_labels.end())
|
||||
item.type_labels.push_back(label);
|
||||
}
|
||||
@@ -348,30 +360,14 @@ PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
|
||||
item.is_cloud_plugin = descriptor.is_cloud_plugin();
|
||||
item.has_local_package = descriptor.has_local_package();
|
||||
item.unauthorized = descriptor.is_unauthorized();
|
||||
item.has_script_capability = descriptor.has_capability_type(Slic3r::PluginCapabilityType::Script);
|
||||
item.is_loaded = loader.is_plugin_loaded(descriptor.plugin_key);
|
||||
item.loading = loader.is_plugin_load_in_progress(descriptor.plugin_key);
|
||||
if (item.is_loaded) {
|
||||
for (const auto& cap : loader.get_loaded_plugin_capabilities(descriptor.plugin_key)) {
|
||||
if (cap) {
|
||||
item.capabilities.push_back({cap->name, plugin_capability_type_display_name(cap->type),
|
||||
plugin_capability_type_to_string(cap->type), cap->enabled, true, false});
|
||||
if (cap->type == PluginCapabilityType::Script)
|
||||
item.has_script_capability = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (item.capabilities.empty()) {
|
||||
for (PluginCapabilityType type : descriptor.capability_types) {
|
||||
const std::string type_key = plugin_capability_type_to_string(type);
|
||||
const auto existing = std::find_if(item.capabilities.begin(), item.capabilities.end(),
|
||||
[&type_key](const PluginCapabilityView& capability) {
|
||||
return capability.type_key == type_key;
|
||||
});
|
||||
if (existing != item.capabilities.end())
|
||||
continue;
|
||||
item.capabilities.push_back({std::string{}, plugin_capability_type_display_name(type), type_key, false, false, false});
|
||||
}
|
||||
item.is_loaded = manager.is_plugin_loaded(descriptor.plugin_key);
|
||||
item.loading = manager.is_plugin_load_in_progress(descriptor.plugin_key);
|
||||
item.has_script_capability = false;
|
||||
for (const auto& capability : capabilities) {
|
||||
item.capabilities.push_back({capability->name(), plugin_capability_type_display_name(capability->type()),
|
||||
plugin_capability_type_to_string(capability->type()), capability->is_enabled(), true, false});
|
||||
if (capability->type() == PluginCapabilityType::Script)
|
||||
item.has_script_capability = true;
|
||||
}
|
||||
item.sharing_token = descriptor.sharing_token;
|
||||
item.thumbnail_url = descriptor.thumbnail_url;
|
||||
@@ -447,7 +443,7 @@ PluginsDialog::PluginsDialog(wxWindow* parent, wxWindowID id, const wxString&, c
|
||||
: WebViewHostDialog(parent, id, _L("Plugins"), pos, size, style)
|
||||
{ create_webview("web/dialog/PluginsDialog/index.html", _L("Plugins"), wxSize(900, 820), wxSize(760, 715)); }
|
||||
|
||||
PluginsDialog::~PluginsDialog() = default;
|
||||
PluginsDialog::~PluginsDialog() { m_alive->store(false, std::memory_order_release); }
|
||||
|
||||
void PluginsDialog::set_open_terminal_dlg_fn()
|
||||
{
|
||||
@@ -467,8 +463,8 @@ void PluginsDialog::resolve_pending_activation()
|
||||
if (m_activating_plugin_key.empty())
|
||||
return;
|
||||
|
||||
PluginLoader& loader = PluginManager::instance().get_loader();
|
||||
if (loader.is_plugin_load_in_progress(m_activating_plugin_key))
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
if (manager.is_plugin_load_in_progress(m_activating_plugin_key))
|
||||
return; // Still loading: keep the "Activating..." message until the load resolves.
|
||||
|
||||
const std::string plugin_key = m_activating_plugin_key;
|
||||
@@ -478,7 +474,7 @@ void PluginsDialog::resolve_pending_activation()
|
||||
PluginDescriptor descriptor;
|
||||
if (get_descriptor(plugin_key, descriptor) && descriptor.has_error())
|
||||
show_status(wxString::Format(_L("Failed to activate \"%s\"."), plugin_display_name(plugin_key)), "error");
|
||||
else if (loader.is_plugin_loaded(plugin_key))
|
||||
else if (manager.is_plugin_loaded(plugin_key))
|
||||
show_status(wxString::Format(_L("Activated \"%s\"."), plugin_display_name(plugin_key)), "success");
|
||||
// Otherwise it ended up inactive (toggled off or cancelled mid-load): stay silent.
|
||||
}
|
||||
@@ -543,18 +539,13 @@ nlohmann::json PluginsDialog::build_plugins_payload() const
|
||||
response["sort_order"] = to_string(m_plugin_sort_order);
|
||||
response["data"] = nlohmann::json::array();
|
||||
|
||||
const auto& catalog = PluginManager::instance().get_catalog();
|
||||
const auto valid = catalog.get_all_plugin_descriptors();
|
||||
const auto invalid = catalog.get_invalid_plugins();
|
||||
BOOST_LOG_TRIVIAL(info) << "Prepared " << valid.size() + invalid.size() << " plugin rows for Plugins dialog";
|
||||
const auto rows = PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true);
|
||||
BOOST_LOG_TRIVIAL(info) << "Prepared " << rows.size() << " plugin rows for Plugins dialog";
|
||||
|
||||
std::vector<PluginDialogItem> items;
|
||||
items.reserve(valid.size() + invalid.size());
|
||||
items.reserve(rows.size());
|
||||
|
||||
for (const PluginDescriptor& row : valid)
|
||||
items.push_back(build_plugin_dialog_item(row));
|
||||
|
||||
for (const PluginDescriptor& row : invalid)
|
||||
for (const PluginDescriptor& row : rows)
|
||||
items.push_back(build_plugin_dialog_item(row));
|
||||
|
||||
// In-place sort
|
||||
@@ -568,19 +559,19 @@ nlohmann::json PluginsDialog::build_plugins_payload() const
|
||||
|
||||
bool PluginsDialog::get_descriptor(const std::string& plugin_key, PluginDescriptor& descriptor) const
|
||||
{
|
||||
const auto& catalog = PluginManager::instance().get_catalog();
|
||||
if (const PluginDescriptor* valid_descriptor = catalog.find_valid_plugin_descriptor(plugin_key)) {
|
||||
descriptor = *valid_descriptor;
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
if (manager.try_get_valid_plugin_descriptor(plugin_key, descriptor))
|
||||
return true;
|
||||
}
|
||||
return catalog.try_get_invalid_plugin_descriptor(plugin_key, descriptor);
|
||||
return manager.try_get_plugin_descriptor(plugin_key, descriptor) && descriptor.is_invalid_package();
|
||||
}
|
||||
|
||||
std::shared_ptr<LoadedPluginCapability> PluginsDialog::get_capability(const std::string& plugin_key,
|
||||
PluginCapabilityType type,
|
||||
const std::string& capability_name) const
|
||||
std::shared_ptr<PluginCapabilityInterface> PluginsDialog::get_capability(const std::string& plugin_key,
|
||||
PluginCapabilityType type,
|
||||
const std::string& capability_name) const
|
||||
{
|
||||
return PluginManager::instance().get_loader().get_plugin_capability_by_name(plugin_key, type, capability_name);
|
||||
// only_enabled=false: this is an existence check used to gate both enabling and disabling a
|
||||
// capability from the dialog, so a currently-disabled capability must still resolve.
|
||||
return PluginManager::instance().get_plugin_capability(plugin_key, capability_name, type, /*only_enabled=*/false);
|
||||
}
|
||||
|
||||
void PluginsDialog::refresh_plugin_catalog_async(const wxString& title, const wxString& message, bool fetch_cloud)
|
||||
@@ -616,7 +607,7 @@ void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
if (!enabled) {
|
||||
if (!manager.get_loader().unload_plugin(plugin_key)) {
|
||||
if (!manager.unload_plugin(plugin_key)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to unload plugin from Plugins dialog: " << plugin_key;
|
||||
show_status(_L("Failed to unload plugin."), "warn");
|
||||
send_plugins();
|
||||
@@ -632,9 +623,6 @@ void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for capabilities that are currently in use
|
||||
auto loaded_capabilities = manager.get_loader().get_loaded_plugin_capabilities(plugin_key);
|
||||
|
||||
if (!available_actions.can_toggle) {
|
||||
if (dialog_item.unauthorized && available_actions.toggle_installs_cloud_plugin == false && row_data.has_local_package() == false) {
|
||||
const std::string install_error = "Unauthorized cloud plugins cannot be installed.";
|
||||
@@ -666,25 +654,25 @@ void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
|
||||
}
|
||||
}
|
||||
|
||||
if (manager.get_loader().is_plugin_loaded(plugin_key)) {
|
||||
if (manager.is_plugin_loaded(plugin_key)) {
|
||||
send_plugins();
|
||||
return;
|
||||
}
|
||||
|
||||
const PluginDescriptor* descriptor_ptr = manager.get_catalog().find_valid_plugin_descriptor(plugin_key);
|
||||
if (!descriptor_ptr) {
|
||||
PluginDescriptor valid_descriptor;
|
||||
if (!manager.try_get_valid_plugin_descriptor(plugin_key, valid_descriptor)) {
|
||||
show_status(_L("Plugin manifest was not found."), "warn");
|
||||
send_plugins();
|
||||
return;
|
||||
}
|
||||
|
||||
if (manager.get_loader().is_plugin_load_in_progress(plugin_key)) {
|
||||
if (manager.is_plugin_load_in_progress(plugin_key)) {
|
||||
send_plugins();
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Starting plugin load from web dialog: " << plugin_key;
|
||||
manager.get_loader().load_plugin(manager.get_catalog(), plugin_key);
|
||||
manager.load_plugin(plugin_key);
|
||||
send_plugins();
|
||||
// Loading is asynchronous: show a pending message now; resolve_pending_activation() turns it into
|
||||
// Activated/Failed once the loader's completion callback runs update_plugin_dialog_ui().
|
||||
@@ -713,14 +701,14 @@ void PluginsDialog::toggle_plugin_capability(const std::string& plugin_key, Plug
|
||||
return;
|
||||
}
|
||||
|
||||
PluginLoader& loader = PluginManager::instance().get_loader();
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
if (enabled) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Enabling plugin capability: " << plugin_key << " | " << capability_name;
|
||||
loader.enable_capability(plugin_key, capability_name, type);
|
||||
manager.set_capability_enabled(plugin_key, capability_name, true);
|
||||
} else {
|
||||
// check if the capability is currently in use here.
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Disabling plugin capability: " << plugin_key << " | " << capability_name;
|
||||
loader.disable_capability(plugin_key, capability_name, type);
|
||||
manager.set_capability_enabled(plugin_key, capability_name, false);
|
||||
}
|
||||
|
||||
send_plugins();
|
||||
@@ -795,8 +783,7 @@ bool PluginsDialog::install_plugin_package(const std::string& package_path)
|
||||
};
|
||||
|
||||
try {
|
||||
if (!PluginManager::instance().get_loader().inspect_local_plugin_package(package_file, plugin_descriptor, existing_installation,
|
||||
error))
|
||||
if (!PluginManager::instance().inspect_local_plugin_package(package_file, plugin_descriptor, existing_installation, error))
|
||||
return report_inspection_failure();
|
||||
} catch (const std::exception& ex) {
|
||||
error = ex.what();
|
||||
@@ -898,46 +885,18 @@ wxString PluginsDialog::plugin_display_name(const std::string& plugin_key) const
|
||||
|
||||
void PluginsDialog::run_script_plugin(const std::string& plugin_key, const std::string& capability_name)
|
||||
{
|
||||
if (plugin_key.empty() || capability_name.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Ignoring run_script_plugin with an empty plugin key or capability name";
|
||||
send_plugins();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
auto cap = manager.get_loader().get_plugin_capability_by_name(
|
||||
plugin_key, Slic3r::PluginCapabilityType::Script, capability_name);
|
||||
if (!cap) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Ignoring stale run request for missing script capability. plugin_key=" << plugin_key
|
||||
<< " capability_name=" << capability_name;
|
||||
send_plugins();
|
||||
return;
|
||||
}
|
||||
if (!cap->enabled) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Ignoring stale run request for disabled script capability. plugin_key=" << plugin_key
|
||||
<< " capability_name=" << capability_name;
|
||||
send_plugins();
|
||||
return;
|
||||
}
|
||||
|
||||
// A plugin's modal orca.host.ui dialog or the result message box pumps a nested event
|
||||
// loop; the WebView could re-dispatch this command mid-run. Refuse the overlapping run.
|
||||
if (m_script_running) {
|
||||
BOOST_LOG_TRIVIAL(info) << "Ignoring run_script_plugin; a plugin is already running. plugin_key=" << plugin_key;
|
||||
return;
|
||||
}
|
||||
m_script_running = true;
|
||||
ScopeGuard running_guard([this]() { m_script_running = false; });
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Run script plugin requested from Plugins dialog. plugin_key=" << plugin_key
|
||||
<< " capability_name=" << capability_name;
|
||||
std::string error;
|
||||
ExecutionResult result;
|
||||
|
||||
auto complete_with_error = [this, &manager, &plugin_key](const std::string& plugin_error, const wxString& status_message) {
|
||||
const std::string normalized_error = plugin_error.empty() ? "Script plugin failed." : plugin_error;
|
||||
if (!manager.get_catalog().set_plugin_error(plugin_key, normalized_error))
|
||||
if (!manager.set_plugin_error(plugin_key, normalized_error))
|
||||
BOOST_LOG_TRIVIAL(warning) << "Failed to record plugin error. plugin_key=" << plugin_key;
|
||||
|
||||
if (!manager.get_loader().unload_plugin(plugin_key))
|
||||
if (!manager.unload_plugin(plugin_key))
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to unload plugin after script error. plugin_key=" << plugin_key;
|
||||
|
||||
send_plugins();
|
||||
@@ -948,74 +907,12 @@ void PluginsDialog::run_script_plugin(const std::string& plugin_key, const std::
|
||||
show_status(message, "error");
|
||||
};
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!get_descriptor(plugin_key, descriptor)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Cannot run script plugin because manifest was not found. plugin_key=" << plugin_key;
|
||||
complete_with_error("Plugin manifest was not found.", _L("Plugin manifest was not found."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (descriptor.has_error()) {
|
||||
complete_with_error(descriptor.normalized_error(), wxString());
|
||||
return;
|
||||
}
|
||||
|
||||
// Should not reach here, handle for extra safety
|
||||
if (!descriptor.is_metadata_valid()) {
|
||||
std::string plugin_type_str = plugin_capability_type_to_string(descriptor.primary_capability_type());
|
||||
std::string metadata_valid = descriptor.is_metadata_valid() ? "true" : "false";
|
||||
const std::string plugin_error = "Cannot run plugin because its metadata is invalid:\n\tplugin type: " + plugin_type_str +
|
||||
"\n\tmetadata_valid: " + metadata_valid;
|
||||
BOOST_LOG_TRIVIAL(error) << "Cannot run plugin because its metadata is invalid. plugin_key=" << plugin_key
|
||||
<< " is_metadata_valid=" << descriptor.is_metadata_valid()
|
||||
<< " type=" << plugin_capability_type_to_string(descriptor.primary_capability_type());
|
||||
complete_with_error(plugin_error, _L("Only plugins with valid metadata can be run from this dialog."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Should not reach here as non-loaded plugins have disabled run buttons, handle for extra safety
|
||||
if (!manager.get_loader().is_plugin_loaded(plugin_key)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Cannot run script plugin because it is not loaded. plugin_key=" << plugin_key;
|
||||
complete_with_error("Load the script plugin before running it: Cannot run script plugin because it is not loaded.",
|
||||
_L("Load the script plugin before running it."));
|
||||
return;
|
||||
}
|
||||
|
||||
auto plugin = std::dynamic_pointer_cast<Slic3r::ScriptPluginCapability>(cap->instance);
|
||||
if (!plugin) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Loaded plugin does not implement ScriptPluginCapability. plugin_key=" << plugin_key;
|
||||
complete_with_error("The selected plugin is not a runnable script plugin: Loaded plugin does not implement ScriptPluginCapability.",
|
||||
_L("The selected plugin is not a runnable script plugin."));
|
||||
return;
|
||||
}
|
||||
|
||||
std::string error;
|
||||
ExecutionResult result;
|
||||
|
||||
// Script plugins run on the main/UI thread (not a worker). They hold live, non-owning
|
||||
// ModelObject*/ModelVolume*/ModelInstance* aliases into host data and can mint ObjectIDs,
|
||||
// which libslic3r requires on the main thread (ObjectID.hpp's non-atomic s_last_id). Running
|
||||
// here makes those reads/instantiations legal and means nothing mutates the model underneath
|
||||
// a run. The trade-off is that a slow execute() freezes the UI, so the contract is to keep
|
||||
// execute() quick and offload heavy work to the plugin's own threading.Thread. orca.host.ui
|
||||
// calls already no-op their main-thread marshaling here.
|
||||
{
|
||||
wxBusyCursor busy;
|
||||
try {
|
||||
PythonGILState gil;
|
||||
result = plugin->execute();
|
||||
} catch (const std::exception& ex) {
|
||||
error = ex.what();
|
||||
BOOST_LOG_TRIVIAL(error) << "Script plugin execution threw exception. plugin_key=" << plugin_key << " error=" << error;
|
||||
} catch (...) {
|
||||
error = "Unknown error";
|
||||
BOOST_LOG_TRIVIAL(error) << "Script plugin execution threw unknown exception. plugin_key=" << plugin_key;
|
||||
}
|
||||
result = manager.run_script_capability(plugin_key, capability_name, error);
|
||||
}
|
||||
|
||||
if (!error.empty()) {
|
||||
plugin.reset();
|
||||
cap.reset();
|
||||
complete_with_error(error, wxString());
|
||||
return;
|
||||
}
|
||||
@@ -1023,11 +920,8 @@ void PluginsDialog::run_script_plugin(const std::string& plugin_key, const std::
|
||||
BOOST_LOG_TRIVIAL(info) << "Script plugin execution completed. plugin_key=" << plugin_key
|
||||
<< " status=" << static_cast<int>(result.status) << " message=" << result.message << " data=" << result.data;
|
||||
|
||||
const bool failed = result.status == PluginResult::RecoverableError || result.status == PluginResult::FatalError;
|
||||
const bool failed = result.status == PluginResult::RecoverableError || result.status == PluginResult::FatalError || !error.empty();
|
||||
if (failed) {
|
||||
plugin.reset();
|
||||
cap.reset();
|
||||
// complete_with_error normalizes an empty message to "Script plugin failed." and reports via the status bar.
|
||||
complete_with_error(result.message, wxString());
|
||||
return;
|
||||
}
|
||||
@@ -1189,18 +1083,35 @@ void PluginsDialog::reinstall_local_plugin(const std::string& plugin_key)
|
||||
if (plugin_key.empty())
|
||||
return;
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
const bool was_loaded = manager.get_loader().is_plugin_loaded(plugin_key);
|
||||
if (!manager.get_loader().unload_plugin(plugin_key)) {
|
||||
show_status(_L("Failed to unload plugin."), "warn");
|
||||
send_plugins();
|
||||
return;
|
||||
const bool was_loaded = PluginManager::instance().is_plugin_loaded(plugin_key);
|
||||
std::pair<bool, std::string> reload_result{false, ""};
|
||||
try {
|
||||
reload_result = run_with_dialog_wait(
|
||||
[plugin_key, was_loaded]() -> std::pair<bool, std::string> {
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
if (!manager.unload_plugin(plugin_key))
|
||||
return {false, "Failed to unload plugin."};
|
||||
|
||||
manager.load_plugin(plugin_key, false);
|
||||
std::string error;
|
||||
if (!manager.wait_for_plugin_load(plugin_key, std::chrono::minutes(5), error) ||
|
||||
!manager.is_plugin_loaded(plugin_key))
|
||||
return {false, error.empty() ? "Plugin failed to load." : error};
|
||||
|
||||
if (!was_loaded && !manager.unload_plugin(plugin_key))
|
||||
return {false, "Plugin reloaded, but failed to restore the inactive state."};
|
||||
|
||||
return {true, {}};
|
||||
},
|
||||
_L("Reloading plugin"), _L("Reloading plugin"));
|
||||
} catch (const std::exception& ex) {
|
||||
reload_result = {false, ex.what()};
|
||||
} catch (...) {
|
||||
reload_result = {false, "Unknown plugin reload error."};
|
||||
}
|
||||
|
||||
manager.get_loader().load_plugin(manager.get_catalog(), plugin_key, false);
|
||||
|
||||
if (!was_loaded && !manager.get_loader().unload_plugin(plugin_key)) {
|
||||
show_status(_L("Plugin reloaded, but failed to restore the inactive state."), "warn");
|
||||
if (!reload_result.first) {
|
||||
show_status(reload_result.second.empty() ? _L("Failed to reload plugin.") : from_u8(reload_result.second), "warn");
|
||||
send_plugins();
|
||||
return;
|
||||
}
|
||||
@@ -1216,7 +1127,7 @@ void PluginsDialog::reinstall_cloud_plugin(const PluginDescriptor& plugin)
|
||||
return;
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
const bool was_loaded = manager.get_loader().is_plugin_loaded(plugin_key);
|
||||
const bool was_loaded = PluginManager::instance().is_plugin_loaded(plugin_key);
|
||||
|
||||
std::string error;
|
||||
if (plugin.has_local_package()) {
|
||||
@@ -1226,7 +1137,7 @@ void PluginsDialog::reinstall_cloud_plugin(const PluginDescriptor& plugin)
|
||||
return;
|
||||
}
|
||||
|
||||
manager.get_catalog().update_cloud_catalog(std::vector<PluginDescriptor>{as_cloud_only_descriptor(plugin)});
|
||||
manager.update_cloud_catalog(std::vector<PluginDescriptor>{as_cloud_only_descriptor(plugin)});
|
||||
}
|
||||
|
||||
if (!install_cloud_plugin(plugin_key, plugin.version, from_u8(plugin.name))) {
|
||||
@@ -1235,7 +1146,30 @@ void PluginsDialog::reinstall_cloud_plugin(const PluginDescriptor& plugin)
|
||||
}
|
||||
|
||||
if (was_loaded) {
|
||||
manager.get_loader().load_plugin(manager.get_catalog(), plugin_key);
|
||||
std::pair<bool, std::string> reload_result{false, ""};
|
||||
try {
|
||||
reload_result = run_with_dialog_wait(
|
||||
[plugin_key]() -> std::pair<bool, std::string> {
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
manager.load_plugin(plugin_key);
|
||||
std::string error;
|
||||
if (!manager.wait_for_plugin_load(plugin_key, std::chrono::minutes(5), error) ||
|
||||
!manager.is_plugin_loaded(plugin_key))
|
||||
return {false, error.empty() ? "Plugin failed to load." : error};
|
||||
return {true, {}};
|
||||
},
|
||||
_L("Reloading plugin"), _L("Reloading plugin"));
|
||||
} catch (const std::exception& ex) {
|
||||
reload_result = {false, ex.what()};
|
||||
} catch (...) {
|
||||
reload_result = {false, "Unknown plugin reload error."};
|
||||
}
|
||||
|
||||
if (!reload_result.first) {
|
||||
show_status(reload_result.second.empty() ? _L("Failed to reload plugin.") : from_u8(reload_result.second), "warn");
|
||||
send_plugins();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
send_plugins();
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "PluginSort.hpp"
|
||||
#include "slic3r/plugin/PluginDescriptor.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
@@ -17,6 +19,7 @@
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <wx/evtloop.h>
|
||||
#include <wx/app.h>
|
||||
#include <wx/progdlg.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/timer.h>
|
||||
@@ -25,7 +28,7 @@ class wxTimer;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
struct LoadedPluginCapability;
|
||||
class PluginCapabilityInterface;
|
||||
enum class PluginCapabilityType;
|
||||
|
||||
namespace GUI {
|
||||
@@ -55,7 +58,7 @@ private:
|
||||
nlohmann::json build_plugins_payload() const;
|
||||
|
||||
bool get_descriptor(const std::string& plugin_key, Slic3r::PluginDescriptor& descriptor) const;
|
||||
std::shared_ptr<LoadedPluginCapability> get_capability(const std::string& plugin_key, PluginCapabilityType type, const std::string& capability_name) const;
|
||||
std::shared_ptr<PluginCapabilityInterface> get_capability(const std::string& plugin_key, PluginCapabilityType type, const std::string& capability_name) const;
|
||||
|
||||
void refresh_plugin_catalog_async(const wxString& title, const wxString& message, bool fetch_cloud);
|
||||
void refresh_plugins();
|
||||
@@ -91,34 +94,51 @@ private:
|
||||
const wxString& title,
|
||||
const wxString& message,
|
||||
int maximum = 100,
|
||||
int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE)
|
||||
int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE,
|
||||
bool finish_after_dialog_destroyed = false)
|
||||
{
|
||||
const auto alive = m_alive;
|
||||
wxProgressDialog* progress = new wxProgressDialog(title, message, maximum, this, style);
|
||||
wxTimer* timer = new wxTimer();
|
||||
|
||||
timer->Bind(wxEVT_TIMER, [progress, message](wxTimerEvent&) {
|
||||
if (progress)
|
||||
timer->Bind(wxEVT_TIMER, [alive, progress, message](wxTimerEvent&) {
|
||||
if (alive->load(std::memory_order_acquire) && progress)
|
||||
progress->Pulse(message);
|
||||
});
|
||||
|
||||
timer->Start(100);
|
||||
|
||||
std::thread([this,
|
||||
std::thread([alive,
|
||||
progress,
|
||||
timer,
|
||||
run = std::forward<Run>(run),
|
||||
on_finish = std::forward<OnFinish>(on_finish)]() mutable {
|
||||
on_finish = std::forward<OnFinish>(on_finish),
|
||||
finish_after_dialog_destroyed]() mutable {
|
||||
try {
|
||||
run();
|
||||
} catch (const std::exception& ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed: " << ex.what();
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed with an unknown exception";
|
||||
}
|
||||
|
||||
CallAfter([progress, timer, on_finish = std::move(on_finish)]() mutable {
|
||||
if (wxTheApp == nullptr)
|
||||
return;
|
||||
|
||||
wxTheApp->CallAfter([alive,
|
||||
progress,
|
||||
timer,
|
||||
on_finish = std::move(on_finish),
|
||||
finish_after_dialog_destroyed]() mutable {
|
||||
timer->Stop();
|
||||
delete timer;
|
||||
|
||||
progress->Destroy();
|
||||
on_finish();
|
||||
if (alive->load(std::memory_order_acquire)) {
|
||||
progress->Destroy();
|
||||
on_finish();
|
||||
} else if (finish_after_dialog_destroyed) {
|
||||
on_finish();
|
||||
}
|
||||
});
|
||||
}).detach();
|
||||
}
|
||||
@@ -157,7 +177,7 @@ private:
|
||||
state->exception = std::current_exception();
|
||||
}
|
||||
},
|
||||
on_finish, title, message, maximum, style);
|
||||
on_finish, title, message, maximum, style, true);
|
||||
|
||||
if (!finished)
|
||||
loop.Run();
|
||||
@@ -190,7 +210,7 @@ private:
|
||||
state->exception = std::current_exception();
|
||||
}
|
||||
},
|
||||
on_finish, title, message, maximum, style);
|
||||
on_finish, title, message, maximum, style, true);
|
||||
|
||||
if (!finished)
|
||||
loop.Run();
|
||||
@@ -210,6 +230,7 @@ private:
|
||||
}
|
||||
|
||||
std::function<void()> m_open_terminal_dlg_fn;
|
||||
std::shared_ptr<std::atomic<bool>> m_alive = std::make_shared<std::atomic<bool>>(true);
|
||||
PluginSortKey m_plugin_sort_key = PluginSortKey::None;
|
||||
PluginSortOrder m_plugin_sort_order = PluginSortOrder::Asc;
|
||||
|
||||
|
||||
@@ -273,7 +273,7 @@ static void run_post_process_plugins(const ConfigOptionStrings& capabilities,
|
||||
// Hand the plugin its own [tool.orcaslicer.plugin.settings] as ctx.params (same plugin_key the
|
||||
// capability was resolved by), mirroring the in-pipeline dispatcher in GUI_App.cpp.
|
||||
const std::string plugin_key = ref.uuid.empty() ? ref.name : ref.uuid;
|
||||
ctx.params = PluginManager::instance().get_loader().get_plugin_settings(plugin_key);
|
||||
ctx.params = PluginManager::instance().get_plugin_settings(plugin_key);
|
||||
|
||||
ExecutionResult exec_result;
|
||||
try {
|
||||
|
||||
@@ -229,35 +229,33 @@ std::unique_ptr<NetworkAgent> create_agent_from_config(const std::string& log_di
|
||||
|
||||
void NetworkAgentFactory::register_python_plugin(const std::string& plugin_key)
|
||||
{
|
||||
auto capabilities = PluginManager::instance().get_loader().get_plugin_capabilities_by_type(
|
||||
plugin_key, PluginCapabilityType::PrinterConnection);
|
||||
for (const auto& capability : capabilities)
|
||||
if (capability && capability->enabled)
|
||||
register_python_printer_agent(plugin_key, capability->name);
|
||||
for (const auto& capability : PluginManager::instance().get_plugin_capabilities(plugin_key, PluginCapabilityType::PrinterConnection,
|
||||
/*only_enabled=*/true))
|
||||
register_python_printer_agent(plugin_key, capability->name());
|
||||
}
|
||||
|
||||
void NetworkAgentFactory::register_python_printer_agent(const std::string& plugin_key, const std::string& capability_name)
|
||||
{
|
||||
PluginManager& plugin_manager = PluginManager::instance();
|
||||
|
||||
auto cap = plugin_manager.get_loader().get_plugin_capability_by_name(plugin_key, PluginCapabilityType::PrinterConnection,
|
||||
capability_name);
|
||||
auto cap = plugin_manager.get_plugin_capability(plugin_key, capability_name, PluginCapabilityType::PrinterConnection,
|
||||
/*only_enabled=*/false);
|
||||
if (!cap) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Printer-agent capability '" << capability_name << "' not found for plugin '" << plugin_key << "'";
|
||||
return;
|
||||
}
|
||||
if (!cap->enabled) {
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!plugin_manager.try_get_plugin_descriptor(plugin_key, descriptor)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Could not find descriptor for plugin key: '" << plugin_key;
|
||||
return;
|
||||
}
|
||||
if (!cap->is_enabled()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Printer-agent capability '" << capability_name << "' is disabled for plugin '" << plugin_key << "'";
|
||||
return;
|
||||
}
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!plugin_manager.get_catalog().try_get_plugin_descriptor(plugin_key, descriptor)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Could not find descriptor for plugin key: '" << plugin_key;
|
||||
return;
|
||||
}
|
||||
|
||||
auto plugin = std::dynamic_pointer_cast<PrinterAgentPluginCapability>(cap->instance);
|
||||
auto plugin = std::dynamic_pointer_cast<PrinterAgentPluginCapability>(cap);
|
||||
|
||||
if (!plugin) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Loaded plugin capability '" << capability_name << "' is not a printer-agent plugin";
|
||||
@@ -305,9 +303,10 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
|
||||
|
||||
// Python work above may allow disable/unload/reload to replace this capability.
|
||||
// Re-resolve without holding the registry mutex to avoid lock inversion and reentrancy.
|
||||
auto current_cap = plugin_manager.get_loader().get_plugin_capability_by_name(plugin_key, PluginCapabilityType::PrinterConnection,
|
||||
capability_name);
|
||||
if (current_cap != cap || !current_cap->enabled) {
|
||||
// only_enabled defaults to true here, so a capability that changed identity (reload) or was
|
||||
// merely disabled both surface the same way: current_cap no longer equals cap.
|
||||
auto current_cap = plugin_manager.get_plugin_capability(plugin_key, capability_name, PluginCapabilityType::PrinterConnection);
|
||||
if (current_cap != cap) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "Printer-agent capability '" << capability_name << "' from plugin '" << plugin_key
|
||||
<< "' changed or was disabled during registration";
|
||||
return;
|
||||
@@ -322,7 +321,9 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
|
||||
[&plugin_key, &capability_name](const auto& entry) {
|
||||
return entry.first.first == plugin_key && entry.first.second == capability_name;
|
||||
});
|
||||
if (token_it == s_python_printer_agent_registration_tokens.end() || token_it->second != registration_token || !cap->enabled) {
|
||||
const bool cap_still_enabled = cap->is_enabled();
|
||||
if (token_it == s_python_printer_agent_registration_tokens.end() || token_it->second != registration_token ||
|
||||
!cap_still_enabled) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "Printer-agent capability '" << capability_name << "' from plugin '" << plugin_key
|
||||
<< "' was disabled before registry commit";
|
||||
return;
|
||||
@@ -448,6 +449,18 @@ void NetworkAgentFactory::deregister_python_printer_agent(const std::string& plu
|
||||
if (cached_agent)
|
||||
cached_agent->disconnect_printer();
|
||||
|
||||
// The GUI may still own the active capability separately from the factory cache. Clear that
|
||||
// handle before the plugin module is torn down, otherwise NetworkAgent can retain a Python
|
||||
// object whose implementation is about to be unloaded.
|
||||
if (!agent_id.empty() && wxTheApp) {
|
||||
NetworkAgent* network_agent = GUI::wxGetApp().getAgent();
|
||||
if (network_agent) {
|
||||
const auto active_agent = network_agent->get_printer_agent();
|
||||
if (active_agent && active_agent->get_agent_info().id == agent_id)
|
||||
network_agent->set_printer_agent(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Deregistered Python printer-agent capability '" << capability_name << "' from plugin '"
|
||||
<< plugin_key << "' with agent ID '" << agent_id << "'";
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include <cstdlib>
|
||||
@@ -44,8 +45,16 @@ bool is_inside_allowed_root(const std::filesystem::path& candidate, const std::f
|
||||
auto root_it = canon_root.begin();
|
||||
auto root_end = canon_root.end();
|
||||
|
||||
const auto same_component = [](const fs::path& lhs, const fs::path& rhs) {
|
||||
#ifdef _WIN32
|
||||
return boost::algorithm::iequals(lhs.native(), rhs.native());
|
||||
#else
|
||||
return lhs == rhs;
|
||||
#endif
|
||||
};
|
||||
|
||||
// Consume matching components
|
||||
while (root_it != root_end && cand_it != cand_end && *root_it == *cand_it) {
|
||||
while (root_it != root_end && cand_it != cand_end && same_component(*root_it, *cand_it)) {
|
||||
++root_it;
|
||||
++cand_it;
|
||||
}
|
||||
|
||||
@@ -1,714 +0,0 @@
|
||||
#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
|
||||
@@ -1,76 +0,0 @@
|
||||
#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
|
||||
@@ -56,7 +56,6 @@ struct PluginDescriptor
|
||||
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
|
||||
@@ -68,6 +67,10 @@ struct PluginDescriptor
|
||||
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.
|
||||
// Package auto-load flag, read from .install_state.json. Defaults to FALSE: a package with no
|
||||
// sidecar has never been installed through Orca and carries no auto-load intent, so it must not
|
||||
// be loaded at startup. Installing a package writes the sidecar with enabled = true.
|
||||
bool enabled = false;
|
||||
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.
|
||||
|
||||
@@ -76,22 +79,13 @@ struct PluginDescriptor
|
||||
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()); }
|
||||
// A package that exists on disk but whose manifest could not be parsed.
|
||||
//
|
||||
// Deliberately not just !is_metadata_valid(): a cloud plugin that is merely available
|
||||
// (subscribed but not installed) also has metadata_valid == false — the cloud service never
|
||||
// sets it — yet it is a valid catalog row, not a broken package. What separates the two is
|
||||
// whether there is a local package behind the descriptor at all.
|
||||
bool is_invalid_package() const { return !metadata_valid && has_local_package(); }
|
||||
|
||||
std::string normalized_error() const
|
||||
{
|
||||
@@ -159,8 +153,6 @@ inline void apply_plugin_metadata_fallbacks(PluginDescriptor& target, const Plug
|
||||
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())
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
#include "PluginFsUtils.hpp"
|
||||
|
||||
#include "PythonFileUtils.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <utility>
|
||||
|
||||
#include "PluginAuditManager.hpp"
|
||||
|
||||
@@ -107,4 +110,148 @@ bool delete_plugin_root(const boost::filesystem::path& resolved_root,
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ── Discovery ───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
namespace {
|
||||
|
||||
// Derive a discovered descriptor's operational plugin_key: the cloud 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 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());
|
||||
}
|
||||
|
||||
void scan_plugin_directory(const std::string& dir_path, std::vector<PluginDescriptor>& out)
|
||||
{
|
||||
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);
|
||||
|
||||
// No usable entry file: keep the package as an invalid row so the UI can show it and
|
||||
// its error, rather than dropping it silently.
|
||||
if (entry_path.empty()) {
|
||||
descriptor.set_error(entry_error);
|
||||
read_install_state(plugin_dir, descriptor);
|
||||
assign_discovered_plugin_key(descriptor, plugin_dir);
|
||||
out.push_back(std::move(descriptor));
|
||||
BOOST_LOG_TRIVIAL(warning) << "Invalid plugin package: " << plugin_dir.string() << " - " << out.back().error;
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string meta_error;
|
||||
const bool is_wheel = entry_path.extension() == ".whl";
|
||||
const bool parsed = is_wheel ? read_wheel_plugin_metadata(entry_path, descriptor, meta_error) :
|
||||
read_python_plugin_metadata(entry_path, descriptor, meta_error);
|
||||
if (!parsed) {
|
||||
descriptor.set_error(meta_error);
|
||||
read_install_state(plugin_dir, descriptor);
|
||||
assign_discovered_plugin_key(descriptor, entry_path);
|
||||
out.push_back(std::move(descriptor));
|
||||
BOOST_LOG_TRIVIAL(warning) << (is_wheel ? "Invalid wheel plugin: " : "Invalid .py plugin: ")
|
||||
<< plugin_dir.string() << " - " << out.back().error;
|
||||
continue;
|
||||
}
|
||||
|
||||
descriptor.entry_path = entry_path.string();
|
||||
descriptor.set_metadata_valid(true);
|
||||
descriptor.clear_error();
|
||||
|
||||
// Cloud identity and the package-level auto-load flag. plugin_key is always derived
|
||||
// below, never read from the sidecar.
|
||||
read_install_state(plugin_dir, descriptor);
|
||||
assign_discovered_plugin_key(descriptor, entry_path);
|
||||
|
||||
out.push_back(std::move(descriptor));
|
||||
BOOST_LOG_TRIVIAL(info) << "Discovered plugin: " << out.back().name << " (version: " << out.back().version << ")";
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Error scanning directory " << dir_path << ": " << ex.what();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<std::string> get_plugin_directories(const std::string& cloud_user_id)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
std::vector<std::string> dirs;
|
||||
|
||||
// Creates the directory when missing — callers (notably the install path) rely on it existing.
|
||||
auto add_or_create_dir = [&dirs](const fs::path& path) {
|
||||
if (fs::exists(path) && fs::is_directory(path)) {
|
||||
dirs.push_back(path.string());
|
||||
return;
|
||||
}
|
||||
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_user_id.empty())
|
||||
add_or_create_dir(fs::path(get_cloud_plugin_dir(cloud_user_id)));
|
||||
|
||||
return dirs;
|
||||
}
|
||||
|
||||
std::vector<PluginDescriptor> discover_plugin_packages(const std::vector<std::string>& dirs, std::string& error)
|
||||
{
|
||||
error.clear();
|
||||
|
||||
const auto start_time = std::chrono::steady_clock::now();
|
||||
std::vector<PluginDescriptor> discovered;
|
||||
|
||||
try {
|
||||
BOOST_LOG_TRIVIAL(info) << "Scanning " << dirs.size() << " plugin directories...";
|
||||
|
||||
for (const std::string& dir : dirs)
|
||||
scan_plugin_directory(dir, discovered);
|
||||
|
||||
const auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start_time);
|
||||
BOOST_LOG_TRIVIAL(info) << "Plugin discovery completed in " << duration.count() << "ms. Found " << discovered.size()
|
||||
<< " plugin manifests";
|
||||
} catch (const std::exception& ex) {
|
||||
error = std::string("Plugin discovery failed: ") + ex.what();
|
||||
BOOST_LOG_TRIVIAL(error) << error;
|
||||
} catch (...) {
|
||||
error = "Plugin discovery failed: unknown error";
|
||||
BOOST_LOG_TRIVIAL(error) << error;
|
||||
}
|
||||
|
||||
return discovered;
|
||||
}
|
||||
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -32,4 +32,20 @@ bool delete_plugin_root(const boost::filesystem::path& resolved_root,
|
||||
const std::string& plugin_id,
|
||||
std::string& error);
|
||||
|
||||
// The directories plugins are discovered from: {data_dir}/orca_plugins, plus the per-user cloud
|
||||
// directory when cloud_user_id is non-empty.
|
||||
//
|
||||
// NOTE: this CREATES the directories if they do not exist. Callers rely on that side effect — the
|
||||
// install path writes into them without creating them itself.
|
||||
std::vector<std::string> get_plugin_directories(const std::string& cloud_user_id);
|
||||
|
||||
// Scan the given directories for plugin packages (manifest-only; no Python is loaded, no state is
|
||||
// kept). Pure: directories in, descriptors out.
|
||||
//
|
||||
// Returns every package found, valid and invalid alike: a package whose manifest could not be
|
||||
// parsed comes back with metadata_valid == false and its error set (descriptor.is_invalid_package()).
|
||||
// The package-level auto-load flag is read from each package's .install_state.json into
|
||||
// descriptor.enabled. Capabilities are NOT discovered here — a package has none until it is loaded.
|
||||
std::vector<PluginDescriptor> discover_plugin_packages(const std::vector<std::string>& dirs, std::string& error);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace Slic3r::plugin_hooks {
|
||||
namespace {
|
||||
@@ -25,12 +26,16 @@ void install_capability_resolver()
|
||||
{
|
||||
ConfigBase::set_resolve_capability_fn([](const std::string& cap_name, const std::string& cap_type) {
|
||||
PluginManager& plugin_mgr = PluginManager::instance();
|
||||
auto plugin_cap = plugin_mgr.get_loader().try_get_plugin_capability_by_name_and_type(cap_name, plugin_capability_type_from_string(cap_type));
|
||||
const PluginCapabilityType type = plugin_capability_type_from_string(cap_type);
|
||||
// only_enabled = false: this resolves the reference a preset STORES, which must stay
|
||||
// resolvable whether or not the user currently has the capability enabled. Filtering on
|
||||
// the enable flag here would quietly drop the reference out of the preset's manifest.
|
||||
auto plugin_cap = plugin_mgr.get_plugin_capability(cap_name, type, /*only_enabled=*/false);
|
||||
if (!plugin_cap)
|
||||
return std::string();
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!plugin_mgr.get_catalog().try_get_plugin_descriptor(plugin_cap->plugin_key, descriptor))
|
||||
if (!plugin_mgr.try_get_plugin_descriptor_for_capability(cap_name, type, descriptor))
|
||||
return std::string();
|
||||
|
||||
// Cloud plugins are resolved at runtime via the UUID in the middle field, so the first
|
||||
@@ -62,11 +67,17 @@ void install_slicing_pipeline_hook()
|
||||
execute_capabilities_from_refs<SlicingPipelinePluginCapability>(
|
||||
*caps, plugs, PluginCapabilityType::SlicingPipeline,
|
||||
[&](std::shared_ptr<SlicingPipelinePluginCapability> cap, const PluginCapabilityRef& ref) {
|
||||
const std::string plugin_key = ref.uuid.empty() ? ref.name : ref.uuid;
|
||||
ExecutionResult r;
|
||||
try {
|
||||
// Read manager state before acquiring the GIL so this path does not take
|
||||
// m_mutex in the opposite order to plugin teardown.
|
||||
const auto plugin_settings = PluginManager::instance().get_plugin_settings(plugin_key);
|
||||
// GIL is acquired per capability (not once for the whole dispatch) so it
|
||||
// is released between capabilities.
|
||||
PythonGILState gil;
|
||||
if (!gil)
|
||||
throw std::runtime_error("Python interpreter is shutting down");
|
||||
// throw_if_canceled() is protected on PrintBase; canceled() is the public
|
||||
// equivalent check (same cancel flag), so honor cancellation via it.
|
||||
if (print.canceled())
|
||||
@@ -78,8 +89,7 @@ void install_slicing_pipeline_hook()
|
||||
ctx.object = object;
|
||||
// hand the plugin its own [tool.orcaslicer.plugin.settings] as ctx.params
|
||||
// (same plugin_key the capability was resolved by, so it always matches).
|
||||
const std::string plugin_key = ref.uuid.empty() ? ref.name : ref.uuid;
|
||||
ctx.params = PluginManager::instance().get_loader().get_plugin_settings(plugin_key);
|
||||
ctx.params = plugin_settings;
|
||||
r = cap->execute(ctx);
|
||||
} catch (const CanceledException&) {
|
||||
throw; // cancellation must reach process(), never become a slicing error
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,211 +4,45 @@
|
||||
|
||||
#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();
|
||||
};
|
||||
|
||||
struct Plugin;
|
||||
} // 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::plugin_loader {
|
||||
bool load(const PluginDescriptor& descriptor,
|
||||
bool skip_deps,
|
||||
const std::vector<std::string>& capabilities_to_enable,
|
||||
const std::function<std::string(const Plugin&)>& registry_precheck,
|
||||
Plugin& out,
|
||||
std::string& error);
|
||||
|
||||
namespace Slic3r {
|
||||
// Run on_unload() on every capability, drop them, and release the module. Safe to call on a
|
||||
// not-loaded Plugin, and safe after the interpreter has been finalized (in which case the module
|
||||
// reference is deliberately leaked rather than DECREF'd).
|
||||
void unload(Plugin& plugin);
|
||||
|
||||
class PluginLoader
|
||||
{
|
||||
public:
|
||||
enum CallbackType {
|
||||
Load,
|
||||
Unload,
|
||||
};
|
||||
// Install Python dependencies into the shared packages directory via the bundled uv. Blocks, with
|
||||
// a 120 s cap.
|
||||
bool install_packages(const std::vector<std::string>& pkgs, std::string& error);
|
||||
|
||||
using PluginLoadInProgress = std::unordered_set<std::string>;
|
||||
using PluginLoadErrors = std::unordered_map<std::string, std::string>;
|
||||
// Read a local .py/.whl package's metadata without installing it, and report whether a package is
|
||||
// already installed under the same directory.
|
||||
bool inspect_local_plugin_package(const boost::filesystem::path& filepath,
|
||||
PluginDescriptor& plugin_descriptor,
|
||||
bool& existing_installation,
|
||||
std::string& error);
|
||||
|
||||
using PluginLifecycleCompleteFn = std::function<void(const std::string&)>;
|
||||
// Copy a .py/.whl package into the plugin directory (the per-user cloud directory when the
|
||||
// descriptor carries a cloud UUID and cloud_user_id is non-empty) and write its
|
||||
// .install_state.json sidecar, backing up and restoring any existing installation on failure.
|
||||
bool install_plugin(const boost::filesystem::path& filepath,
|
||||
const std::string& cloud_user_id,
|
||||
PluginDescriptor& plugin_descriptor,
|
||||
std::string& error);
|
||||
bool install_plugin(const boost::filesystem::path& filepath, const std::string& cloud_user_id, std::string& error);
|
||||
|
||||
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;
|
||||
// the plugin's [tool.orcaslicer.plugin.settings] table (empty if the plugin is unknown).
|
||||
std::map<std::string, std::string> get_plugin_settings(const std::string& plugin_key) 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
|
||||
} // namespace Slic3r::plugin_loader
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,28 +5,110 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <libslic3r/Config.hpp>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include <pybind11/embed.h>
|
||||
|
||||
#include "CloudPluginService.hpp"
|
||||
#include "PluginCatalog.hpp"
|
||||
#include "PluginLoader.hpp"
|
||||
#include "PluginFsUtils.hpp"
|
||||
#include "PluginDescriptor.hpp"
|
||||
#include "PluginLoader.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class OrcaCloudServiceAgent;
|
||||
|
||||
// Identity of a single capability, as published to lifecycle subscribers. Purely a message
|
||||
// payload — capabilities are looked up by (plugin_key, name) linear scan, so unlike the registry
|
||||
// key this replaces, it needs no hash and no equality.
|
||||
struct PluginCapabilityId
|
||||
{
|
||||
PluginCapabilityType type = PluginCapabilityType::Unknown;
|
||||
std::string name;
|
||||
std::string plugin_key; // owning package
|
||||
};
|
||||
|
||||
// One discovered plugin package: one .py/.whl file -> one descriptor + one Python module +
|
||||
// N materialized capabilities.
|
||||
//
|
||||
// PluginManager holds every discovered plugin in a single std::vector<Plugin>, loaded or not;
|
||||
// module == nullptr means the package was discovered but is not loaded. "What exists" and
|
||||
// "what is live" are therefore the same vector, distinguished by a null check.
|
||||
//
|
||||
// A capability carries its own name, type and enable flag (PluginCapabilityInterface), cached by
|
||||
// the loader at materialization, and its owning package via audit_plugin_key(). Capability state
|
||||
// therefore lives exactly as long as the capability: a package that is not loaded has none. The
|
||||
// only durable record is the .install_state.json sidecar, which the loader seeds enable flags from.
|
||||
// The descriptor holds package state only.
|
||||
struct Plugin
|
||||
{
|
||||
PluginDescriptor descriptor; // All plugin state and metadata lives here.
|
||||
PyObject* module = nullptr; // Python module object, shared by all capabilities. nullptr => not loaded.
|
||||
std::string module_name; // Root name used in sys.modules, including package submodules.
|
||||
std::vector<std::string> plugin_sys_paths; // Paths added for this plugin load.
|
||||
std::vector<std::string> plugin_modules; // Plugin-originated sys.modules entries.
|
||||
|
||||
// Materialized capability instances. Empty unless loaded.
|
||||
std::vector<std::shared_ptr<PluginCapabilityInterface>> capabilities;
|
||||
|
||||
Plugin() = default;
|
||||
Plugin(const Plugin&) = delete;
|
||||
Plugin& operator=(const Plugin&) = delete;
|
||||
|
||||
// Move transfers module ownership; the moved-from package must not Py_DECREF it. Move
|
||||
// assignment is required: the manager's vector is erased from and reordered.
|
||||
Plugin(Plugin&& other) noexcept
|
||||
: descriptor(std::move(other.descriptor)),
|
||||
module(other.module),
|
||||
module_name(std::move(other.module_name)),
|
||||
plugin_sys_paths(std::move(other.plugin_sys_paths)),
|
||||
plugin_modules(std::move(other.plugin_modules)),
|
||||
capabilities(std::move(other.capabilities))
|
||||
{
|
||||
other.module = nullptr;
|
||||
}
|
||||
|
||||
Plugin& operator=(Plugin&& other) noexcept
|
||||
{
|
||||
if (this != &other) {
|
||||
release_module();
|
||||
descriptor = std::move(other.descriptor);
|
||||
module = other.module;
|
||||
module_name = std::move(other.module_name);
|
||||
plugin_sys_paths = std::move(other.plugin_sys_paths);
|
||||
plugin_modules = std::move(other.plugin_modules);
|
||||
capabilities = std::move(other.capabilities);
|
||||
other.module = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
~Plugin() { release_module(); }
|
||||
|
||||
bool is_loaded() const { return module != nullptr; }
|
||||
|
||||
// Removes the module namespace and plugin-owned sys.path entries before dropping the module
|
||||
// reference. If the interpreter is already finalized, PythonInterpreter deliberately leaves
|
||||
// the raw reference untouched because there is no safe way to DECREF it.
|
||||
void release_module();
|
||||
};
|
||||
|
||||
class PluginManager
|
||||
{
|
||||
public:
|
||||
using PluginLifecycleCompleteFn = std::function<void(const std::string& /*plugin_key*/)>;
|
||||
using CapabilityLifecycleFn = std::function<void(const PluginCapabilityId&)>;
|
||||
|
||||
static PluginManager& instance();
|
||||
|
||||
~PluginManager();
|
||||
@@ -37,35 +119,86 @@ public:
|
||||
// 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.
|
||||
// Reject new plugin loads. Called early in app teardown, before shutdown() drains.
|
||||
void set_shutting_down();
|
||||
|
||||
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.
|
||||
// Manually trigger a manifest-only rescan. 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; }
|
||||
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;
|
||||
|
||||
|
||||
std::vector<PluginDescriptor> get_plugin_descriptors(bool include_invalid = false) const;
|
||||
// Packages that materialize at least one capability of this type. A package's capability types
|
||||
// are only known once it has been loaded, so a never-loaded package never matches.
|
||||
bool try_get_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& out) const;
|
||||
// Same, but only for packages that are loadable (i.e. not an invalid package).
|
||||
bool try_get_valid_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& out) const;
|
||||
// Packages whose .install_state.json marks them for auto-load.
|
||||
std::vector<std::string> get_enabled_plugin_keys() const;
|
||||
// The package owning a loaded capability, for the by-name dispatch path.
|
||||
bool try_get_plugin_descriptor_for_capability(const std::string& capability_name,
|
||||
PluginCapabilityType type,
|
||||
PluginDescriptor& out) const;
|
||||
|
||||
std::vector<std::shared_ptr<PluginCapabilityInterface>> get_plugin_capabilities(
|
||||
const std::string& plugin_key = "", // "" => all plugins
|
||||
PluginCapabilityType type = PluginCapabilityType::Unknown, // Unknown => all types
|
||||
bool only_enabled = true) const;
|
||||
std::shared_ptr<PluginCapabilityInterface> get_plugin_capability(const std::string& plugin_key,
|
||||
const std::string& capability_name,
|
||||
PluginCapabilityType type = PluginCapabilityType::Unknown,
|
||||
bool only_enabled = true) const;
|
||||
std::shared_ptr<PluginCapabilityInterface> get_plugin_capability(const std::string& capability_name,
|
||||
PluginCapabilityType type = PluginCapabilityType::Unknown,
|
||||
bool only_enabled = true) const;
|
||||
|
||||
void load_plugin(const std::string& plugin_key, bool skip_deps = false, std::vector<std::string> capabilities_to_enable = {});
|
||||
bool unload_plugin(const std::string& plugin_key);
|
||||
void unload_all_plugins();
|
||||
void unload_cloud_plugins();
|
||||
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; // false on timeout
|
||||
bool wait_for_plugin_load(const std::string& plugin_key, std::chrono::milliseconds timeout, std::string& error) const;
|
||||
bool cancel_plugin_load(const std::string& plugin_key);
|
||||
std::string get_plugin_load_error(const std::string& plugin_key) const;
|
||||
|
||||
void set_capability_enabled(const std::string& plugin_key, const std::string& capability_name, bool enabled);
|
||||
|
||||
// The plugin's [tool.orcaslicer.plugin.settings] table (empty if the plugin is unknown). This will be replaced once the config is merged in.
|
||||
std::map<std::string, std::string> get_plugin_settings(const std::string& plugin_key) const;
|
||||
// Sets the cloud user whose _subscribed/{user_id} directory is scanned and installed into.
|
||||
void set_cloud_user(const std::string& user_id);
|
||||
|
||||
void subscribe_on_load_callback(PluginLifecycleCompleteFn fn);
|
||||
void subscribe_on_unload_callback(PluginLifecycleCompleteFn fn);
|
||||
void subscribe_on_capability_load_callback(CapabilityLifecycleFn fn);
|
||||
void subscribe_on_capability_unload_callback(CapabilityLifecycleFn fn);
|
||||
|
||||
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 inspect_local_plugin_package(const boost::filesystem::path& filepath,
|
||||
PluginDescriptor& plugin_descriptor,
|
||||
bool& existing_installation,
|
||||
std::string& error) const;
|
||||
|
||||
bool set_plugin_error(const std::string& plugin_key, std::string error);
|
||||
bool clear_plugin_error(const std::string& plugin_key);
|
||||
|
||||
void fetch_plugins_from_cloud(std::vector<std::string>* out_not_found = nullptr, std::vector<std::string>* out_unauthorized = nullptr);
|
||||
void update_cloud_catalog(const std::vector<PluginDescriptor>& cloud_list);
|
||||
void clear_cloud_plugin_catalog();
|
||||
|
||||
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);
|
||||
// If the version is empty, take the latest version.
|
||||
bool update_cloud_plugin(const std::string& plugin_key, std::string& error, std::string version = "");
|
||||
|
||||
@@ -75,32 +208,101 @@ public:
|
||||
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);
|
||||
|
||||
ExecutionResult run_script_capability(const std::string& plugin_key, const std::string& capability_name, std::string& error);
|
||||
|
||||
private:
|
||||
PluginManager() = default;
|
||||
PluginManager(const PluginManager&) = delete;
|
||||
PluginManager& operator=(const PluginManager&) = delete;
|
||||
|
||||
enum class CallbackType { Load, Unload };
|
||||
|
||||
// Caller holds m_mutex. The returned pointer is invalidated by any push_back into m_plugins,
|
||||
// so it must never escape the lock.
|
||||
Plugin* find_plugin_locked(const std::string& plugin_key);
|
||||
const Plugin* find_plugin_locked(const std::string& plugin_key) const;
|
||||
|
||||
void load_plugin_impl(const std::string& plugin_key, bool skip_deps, const std::vector<std::string>& capabilities_to_enable);
|
||||
// Caller holds m_mutex. Non-empty return means the load must be abandoned.
|
||||
std::string check_registry_locked(const std::string& plugin_key, const Plugin& candidate) const;
|
||||
|
||||
void run_discovery(bool async, bool clear);
|
||||
void run_discovery_task(bool clear);
|
||||
void merge_discovered_plugins(std::vector<PluginDescriptor> discovered, bool clear);
|
||||
|
||||
// Unload every loaded Plugin matching should_remove, then erase every matching entry (loaded or
|
||||
// not). Unloading runs outside m_mutex (Python teardown and lifecycle callbacks can re-enter the
|
||||
// manager), so the snapshot-unload cycle retries until a pass finds nothing left to unload; the
|
||||
// final check and the erase itself run in one critical section so a concurrent load cannot make a
|
||||
// live entry reach vector compaction and move-assignment. after_erase_locked, if given, runs right
|
||||
// after the erase while m_mutex is still held.
|
||||
void unload_and_erase_if(const std::function<bool(const Plugin&)>& should_remove,
|
||||
const std::function<void()>& after_erase_locked = {});
|
||||
|
||||
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 release_load_slot(const std::string& plugin_key);
|
||||
void cancel_and_wait_for_capabilities(
|
||||
const std::vector<std::shared_ptr<PluginCapabilityInterface>>& capabilities);
|
||||
|
||||
// Snapshot subscribers under m_mutex so they can be invoked without holding it.
|
||||
std::vector<PluginLifecycleCompleteFn> copy_callbacks(CallbackType type) const;
|
||||
std::vector<CapabilityLifecycleFn> copy_capability_callbacks(CallbackType type) const;
|
||||
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 PluginCapabilityId& id);
|
||||
void run_on_capability_unload_callbacks(const PluginCapabilityId& id);
|
||||
void clear_callbacks();
|
||||
|
||||
// Writes the sidecar for a loaded plugin (enabled=true plus the current per-capability flags).
|
||||
void write_loaded_plugin_install_state(const std::string& plugin_key);
|
||||
|
||||
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;
|
||||
|
||||
// Leaf lock: code holding m_mutex must not call Python, acquire the GIL, invoke lifecycle
|
||||
// callbacks, or re-enter the manager. Live plugin payloads are detached and torn down after
|
||||
// releasing this lock.
|
||||
mutable std::mutex m_mutex;
|
||||
|
||||
std::unordered_map<PluginCapabilityType, std::vector<std::function<void(const std::vector<PluginDescriptor>&)>>> m_loaded_plugin_changed_callbacks;
|
||||
// Every discovered plugin, loaded or not. module == nullptr => not loaded.
|
||||
std::vector<Plugin> m_plugins;
|
||||
|
||||
std::unordered_set<std::string> m_load_in_progress;
|
||||
// Keys whose in-flight load has been cancelled. Cancellation does NOT remove the key from
|
||||
// m_load_in_progress: the detached worker is still inside the loader touching Python, and it
|
||||
// releases its own slot only once it has unwound. Dropping the key here would let
|
||||
// wait_for_all_plugin_loads() return early, after which shutdown() unloads everything and
|
||||
// GUI_App finalizes the interpreter — leaving the live worker to run Python against it.
|
||||
std::unordered_set<std::string> m_cancelled;
|
||||
std::map<std::string, std::string> m_load_errors;
|
||||
mutable std::condition_variable m_load_cv;
|
||||
|
||||
std::map<CallbackType, std::vector<PluginLifecycleCompleteFn>> m_callbacks;
|
||||
std::map<CallbackType, std::vector<CapabilityLifecycleFn>> m_capability_callbacks;
|
||||
|
||||
bool m_discovery_complete = false;
|
||||
bool m_discovery_in_progress = false;
|
||||
std::string m_discovery_error;
|
||||
mutable std::condition_variable m_discovery_cv;
|
||||
|
||||
std::string m_cloud_user_id;
|
||||
std::atomic<bool> m_shutting_down{false};
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
// Resolve each configured capability reference to a loaded capability of type T and run `execute`.
|
||||
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();
|
||||
PluginManager& plugin_mgr = PluginManager::instance();
|
||||
|
||||
// Log prefix derived from the capability type so each capability family (Printer connection,
|
||||
// Slicing Pipeline, ...) tags its dispatch diagnostics with its own display name.
|
||||
@@ -108,7 +310,7 @@ void execute_capabilities_from_refs(const ConfigOptionStrings& capabilities,
|
||||
|
||||
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))) {
|
||||
if (has_any && !plugin_mgr.wait_for_all_plugin_loads(std::chrono::seconds(10))) {
|
||||
BOOST_LOG_TRIVIAL(warning) << tag << ": timed out waiting for plugin loads; unresolved capabilities will be skipped";
|
||||
}
|
||||
|
||||
@@ -116,9 +318,6 @@ void execute_capabilities_from_refs(const ConfigOptionStrings& capabilities,
|
||||
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) {
|
||||
@@ -135,25 +334,26 @@ void execute_capabilities_from_refs(const ConfigOptionStrings& capabilities,
|
||||
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);
|
||||
const std::string cap_name = ref->capability_name;
|
||||
const std::string plugin_key = ref->uuid.empty() ? ref->name : ref->uuid;
|
||||
|
||||
// only_enabled = false so that "not loaded" and "loaded but disabled" stay distinguishable
|
||||
// and each keeps its own diagnostic.
|
||||
auto cap = plugin_mgr.get_plugin_capability(plugin_key, cap_name, type, /*only_enabled=*/false);
|
||||
if (!cap) {
|
||||
BOOST_LOG_TRIVIAL(warning) << tag << ": no loaded capability '" << cap_name
|
||||
<< "' for plugin '" << plugin_key << "'; skipping";
|
||||
continue;
|
||||
}
|
||||
if (!cap->enabled) {
|
||||
BOOST_LOG_TRIVIAL(warning) << tag << ": capability '" << cap_name
|
||||
<< "' for plugin '" << plugin_key << "' is disabled; skipping";
|
||||
BOOST_LOG_TRIVIAL(warning) << tag << ": no loaded capability '" << cap_name << "' for plugin '" << plugin_key << "'; skipping";
|
||||
continue;
|
||||
}
|
||||
|
||||
auto plugin_capability = std::dynamic_pointer_cast<T>(cap->instance);
|
||||
if (!cap->is_enabled()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << tag << ": capability '" << cap_name << "' for plugin '" << plugin_key
|
||||
<< "' is disabled; skipping";
|
||||
continue;
|
||||
}
|
||||
|
||||
auto plugin_capability = std::dynamic_pointer_cast<T>(cap);
|
||||
if (!plugin_capability) {
|
||||
BOOST_LOG_TRIVIAL(warning) << tag << ": capability '" << cap_name
|
||||
<< "' (plugin_key=" << cap->plugin_key
|
||||
BOOST_LOG_TRIVIAL(warning) << tag << ": capability '" << cap_name << "' (plugin_key=" << cap->audit_plugin_key()
|
||||
<< ") is not a " << plugin_capability_type_to_string(type) << "; skipping";
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -116,15 +116,19 @@ std::string resolve_recovery_url(const PluginCapabilityRef& ref)
|
||||
// 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};
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
|
||||
// Only a loaded package exposes capabilities; a discovered one carries their names in its
|
||||
// descriptor but has materialized nothing.
|
||||
if (!mgr.is_plugin_loaded(plugin_key))
|
||||
return {false, false};
|
||||
|
||||
const auto capability = mgr.get_plugin_capability(plugin_key, ref.capability_name, PluginCapabilityType::Unknown,
|
||||
/*only_enabled=*/false);
|
||||
if (!capability)
|
||||
return {false, false};
|
||||
|
||||
return {true, capability->is_enabled()};
|
||||
}
|
||||
|
||||
void refresh_missing_plugins(const PresetBundle& preset_bundle)
|
||||
@@ -162,8 +166,7 @@ void refresh_missing_plugins(Preset::Type type, const ConfigOptionStrings* manif
|
||||
if (manifest == nullptr)
|
||||
return;
|
||||
|
||||
PluginLoader& loader = PluginManager::instance().get_loader();
|
||||
const PluginCatalog& catalog = PluginManager::instance().get_catalog();
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
for (const std::string& entry : manifest->values) {
|
||||
const auto ref = parse_capability_ref(entry);
|
||||
if (!ref)
|
||||
@@ -175,9 +178,9 @@ void refresh_missing_plugins(Preset::Type type, const ConfigOptionStrings* manif
|
||||
continue;
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
const bool in_catalog = catalog.try_get_plugin_descriptor(key, descriptor);
|
||||
const bool in_catalog = mgr.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 bool loaded = installed && mgr.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;
|
||||
@@ -311,7 +314,7 @@ void resolve_missing_plugins(const std::vector<std::string>& refs, PluginInstall
|
||||
// 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())
|
||||
if (mgr.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);
|
||||
@@ -322,14 +325,13 @@ void resolve_missing_plugins(const std::vector<std::string>& refs, PluginInstall
|
||||
continue;
|
||||
}
|
||||
PluginDescriptor descriptor;
|
||||
if (!mgr.get_catalog().try_get_plugin_descriptor(uuid, descriptor)) {
|
||||
if (!mgr.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)) {
|
||||
mgr.load_plugin(descriptor.plugin_key, false);
|
||||
if (!mgr.wait_for_plugin_load(descriptor.plugin_key, std::chrono::minutes(5), error) ||
|
||||
!mgr.is_plugin_loaded(descriptor.plugin_key)) {
|
||||
report_install_failure(descriptor.name + ": " + (error.empty() ? "plugin failed to load." : error));
|
||||
}
|
||||
}
|
||||
@@ -341,8 +343,7 @@ void resolve_missing_plugins(const std::vector<std::string>& refs, PluginInstall
|
||||
|
||||
void resolve_inactive_plugins(const std::vector<std::string>& refs)
|
||||
{
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
PluginCatalog& catalog = mgr.get_catalog();
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
|
||||
// Group the requested capabilities by owning plugin so each plugin is loaded once with the full
|
||||
// set to enable.
|
||||
@@ -353,7 +354,7 @@ void resolve_inactive_plugins(const std::vector<std::string>& refs)
|
||||
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())
|
||||
if (!mgr.try_get_plugin_descriptor(key, descriptor) || !descriptor.has_local_package())
|
||||
continue;
|
||||
by_plugin[descriptor.plugin_key].push_back(ref->capability_name);
|
||||
}
|
||||
@@ -367,13 +368,11 @@ void resolve_inactive_plugins(const std::vector<std::string>& refs)
|
||||
// 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();
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
for (auto& [plugin_key, capabilities] : work) {
|
||||
loader.load_plugin(catalog, plugin_key, /*skip_deps=*/false, capabilities);
|
||||
mgr.load_plugin(plugin_key, /*skip_deps=*/false, capabilities);
|
||||
std::string error;
|
||||
loader.wait_for_plugin_load(plugin_key, std::chrono::minutes(5), error);
|
||||
mgr.wait_for_plugin_load(plugin_key, std::chrono::minutes(5), error);
|
||||
}
|
||||
GUI::wxGetApp().CallAfter([]() {
|
||||
if (GUI::Plater* plater = GUI::wxGetApp().plater())
|
||||
@@ -394,8 +393,4 @@ void open_missing_plugins_on_cloud(const std::vector<std::string>& local_refs)
|
||||
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
|
||||
|
||||
@@ -81,8 +81,6 @@ 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
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <pybind11/embed.h>
|
||||
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "PythonPluginInterface.hpp"
|
||||
#include "PythonInterpreter.hpp"
|
||||
@@ -20,29 +21,32 @@
|
||||
// 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; \
|
||||
#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_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__)); \
|
||||
do { \
|
||||
::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this); \
|
||||
::Slic3r::PythonGILState _orca_python_gil; \
|
||||
if (!_orca_python_gil) \
|
||||
throw std::runtime_error("Python interpreter is shutting down"); \
|
||||
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 {
|
||||
@@ -54,36 +58,23 @@ public:
|
||||
// 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);
|
||||
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);
|
||||
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);
|
||||
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_unload);
|
||||
}
|
||||
|
||||
void on_cancelled() override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -97,11 +88,7 @@ public:
|
||||
PluginCapabilityType get_type() const override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||||
[] {},
|
||||
PYBIND11_OVERRIDE,
|
||||
PluginCapabilityType,
|
||||
PluginCapabilityInterface,
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, PluginCapabilityType, PluginCapabilityInterface,
|
||||
get_type);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -492,35 +492,25 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
|
||||
|
||||
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))
|
||||
PluginInstallState state;
|
||||
if (!read_install_state(plugin_dir, state))
|
||||
return;
|
||||
|
||||
boost::nowide::ifstream f(sidecar_path.string());
|
||||
if (!f)
|
||||
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.installed_version.empty())
|
||||
entry.installed_version = state.installed_version;
|
||||
if (!state.cloud_uuid.empty())
|
||||
entry.cloud = CloudPluginState{state.cloud_uuid, true, false, false};
|
||||
|
||||
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 (...) {}
|
||||
// Package-level auto-load flag only. The per-capability enable flags stay in the sidecar: a
|
||||
// capability has no existence — and so no state — until it is materialized, at which point the
|
||||
// loader seeds the flag onto the capability itself.
|
||||
entry.enabled = state.enabled;
|
||||
}
|
||||
|
||||
bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstallState& out)
|
||||
@@ -616,6 +606,10 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
|
||||
|
||||
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry)
|
||||
{
|
||||
// Install-time writer: the package is not loaded, so its capabilities are not known yet and the
|
||||
// sidecar is (re)initialized to "auto-load, nothing disabled". PluginManager writes the real
|
||||
// per-capability flags once the package is loaded, via the (dir, entry, enabled, capabilities)
|
||||
// overload.
|
||||
return write_install_state(plugin_dir, entry, true, {});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,17 +15,77 @@
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <iomanip>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
thread_local PythonInterpreter* PythonRuntimeLease::s_owner = nullptr;
|
||||
thread_local unsigned int PythonRuntimeLease::s_depth = 0;
|
||||
|
||||
PythonRuntimeLease::PythonRuntimeLease(PythonInterpreter& interpreter)
|
||||
{
|
||||
if (s_owner == &interpreter) {
|
||||
m_interpreter = &interpreter;
|
||||
++s_depth;
|
||||
return;
|
||||
}
|
||||
|
||||
m_lock = std::shared_lock<std::shared_mutex>(interpreter.m_runtime_mutex);
|
||||
if (!interpreter.m_initialized.load(std::memory_order_acquire)) {
|
||||
m_lock.unlock();
|
||||
return;
|
||||
}
|
||||
|
||||
m_interpreter = &interpreter;
|
||||
s_owner = &interpreter;
|
||||
s_depth = 1;
|
||||
}
|
||||
|
||||
PythonRuntimeLease::PythonRuntimeLease(PythonRuntimeLease&& other) noexcept
|
||||
: m_interpreter(other.m_interpreter), m_lock(std::move(other.m_lock))
|
||||
{
|
||||
other.m_interpreter = nullptr;
|
||||
}
|
||||
|
||||
PythonRuntimeLease& PythonRuntimeLease::operator=(PythonRuntimeLease&& other) noexcept
|
||||
{
|
||||
if (this != &other) {
|
||||
release();
|
||||
m_lock = std::move(other.m_lock);
|
||||
m_interpreter = other.m_interpreter;
|
||||
other.m_interpreter = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
void PythonRuntimeLease::release()
|
||||
{
|
||||
if (!m_interpreter)
|
||||
return;
|
||||
|
||||
if (s_owner == m_interpreter && s_depth > 0) {
|
||||
--s_depth;
|
||||
if (s_depth == 0)
|
||||
s_owner = nullptr;
|
||||
}
|
||||
m_interpreter = nullptr;
|
||||
}
|
||||
|
||||
PythonRuntimeLease::~PythonRuntimeLease()
|
||||
{
|
||||
release();
|
||||
}
|
||||
|
||||
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;
|
||||
if (!gil)
|
||||
return;
|
||||
|
||||
// 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++
|
||||
@@ -108,8 +168,11 @@ std::string executable_name(const char* base)
|
||||
#endif
|
||||
}
|
||||
|
||||
bool add_sys_path_entry(const boost::filesystem::path& path, std::string& error)
|
||||
bool add_sys_path_entry(const boost::filesystem::path& path, std::string& error, bool* inserted = nullptr)
|
||||
{
|
||||
if (inserted)
|
||||
*inserted = false;
|
||||
|
||||
PyObject* sys_path = PySys_GetObject("path");
|
||||
if (!sys_path || !PyList_Check(sys_path)) {
|
||||
error = "Python sys.path is not available";
|
||||
@@ -136,6 +199,9 @@ bool add_sys_path_entry(const boost::filesystem::path& path, std::string& error)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (inserted)
|
||||
*inserted = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -387,94 +453,23 @@ std::string PythonInterpreter::bundled_uv_path()
|
||||
|
||||
bool PythonInterpreter::initialize()
|
||||
{
|
||||
if (m_initialized) {
|
||||
std::unique_lock<std::shared_mutex> runtime_lock(m_runtime_mutex);
|
||||
if (m_initialized.load(std::memory_order_acquire)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
m_last_error.clear();
|
||||
m_plugin_path_users.clear();
|
||||
m_plugin_path_owned.clear();
|
||||
m_plugin_module_users.clear();
|
||||
m_plugin_module_owned.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
|
||||
const std::string python_home = find_bundled_python_home().string();
|
||||
|
||||
if (python_home.empty()) {
|
||||
m_last_error = "Could not locate bundled Python installation";
|
||||
@@ -493,6 +488,8 @@ bool PythonInterpreter::initialize()
|
||||
return false;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Found bundled Python home: " << python_home;
|
||||
|
||||
// Verify Python standard library directory exists
|
||||
#ifdef _WIN32
|
||||
fs::path python_lib = fs::path(python_home) / "Lib";
|
||||
@@ -539,6 +536,10 @@ bool PythonInterpreter::initialize()
|
||||
// Set Python home - this is the prefix where Python libraries are located
|
||||
PyConfig config;
|
||||
PyConfig_InitPythonConfig(&config);
|
||||
// Do not let the host process's PYTHONPATH or user site-packages override the bundled
|
||||
// runtime used by plugins.
|
||||
config.use_environment = 0;
|
||||
config.user_site_directory = 0;
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug) << "Calling PyConfig_SetBytesString with home=" << python_home;
|
||||
PyStatus status = PyConfig_SetBytesString(&config, &config.home, python_home.c_str());
|
||||
@@ -631,11 +632,10 @@ bool PythonInterpreter::initialize()
|
||||
// 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();
|
||||
m_initialized.store(true, std::memory_order_release);
|
||||
BOOST_LOG_TRIVIAL(debug) << "Main thread released Python GIL after initialization";
|
||||
return true;
|
||||
|
||||
@@ -650,7 +650,8 @@ PythonInterpreter::~PythonInterpreter() { shutdown(); }
|
||||
|
||||
void PythonInterpreter::shutdown()
|
||||
{
|
||||
if (!m_initialized)
|
||||
std::unique_lock<std::shared_mutex> runtime_lock(m_runtime_mutex);
|
||||
if (!m_initialized.load(std::memory_order_acquire))
|
||||
return;
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Python interpreter shutdown enter";
|
||||
@@ -668,80 +669,305 @@ void PythonInterpreter::shutdown()
|
||||
m_interpreter.reset();
|
||||
BOOST_LOG_TRIVIAL(info) << "Python interpreter finalized";
|
||||
|
||||
m_initialized = false;
|
||||
m_plugin_path_users.clear();
|
||||
m_plugin_path_owned.clear();
|
||||
m_plugin_module_users.clear();
|
||||
m_plugin_module_owned.clear();
|
||||
m_initialized.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool PythonInterpreter::add_sys_path(const std::string& path, std::string& error)
|
||||
bool PythonInterpreter::add_plugin_sys_path(const std::string& path, std::string& error)
|
||||
{
|
||||
if (!m_initialized) {
|
||||
if (!m_initialized.load(std::memory_order_acquire)) {
|
||||
error = "Python interpreter not initialized";
|
||||
return false;
|
||||
}
|
||||
|
||||
PythonGILState gil;
|
||||
if (!gil) {
|
||||
error = "Python interpreter is shutting down";
|
||||
return false;
|
||||
}
|
||||
|
||||
return add_plugin_sys_path_locked(path, error);
|
||||
}
|
||||
|
||||
bool PythonInterpreter::add_plugin_sys_path_locked(const std::string& path, std::string& error)
|
||||
{
|
||||
bool inserted = false;
|
||||
if (!add_sys_path_entry(boost::filesystem::path(path), error, &inserted))
|
||||
return false;
|
||||
|
||||
auto users = m_plugin_path_users.find(path);
|
||||
if (users == m_plugin_path_users.end()) {
|
||||
m_plugin_path_owned[path] = inserted;
|
||||
m_plugin_path_users.emplace(path, 1);
|
||||
} else {
|
||||
++users->second;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PythonInterpreter::remove_plugin_sys_paths_locked(const std::vector<std::string>& paths)
|
||||
{
|
||||
PyObject* sys_path = PySys_GetObject("path");
|
||||
if (!sys_path || !PyList_Check(sys_path)) {
|
||||
error = "Python sys.path is not available";
|
||||
return false;
|
||||
PyErr_Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
for (const std::string& path : paths) {
|
||||
auto users = m_plugin_path_users.find(path);
|
||||
if (users == m_plugin_path_users.end())
|
||||
continue;
|
||||
|
||||
if (--users->second != 0)
|
||||
continue;
|
||||
|
||||
const bool owned = m_plugin_path_owned[path];
|
||||
m_plugin_path_users.erase(users);
|
||||
m_plugin_path_owned.erase(path);
|
||||
if (!owned)
|
||||
continue;
|
||||
|
||||
PyObjectPtr py_path(PyUnicode_DecodeFSDefault(path.c_str()));
|
||||
if (!py_path) {
|
||||
PyErr_Clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
for (Py_ssize_t index = PyList_Size(sys_path) - 1; index >= 0; --index) {
|
||||
PyObject* entry = PyList_GetItem(sys_path, index); // borrowed reference
|
||||
int equal = entry ? PyObject_RichCompareBool(entry, py_path.get(), Py_EQ) : 0;
|
||||
if (equal == 1 && PySequence_DelItem(sys_path, index) != 0)
|
||||
PyErr_Clear();
|
||||
else if (equal < 0)
|
||||
PyErr_Clear();
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
void PythonInterpreter::record_plugin_modules_locked(const std::string& module_name,
|
||||
const std::vector<std::string>& plugin_paths,
|
||||
std::vector<std::string>* plugin_modules)
|
||||
{
|
||||
if (!m_initialized) {
|
||||
error = "Python interpreter not initialized";
|
||||
return false;
|
||||
if (!plugin_modules)
|
||||
return;
|
||||
|
||||
PyObject* modules = PyImport_GetModuleDict();
|
||||
if (!modules)
|
||||
return;
|
||||
|
||||
PyObjectPtr names(PyDict_Keys(modules));
|
||||
if (!names) {
|
||||
PyErr_Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string prefix = module_name + ".";
|
||||
const auto path_matches = [](const std::string& file, const std::string& root) {
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
boost::system::error_code ec;
|
||||
fs::path file_path = fs::weakly_canonical(fs::path(file), ec);
|
||||
if (ec) {
|
||||
ec.clear();
|
||||
file_path = fs::absolute(fs::path(file), ec);
|
||||
}
|
||||
if (ec)
|
||||
return false;
|
||||
|
||||
ec.clear();
|
||||
fs::path root_path = fs::weakly_canonical(fs::path(root), ec);
|
||||
if (ec) {
|
||||
ec.clear();
|
||||
root_path = fs::absolute(fs::path(root), ec);
|
||||
}
|
||||
if (ec)
|
||||
return false;
|
||||
|
||||
const std::string file_string = file_path.generic_string();
|
||||
std::string root_string = root_path.generic_string();
|
||||
if (root_string.empty())
|
||||
return false;
|
||||
if (root_string.back() != '/')
|
||||
root_string.push_back('/');
|
||||
return file_string.compare(0, root_string.size(), root_string) == 0;
|
||||
};
|
||||
|
||||
const Py_ssize_t count = PyList_Size(names.get());
|
||||
for (Py_ssize_t index = 0; index < count; ++index) {
|
||||
PyObject* name_obj = PyList_GetItem(names.get(), index); // borrowed reference
|
||||
if (!name_obj || !PyUnicode_Check(name_obj))
|
||||
continue;
|
||||
|
||||
const char* name_utf8 = PyUnicode_AsUTF8(name_obj);
|
||||
if (!name_utf8) {
|
||||
PyErr_Clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string name(name_utf8);
|
||||
const bool is_plugin_namespace = name == module_name || name.compare(0, prefix.size(), prefix) == 0;
|
||||
bool is_plugin_path_module = false;
|
||||
if (!is_plugin_namespace) {
|
||||
PyObject* module = PyDict_GetItem(modules, name_obj); // borrowed reference
|
||||
if (!module)
|
||||
continue;
|
||||
|
||||
PyObjectPtr file(PyObject_GetAttrString(module, "__file__"));
|
||||
if (file && PyUnicode_Check(file.get())) {
|
||||
const char* file_utf8 = PyUnicode_AsUTF8(file.get());
|
||||
if (file_utf8) {
|
||||
for (const std::string& path : plugin_paths) {
|
||||
if (path_matches(file_utf8, path)) {
|
||||
is_plugin_path_module = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
PyErr_Clear();
|
||||
}
|
||||
} else {
|
||||
PyErr_Clear();
|
||||
}
|
||||
|
||||
if (!is_plugin_path_module) {
|
||||
PyObjectPtr package_path(PyObject_GetAttrString(module, "__path__"));
|
||||
if (package_path) {
|
||||
const Py_ssize_t path_count = PySequence_Size(package_path.get());
|
||||
if (path_count < 0) {
|
||||
PyErr_Clear();
|
||||
} else {
|
||||
for (Py_ssize_t path_index = 0; path_index < path_count && !is_plugin_path_module; ++path_index) {
|
||||
PyObjectPtr path_entry(PySequence_GetItem(package_path.get(), path_index));
|
||||
if (!path_entry) {
|
||||
PyErr_Clear();
|
||||
continue;
|
||||
}
|
||||
if (!PyUnicode_Check(path_entry.get()))
|
||||
continue;
|
||||
const char* path_utf8 = PyUnicode_AsUTF8(path_entry.get());
|
||||
if (!path_utf8) {
|
||||
PyErr_Clear();
|
||||
continue;
|
||||
}
|
||||
for (const std::string& path : plugin_paths) {
|
||||
if (path_matches(path_utf8, path)) {
|
||||
is_plugin_path_module = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
PyErr_Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_plugin_namespace && !is_plugin_path_module)
|
||||
continue;
|
||||
|
||||
auto users = m_plugin_module_users.find(name);
|
||||
if (users == m_plugin_module_users.end()) {
|
||||
m_plugin_module_owned[name] = true;
|
||||
m_plugin_module_users.emplace(name, 1);
|
||||
} else {
|
||||
++users->second;
|
||||
}
|
||||
plugin_modules->push_back(name);
|
||||
}
|
||||
}
|
||||
|
||||
void PythonInterpreter::remove_plugin_modules_locked(const std::vector<std::string>& plugin_modules)
|
||||
{
|
||||
PyObject* modules = PyImport_GetModuleDict();
|
||||
if (!modules)
|
||||
return;
|
||||
|
||||
for (const std::string& name : plugin_modules) {
|
||||
auto users = m_plugin_module_users.find(name);
|
||||
if (users == m_plugin_module_users.end())
|
||||
continue;
|
||||
|
||||
if (--users->second != 0)
|
||||
continue;
|
||||
|
||||
const bool owned = m_plugin_module_owned[name];
|
||||
m_plugin_module_users.erase(users);
|
||||
m_plugin_module_owned.erase(name);
|
||||
if (owned && PyDict_DelItemString(modules, name.c_str()) != 0)
|
||||
PyErr_Clear();
|
||||
}
|
||||
}
|
||||
|
||||
void PythonInterpreter::remove_module_tree_locked(const std::string& module_name)
|
||||
{
|
||||
if (module_name.empty())
|
||||
return;
|
||||
|
||||
PyObject* modules = PyImport_GetModuleDict();
|
||||
if (!modules)
|
||||
return;
|
||||
|
||||
PyObjectPtr names(PyDict_Keys(modules));
|
||||
if (!names) {
|
||||
PyErr_Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string prefix = module_name + ".";
|
||||
const Py_ssize_t count = PyList_Size(names.get());
|
||||
for (Py_ssize_t index = 0; index < count; ++index) {
|
||||
PyObject* name_obj = PyList_GetItem(names.get(), index); // borrowed reference
|
||||
if (!name_obj || !PyUnicode_Check(name_obj))
|
||||
continue;
|
||||
|
||||
const char* name_utf8 = PyUnicode_AsUTF8(name_obj);
|
||||
if (!name_utf8) {
|
||||
PyErr_Clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string name(name_utf8);
|
||||
if (name != module_name && name.compare(0, prefix.size(), prefix) != 0)
|
||||
continue;
|
||||
|
||||
if (PyDict_DelItem(modules, name_obj) != 0)
|
||||
PyErr_Clear();
|
||||
}
|
||||
}
|
||||
|
||||
void PythonInterpreter::unload_module(PyObject* module,
|
||||
const std::string& module_name,
|
||||
const std::vector<std::string>& plugin_paths,
|
||||
const std::vector<std::string>& plugin_modules)
|
||||
{
|
||||
if (!module && plugin_paths.empty() && plugin_modules.empty())
|
||||
return;
|
||||
|
||||
if (!m_initialized.load(std::memory_order_acquire))
|
||||
return;
|
||||
|
||||
PythonGILState gil;
|
||||
if (!gil)
|
||||
return;
|
||||
|
||||
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;
|
||||
remove_plugin_modules_locked(plugin_modules);
|
||||
if (plugin_modules.empty())
|
||||
remove_module_tree_locked(module_name);
|
||||
remove_plugin_sys_paths_locked(plugin_paths);
|
||||
Py_XDECREF(module);
|
||||
}
|
||||
|
||||
PyObject* PythonInterpreter::load_module_from_file(const std::string& file_path, std::string& error)
|
||||
PyObject* PythonInterpreter::load_module_from_file(const std::string& file_path,
|
||||
std::string& error,
|
||||
std::vector<std::string>* plugin_paths,
|
||||
std::vector<std::string>* plugin_modules)
|
||||
{
|
||||
if (!m_initialized) {
|
||||
if (!m_initialized.load(std::memory_order_acquire)) {
|
||||
error = "Python interpreter not initialized";
|
||||
return nullptr;
|
||||
}
|
||||
@@ -755,49 +981,27 @@ PyObject* PythonInterpreter::load_module_from_file(const std::string& file_path,
|
||||
}
|
||||
|
||||
PythonGILState gil;
|
||||
if (!gil) {
|
||||
error = "Python interpreter is shutting down";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// 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";
|
||||
const std::string dir = dir_path.string();
|
||||
if (!add_plugin_sys_path_locked(dir, error))
|
||||
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);
|
||||
if (plugin_paths)
|
||||
plugin_paths->push_back(dir);
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
remove_module_tree_locked(module_name);
|
||||
|
||||
// Import the module
|
||||
PyObject* module = PyImport_ImportModule(module_name.c_str());
|
||||
record_plugin_modules_locked(module_name, plugin_paths ? *plugin_paths : std::vector<std::string>{}, plugin_modules);
|
||||
if (!module) {
|
||||
PyObject *ptype, *pvalue, *ptraceback;
|
||||
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
|
||||
@@ -811,9 +1015,13 @@ PyObject* PythonInterpreter::load_module_from_file(const std::string& file_path,
|
||||
return module;
|
||||
}
|
||||
|
||||
PyObject* PythonInterpreter::load_module_from_directory(const std::string& dir_path, const std::string& pkg_name, std::string& error)
|
||||
PyObject* PythonInterpreter::load_module_from_directory(const std::string& dir_path,
|
||||
const std::string& pkg_name,
|
||||
std::string& error,
|
||||
std::vector<std::string>* plugin_paths,
|
||||
std::vector<std::string>* plugin_modules)
|
||||
{
|
||||
if (!m_initialized) {
|
||||
if (!m_initialized.load(std::memory_order_acquire)) {
|
||||
error = "Python interpreter not initialized";
|
||||
return nullptr;
|
||||
}
|
||||
@@ -827,43 +1035,21 @@ PyObject* PythonInterpreter::load_module_from_directory(const std::string& dir_p
|
||||
}
|
||||
|
||||
PythonGILState gil;
|
||||
|
||||
PyObjectPtr sys(PyImport_ImportModule("sys"));
|
||||
if (!sys) {
|
||||
error = "Failed to import sys module";
|
||||
if (!gil) {
|
||||
error = "Python interpreter is shutting down";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyObject* sys_path = PyObject_GetAttrString(sys.get(), "path");
|
||||
if (!sys_path) {
|
||||
error = "Failed to get sys.path";
|
||||
const std::string directory = dir.string();
|
||||
if (!add_plugin_sys_path_locked(directory, error))
|
||||
return nullptr;
|
||||
}
|
||||
if (plugin_paths)
|
||||
plugin_paths->push_back(directory);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
remove_module_tree_locked(pkg_name);
|
||||
|
||||
PyObject* module = PyImport_ImportModule(pkg_name.c_str());
|
||||
record_plugin_modules_locked(pkg_name, plugin_paths ? *plugin_paths : std::vector<std::string>{}, plugin_modules);
|
||||
if (!module) {
|
||||
PyObject *ptype, *pvalue, *ptraceback;
|
||||
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
|
||||
@@ -877,9 +1063,13 @@ PyObject* PythonInterpreter::load_module_from_directory(const std::string& dir_p
|
||||
return module;
|
||||
}
|
||||
|
||||
PyObject* PythonInterpreter::load_module_from_whl(const std::string& file_path, const std::string& pkg_name, std::string& error)
|
||||
PyObject* PythonInterpreter::load_module_from_whl(const std::string& file_path,
|
||||
const std::string& pkg_name,
|
||||
std::string& error,
|
||||
std::vector<std::string>* plugin_paths,
|
||||
std::vector<std::string>* plugin_modules)
|
||||
{
|
||||
if (!m_initialized) {
|
||||
if (!m_initialized.load(std::memory_order_acquire)) {
|
||||
error = "Python interpreter not initialized";
|
||||
return nullptr;
|
||||
}
|
||||
@@ -900,98 +1090,7 @@ PyObject* PythonInterpreter::load_module_from_whl(const std::string& file_path,
|
||||
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 "";
|
||||
return load_module_from_directory(extract_dir.string(), pkg_name, error, plugin_paths, plugin_modules);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -3,9 +3,13 @@
|
||||
|
||||
#include <Python.h>
|
||||
#include <pytypedefs.h>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
namespace pybind11 {
|
||||
@@ -15,6 +19,8 @@ class error_already_set;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class PythonRuntimeLease;
|
||||
|
||||
// Print a Python exception's full traceback to sys.stderr (tee'd to the session
|
||||
// log) WITHOUT consuming err.
|
||||
//
|
||||
@@ -37,7 +43,11 @@ public:
|
||||
bool initialize();
|
||||
|
||||
// Check if interpreter is initialized
|
||||
bool is_initialized() const { return m_initialized; }
|
||||
bool is_initialized() const { return m_initialized.load(std::memory_order_acquire); }
|
||||
|
||||
// Acquire a shared lease on the interpreter. Shutdown takes the exclusive side of this
|
||||
// lock, so a caller that owns a lease can safely acquire the GIL and touch Python objects.
|
||||
PythonRuntimeLease acquire_runtime_lease();
|
||||
|
||||
const std::string& last_error() const { return m_last_error; }
|
||||
|
||||
@@ -56,56 +66,116 @@ public:
|
||||
// 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);
|
||||
// Add a path owned by one plugin load. The path is reference-counted across plugins and is
|
||||
// removed when the final plugin using it is unloaded.
|
||||
bool add_plugin_sys_path(const std::string& path, 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);
|
||||
PyObject* load_module_from_file(const std::string& file_path,
|
||||
std::string& error,
|
||||
std::vector<std::string>* plugin_paths = nullptr,
|
||||
std::vector<std::string>* plugin_modules = nullptr);
|
||||
PyObject* load_module_from_whl(const std::string& whl_path,
|
||||
const std::string& pkg_name,
|
||||
std::string& error,
|
||||
std::vector<std::string>* plugin_paths = nullptr,
|
||||
std::vector<std::string>* plugin_modules = nullptr);
|
||||
PyObject* load_module_from_directory(const std::string& dir_path,
|
||||
const std::string& pkg_name,
|
||||
std::string& error,
|
||||
std::vector<std::string>* plugin_paths = nullptr,
|
||||
std::vector<std::string>* plugin_modules = nullptr);
|
||||
|
||||
// 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);
|
||||
// Remove the complete module namespace and plugin-owned paths, then release the root module
|
||||
// reference. Safe to call with a null module when a load failed after adding paths.
|
||||
void unload_module(PyObject* module,
|
||||
const std::string& module_name,
|
||||
const std::vector<std::string>& plugin_paths,
|
||||
const std::vector<std::string>& plugin_modules);
|
||||
|
||||
// Destructor finalizes Python if shutdown() was not called explicitly.
|
||||
~PythonInterpreter();
|
||||
|
||||
private:
|
||||
friend class PythonRuntimeLease;
|
||||
|
||||
PythonInterpreter() = default;
|
||||
PythonInterpreter(const PythonInterpreter&) = delete;
|
||||
PythonInterpreter& operator=(const PythonInterpreter&) = delete;
|
||||
|
||||
bool m_initialized = false;
|
||||
bool add_plugin_sys_path_locked(const std::string& path, std::string& error);
|
||||
void remove_plugin_sys_paths_locked(const std::vector<std::string>& paths);
|
||||
void record_plugin_modules_locked(const std::string& module_name,
|
||||
const std::vector<std::string>& plugin_paths,
|
||||
std::vector<std::string>* plugin_modules);
|
||||
void remove_plugin_modules_locked(const std::vector<std::string>& plugin_modules);
|
||||
void remove_module_tree_locked(const std::string& module_name);
|
||||
|
||||
std::atomic<bool> m_initialized{false};
|
||||
mutable std::shared_mutex m_runtime_mutex;
|
||||
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;
|
||||
std::unordered_map<std::string, std::size_t> m_plugin_path_users;
|
||||
std::unordered_map<std::string, bool> m_plugin_path_owned;
|
||||
std::unordered_map<std::string, std::size_t> m_plugin_module_users;
|
||||
std::unordered_map<std::string, bool> m_plugin_module_owned;
|
||||
};
|
||||
|
||||
class PythonRuntimeLease
|
||||
{
|
||||
public:
|
||||
PythonRuntimeLease() = default;
|
||||
PythonRuntimeLease(PythonRuntimeLease&& other) noexcept;
|
||||
PythonRuntimeLease& operator=(PythonRuntimeLease&& other) noexcept;
|
||||
~PythonRuntimeLease();
|
||||
|
||||
PythonRuntimeLease(const PythonRuntimeLease&) = delete;
|
||||
PythonRuntimeLease& operator=(const PythonRuntimeLease&) = delete;
|
||||
|
||||
explicit operator bool() const { return m_interpreter != nullptr; }
|
||||
|
||||
private:
|
||||
friend class PythonInterpreter;
|
||||
|
||||
explicit PythonRuntimeLease(PythonInterpreter& interpreter);
|
||||
void release();
|
||||
|
||||
static thread_local PythonInterpreter* s_owner;
|
||||
static thread_local unsigned int s_depth;
|
||||
|
||||
PythonInterpreter* m_interpreter = nullptr;
|
||||
std::shared_lock<std::shared_mutex> m_lock;
|
||||
};
|
||||
|
||||
inline PythonRuntimeLease PythonInterpreter::acquire_runtime_lease()
|
||||
{
|
||||
return PythonRuntimeLease(*this);
|
||||
}
|
||||
|
||||
// RAII helper for Python GIL (Global Interpreter Lock)
|
||||
class PythonGILState
|
||||
{
|
||||
public:
|
||||
PythonGILState() {
|
||||
m_state = PyGILState_Ensure();
|
||||
m_runtime_lease = PythonInterpreter::instance().acquire_runtime_lease();
|
||||
if (m_runtime_lease) {
|
||||
m_state = PyGILState_Ensure();
|
||||
m_acquired = true;
|
||||
}
|
||||
}
|
||||
|
||||
~PythonGILState() {
|
||||
PyGILState_Release(m_state);
|
||||
if (m_acquired)
|
||||
PyGILState_Release(m_state);
|
||||
}
|
||||
|
||||
explicit operator bool() const { return m_acquired; }
|
||||
|
||||
private:
|
||||
PyGILState_STATE m_state;
|
||||
PythonRuntimeLease m_runtime_lease;
|
||||
PyGILState_STATE m_state{};
|
||||
bool m_acquired = false;
|
||||
};
|
||||
|
||||
// RAII helper for Python object references
|
||||
|
||||
@@ -44,19 +44,35 @@ struct PluginInstanceHandle
|
||||
~PluginInstanceHandle()
|
||||
{
|
||||
if (keep_alive) {
|
||||
if (Py_IsInitialized()) {
|
||||
// Dropping a py::object decrefs the Python object, so reacquire the GIL.
|
||||
PythonGILState gil;
|
||||
// Dropping a py::object decrefs the Python object, so acquire a runtime lease and
|
||||
// the GIL. If shutdown has already won the lease, intentionally abandon the wrapper.
|
||||
PythonGILState gil;
|
||||
if (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();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void discard_pending_capture_without_python(const std::string& plugin_key)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
auto capabilities = g_pending_capabilities.find(plugin_key);
|
||||
if (capabilities != g_pending_capabilities.end()) {
|
||||
for (py::object& capability : capabilities->second)
|
||||
(void) capability.release();
|
||||
g_pending_capabilities.erase(capabilities);
|
||||
}
|
||||
|
||||
auto package = g_pending_package.find(plugin_key);
|
||||
if (package != g_pending_package.end()) {
|
||||
(void) package->second.release();
|
||||
g_pending_package.erase(package);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PythonPluginBridge& PythonPluginBridge::instance()
|
||||
@@ -68,6 +84,10 @@ PythonPluginBridge& PythonPluginBridge::instance()
|
||||
void PythonPluginBridge::begin_plugin_capture(const std::string& plugin_key)
|
||||
{
|
||||
PythonGILState gil;
|
||||
if (!gil) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Cannot begin Python plugin capture while the interpreter is shutting down";
|
||||
return;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Beginning Python plugin capture for key " << plugin_key;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
@@ -84,6 +104,10 @@ void PythonPluginBridge::begin_plugin_capture(const std::string& plugin_key)
|
||||
std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(const std::string& plugin_key, std::string& error)
|
||||
{
|
||||
PythonGILState gil;
|
||||
if (!gil) {
|
||||
error = "Python interpreter is shutting down";
|
||||
return {};
|
||||
}
|
||||
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
|
||||
@@ -236,6 +260,13 @@ void PythonPluginBridge::cancel_plugin_capture(const std::string& plugin_key)
|
||||
PythonGILState gil;
|
||||
BOOST_LOG_TRIVIAL(warning) << "Cancelling Python plugin capture for key " << plugin_key;
|
||||
|
||||
if (!gil) {
|
||||
discard_pending_capture_without_python(plugin_key);
|
||||
if (g_active_plugin_key == plugin_key)
|
||||
g_active_plugin_key.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
// Import or dependency setup failed before finalization. Drop anything the module
|
||||
@@ -250,7 +281,8 @@ void PythonPluginBridge::cancel_plugin_capture(const std::string& plugin_key)
|
||||
|
||||
void PythonPluginBridge::clear_pending_captures()
|
||||
{
|
||||
if (!Py_IsInitialized()) {
|
||||
PythonGILState gil;
|
||||
if (!gil) {
|
||||
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";
|
||||
@@ -271,9 +303,6 @@ void PythonPluginBridge::clear_pending_captures()
|
||||
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();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef slic3r_PythonPluginInterface_hpp_
|
||||
#define slic3r_PythonPluginInterface_hpp_
|
||||
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -96,22 +97,77 @@ struct ExecutionResult
|
||||
class PluginCapabilityInterface
|
||||
{
|
||||
public:
|
||||
class RefCounter
|
||||
{
|
||||
public:
|
||||
explicit RefCounter(const PluginCapabilityInterface& iface) : m_iface(&iface) { m_iface->increment(); }
|
||||
|
||||
~RefCounter() { m_iface->decrement(); }
|
||||
|
||||
RefCounter(const RefCounter&) = delete;
|
||||
RefCounter& operator=(const RefCounter&) = delete;
|
||||
|
||||
private:
|
||||
const PluginCapabilityInterface* m_iface;
|
||||
};
|
||||
|
||||
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
|
||||
// DO NOT CALL THESE OUTSIDE THE LOADER'S MATERIALIZATION BLOCK. get_name() is pure virtual and
|
||||
// always implemented in Python: the trampoline routes it through PYBIND11_OVERRIDE_PURE, so every
|
||||
// call acquires the GIL, dispatches into the plugin, and opens a filesystem-enforcement scope.
|
||||
// Capability lookup is by name and runs under the plugin registry lock, so calling this on a
|
||||
// lookup would hold that lock while taking the GIL — inverting the lock order against plugin
|
||||
// dispatch on the slicing threads — and it is undefined after the interpreter is finalized.
|
||||
// get_type() is virtual with a C++ default that the typed bases override in C++, but the untyped
|
||||
// trampoline still routes it to Python, so it is only GIL-free by accident of the base chosen.
|
||||
//
|
||||
// The loader resolves both exactly once, at materialization, under the GIL it already holds, and
|
||||
// caches them below. Everything else reads name()/type().
|
||||
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() {}
|
||||
virtual void on_cancelled() {}
|
||||
|
||||
// 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.
|
||||
// ── C++-only host state, never exposed to Python. Set by the loader at materialization. ──
|
||||
//
|
||||
// The capability owns its own identity and enable flag: they are read once under the GIL, live
|
||||
// exactly as long as the capability does, and are discarded with it on unload. Nothing about a
|
||||
// capability outlives the capability — the durable record is the .install_state.json sidecar.
|
||||
|
||||
// Cached identity. Plain C++ reads, safe under any lock and after the interpreter is gone.
|
||||
const std::string& name() const { return m_name; }
|
||||
PluginCapabilityType type() const { return m_type; }
|
||||
void set_resolved_identity(std::string name, PluginCapabilityType type)
|
||||
{
|
||||
m_name = std::move(name);
|
||||
m_type = type;
|
||||
}
|
||||
|
||||
// Logical enable/disable. A disabled capability stays loaded but is skipped by consumers.
|
||||
// Atomic because dispatch reads it off a shared_ptr handed out by the manager, i.e. without the
|
||||
// registry lock held. Seeded from the sidecar at load; PluginManager writes it through on change.
|
||||
bool is_enabled() const { return m_enabled.load(std::memory_order_acquire); }
|
||||
void set_enabled(bool enabled) { m_enabled.store(enabled, std::memory_order_release); }
|
||||
|
||||
// The owning package (PluginDescriptor::plugin_key), the canonical runtime id. Also scopes
|
||||
// filesystem enforcement for trampoline calls.
|
||||
void set_audit_plugin_key(std::string key) { m_audit_plugin_key = std::move(key); }
|
||||
const std::string& audit_plugin_key() const { return m_audit_plugin_key; }
|
||||
|
||||
void increment() const { m_refs.fetch_add(1, std::memory_order_acq_rel); }
|
||||
void decrement() const { m_refs.fetch_sub(1, std::memory_order_acq_rel); }
|
||||
int ref_count() const { return m_refs.load(std::memory_order_acquire); }
|
||||
|
||||
private:
|
||||
std::string m_name;
|
||||
PluginCapabilityType m_type = PluginCapabilityType::Unknown;
|
||||
std::atomic<bool> m_enabled{true};
|
||||
std::string m_audit_plugin_key;
|
||||
|
||||
mutable std::atomic<int> m_refs{0};
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -105,7 +105,10 @@ struct GilSafeCallable
|
||||
{
|
||||
if (fn) {
|
||||
PythonGILState gil;
|
||||
fn = py::object();
|
||||
if (gil)
|
||||
fn = py::object();
|
||||
else
|
||||
(void) fn.release();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -127,6 +130,8 @@ GUI::PluginWebDialog::MessageHandler make_message_adapter(py::object on_message)
|
||||
return nullptr;
|
||||
return [holder](const json& data) {
|
||||
PythonGILState gil;
|
||||
if (!gil)
|
||||
return;
|
||||
try {
|
||||
holder->fn(json_to_py(data));
|
||||
} catch (py::error_already_set& e) {
|
||||
@@ -343,6 +348,8 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
|
||||
if (close_holder) {
|
||||
on_close = [close_holder]() {
|
||||
PythonGILState gil;
|
||||
if (!gil)
|
||||
return;
|
||||
try {
|
||||
close_holder->fn();
|
||||
} catch (py::error_already_set& e) {
|
||||
@@ -412,7 +419,7 @@ UiProgressHandle* new_progress_dialog(const std::string& title, const std::strin
|
||||
|
||||
bool progress_is_open(int id)
|
||||
{
|
||||
return UiRegistry::instance().get_as<GUI::PluginProgressDialog>(id) != nullptr;
|
||||
return UiRegistry::instance().is_open(id);
|
||||
}
|
||||
|
||||
bool progress_pulse(int id, const std::string& message)
|
||||
@@ -462,7 +469,10 @@ 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.");
|
||||
"thread (marshaled from the plugin thread). See the plugin docs for the window.orca bridge. "
|
||||
"Do not call these from a slicing pipeline hook (SlicingPipeline capability): that hook runs "
|
||||
"on the slicing worker thread, which the UI thread can itself be blocked waiting on, so a "
|
||||
"marshaled UI call from there can deadlock the application.");
|
||||
|
||||
ui.def("message", &ui_message, py::arg("text"), py::arg("title") = "OrcaSlicer", py::arg("buttons") = "ok",
|
||||
py::arg("icon") = "info",
|
||||
|
||||
@@ -9,15 +9,18 @@ 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.
|
||||
//
|
||||
// Not safe to call from a slicing pipeline hook (SlicingPipelinePluginCapability):
|
||||
// that hook runs on the slicing worker thread, which the UI thread can itself be
|
||||
// blocked waiting on, so marshaling a UI call from there can deadlock. Plugin
|
||||
// authors must not call orca.host.ui.* from pipeline hooks.
|
||||
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.
|
||||
// Lifecycle hook: close and tear down every UI window owned by a plugin. PluginManager invokes
|
||||
// this after plugin teardown and also for bulk unload during application shutdown.
|
||||
static void close_windows_for_plugin(const std::string& plugin_key);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "../../PyPluginTrampoline.hpp"
|
||||
|
||||
#include "IPrinterAgent.hpp"
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
class PyPrinterAgentPluginCapabilityTrampoline : public PyPluginCommonTrampoline<PrinterAgentPluginCapability>
|
||||
@@ -216,7 +217,10 @@ public:
|
||||
int request_bind_ticket(std::string* ticket) override
|
||||
{
|
||||
ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
|
||||
pybind11::gil_scoped_acquire gil;
|
||||
::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this);
|
||||
::Slic3r::PythonGILState gil;
|
||||
if (!gil)
|
||||
throw std::runtime_error("Python interpreter is shutting down");
|
||||
pybind11::function override =
|
||||
pybind11::get_override(static_cast<const PrinterAgentPluginCapability*>(this), "request_bind_ticket");
|
||||
if (!override)
|
||||
|
||||
@@ -38,6 +38,8 @@ struct SlicingPipelineContext {
|
||||
class SlicingPipelinePluginCapability : public PluginCapabilityInterface {
|
||||
public:
|
||||
PluginCapabilityType get_type() const override { return PluginCapabilityType::SlicingPipeline; }
|
||||
// Runs on the slicing worker thread. Do not call orca.host.ui.* here: the UI thread can be
|
||||
// blocked waiting on the slicing worker, so a marshaled UI call from this thread can deadlock.
|
||||
virtual ExecutionResult execute(SlicingPipelineContext& ctx) = 0;
|
||||
static void RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes);
|
||||
};
|
||||
|
||||
@@ -2,8 +2,8 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
|
||||
add_executable(${_TEST_NAME}_tests
|
||||
${_TEST_NAME}_tests_main.cpp
|
||||
test_plugin_host_api.cpp
|
||||
test_plugin_capability_identifier.cpp
|
||||
test_plugin_install.cpp
|
||||
test_plugin_lifecycle.cpp
|
||||
test_slicing_pipeline_bindings.cpp
|
||||
test_plugin_sort.cpp
|
||||
)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <slic3r/plugin/PluginLoader.hpp>
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
using Slic3r::PluginCapabilityIdentifier;
|
||||
using Slic3r::PluginCapabilityType;
|
||||
|
||||
TEST_CASE("PluginCapabilityIdentifier equality includes plugin_key", "[plugin][identifier]") {
|
||||
PluginCapabilityIdentifier a{PluginCapabilityType::SlicingPipeline, "Cleanup", "a.py"};
|
||||
PluginCapabilityIdentifier b{PluginCapabilityType::SlicingPipeline, "Cleanup", "b.py"};
|
||||
PluginCapabilityIdentifier a2{PluginCapabilityType::SlicingPipeline, "Cleanup", "a.py"};
|
||||
|
||||
CHECK(a == a2);
|
||||
CHECK_FALSE(a == b); // same (type,name), different plugin_key -> distinct
|
||||
}
|
||||
|
||||
TEST_CASE("PluginCapabilityIdentifier is usable as a hash-map key", "[plugin][identifier]") {
|
||||
std::unordered_map<PluginCapabilityIdentifier, int> m;
|
||||
m[{PluginCapabilityType::SlicingPipeline, "Cleanup", "a.py"}] = 1;
|
||||
m[{PluginCapabilityType::SlicingPipeline, "Cleanup", "b.py"}] = 2; // no collision
|
||||
CHECK(m.size() == 2);
|
||||
CHECK(m.at({PluginCapabilityType::SlicingPipeline, "Cleanup", "a.py"}) == 1);
|
||||
}
|
||||
@@ -57,15 +57,14 @@ TEST_CASE("install_plugin rejects a cloud UUID containing path traversal", "[Plu
|
||||
// Package contents are irrelevant: the UUID is validated before metadata is read.
|
||||
const fs::path py = write_py_file(data_dir_guard.dir / "src", "evil.py", "print('hi')\n");
|
||||
|
||||
PluginLoader loader;
|
||||
loader.set_cloud_user_id("test-user");
|
||||
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
// is_cloud_plugin() -> true; cloud_uuid() -> the traversal string.
|
||||
descriptor.cloud = CloudPluginState{"../../escape", true, false, false, false};
|
||||
|
||||
std::string error;
|
||||
const bool installed = loader.install_plugin(py, descriptor, error);
|
||||
const bool installed = plugin_loader::install_plugin(py, /*cloud_user_id=*/"test-user", descriptor, error);
|
||||
|
||||
REQUIRE_FALSE(installed);
|
||||
CHECK_THAT(error, Catch::Matchers::ContainsSubstring("valid identifier"));
|
||||
@@ -78,11 +77,10 @@ TEST_CASE("install_plugin rejects a side-loaded .py with no PEP 723 metadata", "
|
||||
// No `# /// script` block -> name stays empty and type stays Unknown.
|
||||
const fs::path py = write_py_file(data_dir_guard.dir / "src", "nameless.py", "print('no metadata here')\n");
|
||||
|
||||
PluginLoader loader; // non-cloud: no cloud user id, descriptor has no cloud state
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
std::string error;
|
||||
const bool installed = loader.install_plugin(py, descriptor, error);
|
||||
const bool installed = plugin_loader::install_plugin(py, /*cloud_user_id=*/"", descriptor, error);
|
||||
|
||||
REQUIRE_FALSE(installed);
|
||||
CHECK_THAT(error, Catch::Matchers::ContainsSubstring("PEP 723"));
|
||||
@@ -103,11 +101,10 @@ TEST_CASE("install_plugin accepts a side-loaded .py with complete PEP 723 metada
|
||||
"print('ok')\n";
|
||||
const fs::path py = write_py_file(data_dir_guard.dir / "src", "good.py", contents);
|
||||
|
||||
PluginLoader loader; // non-cloud
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
std::string error;
|
||||
const bool installed = loader.install_plugin(py, descriptor, error);
|
||||
const bool installed = plugin_loader::install_plugin(py, /*cloud_user_id=*/"", descriptor, error);
|
||||
|
||||
// Positive control: a complete side-loaded .py must still install (guards against over-rejection).
|
||||
REQUIRE(installed);
|
||||
@@ -168,10 +165,9 @@ TEST_CASE("install_plugin parses [tool.orcaslicer.plugin.settings] into descript
|
||||
"print('ok')\n";
|
||||
const fs::path py = write_py_file(data_dir_guard.dir / "src", "settings.py", contents);
|
||||
|
||||
PluginLoader loader; // non-cloud
|
||||
PluginDescriptor descriptor;
|
||||
std::string error;
|
||||
const bool installed = loader.install_plugin(py, descriptor, error);
|
||||
const bool installed = plugin_loader::install_plugin(py, /*cloud_user_id=*/"", descriptor, error);
|
||||
|
||||
REQUIRE(installed);
|
||||
CHECK(error.empty());
|
||||
|
||||
760
tests/slic3rutils/test_plugin_lifecycle.cpp
Normal file
760
tests/slic3rutils/test_plugin_lifecycle.cpp
Normal file
@@ -0,0 +1,760 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <libslic3r/Utils.hpp>
|
||||
#include <slic3r/plugin/PluginDescriptor.hpp>
|
||||
#include <slic3r/plugin/PluginManager.hpp>
|
||||
#include <slic3r/plugin/PythonFileUtils.hpp>
|
||||
#include <slic3r/plugin/PythonInterpreter.hpp>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace Slic3r;
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
// Plugin load/unload lifecycle: discovery -> load -> capability materialization -> enable/disable
|
||||
// -> unload.
|
||||
//
|
||||
// Each Catch2 test case runs in its own process (catch_discover_tests), so the PluginManager and
|
||||
// interpreter singletons are brought up at most once per test.
|
||||
|
||||
namespace {
|
||||
|
||||
// Point data_dir() at a throwaway directory for the lifetime of a test and restore the previous
|
||||
// value afterwards, so discovery scans a disposable {data_dir}/orca_plugins tree and tests don't
|
||||
// leak state into each other.
|
||||
struct ScopedDataDir
|
||||
{
|
||||
std::string previous;
|
||||
fs::path dir;
|
||||
|
||||
explicit ScopedDataDir(const std::string& tag)
|
||||
{
|
||||
previous = data_dir();
|
||||
dir = fs::temp_directory_path() / fs::unique_path("orca-" + tag + "-%%%%-%%%%");
|
||||
fs::create_directories(dir);
|
||||
set_data_dir(dir.string());
|
||||
}
|
||||
|
||||
~ScopedDataDir()
|
||||
{
|
||||
set_data_dir(previous);
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(dir, ec);
|
||||
}
|
||||
|
||||
fs::path plugins_dir() const { return dir / "orca_plugins"; }
|
||||
};
|
||||
|
||||
// Brings the plugin system up, and tears it down explicitly at the end of the test.
|
||||
//
|
||||
// Shutting the interpreter down here, rather than leaving it to PythonInterpreter's static
|
||||
// destructor, mirrors what the app does (GUI_App finalizes it before exit). Left to static
|
||||
// destruction, shutdown()'s logging runs after boost::log has torn down its thread-local storage
|
||||
// and throws, aborting the process after the tests have already passed.
|
||||
//
|
||||
// Declare this FIRST in a test so it is destroyed last.
|
||||
struct ScopedPluginManager
|
||||
{
|
||||
bool initialized = false;
|
||||
|
||||
ScopedPluginManager() { initialized = PluginManager::instance().initialize(); }
|
||||
~ScopedPluginManager()
|
||||
{
|
||||
PluginManager::instance().shutdown();
|
||||
PythonInterpreter::instance().shutdown();
|
||||
}
|
||||
};
|
||||
|
||||
// A minimal script plugin exposing exactly one capability, "Echo".
|
||||
const char* const ECHO_PLUGIN_SOURCE = R"PY(# /// script
|
||||
# requires-python = ">=3.12"
|
||||
#
|
||||
# [tool.orcaslicer.plugin]
|
||||
# name = "Echo Plugin"
|
||||
# description = "Plugin lifecycle characterization fixture"
|
||||
# author = "OrcaSlicer"
|
||||
# version = "1.0"
|
||||
# type = "script"
|
||||
# ///
|
||||
import orca
|
||||
|
||||
class Echo(orca.script.ScriptPluginCapabilityBase):
|
||||
def get_name(self):
|
||||
return "Echo"
|
||||
|
||||
def execute(self, ctx):
|
||||
return orca.ExecutionResult.success()
|
||||
|
||||
@orca.plugin
|
||||
class EchoPackage(orca.base):
|
||||
def register_capabilities(self):
|
||||
orca.register_capability(Echo)
|
||||
)PY";
|
||||
|
||||
// Writes {data_dir}/orca_plugins/<stem>/<stem>.py and returns the plugin directory.
|
||||
fs::path write_plugin(const ScopedDataDir& data_dir_guard, const std::string& stem, const std::string& source)
|
||||
{
|
||||
const fs::path plugin_dir = data_dir_guard.plugins_dir() / stem;
|
||||
fs::create_directories(plugin_dir);
|
||||
|
||||
std::ofstream out((plugin_dir / (stem + ".py")).string(), std::ios::binary);
|
||||
out << source;
|
||||
out.close();
|
||||
|
||||
return plugin_dir;
|
||||
}
|
||||
|
||||
// Loads a plugin and blocks until the detached worker thread is done with it.
|
||||
bool load_and_wait(PluginManager& manager,
|
||||
const std::string& plugin_key,
|
||||
std::string& error,
|
||||
std::vector<std::string> capabilities_to_enable = {})
|
||||
{
|
||||
manager.load_plugin(plugin_key, /*skip_deps=*/true, std::move(capabilities_to_enable));
|
||||
return manager.wait_for_plugin_load(plugin_key, std::chrono::seconds(120), error);
|
||||
}
|
||||
|
||||
std::shared_ptr<PluginCapabilityInterface> find_capability(PluginManager& manager, const std::string& plugin_key,
|
||||
const std::string& name)
|
||||
{
|
||||
return manager.get_plugin_capability(plugin_key, name, PluginCapabilityType::Unknown, /*only_enabled=*/false);
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<PluginCapabilityInterface>> capabilities_of(PluginManager& manager, const std::string& plugin_key)
|
||||
{
|
||||
return manager.get_plugin_capabilities(plugin_key, PluginCapabilityType::Unknown, /*only_enabled=*/false);
|
||||
}
|
||||
|
||||
PluginDescriptor descriptor_of(PluginManager& manager, const std::string& plugin_key)
|
||||
{
|
||||
PluginDescriptor descriptor;
|
||||
manager.try_get_plugin_descriptor(plugin_key, descriptor);
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("A discovered script plugin loads and materializes its capability", "[PluginLifecycle][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("lifecycle-load");
|
||||
write_plugin(data_dir_guard, "Echo_Plugin", ECHO_PLUGIN_SOURCE);
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
REQUIRE(manager.try_get_valid_plugin_descriptor("Echo_Plugin", descriptor));
|
||||
CHECK(descriptor.name == "Echo Plugin");
|
||||
|
||||
std::string error;
|
||||
REQUIRE(load_and_wait(manager, "Echo_Plugin", error));
|
||||
INFO("load error: " << error);
|
||||
CHECK(error.empty());
|
||||
|
||||
CHECK(manager.is_plugin_loaded("Echo_Plugin"));
|
||||
CHECK(manager.get_plugin_load_error("Echo_Plugin").empty());
|
||||
|
||||
const auto capabilities = capabilities_of(manager, "Echo_Plugin");
|
||||
REQUIRE(capabilities.size() == 1);
|
||||
|
||||
const auto& echo = capabilities.front();
|
||||
CHECK(echo->name() == "Echo");
|
||||
CHECK(echo->type() == PluginCapabilityType::Script);
|
||||
CHECK(echo->is_enabled());
|
||||
CHECK(echo->audit_plugin_key() == "Echo_Plugin");
|
||||
|
||||
CHECK(manager.get_plugin_capability("Echo_Plugin", "Echo", PluginCapabilityType::Script) == echo);
|
||||
|
||||
manager.unload_plugin("Echo_Plugin");
|
||||
}
|
||||
|
||||
TEST_CASE("Plugin manager can initialize again after shutdown", "[PluginLifecycle][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("lifecycle-reinitialize");
|
||||
write_plugin(data_dir_guard, "Echo_Plugin", ECHO_PLUGIN_SOURCE);
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
manager.shutdown();
|
||||
|
||||
REQUIRE(manager.initialize());
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
std::string error;
|
||||
REQUIRE(load_and_wait(manager, "Echo_Plugin", error));
|
||||
CHECK(manager.is_plugin_loaded("Echo_Plugin"));
|
||||
|
||||
manager.unload_plugin("Echo_Plugin");
|
||||
}
|
||||
|
||||
TEST_CASE("Duplicate discovered plugin keys are reported", "[PluginLifecycle][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("lifecycle-duplicate-key");
|
||||
for (const char* directory_name : {"first", "second"}) {
|
||||
const fs::path plugin_dir = data_dir_guard.plugins_dir() / directory_name;
|
||||
fs::create_directories(plugin_dir);
|
||||
std::ofstream out((plugin_dir / "Shared.py").string(), std::ios::binary);
|
||||
out << ECHO_PLUGIN_SOURCE;
|
||||
}
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
REQUIRE(manager.try_get_plugin_descriptor("Shared", descriptor));
|
||||
CHECK(descriptor.has_error());
|
||||
CHECK(descriptor.normalized_error().find("Duplicate plugin key") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("Unloading a plugin drops the package and its capabilities", "[PluginLifecycle][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("lifecycle-unload");
|
||||
write_plugin(data_dir_guard, "Echo_Plugin", ECHO_PLUGIN_SOURCE);
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
std::string error;
|
||||
REQUIRE(load_and_wait(manager, "Echo_Plugin", error));
|
||||
REQUIRE(manager.is_plugin_loaded("Echo_Plugin"));
|
||||
|
||||
CHECK(manager.unload_plugin("Echo_Plugin"));
|
||||
|
||||
CHECK_FALSE(manager.is_plugin_loaded("Echo_Plugin"));
|
||||
CHECK(manager.get_plugin_capabilities("Echo_Plugin").empty());
|
||||
CHECK(manager.get_plugin_capability("Echo_Plugin", "Echo", PluginCapabilityType::Script) == nullptr);
|
||||
|
||||
// The package stays discovered, but nothing capability-shaped survives the unload.
|
||||
const PluginDescriptor descriptor = descriptor_of(manager, "Echo_Plugin");
|
||||
CHECK(descriptor.plugin_key == "Echo_Plugin");
|
||||
CHECK(capabilities_of(manager, "Echo_Plugin").empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Python module release removes package submodules and owned sys.path", "[PluginLifecycle][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("module-release");
|
||||
const fs::path package_root = data_dir_guard.dir / "reload_package";
|
||||
fs::create_directories(package_root);
|
||||
|
||||
auto write_package = [&](const std::string& value) {
|
||||
std::ofstream init((package_root / "__init__.py").string());
|
||||
init << "import reload_helper\nfrom . import sub\nVALUE = sub.VALUE\n";
|
||||
std::ofstream sub((package_root / "sub.py").string());
|
||||
sub << "VALUE = " << value << "\n";
|
||||
std::ofstream helper((package_root.parent_path() / "reload_helper.py").string());
|
||||
helper << "VALUE = 'helper'\n";
|
||||
};
|
||||
|
||||
write_package("'old'");
|
||||
|
||||
PythonInterpreter& interpreter = PythonInterpreter::instance();
|
||||
std::vector<std::string> paths;
|
||||
std::vector<std::string> modules;
|
||||
std::string error;
|
||||
PyObject* module = interpreter.load_module_from_directory(
|
||||
package_root.parent_path().string(), "reload_package", error, &paths, &modules);
|
||||
REQUIRE(module != nullptr);
|
||||
INFO("module load error: " << error);
|
||||
REQUIRE(error.empty());
|
||||
REQUIRE(paths.size() == 1);
|
||||
|
||||
{
|
||||
PythonGILState gil;
|
||||
REQUIRE(gil);
|
||||
PyObject* modules = PyImport_GetModuleDict();
|
||||
REQUIRE(modules != nullptr);
|
||||
CHECK(PyDict_GetItemString(modules, "reload_package") != nullptr);
|
||||
CHECK(PyDict_GetItemString(modules, "reload_package.sub") != nullptr);
|
||||
CHECK(PyDict_GetItemString(modules, "reload_helper") != nullptr);
|
||||
}
|
||||
|
||||
Plugin loaded;
|
||||
loaded.module = module;
|
||||
loaded.module_name = "reload_package";
|
||||
loaded.plugin_sys_paths = paths;
|
||||
loaded.plugin_modules = modules;
|
||||
loaded.release_module();
|
||||
|
||||
{
|
||||
PythonGILState gil;
|
||||
REQUIRE(gil);
|
||||
PyObject* modules = PyImport_GetModuleDict();
|
||||
REQUIRE(modules != nullptr);
|
||||
CHECK(PyDict_GetItemString(modules, "reload_package") == nullptr);
|
||||
CHECK(PyDict_GetItemString(modules, "reload_package.sub") == nullptr);
|
||||
CHECK(PyDict_GetItemString(modules, "reload_helper") == nullptr);
|
||||
|
||||
PyObject* sys_path = PySys_GetObject("path");
|
||||
REQUIRE(sys_path != nullptr);
|
||||
PyObjectPtr path(PyUnicode_DecodeFSDefault(paths.front().c_str()));
|
||||
REQUIRE(path);
|
||||
CHECK(PySequence_Contains(sys_path, path.get()) == 0);
|
||||
}
|
||||
|
||||
// Ensure the next import executes the new submodule rather than reusing a stale package child.
|
||||
write_package("'new'");
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(package_root / "__pycache__", ec);
|
||||
|
||||
paths.clear();
|
||||
modules.clear();
|
||||
module = interpreter.load_module_from_directory(
|
||||
package_root.parent_path().string(), "reload_package", error, &paths, &modules);
|
||||
REQUIRE(module != nullptr);
|
||||
REQUIRE(error.empty());
|
||||
|
||||
{
|
||||
PythonGILState gil;
|
||||
REQUIRE(gil);
|
||||
PyObjectPtr value(PyObject_GetAttrString(module, "VALUE"));
|
||||
REQUIRE(value);
|
||||
CHECK(std::string(PyUnicode_AsUTF8(value.get())) == "new");
|
||||
}
|
||||
|
||||
loaded.module = module;
|
||||
loaded.module_name = "reload_package";
|
||||
loaded.plugin_sys_paths = paths;
|
||||
loaded.plugin_modules = modules;
|
||||
loaded.release_module();
|
||||
}
|
||||
|
||||
TEST_CASE("A capability disabled in the sidecar loads disabled", "[PluginLifecycle][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("lifecycle-disabled");
|
||||
const fs::path plugin_dir = write_plugin(data_dir_guard, "Echo_Plugin", ECHO_PLUGIN_SOURCE);
|
||||
|
||||
// Pre-seed the sidecar with the capability disabled, as a previous session would have.
|
||||
PluginInstallState state;
|
||||
state.installed_from = "local";
|
||||
state.installed_version = "1.0";
|
||||
state.plugin_name = "Echo Plugin";
|
||||
state.enabled = true;
|
||||
state.capabilities = {{"Echo", false}};
|
||||
REQUIRE(write_install_state(plugin_dir, state));
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
std::string error;
|
||||
REQUIRE(load_and_wait(manager, "Echo_Plugin", error));
|
||||
REQUIRE(manager.is_plugin_loaded("Echo_Plugin"));
|
||||
|
||||
// The capability still materializes — it is loaded, but logically disabled, so consumers skip it.
|
||||
const auto echo = find_capability(manager, "Echo_Plugin", "Echo");
|
||||
REQUIRE(echo != nullptr);
|
||||
CHECK_FALSE(echo->is_enabled());
|
||||
|
||||
CHECK(manager.get_plugin_capabilities("Echo_Plugin", PluginCapabilityType::Unknown, /*only_enabled=*/true).empty());
|
||||
CHECK(manager.get_plugin_capabilities("Echo_Plugin", PluginCapabilityType::Unknown, /*only_enabled=*/false).size() == 1);
|
||||
|
||||
// An empty load request must preserve the persisted disabled state even when the package is
|
||||
// already loaded.
|
||||
std::string no_request_error;
|
||||
REQUIRE(load_and_wait(manager, "Echo_Plugin", no_request_error));
|
||||
CHECK_FALSE(find_capability(manager, "Echo_Plugin", "Echo")->is_enabled());
|
||||
|
||||
manager.unload_plugin("Echo_Plugin");
|
||||
}
|
||||
|
||||
TEST_CASE("Disabling a capability round-trips through the sidecar and survives a reload", "[PluginLifecycle][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("lifecycle-roundtrip");
|
||||
const fs::path plugin_dir = write_plugin(data_dir_guard, "Echo_Plugin", ECHO_PLUGIN_SOURCE);
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
std::string error;
|
||||
REQUIRE(load_and_wait(manager, "Echo_Plugin", error));
|
||||
REQUIRE(find_capability(manager, "Echo_Plugin", "Echo")->is_enabled());
|
||||
|
||||
// Disabling writes the choice through to .install_state.json.
|
||||
manager.set_capability_enabled("Echo_Plugin", "Echo", false);
|
||||
CHECK_FALSE(find_capability(manager, "Echo_Plugin", "Echo")->is_enabled());
|
||||
|
||||
PluginInstallState persisted;
|
||||
REQUIRE(read_install_state(plugin_dir, persisted));
|
||||
REQUIRE(persisted.capabilities.size() == 1);
|
||||
CHECK(persisted.capabilities.front().first == "Echo");
|
||||
CHECK_FALSE(persisted.capabilities.front().second);
|
||||
|
||||
// Unload and reload: the user's choice must survive.
|
||||
REQUIRE(manager.unload_plugin("Echo_Plugin"));
|
||||
|
||||
std::string reload_error;
|
||||
REQUIRE(load_and_wait(manager, "Echo_Plugin", reload_error));
|
||||
|
||||
const auto echo = find_capability(manager, "Echo_Plugin", "Echo");
|
||||
REQUIRE(echo != nullptr);
|
||||
CHECK_FALSE(echo->is_enabled());
|
||||
|
||||
manager.unload_plugin("Echo_Plugin");
|
||||
}
|
||||
|
||||
TEST_CASE("A capability disabled after load stays disabled when rediscovered and reloaded", "[PluginLifecycle][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("lifecycle-reload-live");
|
||||
write_plugin(data_dir_guard, "Echo_Plugin", ECHO_PLUGIN_SOURCE);
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
std::string error;
|
||||
REQUIRE(load_and_wait(manager, "Echo_Plugin", error));
|
||||
|
||||
manager.set_capability_enabled("Echo_Plugin", "Echo", false);
|
||||
REQUIRE(manager.unload_plugin("Echo_Plugin"));
|
||||
|
||||
// Rediscover, as the app does when a plugin is toggled off and back on. The enable flags the
|
||||
// loader seeds from must come from the sidecar just written, not from a stale cache.
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/false);
|
||||
|
||||
std::string reload_error;
|
||||
REQUIRE(load_and_wait(manager, "Echo_Plugin", reload_error));
|
||||
|
||||
const auto echo = find_capability(manager, "Echo_Plugin", "Echo");
|
||||
REQUIRE(echo != nullptr);
|
||||
CHECK_FALSE(echo->is_enabled());
|
||||
|
||||
manager.unload_plugin("Echo_Plugin");
|
||||
}
|
||||
|
||||
TEST_CASE("Re-enabling a disabled capability writes the sidecar back", "[PluginLifecycle][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("lifecycle-reenable");
|
||||
const fs::path plugin_dir = write_plugin(data_dir_guard, "Echo_Plugin", ECHO_PLUGIN_SOURCE);
|
||||
|
||||
PluginInstallState state;
|
||||
state.installed_from = "local";
|
||||
state.plugin_name = "Echo Plugin";
|
||||
state.enabled = true;
|
||||
state.capabilities = {{"Echo", false}};
|
||||
REQUIRE(write_install_state(plugin_dir, state));
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
std::string error;
|
||||
REQUIRE(load_and_wait(manager, "Echo_Plugin", error));
|
||||
|
||||
// An explicit request overrides the persisted disabled state, including on a fresh load.
|
||||
REQUIRE(manager.unload_plugin("Echo_Plugin"));
|
||||
std::string enable_error;
|
||||
REQUIRE(load_and_wait(manager, "Echo_Plugin", enable_error, {"Echo"}));
|
||||
|
||||
const auto echo = find_capability(manager, "Echo_Plugin", "Echo");
|
||||
REQUIRE(echo != nullptr);
|
||||
CHECK(echo->is_enabled());
|
||||
|
||||
PluginInstallState persisted;
|
||||
REQUIRE(read_install_state(plugin_dir, persisted));
|
||||
REQUIRE(persisted.capabilities.size() == 1);
|
||||
CHECK(persisted.capabilities.front().first == "Echo");
|
||||
CHECK(persisted.capabilities.front().second);
|
||||
|
||||
manager.unload_plugin("Echo_Plugin");
|
||||
}
|
||||
|
||||
TEST_CASE("Overwriting a local plugin unloads its live module", "[PluginLifecycle][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("lifecycle-overwrite");
|
||||
const fs::path package_dir = data_dir_guard.dir / "packages";
|
||||
fs::create_directories(package_dir);
|
||||
const fs::path package = package_dir / "Echo_Plugin.py";
|
||||
{
|
||||
std::ofstream out(package.string(), std::ios::binary);
|
||||
out << ECHO_PLUGIN_SOURCE;
|
||||
}
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
std::string error;
|
||||
REQUIRE(manager.install_plugin(package, error));
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
REQUIRE(load_and_wait(manager, "Echo_Plugin", error));
|
||||
REQUIRE(manager.is_plugin_loaded("Echo_Plugin"));
|
||||
|
||||
REQUIRE(manager.install_plugin(package, error));
|
||||
CHECK_FALSE(manager.is_plugin_loaded("Echo_Plugin"));
|
||||
}
|
||||
|
||||
TEST_CASE("capabilities_to_enable selects which capabilities come up enabled", "[PluginLifecycle][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
// Two capabilities in one package; only the second is requested.
|
||||
const char* const two_cap_source = R"PY(# /// script
|
||||
# requires-python = ">=3.12"
|
||||
#
|
||||
# [tool.orcaslicer.plugin]
|
||||
# name = "Duo Plugin"
|
||||
# version = "1.0"
|
||||
# type = "script"
|
||||
# ///
|
||||
import orca
|
||||
|
||||
class Alpha(orca.script.ScriptPluginCapabilityBase):
|
||||
def get_name(self):
|
||||
return "Alpha"
|
||||
|
||||
def execute(self, ctx):
|
||||
return orca.ExecutionResult.success()
|
||||
|
||||
class Beta(orca.script.ScriptPluginCapabilityBase):
|
||||
def get_name(self):
|
||||
return "Beta"
|
||||
|
||||
def execute(self, ctx):
|
||||
return orca.ExecutionResult.success()
|
||||
|
||||
@orca.plugin
|
||||
class DuoPackage(orca.base):
|
||||
def register_capabilities(self):
|
||||
orca.register_capability(Alpha)
|
||||
orca.register_capability(Beta)
|
||||
)PY";
|
||||
|
||||
ScopedDataDir data_dir_guard("lifecycle-select");
|
||||
write_plugin(data_dir_guard, "Duo_Plugin", two_cap_source);
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
std::string error;
|
||||
REQUIRE(load_and_wait(manager, "Duo_Plugin", error, {"Beta"}));
|
||||
|
||||
REQUIRE(capabilities_of(manager, "Duo_Plugin").size() == 2);
|
||||
|
||||
const auto alpha = find_capability(manager, "Duo_Plugin", "Alpha");
|
||||
const auto beta = find_capability(manager, "Duo_Plugin", "Beta");
|
||||
REQUIRE(alpha != nullptr);
|
||||
REQUIRE(beta != nullptr);
|
||||
|
||||
CHECK_FALSE(alpha->is_enabled());
|
||||
CHECK(beta->is_enabled());
|
||||
|
||||
manager.unload_plugin("Duo_Plugin");
|
||||
}
|
||||
|
||||
TEST_CASE("A cancelled load keeps blocking wait_for_all_plugin_loads until the worker exits", "[PluginLifecycle][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
// Stalls inside the module import, so the detached load worker is still executing Python while
|
||||
// the test cancels it.
|
||||
const char* const slow_source = R"PY(# /// script
|
||||
# requires-python = ">=3.12"
|
||||
#
|
||||
# [tool.orcaslicer.plugin]
|
||||
# name = "Slow Load Plugin"
|
||||
# version = "1.0"
|
||||
# type = "script"
|
||||
# ///
|
||||
import time
|
||||
|
||||
import orca
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
class Slow(orca.script.ScriptPluginCapabilityBase):
|
||||
def get_name(self):
|
||||
return "Slow"
|
||||
|
||||
def execute(self, ctx):
|
||||
return orca.ExecutionResult.success()
|
||||
|
||||
@orca.plugin
|
||||
class SlowPackage(orca.base):
|
||||
def register_capabilities(self):
|
||||
orca.register_capability(Slow)
|
||||
)PY";
|
||||
|
||||
ScopedDataDir data_dir_guard("lifecycle-cancel");
|
||||
write_plugin(data_dir_guard, "Slow_Load_Plugin", slow_source);
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
manager.load_plugin("Slow_Load_Plugin", /*skip_deps=*/true);
|
||||
|
||||
// The key is registered before the worker is spawned, so this is not a race.
|
||||
REQUIRE(manager.is_plugin_load_in_progress("Slow_Load_Plugin"));
|
||||
REQUIRE(manager.cancel_plugin_load("Slow_Load_Plugin"));
|
||||
|
||||
// Cancelling must not release the worker's slot. shutdown() unloads everything and GUI_App
|
||||
// finalizes the interpreter as soon as this wait returns, so reporting "no loads in progress"
|
||||
// while the worker is still inside Python is how the app crashes on exit.
|
||||
CHECK(manager.is_plugin_load_in_progress("Slow_Load_Plugin"));
|
||||
CHECK_FALSE(manager.wait_for_all_plugin_loads(std::chrono::milliseconds(0)));
|
||||
|
||||
// The worker releases the slot itself, once it has unwound.
|
||||
CHECK(manager.wait_for_all_plugin_loads(std::chrono::seconds(60)));
|
||||
CHECK_FALSE(manager.is_plugin_load_in_progress("Slow_Load_Plugin"));
|
||||
CHECK_FALSE(manager.is_plugin_loaded("Slow_Load_Plugin"));
|
||||
}
|
||||
|
||||
TEST_CASE("Loading an unknown plugin key records an error instead of crashing", "[PluginLifecycle][Python]")
|
||||
{
|
||||
// discover_plugins() initializes the plugin system (and with it the interpreter), so this
|
||||
// needs the same explicit teardown as the load tests.
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("lifecycle-missing");
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
// Rejected synchronously: no worker thread is spawned for an unknown key.
|
||||
manager.load_plugin("No_Such_Plugin", /*skip_deps=*/true);
|
||||
|
||||
CHECK_FALSE(manager.is_plugin_loaded("No_Such_Plugin"));
|
||||
CHECK(manager.get_plugin_load_error("No_Such_Plugin") == "Plugin not found: No_Such_Plugin");
|
||||
CHECK(manager.get_plugin_capabilities("No_Such_Plugin").empty());
|
||||
}
|
||||
|
||||
TEST_CASE("The startup auto-load list only contains packages whose sidecar enables them", "[PluginLifecycle][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("lifecycle-autoload");
|
||||
|
||||
// No sidecar at all: never installed through Orca, so it carries no auto-load intent.
|
||||
write_plugin(data_dir_guard, "Bare_Plugin", ECHO_PLUGIN_SOURCE);
|
||||
|
||||
// Sidecar with enabled = true: auto-loads.
|
||||
const fs::path on_dir = write_plugin(data_dir_guard, "Enabled_Plugin", ECHO_PLUGIN_SOURCE);
|
||||
PluginInstallState on_state;
|
||||
on_state.installed_from = "local";
|
||||
on_state.enabled = true;
|
||||
REQUIRE(write_install_state(on_dir, on_state));
|
||||
|
||||
// Sidecar with enabled = false: the user turned it off.
|
||||
const fs::path off_dir = write_plugin(data_dir_guard, "Disabled_Plugin", ECHO_PLUGIN_SOURCE);
|
||||
PluginInstallState off_state;
|
||||
off_state.installed_from = "local";
|
||||
off_state.enabled = false;
|
||||
REQUIRE(write_install_state(off_dir, off_state));
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
const std::vector<std::string> keys = manager.get_enabled_plugin_keys();
|
||||
|
||||
CHECK(std::find(keys.begin(), keys.end(), "Enabled_Plugin") != keys.end());
|
||||
CHECK(std::find(keys.begin(), keys.end(), "Disabled_Plugin") == keys.end());
|
||||
CHECK(std::find(keys.begin(), keys.end(), "Bare_Plugin") == keys.end());
|
||||
}
|
||||
|
||||
TEST_CASE("Signing out drops every cloud plugin row, installed or not", "[PluginLifecycle][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
|
||||
ScopedDataDir data_dir_guard("lifecycle-signout");
|
||||
|
||||
// A local package, which must survive sign-out.
|
||||
write_plugin(data_dir_guard, "Echo_Plugin", ECHO_PLUGIN_SOURCE);
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
manager.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
|
||||
// Two cloud rows: one merely available (nothing installed), one with a local package behind it —
|
||||
// the case that used to linger, unloaded but still listed, until the user hit refresh.
|
||||
PluginDescriptor available;
|
||||
available.plugin_key = "11111111-1111-1111-1111-111111111111";
|
||||
available.name = "Available Cloud Plugin";
|
||||
available.cloud = CloudPluginState{available.plugin_key, /*installed=*/false, false, false, false};
|
||||
|
||||
PluginDescriptor installed;
|
||||
installed.plugin_key = "22222222-2222-2222-2222-222222222222";
|
||||
installed.name = "Installed Cloud Plugin";
|
||||
installed.plugin_root = (data_dir_guard.plugins_dir() / "_subscribed" / "user" / installed.plugin_key).string();
|
||||
installed.cloud = CloudPluginState{installed.plugin_key, /*installed=*/true, false, false, false};
|
||||
|
||||
manager.update_cloud_catalog({available, installed});
|
||||
|
||||
const auto has_key = [&manager](const std::string& key) {
|
||||
PluginDescriptor descriptor;
|
||||
return manager.try_get_plugin_descriptor(key, descriptor);
|
||||
};
|
||||
|
||||
REQUIRE(has_key(available.plugin_key));
|
||||
REQUIRE(has_key(installed.plugin_key));
|
||||
REQUIRE(has_key("Echo_Plugin"));
|
||||
|
||||
// Sign out. The per-user _subscribed directory stops being scanned, so both cloud rows are now
|
||||
// stale and must go — not just the one with nothing installed behind it.
|
||||
manager.unload_cloud_plugins();
|
||||
manager.clear_cloud_plugin_catalog();
|
||||
manager.set_cloud_user("");
|
||||
|
||||
CHECK_FALSE(has_key(available.plugin_key));
|
||||
CHECK_FALSE(has_key(installed.plugin_key));
|
||||
CHECK(has_key("Echo_Plugin"));
|
||||
}
|
||||
|
||||
TEST_CASE("Unloading a plugin that is not loaded is a no-op", "[PluginLifecycle]")
|
||||
{
|
||||
ScopedDataDir data_dir_guard("lifecycle-noop-unload");
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
|
||||
// Current behavior: unloading an unknown key succeeds (it fires the unload callbacks and
|
||||
// reports success) rather than reporting "nothing to unload".
|
||||
CHECK(manager.unload_plugin("No_Such_Plugin"));
|
||||
CHECK_FALSE(manager.is_plugin_loaded("No_Such_Plugin"));
|
||||
}
|
||||
Reference in New Issue
Block a user